diff --git a/.asf.yaml b/.asf.yaml index 092e06d9716..a19e3a60a2b 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -50,16 +50,10 @@ github: rebase: false collaborators: - - acs-robot - gpordeus - - hsato03 - - FelipeM525 - - lucas-a-martins - - nicoschmdt - - abh1sar - - rosi-shapeblue - - sudo87 - erikbocks + - Imvedansh + - Damans227 protected_branches: ~ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..1b06f3ebf53 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours diff --git a/.github/aw/imports/.gitattributes b/.github/aw/imports/.gitattributes new file mode 100644 index 00000000000..f0516fad90e --- /dev/null +++ b/.github/aw/imports/.gitattributes @@ -0,0 +1,5 @@ +# Mark all cached import files as generated +* linguist-generated=true + +# Use 'ours' merge strategy to keep local cached versions +* merge=ours diff --git a/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md b/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md new file mode 100644 index 00000000000..bc08afb42be --- /dev/null +++ b/.github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md @@ -0,0 +1,73 @@ +--- +# Report formatting guidelines +--- + +## Report Structure Guidelines + +### 1. Header Levels +**Use h3 (###) or lower for all headers in your issue report to maintain proper document hierarchy.** + +When creating GitHub issues or discussions: +- Use `###` (h3) for main sections (e.g., "### Test Summary") +- Use `####` (h4) for subsections (e.g., "#### Device-Specific Results") +- Never use `##` (h2) or `#` (h1) in reports - these are reserved for titles + +### 2. Progressive Disclosure +**Wrap detailed test results in `
Section Name` tags to improve readability and reduce scrolling.** + +Use collapsible sections for: +- Verbose details (full test logs, raw data) +- Secondary information (minor warnings, extra context) +- Per-item breakdowns when there are many items + +Always keep critical information visible (summary, critical issues, key metrics). + +### 3. Report Structure Pattern + +1. **Overview**: 1-2 paragraphs summarizing key findings +2. **Critical Information**: Show immediately (summary stats, critical issues) +3. **Details**: Use `
Section Name` for expanded content +4. **Context**: Add helpful metadata (workflow run, date, trigger) + +### Design Principles (Airbnb-Inspired) + +Reports should: +- **Build trust through clarity**: Most important info immediately visible +- **Exceed expectations**: Add helpful context like trends, comparisons +- **Create delight**: Use progressive disclosure to reduce overwhelm +- **Maintain consistency**: Follow patterns across all reports + +### Example Report Structure + +```markdown +### Summary +- Key metric 1: value +- Key metric 2: value +- Status: ✅/⚠️/❌ + +### Critical Issues +[Always visible - these are important] + +
+View Detailed Results + +[Comprehensive details, logs, traces] + +
+ +
+View All Warnings + +[Minor issues and potential problems] + +
+ +### Recommendations +[Actionable next steps - keep visible] +``` + +## Workflow Run References + +- Format run IDs as links: `[§12345](https://github.com/owner/repo/actions/runs/12345)` +- Include up to 3 most relevant run URLs at end under `**References:**` +- Do NOT add footer attribution (system adds automatically) diff --git a/.github/workflows/daily-repo-status.lock.yml b/.github/workflows/daily-repo-status.lock.yml new file mode 100644 index 00000000000..1d7e7eecd14 --- /dev/null +++ b/.github/workflows/daily-repo-status.lock.yml @@ -0,0 +1,1022 @@ +# +# ___ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | ( +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.45.0). DO NOT EDIT. +# +# To update this file, edit githubnext/agentics/workflows/daily-repo-status.md@d19056381ba48cb1f7c78510c23069701fa7ae87 and run: +# gh aw +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# This workflow creates daily repo status reports. It gathers recent +# activity (issues, PRs, discussions, releases, code changes) and +# engaging GitHub issues with productivity insights, community highlights, +# and project recommendations. +# +# Source: githubnext/agentics/workflows/daily-repo-status.md@ +# +# frontmatter-hash: + +name: "Daily Repo Status" +"on": + schedule: + - cron: "25 18 * * *" + # Friendly format: daily (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Daily Repo Status" + +jobs: + activation: + runs-on: ubuntu- + permissions: + contents: + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45. + with: + destination: /opt/gh-aw/ + - name: Check workflow file + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_WORKFLOW_FILE: "daily-repo-status.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: + runs-on: ubuntu- + permissions: + contents: + issues: + pull-requests: + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/ + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs. + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config. + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools. + GH_AW_WORKFLOW_ID_SANITIZED: + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45. + with: + destination: /opt/gh-aw/ + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0. + with: + persist-credentials: + - name: Create gh-aw temp + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir. + - name: Configure Git + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub + 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 + id: checkout- + if: | + github.event. + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + 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('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Generate agentic run + id: + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.410", + cli_version: "v0.45.0", + workflow_name: "Daily Repo Status", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.18.0", + awmg_version: "v0.1.4", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/ + core.setOutput('model', awInfo.model); + - name: Validate COPILOT_GITHUB_TOKEN + id: validate- + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot- + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0. + - name: Install awf + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.18. + - name: Download container + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.18.0 ghcr.io/github/gh-aw-firewall/squid:0.18.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 node:lts- + - name: Write Safe Outputs + run: | + mkdir -p /opt/gh-aw/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/mcp-logs/ + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' + [ + { + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[repo-status] \". Labels [report daily-status] will be automatically added.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "parent": { + "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123', 'aw_Test123') from a previously created issue in the same workflow run.", + "type": [ + "number", + "string" + ] + }, + "temporary_id": { + "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 3 to 8 alphanumeric characters (e.g., 'aw_abc1', 'aw_Test123'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "pattern": "^aw_[A-Za-z0-9]{4,8}$", + "type": "string" + }, + "title": { + "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_issue" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": + }, + "parent": { + "issueOrPRNumber": + }, + "repo": { + "type": "string", + "maxLength": + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": + } + } + } + } + + - name: Generate Safe Outputs MCP Server + id: safe-outputs- + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT= + + # Set outputs for next + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP + id: safe-outputs- + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools. + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config. + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/ + run: | + # Environment variables are set above to prevent template + export + export + export + export + export + export + + bash /opt/gh-aw/actions/start_safe_outputs_server. + + - name: Start MCP + id: start-mcp- + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo + mkdir -p /tmp/gh-aw/mcp- + + # Export gateway environment variables for MCP config and gateway + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' + + mkdir -p /home/runner/. + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway. + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + + - name: Generate workflow + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt. + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first. + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" + + + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Temporary IDs: Some safe output tools support a temporary ID field (usually named temporary_id) so you can reference newly-created items elsewhere in the SAME agent output (for example, using #aw_abc1 in a later body). + + **IMPORTANT - temporary_id format rules:** + - If you DON'T need to reference the item later, OMIT the temporary_id field entirely (it will be auto-generated if needed) + - If you DO need cross-references/chaining, you MUST match this EXACT validation regex: /^aw_[A-Za-z0-9]{3,8}$/ + - Format: aw_ prefix followed by 3 to 8 alphanumeric characters (A-Z, a-z, 0-9, case-insensitive) + - Valid alphanumeric characters: + - INVALID examples: aw_ab (too short), aw_123456789 (too long), aw_test-id (contains hyphen), aw_id_123 (contains underscore) + - VALID examples: aw_abc, aw_abc1, aw_Test123, aw_A1B2C3D4, + - To generate valid IDs: use 3-8 random alphanumeric characters or omit the field to let the system auto- + + Do NOT invent other aw_* formats — downstream steps will reject them with validation errors matching against /^aw_[A-Za-z0-9]{3,8}$/i. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: # + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: # + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: # + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: + {{/if}} + + + + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/daily-repo-status.md}} + + - name: Substitute + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt. + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env. + } + }); + - name: Interpolate variables and render + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt. + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt. + run: bash /opt/gh-aw/actions/validate_prompt_placeholders. + - name: Print + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt. + run: bash /opt/gh-aw/actions/print_prompt_summary. + - name: Clean git + run: bash /opt/gh-aw/actions/clean_git_credentials. + - name: Execute GitHub Copilot + id: + # Copilot CLI tool arguments (sorted): + timeout-minutes: + run: | + set -o + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-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 --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.18.0 --skip-pull \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio. + env: + COPILOT_AGENT_RUNNER_TYPE: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config. + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt. + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/ + - name: Configure Git + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to + if: always() + continue-on-error: + run: | + # Copy Copilot session state files to logs folder for artifact + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || + echo "Session state files copied successfully" + + echo "No session-state directory found at $SESSION_STATE_DIR" + + - name: Stop MCP + if: always() + continue-on-error: + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Safe + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0. + with: + name: safe- + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: + - name: Ingest agent + id: + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent + if: always() && env. + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0. + with: + name: agent- + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: + - name: Upload engine output + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0. + with: + name: + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls. + if-no-files-found: + - name: Parse agent logs for step + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall + if: always() + continue-on-error: + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/ + run: | + # Fix permissions on firewall logs so they can be uploaded as + # AWF runs with sudo, creating files owned by + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + + echo 'AWF binary not installed, skipping firewall log summary' + + - name: Upload agent + if: always() + continue-on-error: + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0. + with: + name: agent- + path: | + /tmp/gh-aw/aw-prompts/prompt. + /tmp/gh-aw/aw_info. + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio. + /tmp/gh-aw/agent/ + if-no-files-found: + + conclusion: + needs: + - + - + - + - + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu- + permissions: + contents: + issues: + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45. + with: + destination: /opt/gh-aw/ + - name: Download agent output + continue-on-error: + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0. + with: + name: agent- + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f - + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op + id: + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@d19056381ba48cb1f7c78510c23069701fa7ae87" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/d19056381ba48cb1f7c78510c23069701fa7ae87/workflows/daily-repo-status.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing + id: + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@d19056381ba48cb1f7c78510c23069701fa7ae87" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/d19056381ba48cb1f7c78510c23069701fa7ae87/workflows/daily-repo-status.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent + id: + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@d19056381ba48cb1f7c78510c23069701fa7ae87" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/d19056381ba48cb1f7c78510c23069701fa7ae87/workflows/daily-repo-status.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "daily-repo-status" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op + id: + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@d19056381ba48cb1f7c78510c23069701fa7ae87" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/d19056381ba48cb1f7c78510c23069701fa7ae87/workflows/daily-repo-status.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + + detection: + needs: + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu- + permissions: {} + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + timeout-minutes: + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45. + with: + destination: /opt/gh-aw/ + - name: Download agent + continue-on-error: + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0. + with: + name: agent- + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output + continue-on-error: + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0. + with: + name: agent- + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + WORKFLOW_NAME: "Daily Repo Status" + WORKFLOW_DESCRIPTION: "This workflow creates daily repo status reports. It gathers recent repository\nactivity (issues, PRs, discussions, releases, code changes) and generates\nengaging GitHub issues with productivity insights, community highlights,\nand project recommendations." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and + run: | + mkdir -p /tmp/gh-aw/threat- + touch /tmp/gh-aw/threat-detection/detection. + - name: Validate COPILOT_GITHUB_TOKEN + id: validate- + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot- + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0. + - name: Execute GitHub Copilot + id: + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: + run: | + set -o + COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection. + env: + COPILOT_AGENT_RUNNER_TYPE: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt. + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/ + - name: Parse threat detection + id: + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0. + with: + name: threat-detection. + path: /tmp/gh-aw/threat-detection/detection. + if-no-files-found: + + safe_outputs: + needs: + - + - + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu- + permissions: + contents: + issues: + timeout-minutes: + env: + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "daily-repo-status" + GH_AW_WORKFLOW_NAME: "Daily Repo Status" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/daily-repo-status.md@d19056381ba48cb1f7c78510c23069701fa7ae87" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/d19056381ba48cb1f7c78510c23069701fa7ae87/workflows/daily-repo-status.md" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45. + with: + destination: /opt/gh-aw/ + - name: Download agent output + continue-on-error: + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0. + with: + name: agent- + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f - + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process Safe + id: + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"report\",\"daily-status\"],\"max\":1,\"title_prefix\":\"[repo-status] \"},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); diff --git a/.github/workflows/daily-repo-status.md b/.github/workflows/daily-repo-status.md new file mode 100644 index 00000000000..431b4afb91a --- /dev/null +++ b/.github/workflows/daily-repo-status.md @@ -0,0 +1,54 @@ +--- +description: | + This workflow creates daily repo status reports. It gathers recent repository + activity (issues, PRs, discussions, releases, code changes) and generates + engaging GitHub issues with productivity insights, community highlights, + and project recommendations. + +on: + schedule: daily + workflow_dispatch: + +permissions: + contents: read + issues: read + pull-requests: read + +network: defaults + +tools: + github: + # If in a public repo, setting `lockdown: false` allows + # reading issues, pull requests and comments from 3rd-parties + # If in a private repo this has no particular effect. + lockdown: false + +safe-outputs: + create-issue: + title-prefix: "[repo-status] " + labels: [report, daily-status] +source: githubnext/agentics/workflows/daily-repo-status.md@d19056381ba48cb1f7c78510c23069701fa7ae87 +--- + +# Daily Repo Status + +Create an upbeat daily status report for the repo as a GitHub issue. + +## What to include + +- Recent repository activity (issues, PRs, discussions, releases, code changes) +- Progress tracking, goal reminders and highlights +- Project status and recommendations +- Actionable next steps for maintainers + +## Style + +- Be positive, encouraging, and helpful 🌟 +- Use emojis moderately for engagement +- Keep it concise - adjust length based on actual activity + +## Process + +1. Gather recent activity from the repository +2. Study the repository, its issues and its pull requests +3. Create a new GitHub issue with your findings and insights diff --git a/.github/workflows/issue-triage-agent.lock.yml b/.github/workflows/issue-triage-agent.lock.yml new file mode 100644 index 00000000000..2410f7b9e45 --- /dev/null +++ b/.github/workflows/issue-triage-agent.lock.yml @@ -0,0 +1,1016 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.45.0). DO NOT EDIT. +# +# To update this file, edit github/gh-aw/.github/workflows/issue-triage-agent.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4 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/ +# +# +# Source: github/gh-aw/.github/workflows/issue-triage-agent.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4 +# +# Resolved workflow manifest: +# Imports: +# - github/gh-aw/.github/workflows/shared/reporting.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4 +# +# frontmatter-hash: 7bc83974fa1e47c12b40c3333872e4126711d5c6624022cc78b76047289d8b63 + +name: "Issue Triage Agent" +"on": + schedule: + - cron: "0 14 * * 1-5" + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Issue Triage Agent" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45.0 + with: + destination: /opt/gh-aw/actions + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "issue-triage-agent.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_WORKFLOW_ID_SANITIZED: issuetriageagent + outputs: + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45.0 + with: + destination: /opt/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "copilot", + engine_name: "GitHub Copilot CLI", + model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", + version: "", + agent_version: "0.0.410", + cli_version: "v0.45.0", + workflow_name: "Issue Triage Agent", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.18.0", + awmg_version: "v0.1.4", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.410 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.18.0 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + 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('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.18.0 ghcr.io/github/gh-aw-firewall/squid:0.18.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"add_comment":{"max":1},"add_labels":{"allowed":["bug","feature","enhancement","documentation","question","help-wanted","good-first-issue"],"max":3},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' + [ + { + "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. IMPORTANT: Comments are subject to validation constraints enforced by the MCP server - maximum 65536 characters for the complete comment (including footer which is added automatically), 10 mentions (@username), and 50 links. Exceeding these limits will result in an immediate error with specific guidance. CONSTRAINTS: Maximum 1 comment(s) can be added.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation. CONSTRAINTS: The complete comment (your body text + automatically added footer) must not exceed 65536 characters total. Maximum 10 mentions (@username), maximum 50 links (http/https URLs). A footer (~200-500 characters) is automatically appended with workflow attribution, so leave adequate space. If these limits are exceeded, the tool call will fail with a detailed error message indicating which constraint was violated.", + "type": "string" + }, + "item_number": { + "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). If omitted, the tool will attempt to resolve the target from the current workflow context (triggering issue, PR, or discussion).", + "type": "number" + } + }, + "required": [ + "body" + ], + "type": "object" + }, + "name": "add_comment" + }, + { + "description": "Add labels to an existing GitHub issue or pull request for categorization and filtering. Labels must already exist in the repository. For creating new issues with labels, use create_issue with the labels property instead. CONSTRAINTS: Only these labels are allowed: [bug feature enhancement documentation question help-wanted good-first-issue].", + "inputSchema": { + "additionalProperties": false, + "properties": { + "item_number": { + "description": "Issue or PR number to add labels to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/issues/456). If omitted, adds labels to the item that triggered this workflow.", + "type": "number" + }, + "labels": { + "description": "Label names to add (e.g., ['bug', 'priority-high']). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "name": "add_labels" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + } + } + }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueOrPRNumber": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.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_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "issues,labels" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Temporary IDs: Some safe output tools support a temporary ID field (usually named temporary_id) so you can reference newly-created items elsewhere in the SAME agent output (for example, using #aw_abc1 in a later body). + + **IMPORTANT - temporary_id format rules:** + - If you DON'T need to reference the item later, OMIT the temporary_id field entirely (it will be auto-generated if needed) + - If you DO need cross-references/chaining, you MUST match this EXACT validation regex: /^aw_[A-Za-z0-9]{3,8}$/i + - Format: aw_ prefix followed by 3 to 8 alphanumeric characters (A-Z, a-z, 0-9, case-insensitive) + - Valid alphanumeric characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 + - INVALID examples: aw_ab (too short), aw_123456789 (too long), aw_test-id (contains hyphen), aw_id_123 (contains underscore) + - VALID examples: aw_abc, aw_abc1, aw_Test123, aw_A1B2C3D4, aw_12345678 + - To generate valid IDs: use 3-8 random alphanumeric characters or omit the field to let the system auto-generate + + Do NOT invent other aw_* formats — downstream steps will reject them with validation errors matching against /^aw_[A-Za-z0-9]{3,8}$/i. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/aw/imports/github/gh-aw/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github_workflows_shared_reporting.md}} + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/issue-triage-agent.md}} + GH_AW_PROMPT_EOF + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 5 + run: | + set -o pipefail + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-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 --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.18.0 --skip-pull \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45.0 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.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@94662b1dee8ce96c876ba9f33b3ab8be32de82a4" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github/workflows/issue-triage-agent.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Issue Triage Agent" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github/workflows/issue-triage-agent.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Issue Triage Agent" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github/workflows/issue-triage-agent.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_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Issue Triage Agent" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/issue-triage-agent.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github/workflows/issue-triage-agent.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_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + + detection: + needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: {} + timeout-minutes: 10 + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45.0 + with: + destination: /opt/gh-aw/actions + - name: Download agent artifacts + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output types + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat detection + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Issue Triage Agent" + WORKFLOW_DESCRIPTION: "No description provided" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.410 + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" + mkdir -p /tmp/ + mkdir -p /tmp/gh-aw/ + mkdir -p /tmp/gh-aw/agent/ + mkdir -p /tmp/gh-aw/sandbox/agent/logs/ + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$COPILOT_CLI_INSTRUCTION"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"} 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_results + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + + safe_outputs: + needs: + - agent + - detection + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + 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@94662b1dee8ce96c876ba9f33b3ab8be32de82a4" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/94662b1dee8ce96c876ba9f33b3ab8be32de82a4/.github/workflows/issue-triage-agent.md" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@58d1d157fbac0f1204798500faefc4f7461ebe28 # v0.45.0 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1},\"add_labels\":{\"allowed\":[\"bug\",\"feature\",\"enhancement\",\"documentation\",\"question\",\"help-wanted\",\"good-first-issue\"]},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); diff --git a/.github/workflows/issue-triage-agent.md b/.github/workflows/issue-triage-agent.md new file mode 100644 index 00000000000..06ff227b740 --- /dev/null +++ b/.github/workflows/issue-triage-agent.md @@ -0,0 +1,78 @@ +--- +on: + schedule: 0 14 * * 1-5 + workflow_dispatch: null +permissions: + issues: read +imports: +- github/gh-aw/.github/workflows/shared/reporting.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4 +safe-outputs: + add-comment: {} + add-labels: + allowed: + - bug + - feature + - enhancement + - documentation + - question + - help-wanted + - good-first-issue +source: github/gh-aw/.github/workflows/issue-triage-agent.md@94662b1dee8ce96c876ba9f33b3ab8be32de82a4 +strict: true +timeout-minutes: 5 +tools: + github: + toolsets: + - issues + - labels +--- +# Issue Triage Agent + +List open issues in ${{ github.repository }} that have no labels. For each unlabeled issue, analyze the title and body, then add one of the allowed labels: `bug`, `feature`, `enhancement`, `documentation`, `question`, `help-wanted`, or `good-first-issue`. + +Skip issues that: +- Already have any of these labels +- Have been assigned to any user (especially non-bot users) + +After adding the label to an issue, mention the issue author in a comment using this format (follow shared/reporting.md guidelines): + +**Comment Template**: +```markdown +### 🏷️ Issue Triaged + +Hi @{author}! I've categorized this issue as **{label_name}** based on the following analysis: + +**Reasoning**: {brief_explanation_of_why_this_label} + +
+View Triage Details + +#### Analysis +- **Keywords detected**: {list_of_keywords_that_matched} +- **Issue type indicators**: {what_made_this_fit_the_category} +- **Confidence**: {High/Medium/Low} + +#### Recommended Next Steps +- {context_specific_suggestion_1} +- {context_specific_suggestion_2} + +
+ +**References**: [Triage run §{run_id}](https://github.com/github/gh-aw/actions/runs/{run_id}) +``` + +**Key formatting requirements**: +- Use h3 (###) for the main heading +- Keep reasoning visible for quick understanding +- Wrap detailed analysis in `
` tags +- Include workflow run reference +- Keep total comment concise (collapsed details prevent noise) + +## Batch Comment Optimization + +For efficiency, if multiple issues are triaged in a single run: +1. Add individual labels to each issue +2. Add a brief comment to each issue (using the template above) +3. Optionally: Create a discussion summarizing all triage actions for that run + +This provides both per-issue context and batch visibility. diff --git a/.github/workflows/merge-conflict-checker.yml b/.github/workflows/merge-conflict-checker.yml index 860e1c1b561..a997cb94ccc 100644 --- a/.github/workflows/merge-conflict-checker.yml +++ b/.github/workflows/merge-conflict-checker.yml @@ -18,8 +18,8 @@ name: "PR Merge Conflict Check" on: push: - pull_request_target: - types: [synchronize] + pull_request: + types: [opened, synchronize, reopened] permissions: # added using https://github.com/step-security/secure-workflows contents: read diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49829caf125..cf6f8d39027 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: - --license-filepath - .github/workflows/license-templates/LICENSE.txt - --fuzzy-match-generates-todo - exclude: ^(CHANGES|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)\.md$|^ui/docs/(full|smoke)-test-plan\.template\.md$ + exclude: ^(CHANGES|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)\.md$|^ui/docs/(full|smoke)-test-plan\.template\.md$|^\.github/workflows/.*\.md$|^\.github/aw/.*\.md$ - id: insert-license name: add license for all properties files description: automatically adds a licence header to all properties files that don't have a license header @@ -120,6 +120,7 @@ repos: - --license-filepath - .github/workflows/license-templates/LICENSE.txt - --fuzzy-match-generates-todo + exclude: ^\.github/workflows/.*\.lock\.yml$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: @@ -195,4 +196,4 @@ repos: args: [--config-file=.github/linters/.yamllint.yml] types: [yaml] files: \.ya?ml$ - exclude: ^.*k8s-.*\.ya?ml$ + exclude: ^.*k8s-.*\.ya?ml$|^.github/workflows/.*\.lock\.ya?ml$ diff --git a/api/src/main/java/com/cloud/host/Host.java b/api/src/main/java/com/cloud/host/Host.java index 07a0dfce041..9c011bac319 100644 --- a/api/src/main/java/com/cloud/host/Host.java +++ b/api/src/main/java/com/cloud/host/Host.java @@ -59,6 +59,9 @@ public interface Host extends StateObject, Identity, Partition, HAResour String HOST_INSTANCE_CONVERSION = "host.instance.conversion"; String HOST_OVFTOOL_VERSION = "host.ovftool.version"; String HOST_VIRTV2V_VERSION = "host.virtv2v.version"; + String HOST_SSH_PORT = "host.ssh.port"; + + int DEFAULT_SSH_PORT = 22; /** * @return name of the machine. diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index b92654bfe17..eb47b75ac5b 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -59,7 +59,8 @@ public interface AccountService { User getSystemUser(); - User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID); + User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, + String accountName, Long domainId, String userUUID, boolean isPasswordChangeRequired); User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID, User.Source source); diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 5f7a4fafb74..1644960bdb0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1261,6 +1261,7 @@ public class ApiConstants { public static final String PROVIDER_FOR_2FA = "providerfor2fa"; public static final String ISSUER_FOR_2FA = "issuerfor2fa"; public static final String MANDATE_2FA = "mandate2fa"; + public static final String PASSWORD_CHANGE_REQUIRED = "passwordchangerequired"; public static final String SECRET_CODE = "secretcode"; public static final String LOGIN = "login"; public static final String LOGOUT = "logout"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java index a698a62254e..5a175834560 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/host/AddHostCmd.java @@ -60,7 +60,8 @@ public class AddHostCmd extends BaseCmd { @Parameter(name = ApiConstants.POD_ID, type = CommandType.UUID, entityType = PodResponse.class, required = true, description = "The Pod ID for the host") private Long podId; - @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "The host URL") + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "The host URL, optionally add ssh port (format: 'host:port') for KVM hosts," + + " otherwise falls back to the port defined at the config 'kvm.host.discovery.ssh.port'") private String url; @Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, required = true, description = "The Zone ID for the host") diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java index f03bb1c4ddd..684103cf8d3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmd.java @@ -26,6 +26,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang3.StringUtils; import com.cloud.user.Account; @@ -78,6 +79,12 @@ public class CreateUserCmd extends BaseCmd { @Parameter(name = ApiConstants.USER_ID, type = CommandType.STRING, description = "User UUID, required for adding account from external provisioning system") private String userUUID; + @Parameter(name = ApiConstants.PASSWORD_CHANGE_REQUIRED, + type = CommandType.BOOLEAN, + description = "Provide true to mandate the User to reset password on next login.", + since = "4.23.0") + private Boolean passwordChangeRequired; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -118,6 +125,10 @@ public class CreateUserCmd extends BaseCmd { return userUUID; } + public Boolean isPasswordChangeRequired() { + return BooleanUtils.isTrue(passwordChangeRequired); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -147,7 +158,7 @@ public class CreateUserCmd extends BaseCmd { CallContext.current().setEventDetails("UserName: " + getUserName() + ", FirstName :" + getFirstName() + ", LastName: " + getLastName()); User user = _accountService.createUser(getUserName(), getPassword(), getFirstName(), getLastName(), getEmail(), getTimezone(), getAccountName(), getDomainId(), - getUserUUID()); + getUserUUID(), isPasswordChangeRequired()); if (user != null) { UserResponse response = _responseGenerator.createUserResponse(user); response.setResponseName(getCommandName()); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java index b61550c7087..628ddb96deb 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmd.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.UserResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.region.RegionService; +import org.apache.commons.lang.BooleanUtils; import com.cloud.user.Account; import com.cloud.user.User; @@ -38,6 +39,8 @@ import com.cloud.user.UserAccount; requestHasSensitiveInfo = true, responseHasSensitiveInfo = true) public class UpdateUserCmd extends BaseCmd { + @Inject + private RegionService _regionService; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -85,8 +88,11 @@ public class UpdateUserCmd extends BaseCmd { "This parameter is only used to mandate 2FA, not to disable 2FA", since = "4.18.0.0") private Boolean mandate2FA; - @Inject - private RegionService _regionService; + @Parameter(name = ApiConstants.PASSWORD_CHANGE_REQUIRED, + type = CommandType.BOOLEAN, + description = "Provide true to mandate the User to reset password on next login.", + since = "4.23.0") + private Boolean passwordChangeRequired; ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// @@ -193,4 +199,8 @@ public class UpdateUserCmd extends BaseCmd { public ApiCommandResourceType getApiResourceType() { return ApiCommandResourceType.User; } + + public Boolean isPasswordChangeRequired() { + return BooleanUtils.isTrue(passwordChangeRequired); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java index c20f700fe08..6e3ef4678d2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java @@ -90,6 +90,10 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { @Param(description = "Management Server ID that the user logged to", since = "4.21.0.0") private String managementServerId; + @SerializedName(value = ApiConstants.PASSWORD_CHANGE_REQUIRED) + @Param(description = "Indicates whether the User is required to change password on next login.", since = "4.23.0") + private Boolean passwordChangeRequired; + public String getUsername() { return username; } @@ -223,4 +227,12 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { public void setManagementServerId(String managementServerId) { this.managementServerId = managementServerId; } + + public Boolean getPasswordChangeRequired() { + return passwordChangeRequired; + } + + public void setPasswordChangeRequired(Boolean passwordChangeRequired) { + this.passwordChangeRequired = passwordChangeRequired; + } } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index cbaf6140597..e83db3a2589 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -32,6 +32,7 @@ import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.ValidatedConfigKey; import org.apache.cloudstack.framework.config.Configurable; import com.cloud.exception.ResourceUnavailableException; @@ -53,10 +54,11 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer "false", "Is backup and recovery framework enabled.", false, ConfigKey.Scope.Zone); - ConfigKey BackupProviderPlugin = new ConfigKey<>("Advanced", String.class, + ConfigKey BackupProviderPlugin = new ValidatedConfigKey<>("Advanced", String.class, "backup.framework.provider.plugin", "dummy", - "The backup and recovery provider plugin. Valid plugin values: dummy, veeam, networker and nas", true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key()); + "The backup and recovery provider plugin. Valid plugin values: dummy, veeam, networker and nas", + true, ConfigKey.Scope.Zone, BackupFrameworkEnabled.key(), value -> validateBackupProviderConfig((String)value)); ConfigKey BackupSyncPollingInterval = new ConfigKey<>("Advanced", Long.class, "backup.framework.sync.interval", @@ -249,4 +251,14 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer Capacity getBackupStorageUsedStats(Long zoneId); void checkAndRemoveBackupOfferingBeforeExpunge(VirtualMachine vm); + + static void validateBackupProviderConfig(String value) { + if (value != null && (value.contains(",") || value.trim().contains(" "))) { + throw new IllegalArgumentException("Multiple backup provider plugins are not supported. Please provide a single plugin value."); + } + List validPlugins = List.of("dummy", "veeam", "networker", "nas"); + if (value != null && !validPlugins.contains(value)) { + throw new IllegalArgumentException("Invalid backup provider plugin: " + value + ". Valid plugin values are: " + String.join(", ", validPlugins)); + } + } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java index 8a57ac3eb22..397723dd606 100644 --- a/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java @@ -69,7 +69,7 @@ public class CreateUserCmdTest { } catch (ServerApiException e) { Assert.assertTrue("Received exception as the mock accountService createUser returns null user", true); } - Mockito.verify(accountService, Mockito.times(1)).createUser(null, "Test", null, null, null, null, null, null, null); + Mockito.verify(accountService, Mockito.times(1)).createUser(null, "Test", null, null, null, null, null, null, null, false); } @Test @@ -82,7 +82,7 @@ public class CreateUserCmdTest { Assert.assertEquals(ApiErrorCode.PARAM_ERROR,e.getErrorCode()); Assert.assertEquals("Empty passwords are not allowed", e.getMessage()); } - Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null); + Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null, false); } @Test @@ -95,6 +95,6 @@ public class CreateUserCmdTest { Assert.assertEquals(ApiErrorCode.PARAM_ERROR,e.getErrorCode()); Assert.assertEquals("Empty passwords are not allowed", e.getMessage()); } - Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null); + Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null, true); } } diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmdTest.java new file mode 100644 index 00000000000..f86e51adb5a --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/user/UpdateUserCmdTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.cloudstack.api.command.admin.user; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class UpdateUserCmdTest { + @InjectMocks + private UpdateUserCmd cmd; + + @Test + public void testGetApiResourceId() { + Long userId = 99L; + cmd.setId(userId); + Assert.assertEquals(userId, cmd.getApiResourceId()); + } + + @Test + public void testGetApiResourceType() { + Assert.assertEquals(ApiCommandResourceType.User, cmd.getApiResourceType()); + } + + @Test + public void testIsPasswordChangeRequired_True() { + ReflectionTestUtils.setField(cmd, "passwordChangeRequired", Boolean.TRUE); + Assert.assertTrue(cmd.isPasswordChangeRequired()); + } + + @Test + public void testIsPasswordChangeRequired_False() { + ReflectionTestUtils.setField(cmd, "passwordChangeRequired", Boolean.FALSE); + Assert.assertFalse(cmd.isPasswordChangeRequired()); + } + + @Test + public void testIsPasswordChangeRequired_Null() { + ReflectionTestUtils.setField(cmd, "passwordChangeRequired", null); + Assert.assertFalse(cmd.isPasswordChangeRequired()); + } +} diff --git a/api/src/test/java/org/apache/cloudstack/api/response/LoginCmdResponseTest.java b/api/src/test/java/org/apache/cloudstack/api/response/LoginCmdResponseTest.java new file mode 100644 index 00000000000..7811138fffe --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/response/LoginCmdResponseTest.java @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + + +import org.junit.Assert; +import org.junit.Test; + +public class LoginCmdResponseTest { + + @Test + public void testAllGettersAndSetters() { + LoginCmdResponse response = new LoginCmdResponse(); + + response.setUsername("user1"); + response.setUserId("100"); + response.setDomainId("200"); + response.setTimeout(3600); + response.setAccount("account1"); + response.setFirstName("John"); + response.setLastName("Doe"); + response.setType("admin"); + response.setTimeZone("UTC"); + response.setTimeZoneOffset("+00:00"); + response.setRegistered("true"); + response.setSessionKey("session-key"); + response.set2FAenabled("true"); + response.set2FAverfied("false"); + response.setProviderFor2FA("totp"); + response.setIssuerFor2FA("cloudstack"); + response.setManagementServerId("ms-1"); + + Assert.assertEquals("user1", response.getUsername()); + Assert.assertEquals("100", response.getUserId()); + Assert.assertEquals("200", response.getDomainId()); + Assert.assertEquals(Integer.valueOf(3600), response.getTimeout()); + Assert.assertEquals("account1", response.getAccount()); + Assert.assertEquals("John", response.getFirstName()); + Assert.assertEquals("Doe", response.getLastName()); + Assert.assertEquals("admin", response.getType()); + Assert.assertEquals("UTC", response.getTimeZone()); + Assert.assertEquals("+00:00", response.getTimeZoneOffset()); + Assert.assertEquals("true", response.getRegistered()); + Assert.assertEquals("session-key", response.getSessionKey()); + Assert.assertEquals("true", response.is2FAenabled()); + Assert.assertEquals("false", response.is2FAverfied()); + Assert.assertEquals("totp", response.getProviderFor2FA()); + Assert.assertEquals("cloudstack", response.getIssuerFor2FA()); + Assert.assertEquals("ms-1", response.getManagementServerId()); + } + + @Test + public void testPasswordChangeRequired_True() { + LoginCmdResponse response = new LoginCmdResponse(); + response.setPasswordChangeRequired(true); + Assert.assertTrue(response.getPasswordChangeRequired()); + } + + @Test + public void testPasswordChangeRequired_False() { + LoginCmdResponse response = new LoginCmdResponse(); + response.setPasswordChangeRequired(false); + Assert.assertFalse(response.getPasswordChangeRequired()); + } + + @Test + public void testPasswordChangeRequired_Null() { + LoginCmdResponse response = new LoginCmdResponse(); + response.setPasswordChangeRequired(null); + Assert.assertNull("Boolean.parseBoolean(null) should return null", response.getPasswordChangeRequired()); + } +} diff --git a/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java b/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java index a3401d6faed..275468214f5 100644 --- a/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/ModifyStoragePoolAnswer.java @@ -46,6 +46,10 @@ public class ModifyStoragePoolAnswer extends Answer { templateInfo = tInfo; } + public ModifyStoragePoolAnswer(final Command command, final boolean success, final String details) { + super(command, success, details); + } + public ModifyStoragePoolAnswer(ModifyStoragePoolCommand cmd, boolean success, String details) { super(cmd, success, details); } diff --git a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java index 8e68f4f1e41..f5ad5fbea2c 100644 --- a/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java +++ b/core/src/main/java/org/apache/cloudstack/backup/RestoreBackupCommand.java @@ -34,9 +34,9 @@ public class RestoreBackupCommand extends Command { private List backupVolumesUUIDs; private List restoreVolumePools; private List restoreVolumePaths; + private List backupFiles; private String diskType; private Boolean vmExists; - private String restoreVolumeUUID; private VirtualMachine.State vmState; private Integer mountTimeout; @@ -92,6 +92,14 @@ public class RestoreBackupCommand extends Command { this.restoreVolumePaths = restoreVolumePaths; } + public List getBackupFiles() { + return backupFiles; + } + + public void setBackupFiles(List backupFiles) { + this.backupFiles = backupFiles; + } + public Boolean isVmExists() { return vmExists; } @@ -116,14 +124,6 @@ public class RestoreBackupCommand extends Command { this.mountOptions = mountOptions; } - public String getRestoreVolumeUUID() { - return restoreVolumeUUID; - } - - public void setRestoreVolumeUUID(String restoreVolumeUUID) { - this.restoreVolumeUUID = restoreVolumeUUID; - } - public VirtualMachine.State getVmState() { return vmState; } diff --git a/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java b/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java index 0aa5805b160..4d63fae3356 100644 --- a/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java +++ b/engine/components-api/src/main/java/com/cloud/agent/AgentManager.java @@ -54,6 +54,10 @@ public interface AgentManager { "This timeout overrides the wait global config. This holds a comma separated key value pairs containing timeout (in seconds) for specific commands. " + "For example: DhcpEntryCommand=600, SavePasswordCommand=300, VmDataCommand=300", false); + ConfigKey KVMHostDiscoverySshPort = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Integer.class, + "kvm.host.discovery.ssh.port", String.valueOf(Host.DEFAULT_SSH_PORT), "SSH port used for KVM host discovery and any other operations on host (using SSH)." + + " Please note that this is applicable when port is not defined through host url while adding the KVM host.", true, ConfigKey.Scope.Cluster); + enum TapAgentsAction { Add, Del, Contains, } @@ -172,4 +176,6 @@ public interface AgentManager { void propagateChangeToAgents(Map params); boolean transferDirectAgentsFromMS(String fromMsUuid, long fromMsId, long timeoutDurationInMs, boolean excludeHostsInMaintenance); + + int getHostSshPort(HostVO host); } diff --git a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java index f4651237f32..e724f5d081b 100755 --- a/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java +++ b/engine/components-api/src/main/java/com/cloud/resource/ResourceManager.java @@ -167,6 +167,8 @@ public interface ResourceManager extends ResourceService, Configurable { public HostVO findHostByGuid(String guid); + HostVO findHostByGuidPrefix(String guid); + public HostVO findHostByName(String name); HostStats getHostStatistics(Host host); diff --git a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java index 439bdf92ddd..527c5259376 100644 --- a/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/main/java/com/cloud/agent/manager/AgentManagerImpl.java @@ -42,6 +42,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.StringUtils; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.command.ReconcileCommandService; @@ -64,7 +65,6 @@ import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToSt import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.ThreadContext; import com.cloud.agent.AgentManager; @@ -2111,7 +2111,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl return new ConfigKey[] { CheckTxnBeforeSending, Workers, Port, Wait, AlertWait, DirectAgentLoadSize, DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable, ReadyCommandWait, GranularWaitTimeForCommands, RemoteAgentSslHandshakeTimeout, RemoteAgentMaxConcurrentNewConnections, - RemoteAgentNewConnectionsMonitorInterval }; + RemoteAgentNewConnectionsMonitorInterval, KVMHostDiscoverySshPort }; } protected class SetHostParamsListener implements Listener { @@ -2234,6 +2234,25 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl return true; } + @Override + public int getHostSshPort(HostVO host) { + if (host == null) { + return KVMHostDiscoverySshPort.value(); + } + + if (host.getHypervisorType() != HypervisorType.KVM) { + return Host.DEFAULT_SSH_PORT; + } + + _hostDao.loadDetails(host); + String hostPort = host.getDetail(Host.HOST_SSH_PORT); + if (StringUtils.isBlank(hostPort)) { + return KVMHostDiscoverySshPort.valueIn(host.getClusterId()); + } + + return Integer.parseInt(hostPort); + } + private GlobalLock getHostJoinLock(Long hostId) { return GlobalLock.getInternLock(String.format("%s-%s", "Host-Join", hostId)); } diff --git a/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java b/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java index fb42f247788..43d83a672c0 100644 --- a/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java +++ b/engine/orchestration/src/test/java/com/cloud/agent/manager/AgentManagerImplTest.java @@ -22,6 +22,7 @@ import com.cloud.agent.api.ReadyCommand; import com.cloud.agent.api.StartupCommand; import com.cloud.agent.api.StartupRoutingCommand; import com.cloud.exception.ConnectionException; +import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; @@ -104,4 +105,36 @@ public class AgentManagerImplTest { Assert.assertEquals(50, result); } + + @Test + public void testGetHostSshPortWithHostNull() { + int hostSshPort = mgr.getHostSshPort(null); + Assert.assertEquals(22, hostSshPort); + } + + @Test + public void testGetHostSshPortWithNonKVMHost() { + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer); + int hostSshPort = mgr.getHostSshPort(host); + Assert.assertEquals(22, hostSshPort); + } + + @Test + public void testGetHostSshPortWithKVMHostDefaultPort() { + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(host.getClusterId()).thenReturn(1L); + int hostSshPort = mgr.getHostSshPort(host); + Assert.assertEquals(22, hostSshPort); + } + + @Test + public void testGetHostSshPortWithKVMHostCustomPort() { + HostVO host = Mockito.mock(HostVO.class); + Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM); + Mockito.when(host.getDetail(Host.HOST_SSH_PORT)).thenReturn(String.valueOf(3922)); + int hostSshPort = mgr.getHostSshPort(host); + Assert.assertEquals(3922, hostSshPort); + } } diff --git a/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java index 4c752ff9b4f..a8888f98ad2 100644 --- a/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java @@ -171,7 +171,7 @@ public class ClusterDetailsDaoImpl extends ResourceDetailsDaoBase implements HostDao SearchCriteria sc = TypeStatusStateSearch.create(); sc.setParameters("type", Host.Type.Routing); sc.setParameters("cluster", clusterId); - List list = listBy(sc, new Filter(1)); + List list = listBy(sc, new Filter(1, true)); return list.isEmpty() ? null : list.get(0); } diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 286d4ff7542..727c4fe8ef2 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -383,7 +383,7 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol public VolumeDaoImpl() { AllFieldsSearch = createSearchBuilder(); - AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.EQ); + AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.IN); AllFieldsSearch.and("accountId", AllFieldsSearch.entity().getAccountId(), Op.EQ); AllFieldsSearch.and("dcId", AllFieldsSearch.entity().getDataCenterId(), Op.EQ); AllFieldsSearch.and("pod", AllFieldsSearch.entity().getPodId(), Op.EQ); @@ -581,17 +581,16 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol @Override public List listVolumesToBeDestroyed() { - SearchCriteria sc = AllFieldsSearch.create(); - sc.setParameters("state", Volume.State.Destroy); - - return listBy(sc); + return listVolumesToBeDestroyed(null); } @Override public List listVolumesToBeDestroyed(Date date) { SearchCriteria sc = AllFieldsSearch.create(); - sc.setParameters("state", Volume.State.Destroy); - sc.setParameters("updateTime", date); + sc.setParameters("state", Volume.State.Destroy, Volume.State.Expunging); + if (date != null) { + sc.setParameters("updateTime", date); + } return listBy(sc); } diff --git a/engine/schema/src/main/java/com/cloud/user/UserVO.java b/engine/schema/src/main/java/com/cloud/user/UserVO.java index 6e355e102e6..d74aa7ed41b 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserVO.java @@ -123,8 +123,8 @@ public class UserVO implements User, Identity, InternalIdentity { } public UserVO(long id) { + this(); this.id = id; - this.uuid = UUID.randomUUID().toString(); } public UserVO(long accountId, String username, String password, String firstName, String lastName, String email, String timezone, String uuid, Source source) { diff --git a/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java index dae5f3a3467..67b70571cb4 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/AccountDao.java @@ -21,13 +21,11 @@ import java.util.List; import com.cloud.user.Account; import com.cloud.user.AccountVO; -import com.cloud.user.User; import com.cloud.utils.Pair; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDao; public interface AccountDao extends GenericDao { - Pair findUserAccountByApiKey(String apiKey); List findAccountsLike(String accountName); diff --git a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java index f5f95d5da1f..48b29fac45e 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/AccountDaoImpl.java @@ -16,8 +16,6 @@ // under the License. package com.cloud.user.dao; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.util.Date; import java.util.List; @@ -27,10 +25,7 @@ import org.springframework.stereotype.Component; import com.cloud.user.Account; import com.cloud.user.Account.State; import com.cloud.user.AccountVO; -import com.cloud.user.User; -import com.cloud.user.UserVO; import com.cloud.utils.Pair; -import com.cloud.utils.crypt.DBEncryptionUtil; import com.cloud.utils.db.Filter; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.GenericSearchBuilder; @@ -38,13 +33,9 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria.Func; import com.cloud.utils.db.SearchCriteria.Op; -import com.cloud.utils.db.TransactionLegacy; @Component public class AccountDaoImpl extends GenericDaoBase implements AccountDao { - private static final String FIND_USER_ACCOUNT_BY_API_KEY = "SELECT u.id, u.username, u.account_id, u.secret_key, u.state, u.api_key_access, " - + "a.id, a.account_name, a.type, a.role_id, a.domain_id, a.state, a.api_key_access " + "FROM `cloud`.`user` u, `cloud`.`account` a " - + "WHERE u.account_id = a.id AND u.api_key = ? and u.removed IS NULL"; protected final SearchBuilder AllFieldsSearch; protected final SearchBuilder AccountTypeSearch; @@ -132,51 +123,6 @@ public class AccountDaoImpl extends GenericDaoBase implements A return listBy(sc); } - @Override - public Pair findUserAccountByApiKey(String apiKey) { - TransactionLegacy txn = TransactionLegacy.currentTxn(); - PreparedStatement pstmt = null; - Pair userAcctPair = null; - try { - String sql = FIND_USER_ACCOUNT_BY_API_KEY; - pstmt = txn.prepareAutoCloseStatement(sql); - pstmt.setString(1, apiKey); - ResultSet rs = pstmt.executeQuery(); - // TODO: make sure we don't have more than 1 result? ApiKey had better be unique - if (rs.next()) { - User u = new UserVO(rs.getLong(1)); - u.setUsername(rs.getString(2)); - u.setAccountId(rs.getLong(3)); - u.setSecretKey(DBEncryptionUtil.decrypt(rs.getString(4))); - u.setState(State.getValueOf(rs.getString(5))); - boolean apiKeyAccess = rs.getBoolean(6); - if (rs.wasNull()) { - u.setApiKeyAccess(null); - } else { - u.setApiKeyAccess(apiKeyAccess); - } - - AccountVO a = new AccountVO(rs.getLong(7)); - a.setAccountName(rs.getString(8)); - a.setType(Account.Type.getFromValue(rs.getInt(9))); - a.setRoleId(rs.getLong(10)); - a.setDomainId(rs.getLong(11)); - a.setState(State.getValueOf(rs.getString(12))); - apiKeyAccess = rs.getBoolean(13); - if (rs.wasNull()) { - a.setApiKeyAccess(null); - } else { - a.setApiKeyAccess(apiKeyAccess); - } - - userAcctPair = new Pair(u, a); - } - } catch (Exception e) { - logger.warn("Exception finding user/acct by api key: " + apiKey, e); - } - return userAcctPair; - } - @Override public List findAccountsLike(String accountName) { return findAccountsLike(accountName, null).first(); @@ -341,11 +287,9 @@ public class AccountDaoImpl extends GenericDaoBase implements A domain_id = account_vo.getDomainId(); } catch (Exception e) { - logger.warn("getDomainIdForGivenAccountId: Exception :" + e.getMessage()); - } - finally { - return domain_id; + logger.warn("Can not get DomainId for the given AccountId; exception message : {}", e.getMessage()); } + return domain_id; } @Override diff --git a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java index d0cfcc3d439..93b49bc20a1 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java @@ -48,6 +48,8 @@ public class UserDetailVO implements ResourceDetail { public static final String Setup2FADetail = "2FASetupStatus"; public static final String PasswordResetToken = "PasswordResetToken"; public static final String PasswordResetTokenExpiryDate = "PasswordResetTokenExpiryDate"; + public static final String PasswordChangeRequired = "PasswordChangeRequired"; + public static final String OauthLogin = "OauthLogin"; public UserDetailVO() { } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java index 4cb40b5eaf6..5f32a350232 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/ImageStoreDaoImpl.java @@ -202,7 +202,7 @@ public class ImageStoreDaoImpl extends GenericDaoBase implem sc.setParameters("dataCenterId", dataCenterId); sc.setParameters("protocol", protocol); sc.setParameters("role", DataStoreRole.Image); - Filter filter = new Filter(1); + Filter filter = new Filter(1, true); List results = listBy(sc, filter); return results.size() == 0 ? null : results.get(0); } diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 7132d61ed3b..436f991afbd 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -436,6 +436,9 @@ public class VolumeServiceImpl implements VolumeService { // no need to change state in volumes table volume.processEventOnly(Event.DestroyRequested); } else if (volume.getDataStore().getRole() == DataStoreRole.Primary) { + if (vol.getState() == Volume.State.Expunging) { + logger.info("Volume {} is already in Expunging, retrying", volume); + } volume.processEvent(Event.ExpungeRequested); } diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceTest.java b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ValidatedConfigKey.java similarity index 53% rename from framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceTest.java rename to framework/config/src/main/java/org/apache/cloudstack/framework/config/ValidatedConfigKey.java index cdcfc87cd4e..4fcbe4151d3 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ResourceTest.java +++ b/framework/config/src/main/java/org/apache/cloudstack/framework/config/ValidatedConfigKey.java @@ -14,27 +14,25 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +package org.apache.cloudstack.framework.config; -package org.apache.cloudstack.quota.activationrule.presetvariables; +import java.util.function.Consumer; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +public class ValidatedConfigKey extends ConfigKey { + private final Consumer validator; -@RunWith(MockitoJUnitRunner.class) -public class ResourceTest { - - @Test - public void toStringTestReturnAJson() { - Resource variable = new Resource(); - - String expected = ToStringBuilder.reflectionToString(variable, ToStringStyle.JSON_STYLE); - String result = variable.toString(); - - Assert.assertEquals(expected, result); + public ValidatedConfigKey(String category, Class type, String name, String defaultValue, String description, boolean dynamic, Scope scope, String parent, Consumer validator) { + super(category, type, name, defaultValue, description, dynamic, scope, parent); + this.validator = validator; } + public Consumer getValidator() { + return validator; + } + + public void validateValue(String value) { + if (validator != null) { + validator.accept((T) value); + } + } } diff --git a/framework/db/src/main/java/com/cloud/utils/db/Filter.java b/framework/db/src/main/java/com/cloud/utils/db/Filter.java index 90e42952a99..375e508c55f 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/Filter.java +++ b/framework/db/src/main/java/com/cloud/utils/db/Filter.java @@ -57,7 +57,18 @@ public class Filter { } public Filter(long limit) { - _orderBy = " ORDER BY RAND()"; + this(limit, false); + } + + /** + * Constructor for creating a filter with random ordering + * @param limit the maximum number of results to return + * @param randomize if true, orders results randomly + */ + public Filter(long limit, boolean randomize) { + if (randomize) { + _orderBy = " ORDER BY RAND()" ; + } _limit = limit; } diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java index 87d9f11d20b..dcd863465d1 100644 --- a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java +++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java @@ -347,7 +347,7 @@ public abstract class GenericDaoBase extends Compone @Override @DB() public T lockOneRandomRow(final SearchCriteria sc, final boolean exclusive) { - final Filter filter = new Filter(1); + final Filter filter = new Filter(1, true); final List beans = search(sc, filter, exclusive, true); return beans.isEmpty() ? null : beans.get(0); } @@ -927,7 +927,7 @@ public abstract class GenericDaoBase extends Compone @DB() protected T findOneIncludingRemovedBy(final SearchCriteria sc) { - Filter filter = new Filter(1); + Filter filter = new Filter(1, true); List results = searchIncludingRemoved(sc, filter, null, false); assert results.size() <= 1 : "Didn't the limiting worked?"; return results.size() == 0 ? null : results.get(0); @@ -1335,7 +1335,7 @@ public abstract class GenericDaoBase extends Compone Filter filter = null; final long batchSizeFinal = ObjectUtils.defaultIfNull(batchSize, 0L); if (batchSizeFinal > 0) { - filter = new Filter(null, batchSizeFinal); + filter = new Filter(batchSizeFinal); } int expunged = 0; int currentExpunged = 0; diff --git a/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java b/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java index 079611ab69f..9f040e7a5e3 100644 --- a/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java +++ b/framework/db/src/test/java/com/cloud/utils/db/FilterTest.java @@ -41,4 +41,62 @@ public class FilterTest { Assert.assertTrue(filter.getOrderBy().split(",").length == 3); Assert.assertTrue(filter.getOrderBy().split(",")[2].trim().toLowerCase().equals("test.fld_int asc")); } + + + @Test + public void testFilterWithLimitOnly() { + Filter filter = new Filter(5); + + Assert.assertEquals(Long.valueOf(5), filter.getLimit()); + Assert.assertNull(filter.getOrderBy()); + Assert.assertNull(filter.getOffset()); + } + + @Test + public void testFilterWithLimitAndRandomizeFalse() { + Filter filter = new Filter(10, false); + + Assert.assertEquals(Long.valueOf(10), filter.getLimit()); + Assert.assertNull(filter.getOrderBy()); + Assert.assertNull(filter.getOffset()); + } + + @Test + public void testFilterWithLimitAndRandomizeTrue() { + Filter filter = new Filter(3, true); + + Assert.assertNull(filter.getLimit()); + Assert.assertNotNull(filter.getOrderBy()); + Assert.assertTrue(filter.getOrderBy().contains("ORDER BY RAND()")); + Assert.assertTrue(filter.getOrderBy().contains("LIMIT 3")); + Assert.assertEquals(" ORDER BY RAND() LIMIT 3", filter.getOrderBy()); + } + + @Test + public void testFilterRandomizeWithDifferentLimits() { + Filter filter1 = new Filter(1, true); + Filter filter10 = new Filter(10, true); + Filter filter100 = new Filter(100, true); + + Assert.assertEquals(" ORDER BY RAND() LIMIT 1", filter1.getOrderBy()); + Assert.assertEquals(" ORDER BY RAND() LIMIT 10", filter10.getOrderBy()); + Assert.assertEquals(" ORDER BY RAND() LIMIT 100", filter100.getOrderBy()); + } + + @Test + public void testFilterConstructorBackwardsCompatibility() { + // Test that Filter(long) behaves differently now (no ORDER BY RAND()) + // compared to Filter(long, true) which preserves old behavior + Filter simpleLimitFilter = new Filter(1); + Filter randomFilter = new Filter(1, true); + + // Simple limit filter should just set limit + Assert.assertEquals(Long.valueOf(1), simpleLimitFilter.getLimit()); + Assert.assertNull(simpleLimitFilter.getOrderBy()); + + // Random filter should set orderBy with RAND() + Assert.assertNull(randomFilter.getLimit()); + Assert.assertNotNull(randomFilter.getOrderBy()); + Assert.assertTrue(randomFilter.getOrderBy().contains("RAND()")); + } } diff --git a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java index 308600341c3..ebf514f532f 100644 --- a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java +++ b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java @@ -20,6 +20,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import org.junit.Assert; import org.junit.Before; @@ -263,4 +264,71 @@ public class GenericDaoBaseTest { " INNER JOIN tableA tableA2Alias ON tableC.column3=tableA2Alias.column2 " + " INNER JOIN tableA tableA3Alias ON tableD.column4=tableA3Alias.column3 AND tableD.column5=? ", joinString.toString()); } + + + @Test + public void testLockOneRandomRowUsesRandomFilter() { + // Create a mock DAO to test lockOneRandomRow behavior + GenericDaoBase testDao = Mockito.mock(GenericDaoBase.class); + + // Capture the filter passed to the search method + final Filter[] capturedFilter = new Filter[1]; + + Mockito.when(testDao.lockOneRandomRow(Mockito.any(SearchCriteria.class), Mockito.anyBoolean())) + .thenCallRealMethod(); + + Mockito.when(testDao.search(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), + Mockito.anyBoolean(), Mockito.anyBoolean())) + .thenAnswer(invocation -> { + capturedFilter[0] = invocation.getArgument(1); + return new ArrayList(); + }); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + testDao.lockOneRandomRow(sc, true); + + // Verify that the filter uses random ordering + Assert.assertNotNull(capturedFilter[0]); + Assert.assertNotNull(capturedFilter[0].getOrderBy()); + Assert.assertTrue(capturedFilter[0].getOrderBy().contains("ORDER BY RAND()")); + Assert.assertTrue(capturedFilter[0].getOrderBy().contains("LIMIT 1")); + } + + @Test + public void testLockOneRandomRowReturnsNullOnEmptyResult() { + GenericDaoBase testDao = Mockito.mock(GenericDaoBase.class); + + Mockito.when(testDao.lockOneRandomRow(Mockito.any(SearchCriteria.class), Mockito.anyBoolean())) + .thenCallRealMethod(); + + Mockito.when(testDao.search(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), + Mockito.anyBoolean(), Mockito.anyBoolean())) + .thenReturn(new ArrayList()); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + DbTestVO result = testDao.lockOneRandomRow(sc, true); + + Assert.assertNull(result); + } + + @Test + public void testLockOneRandomRowReturnsFirstElement() { + GenericDaoBase testDao = Mockito.mock(GenericDaoBase.class); + DbTestVO expectedResult = new DbTestVO(); + List resultList = new ArrayList<>(); + resultList.add(expectedResult); + + Mockito.when(testDao.lockOneRandomRow(Mockito.any(SearchCriteria.class), Mockito.anyBoolean())) + .thenCallRealMethod(); + + Mockito.when(testDao.search(Mockito.any(SearchCriteria.class), Mockito.any(Filter.class), + Mockito.anyBoolean(), Mockito.anyBoolean())) + .thenReturn(resultList); + + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + DbTestVO result = testDao.lockOneRandomRow(sc, true); + + Assert.assertNotNull(result); + Assert.assertEquals(expectedResult, result); + } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java index 7949259bc82..816144aa2f1 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/QuotaManagerImpl.java @@ -429,7 +429,7 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { } injectPresetVariablesIntoJsInterpreter(jsInterpreter, presetVariables); - jsInterpreter.injectVariable("lastTariffs", lastAppliedTariffsList.toString()); + jsInterpreter.injectVariable("lastTariffs", lastAppliedTariffsList); String scriptResult = jsInterpreter.executeScript(activationRule).toString(); @@ -459,12 +459,12 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { protected void injectPresetVariablesIntoJsInterpreter(JsInterpreter jsInterpreter, PresetVariables presetVariables) { jsInterpreter.discardCurrentVariables(); - jsInterpreter.injectVariable("account", presetVariables.getAccount().toString()); - jsInterpreter.injectVariable("domain", presetVariables.getDomain().toString()); + jsInterpreter.injectVariable("account", presetVariables.getAccount()); + jsInterpreter.injectVariable("domain", presetVariables.getDomain()); GenericPresetVariable project = presetVariables.getProject(); if (project != null) { - jsInterpreter.injectVariable("project", project.toString()); + jsInterpreter.injectVariable("project", project); } @@ -474,8 +474,8 @@ public class QuotaManagerImpl extends ManagerBase implements QuotaManager { } jsInterpreter.injectVariable("resourceType", presetVariables.getResourceType()); - jsInterpreter.injectVariable("value", presetVariables.getValue().toString()); - jsInterpreter.injectVariable("zone", presetVariables.getZone().toString()); + jsInterpreter.injectVariable("value", presetVariables.getValue()); + jsInterpreter.injectVariable("zone", presetVariables.getZone()); } /** diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java index 2420d577f10..e34c8d3f31d 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Account.java @@ -36,7 +36,6 @@ public class Account extends GenericPresetVariable { public void setRole(Role role) { this.role = role; - fieldNamesToIncludeInToString.add("role"); } public String getCreated() { @@ -45,6 +44,5 @@ public class Account extends GenericPresetVariable { public void setCreated(Date created) { this.created = DateUtil.displayDateInTimezone(TimeZone.getTimeZone("GMT"), created); - fieldNamesToIncludeInToString.add("created"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java index d8457d294ec..e3927d967f7 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOffering.java @@ -29,6 +29,5 @@ public class BackupOffering extends GenericPresetVariable { public void setExternalId(String externalId) { this.externalId = externalId; - fieldNamesToIncludeInToString.add("externalId"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java index 09182711ca8..74cb695010b 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOffering.java @@ -32,7 +32,6 @@ public class ComputeOffering extends GenericPresetVariable { public void setCustomized(boolean customized) { this.customized = customized; - fieldNamesToIncludeInToString.add("customized"); } public boolean offerHa() { @@ -41,7 +40,5 @@ public class ComputeOffering extends GenericPresetVariable { public void setOfferHa(boolean offerHa) { this.offerHa = offerHa; - fieldNamesToIncludeInToString.add("offerHa"); } - } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java index e59f78af8d9..48fee552c5a 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Configuration.java @@ -30,6 +30,5 @@ public class Configuration extends GenericPresetVariable{ public void setForceHa(boolean forceHa) { this.forceHa = forceHa; - fieldNamesToIncludeInToString.add("forceHa"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java index b2f5f69502f..68ba6a331eb 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/DiskOfferingPresetVariables.java @@ -61,7 +61,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setBytesReadRate(Long bytesReadRate) { this.bytesReadRate = bytesReadRate; - fieldNamesToIncludeInToString.add("bytesReadRate"); } public Long getBytesReadBurst() { @@ -70,7 +69,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setBytesReadBurst(Long bytesReadBurst) { this.bytesReadBurst = bytesReadBurst; - fieldNamesToIncludeInToString.add("bytesReadBurst"); } public Long getBytesReadBurstLength() { @@ -79,7 +77,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setBytesReadBurstLength(Long bytesReadBurstLength) { this.bytesReadBurstLength = bytesReadBurstLength; - fieldNamesToIncludeInToString.add("bytesReadBurstLength"); } public Long getBytesWriteRate() { @@ -88,7 +85,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setBytesWriteRate(Long bytesWriteRate) { this.bytesWriteRate = bytesWriteRate; - fieldNamesToIncludeInToString.add("bytesWriteRate"); } public Long getBytesWriteBurst() { @@ -97,7 +93,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setBytesWriteBurst(Long bytesWriteBurst) { this.bytesWriteBurst = bytesWriteBurst; - fieldNamesToIncludeInToString.add("bytesWriteBurst"); } public Long getBytesWriteBurstLength() { @@ -106,7 +101,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setBytesWriteBurstLength(Long bytesWriteBurstLength) { this.bytesWriteBurstLength = bytesWriteBurstLength; - fieldNamesToIncludeInToString.add("bytesWriteBurstLength"); } public Long getIopsReadRate() { @@ -115,7 +109,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setIopsReadRate(Long iopsReadRate) { this.iopsReadRate = iopsReadRate; - fieldNamesToIncludeInToString.add("iopsReadRate"); } public Long getIopsReadBurst() { @@ -124,7 +117,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setIopsReadBurst(Long iopsReadBurst) { this.iopsReadBurst = iopsReadBurst; - fieldNamesToIncludeInToString.add("iopsReadBurst"); } public Long getIopsReadBurstLength() { @@ -133,7 +125,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setIopsReadBurstLength(Long iopsReadBurstLength) { this.iopsReadBurstLength = iopsReadBurstLength; - fieldNamesToIncludeInToString.add("iopsReadBurstLength"); } public Long getIopsWriteRate() { @@ -142,7 +133,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setIopsWriteRate(Long iopsWriteRate) { this.iopsWriteRate = iopsWriteRate; - fieldNamesToIncludeInToString.add("iopsWriteRate"); } public Long getIopsWriteBurst() { @@ -151,7 +141,6 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setIopsWriteBurst(Long iopsWriteBurst) { this.iopsWriteBurst = iopsWriteBurst; - fieldNamesToIncludeInToString.add("iopsWriteBurst"); } public Long getIopsWriteBurstLength() { @@ -160,6 +149,5 @@ public class DiskOfferingPresetVariables extends GenericPresetVariable { public void setIopsWriteBurstLength(Long iopsWriteBurstLength) { this.iopsWriteBurstLength = iopsWriteBurstLength; - fieldNamesToIncludeInToString.add("iopsWriteBurstLength"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java index 6d83da4cd8f..cbdfa3e4bb4 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Domain.java @@ -27,7 +27,6 @@ public class Domain extends GenericPresetVariable { public void setPath(String path) { this.path = path; - fieldNamesToIncludeInToString.add("path"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java index 7073d2760d7..4db099ca479 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariable.java @@ -17,10 +17,8 @@ package org.apache.cloudstack.quota.activationrule.presetvariables; -import java.util.HashSet; -import java.util.Set; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; public class GenericPresetVariable { @PresetVariableDefinition(description = "ID of the resource.") @@ -29,15 +27,12 @@ public class GenericPresetVariable { @PresetVariableDefinition(description = "Name of the resource.") private String name; - protected transient Set fieldNamesToIncludeInToString = new HashSet<>(); - public String getId() { return id; } public void setId(String id) { this.id = id; - fieldNamesToIncludeInToString.add("id"); } public String getName() { @@ -46,15 +41,10 @@ public class GenericPresetVariable { public void setName(String name) { this.name = name; - fieldNamesToIncludeInToString.add("name"); } - /*** - * Converts the preset variable into a valid JSON object that will be injected into the JS interpreter. - * This method should not be overridden or changed. - */ @Override - public final String toString() { - return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, fieldNamesToIncludeInToString.toArray(new String[0])); + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java index 4a0fd2f5a07..6d54a143834 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Host.java @@ -32,7 +32,6 @@ public class Host extends GenericPresetVariable { public void setTags(List tags) { this.tags = tags; - fieldNamesToIncludeInToString.add("tags"); } public Boolean getIsTagARule() { @@ -41,6 +40,5 @@ public class Host extends GenericPresetVariable { public void setIsTagARule(Boolean isTagARule) { this.isTagARule = isTagARule; - fieldNamesToIncludeInToString.add("isTagARule"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java index 2a6ad132f63..a2fca7b80fd 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelper.java @@ -254,7 +254,7 @@ public class PresetVariableHelper { Role role = new Role(); role.setId(roleVo.getUuid()); role.setName(roleVo.getName()); - role.setType(roleVo.getRoleType()); + role.setType(roleVo.getRoleType().toString()); return role; } @@ -538,8 +538,8 @@ public class PresetVariableHelper { value.setDiskOffering(getPresetVariableValueDiskOffering(volumeVo.getDiskOfferingId())); value.setId(volumeVo.getUuid()); value.setName(volumeVo.getName()); - value.setProvisioningType(volumeVo.getProvisioningType()); - value.setVolumeType(volumeVo.getVolumeType()); + value.setVolumeType(volumeVo.getVolumeType().toString()); + value.setProvisioningType(volumeVo.getProvisioningType().toString()); Long poolId = volumeVo.getPoolId(); if (poolId == null) { @@ -594,7 +594,7 @@ public class PresetVariableHelper { storage = new Storage(); storage.setId(storagePoolVo.getUuid()); storage.setName(storagePoolVo.getName()); - storage.setScope(storagePoolVo.getScope()); + storage.setScope(storagePoolVo.getScope().toString()); List storagePoolTagVOList = storagePoolTagsDao.findStoragePoolTags(storageId); List storageTags = new ArrayList<>(); boolean isTagARule = false; @@ -663,7 +663,7 @@ public class PresetVariableHelper { value.setId(snapshotVo.getUuid()); value.setName(snapshotVo.getName()); value.setSize(ByteScaleUtils.bytesToMebibytes(snapshotVo.getSize())); - value.setSnapshotType(Snapshot.Type.values()[snapshotVo.getSnapshotType()]); + value.setSnapshotType(Snapshot.Type.values()[snapshotVo.getSnapshotType()].toString()); value.setStorage(getPresetVariableValueStorage(getSnapshotDataStoreId(snapshotId, usageRecord.getZoneId()), usageType)); value.setTags(getPresetVariableValueResourceTags(snapshotId, ResourceObjectType.Snapshot)); Hypervisor.HypervisorType hypervisorType = snapshotVo.getHypervisorType(); @@ -732,7 +732,7 @@ public class PresetVariableHelper { value.setId(vmSnapshotVo.getUuid()); value.setName(vmSnapshotVo.getName()); value.setTags(getPresetVariableValueResourceTags(vmSnapshotId, ResourceObjectType.VMSnapshot)); - value.setVmSnapshotType(vmSnapshotVo.getType()); + value.setVmSnapshotType(vmSnapshotVo.getType().toString()); VMInstanceVO vmVo = vmInstanceDao.findByIdIncludingRemoved(vmSnapshotVo.getVmId()); if (vmVo != null && vmVo.getHypervisorType() != null) { diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java index 3f953b3a4ff..3c61786cb0a 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Role.java @@ -17,19 +17,16 @@ package org.apache.cloudstack.quota.activationrule.presetvariables; -import org.apache.cloudstack.acl.RoleType; - public class Role extends GenericPresetVariable { @PresetVariableDefinition(description = "Role type of the resource's owner.") - private RoleType type; + private String type; - public RoleType getType() { + public String getType() { return type; } - public void setType(RoleType type) { + public void setType(String type) { this.type = type; - fieldNamesToIncludeInToString.add("type"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java index 9b6cfb31092..8ddae82f383 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Storage.java @@ -19,8 +19,6 @@ package org.apache.cloudstack.quota.activationrule.presetvariables; import java.util.List; -import com.cloud.storage.ScopeType; - public class Storage extends GenericPresetVariable { @PresetVariableDefinition(description = "List of string representing the tags of the storage where the volume is (i.e.: [\"a\", \"b\"]).") private List tags; @@ -29,7 +27,7 @@ public class Storage extends GenericPresetVariable { private Boolean isTagARule; @PresetVariableDefinition(description = "Scope of the storage where the volume is. Values can be: ZONE, CLUSTER or HOST. Applicable only for primary storages.") - private ScopeType scope; + private String scope; public List getTags() { return tags; @@ -37,7 +35,6 @@ public class Storage extends GenericPresetVariable { public void setTags(List tags) { this.tags = tags; - fieldNamesToIncludeInToString.add("tags"); } public Boolean getIsTagARule() { @@ -46,16 +43,14 @@ public class Storage extends GenericPresetVariable { public void setIsTagARule(Boolean isTagARule) { this.isTagARule = isTagARule; - fieldNamesToIncludeInToString.add("isTagARule"); } - public ScopeType getScope() { + public String getScope() { return scope; } - public void setScope(ScopeType scope) { + public void setScope(String scope) { this.scope = scope; - fieldNamesToIncludeInToString.add("scope"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java index 3703820a1a4..9414908b3a2 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Tariff.java @@ -28,6 +28,5 @@ public class Tariff extends GenericPresetVariable { public void setValue(BigDecimal value) { this.value = value; - fieldNamesToIncludeInToString.add("value"); } } diff --git a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java index 77e539db0f3..ac776d13c57 100644 --- a/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java +++ b/framework/quota/src/main/java/org/apache/cloudstack/quota/activationrule/presetvariables/Value.java @@ -20,10 +20,6 @@ package org.apache.cloudstack.quota.activationrule.presetvariables; import java.util.List; import java.util.Map; -import com.cloud.storage.Snapshot; -import com.cloud.storage.Storage.ProvisioningType; -import com.cloud.storage.Volume; -import com.cloud.vm.snapshot.VMSnapshot; import org.apache.cloudstack.quota.constant.QuotaTypes; public class Value extends GenericPresetVariable { @@ -61,13 +57,13 @@ public class Value extends GenericPresetVariable { private Long virtualSize; @PresetVariableDefinition(description = "Provisioning type of the resource. Values can be: thin, sparse or fat.", supportedTypes = {QuotaTypes.VOLUME}) - private ProvisioningType provisioningType; + private String provisioningType; @PresetVariableDefinition(description = "Type of the snapshot. Values can be: MANUAL, RECURRING, HOURLY, DAILY, WEEKLY and MONTHLY.", supportedTypes = {QuotaTypes.SNAPSHOT}) - private Snapshot.Type snapshotType; + private String snapshotType; @PresetVariableDefinition(description = "Type of the VM snapshot. Values can be: Disk or DiskAndMemory.", supportedTypes = {QuotaTypes.VM_SNAPSHOT}) - private VMSnapshot.Type vmSnapshotType; + private String vmSnapshotType; @PresetVariableDefinition(description = "Computing offering of the VM.", supportedTypes = {QuotaTypes.RUNNING_VM, QuotaTypes.ALLOCATED_VM}) private ComputeOffering computeOffering; @@ -96,7 +92,7 @@ public class Value extends GenericPresetVariable { private String volumeFormat; @PresetVariableDefinition(description = "The volume type. Values can be: UNKNOWN, ROOT, SWAP, DATADISK and ISO.", supportedTypes = {QuotaTypes.VOLUME}) - private Volume.Type volumeType; + private String volumeType; private String state; @@ -106,7 +102,6 @@ public class Value extends GenericPresetVariable { public void setHost(Host host) { this.host = host; - fieldNamesToIncludeInToString.add("host"); } public String getOsName() { @@ -115,7 +110,6 @@ public class Value extends GenericPresetVariable { public void setOsName(String osName) { this.osName = osName; - fieldNamesToIncludeInToString.add("osName"); } public List getAccountResources() { @@ -124,7 +118,6 @@ public class Value extends GenericPresetVariable { public void setAccountResources(List accountResources) { this.accountResources = accountResources; - fieldNamesToIncludeInToString.add("accountResources"); } public Map getTags() { @@ -133,7 +126,6 @@ public class Value extends GenericPresetVariable { public void setTags(Map tags) { this.tags = tags; - fieldNamesToIncludeInToString.add("tags"); } public String getTag() { @@ -142,7 +134,6 @@ public class Value extends GenericPresetVariable { public void setTag(String tag) { this.tag = tag; - fieldNamesToIncludeInToString.add("tag"); } public Long getSize() { @@ -151,34 +142,30 @@ public class Value extends GenericPresetVariable { public void setSize(Long size) { this.size = size; - fieldNamesToIncludeInToString.add("size"); } - public ProvisioningType getProvisioningType() { + public String getProvisioningType() { return provisioningType; } - public void setProvisioningType(ProvisioningType provisioningType) { + public void setProvisioningType(String provisioningType) { this.provisioningType = provisioningType; - fieldNamesToIncludeInToString.add("provisioningType"); } - public Snapshot.Type getSnapshotType() { + public String getSnapshotType() { return snapshotType; } - public void setSnapshotType(Snapshot.Type snapshotType) { + public void setSnapshotType(String snapshotType) { this.snapshotType = snapshotType; - fieldNamesToIncludeInToString.add("snapshotType"); } - public VMSnapshot.Type getVmSnapshotType() { + public String getVmSnapshotType() { return vmSnapshotType; } - public void setVmSnapshotType(VMSnapshot.Type vmSnapshotType) { + public void setVmSnapshotType(String vmSnapshotType) { this.vmSnapshotType = vmSnapshotType; - fieldNamesToIncludeInToString.add("vmSnapshotType"); } public ComputeOffering getComputeOffering() { @@ -187,7 +174,6 @@ public class Value extends GenericPresetVariable { public void setComputeOffering(ComputeOffering computeOffering) { this.computeOffering = computeOffering; - fieldNamesToIncludeInToString.add("computeOffering"); } public GenericPresetVariable getTemplate() { @@ -196,7 +182,6 @@ public class Value extends GenericPresetVariable { public void setTemplate(GenericPresetVariable template) { this.template = template; - fieldNamesToIncludeInToString.add("template"); } public DiskOfferingPresetVariables getDiskOffering() { @@ -205,7 +190,6 @@ public class Value extends GenericPresetVariable { public void setDiskOffering(DiskOfferingPresetVariables diskOffering) { this.diskOffering = diskOffering; - fieldNamesToIncludeInToString.add("diskOffering"); } public Storage getStorage() { @@ -214,7 +198,6 @@ public class Value extends GenericPresetVariable { public void setStorage(Storage storage) { this.storage = storage; - fieldNamesToIncludeInToString.add("storage"); } public ComputingResources getComputingResources() { @@ -223,7 +206,6 @@ public class Value extends GenericPresetVariable { public void setComputingResources(ComputingResources computingResources) { this.computingResources = computingResources; - fieldNamesToIncludeInToString.add("computingResources"); } public Long getVirtualSize() { @@ -232,7 +214,6 @@ public class Value extends GenericPresetVariable { public void setVirtualSize(Long virtualSize) { this.virtualSize = virtualSize; - fieldNamesToIncludeInToString.add("virtualSize"); } public BackupOffering getBackupOffering() { @@ -241,12 +222,10 @@ public class Value extends GenericPresetVariable { public void setBackupOffering(BackupOffering backupOffering) { this.backupOffering = backupOffering; - fieldNamesToIncludeInToString.add("backupOffering"); } public void setHypervisorType(String hypervisorType) { this.hypervisorType = hypervisorType; - fieldNamesToIncludeInToString.add("hypervisorType"); } public String getHypervisorType() { @@ -255,20 +234,18 @@ public class Value extends GenericPresetVariable { public void setVolumeFormat(String volumeFormat) { this.volumeFormat = volumeFormat; - fieldNamesToIncludeInToString.add("volumeFormat"); } public String getVolumeFormat() { return volumeFormat; } - public Volume.Type getVolumeType() { + public String getVolumeType() { return volumeType; } - public void setVolumeType(Volume.Type volumeType) { + public void setVolumeType(String volumeType) { this.volumeType = volumeType; - fieldNamesToIncludeInToString.add("volumeType"); } public String getState() { @@ -277,6 +254,5 @@ public class Value extends GenericPresetVariable { public void setState(String state) { this.state = state; - fieldNamesToIncludeInToString.add("state"); } } diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java index 3b2ea54e86d..a33faa054de 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/QuotaManagerImplTest.java @@ -267,12 +267,12 @@ public class QuotaManagerImplTest { quotaManagerImplSpy.injectPresetVariablesIntoJsInterpreter(jsInterpreterMock, presetVariablesMock); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock, Mockito.never()).injectVariable(Mockito.eq("project"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.anyString()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.any()); + Mockito.verify(jsInterpreterMock, Mockito.never()).injectVariable(Mockito.eq("project"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.any()); } @Test @@ -288,12 +288,12 @@ public class QuotaManagerImplTest { quotaManagerImplSpy.injectPresetVariablesIntoJsInterpreter(jsInterpreterMock, presetVariablesMock); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("project"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.anyString()); - Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.anyString()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("account"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("domain"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("project"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("resourceType"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("value"), Mockito.any()); + Mockito.verify(jsInterpreterMock).injectVariable(Mockito.eq("zone"), Mockito.any()); } @Test diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/AccountTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/AccountTest.java deleted file mode 100644 index 1e62235e71c..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/AccountTest.java +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class AccountTest { - - @Test - public void setRoleTestAddFieldRoleToCollection() { - Account variable = new Account(); - variable.setRole(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("role")); - } -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOfferingTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOfferingTest.java deleted file mode 100644 index 57c18f936f2..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/BackupOfferingTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class BackupOfferingTest { - @Test - public void setExternalIdTestAddFieldExternalIdToCollection() { - BackupOffering backupOffering = new BackupOffering(); - backupOffering.setExternalId("any-external-id"); - Assert.assertTrue(backupOffering.fieldNamesToIncludeInToString.contains("externalId")); - } - -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOfferingTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOfferingTest.java deleted file mode 100644 index 5fbcbe76476..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputeOfferingTest.java +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class ComputeOfferingTest { - - @Test - public void setCustomizedTestAddFieldCustomizedToCollection() { - ComputeOffering variable = new ComputeOffering(); - variable.setCustomized(true); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("customized")); - } - -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputingResourcesTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputingResourcesTest.java deleted file mode 100644 index f7978f16e04..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ComputingResourcesTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class ComputingResourcesTest { - - @Test - public void toStringTestReturnAJson() { - ComputingResources variable = new ComputingResources(); - - String expected = ToStringBuilder.reflectionToString(variable, ToStringStyle.JSON_STYLE); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/DomainTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/DomainTest.java deleted file mode 100644 index f245b4637e6..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/DomainTest.java +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class DomainTest { - - @Test - public void setPathTestAddFieldPathToCollection() { - Domain variable = new Domain(); - variable.setPath("test path"); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("path")); - } - -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariableTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariableTest.java deleted file mode 100644 index 4f594ee5d00..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/GenericPresetVariableTest.java +++ /dev/null @@ -1,73 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class GenericPresetVariableTest { - - @Test - public void setIdTestAddFieldIdToCollection() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setId("test"); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("id")); - } - - @Test - public void setNameTestAddFieldNameToCollection() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setName("test"); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("name")); - } - - @Test - public void toStringTestSetAllFieldsAndReturnAJson() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setId("test id"); - variable.setName("test name"); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "id", "name"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - - @Test - public void toStringTestSetSomeFieldsAndReturnAJson() { - GenericPresetVariable variable = new GenericPresetVariable(); - variable.setId("test id"); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "id"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - - variable = new GenericPresetVariable(); - variable.setName("test name"); - - expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name"); - result = variable.toString(); - - Assert.assertEquals(expected, result); - } -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/HostTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/HostTest.java deleted file mode 100644 index 87aae7788e2..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/HostTest.java +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class HostTest { - - @Test - public void setTagsTestAddFieldTagsToCollection() { - Host variable = new Host(); - variable.setTags(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tags")); - } -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java index c692cb7c1e7..85397503587 100644 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java +++ b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/PresetVariableHelperTest.java @@ -215,14 +215,14 @@ public class PresetVariableHelperTest { value.setTags(Collections.singletonMap("tag1", "value1")); value.setTemplate(getGenericPresetVariableForTests()); value.setDiskOffering(getDiskOfferingForTests()); - value.setProvisioningType(ProvisioningType.THIN); + value.setProvisioningType(ProvisioningType.THIN.toString()); value.setStorage(getStorageForTests()); value.setSize(ByteScaleUtils.GiB); - value.setSnapshotType(Snapshot.Type.HOURLY); + value.setSnapshotType(Snapshot.Type.HOURLY.toString()); value.setTag("tag_test"); - value.setVmSnapshotType(VMSnapshot.Type.Disk); + value.setVmSnapshotType(VMSnapshot.Type.Disk.toString()); value.setComputingResources(getComputingResourcesForTests()); - value.setVolumeType(Volume.Type.DATADISK); + value.setVolumeType(Volume.Type.DATADISK.toString()); return value; } @@ -272,7 +272,7 @@ public class PresetVariableHelperTest { storage.setId("storage_id"); storage.setName("storage_name"); storage.setTags(Arrays.asList("tag1", "tag2")); - storage.setScope(ScopeType.ZONE); + storage.setScope(ScopeType.ZONE.toString()); return storage; } @@ -298,9 +298,9 @@ public class PresetVariableHelperTest { private List getVmDetailsForTests() { List details = new LinkedList<>(); - details.add(new VMInstanceDetailVO(1l, "test_with_value", "277", false)); - details.add(new VMInstanceDetailVO(1l, "test_with_invalid_value", "invalid", false)); - details.add(new VMInstanceDetailVO(1l, "test_with_null", null, false)); + details.add(new VMInstanceDetailVO(1L, "test_with_value", "277", false)); + details.add(new VMInstanceDetailVO(1L, "test_with_invalid_value", "invalid", false)); + details.add(new VMInstanceDetailVO(1L, "test_with_null", null, false)); return details; } @@ -309,13 +309,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getName(), result.getName()); } - private void validateFieldNamesToIncludeInToString(List expected, GenericPresetVariable resultObject) { - List result = new ArrayList<>(resultObject.fieldNamesToIncludeInToString); - Collections.sort(expected); - Collections.sort(result); - Assert.assertEquals(expected, result); - } - private BackupOffering getBackupOfferingForTests() { BackupOffering backupOffering = new BackupOffering(); backupOffering.setId("backup_offering_id"); @@ -415,7 +408,6 @@ public class PresetVariableHelperTest { Assert.assertNotNull(result.getProject()); assertPresetVariableIdAndName(account, result.getProject()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result.getProject()); } @Test @@ -430,10 +422,9 @@ public class PresetVariableHelperTest { Mockito.doReturn(account.getName()).when(accountVoMock).getName(); Mockito.doReturn(account.getCreated()).when(accountVoMock).getCreated(); - Account result = presetVariableHelperSpy.getPresetVariableAccount(1l); + Account result = presetVariableHelperSpy.getPresetVariableAccount(1L); assertPresetVariableIdAndName(account, result); - validateFieldNamesToIncludeInToString(Arrays.asList("created", "id", "name"), result); } @Test @@ -463,18 +454,16 @@ public class PresetVariableHelperTest { Role role = new Role(); role.setId("test_id"); role.setName("test_name"); - role.setType(roleType); + role.setType(roleType.toString()); Mockito.doReturn(role.getId()).when(roleVoMock).getUuid(); Mockito.doReturn(role.getName()).when(roleVoMock).getName(); - Mockito.doReturn(role.getType()).when(roleVoMock).getRoleType(); + Mockito.doReturn(RoleType.fromString(role.getType())).when(roleVoMock).getRoleType(); - Role result = presetVariableHelperSpy.getPresetVariableRole(1l); + Role result = presetVariableHelperSpy.getPresetVariableRole(1L); assertPresetVariableIdAndName(role, result); Assert.assertEquals(role.getType(), result.getType()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "type"), result); }); } @@ -489,12 +478,10 @@ public class PresetVariableHelperTest { Mockito.doReturn(domain.getName()).when(domainVoMock).getName(); Mockito.doReturn(domain.getPath()).when(domainVoMock).getPath(); - Domain result = presetVariableHelperSpy.getPresetVariableDomain(1l); + Domain result = presetVariableHelperSpy.getPresetVariableDomain(1L); assertPresetVariableIdAndName(domain, result); Assert.assertEquals(domain.getPath(), result.getPath()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "path"), result); } @Test @@ -507,10 +494,9 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getId()).when(dataCenterVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(dataCenterVoMock).getName(); - GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableZone(1l); + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableZone(1L); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result); } @Test @@ -531,7 +517,6 @@ public class PresetVariableHelperTest { Value result = presetVariableHelperSpy.getPresetVariableValue(usageVoMock); Assert.assertEquals(resources, result.getAccountResources()); - validateFieldNamesToIncludeInToString(Arrays.asList("accountResources"), result); } @Test @@ -541,7 +526,7 @@ public class PresetVariableHelperTest { Mockito.doReturn(new Date()).when(usageVoMock).getEndDate(); Mockito.doReturn(expected).when(usageDaoMock).listAccountResourcesInThePeriod(Mockito.anyLong(), Mockito.anyInt(), Mockito.any(Date.class), Mockito.any(Date.class)); - List result = presetVariableHelperSpy.getPresetVariableAccountResources(usageVoMock, 1l, 0); + List result = presetVariableHelperSpy.getPresetVariableAccountResources(usageVoMock, 1L, 0); for (int i = 0; i < expected.size(); i++) { Assert.assertEquals(expected.get(i).first(), result.get(i).getZoneId()); @@ -590,8 +575,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expected.getTemplate(), result.getTemplate()); Assert.assertEquals(hypervisorType.name(), result.getHypervisorType()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "osName", "tags", "template", "hypervisorType"), result); }); } @@ -614,7 +597,6 @@ public class PresetVariableHelperTest { public void setPresetVariableHostInValueIfUsageTypeIsRunningVmTestQuotaTypeIsRunningVmSetHost() { Value result = new Value(); Host expectedHost = getHostForTests(); - List expectedHostTags = getHostTagsForTests(); Mockito.doReturn(expectedHost).when(presetVariableHelperSpy).getPresetVariableValueHost(Mockito.anyLong()); presetVariableHelperSpy.setPresetVariableHostInValueIfUsageTypeIsRunningVm(result, UsageTypes.RUNNING_VM, vmInstanceVoMock); @@ -623,7 +605,6 @@ public class PresetVariableHelperTest { assertPresetVariableIdAndName(expectedHost, result.getHost()); Assert.assertEquals(expectedHost.getTags(), result.getHost().getTags()); - validateFieldNamesToIncludeInToString(Arrays.asList("host"), result); } @Test @@ -638,11 +619,10 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getName()).when(hostVoMock).getName(); Mockito.doReturn(hostTagVOListMock).when(hostTagsDaoMock).getHostTags(Mockito.anyLong()); - Host result = presetVariableHelperSpy.getPresetVariableValueHost(1l); + Host result = presetVariableHelperSpy.getPresetVariableValueHost(1L); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getTags(), result.getTags()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "tags"), result); } @Test @@ -657,12 +637,11 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getName()).when(hostVoMock).getName(); Mockito.doReturn(hostTagVOListMock).when(hostTagsDaoMock).getHostTags(Mockito.anyLong()); - Host result = presetVariableHelperSpy.getPresetVariableValueHost(1l); + Host result = presetVariableHelperSpy.getPresetVariableValueHost(1L); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(new ArrayList<>(), result.getTags()); Assert.assertTrue(result.getIsTagARule()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "tags"), result); } @Test @@ -674,7 +653,7 @@ public class PresetVariableHelperTest { String expected = "os_display_name"; Mockito.doReturn(expected).when(guestOsVoMock).getDisplayName(); - String result = presetVariableHelperSpy.getPresetVariableValueOsName(1l); + String result = presetVariableHelperSpy.getPresetVariableValueOsName(1L); Assert.assertEquals(expected, result); } @@ -692,7 +671,6 @@ public class PresetVariableHelperTest { assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.isCustomized(), result.isCustomized()); Assert.assertEquals(expected.offerHa(), result.offerHa()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "customized", "offerHa"), result); } @Test @@ -706,10 +684,8 @@ public class PresetVariableHelperTest { assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.isCustomized(), result.isCustomized()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "customized"), result); } - @Test public void getPresetVariableValueTemplateTestSetValuesAndReturnObject() { VMTemplateVO vmTemplateVoMock = Mockito.mock(VMTemplateVO.class); @@ -720,10 +696,9 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getId()).when(vmTemplateVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(vmTemplateVoMock).getName(); - GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueTemplate(1l); + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueTemplate(1L); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result); } @Test @@ -735,7 +710,7 @@ public class PresetVariableHelperTest { Mockito.doReturn(listExpected).when(resourceTagDaoMock).listBy(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Arrays.asList(ResourceObjectType.values()).forEach(type -> { - Map result = presetVariableHelperSpy.getPresetVariableValueResourceTags(1l, type); + Map result = presetVariableHelperSpy.getPresetVariableValueResourceTags(1L, type); for (ResourceTag expected: listExpected) { Assert.assertEquals(expected.getValue(), result.get(expected.getKey())); @@ -760,15 +735,15 @@ public class PresetVariableHelperTest { VolumeVO volumeVoMock = Mockito.mock(VolumeVO.class); Mockito.doReturn(volumeVoMock).when(volumeDaoMock).findByIdIncludingRemoved(Mockito.anyLong()); - Mockito.doReturn(1l).when(volumeVoMock).getPoolId(); + Mockito.doReturn(1L).when(volumeVoMock).getPoolId(); mockMethodValidateIfObjectIsNull(); Mockito.doReturn(expected.getId()).when(volumeVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(volumeVoMock).getName(); Mockito.doReturn(expected.getDiskOffering()).when(presetVariableHelperSpy).getPresetVariableValueDiskOffering(Mockito.anyLong()); - Mockito.doReturn(expected.getProvisioningType()).when(volumeVoMock).getProvisioningType(); - Mockito.doReturn(expected.getVolumeType()).when(volumeVoMock).getVolumeType(); + Mockito.doReturn(ProvisioningType.getProvisioningType(expected.getProvisioningType())).when(volumeVoMock).getProvisioningType(); + Mockito.doReturn(Volume.Type.valueOf(expected.getVolumeType())).when(volumeVoMock).getVolumeType(); Mockito.doReturn(expected.getStorage()).when(presetVariableHelperSpy).getPresetVariableValueStorage(Mockito.anyLong(), Mockito.anyInt()); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Mockito.doReturn(expected.getSize()).when(volumeVoMock).getSize(); @@ -789,8 +764,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); Assert.assertEquals(imageFormat.name(), result.getVolumeFormat()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "diskOffering", "provisioningType", "volumeType", "storage", "tags", "size", "volumeFormat"), result); } Mockito.verify(presetVariableHelperSpy, Mockito.times(ImageFormat.values().length)).getPresetVariableValueResourceTags(Mockito.anyLong(), @@ -811,8 +784,8 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getId()).when(volumeVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(volumeVoMock).getName(); Mockito.doReturn(expected.getDiskOffering()).when(presetVariableHelperSpy).getPresetVariableValueDiskOffering(Mockito.anyLong()); - Mockito.doReturn(expected.getProvisioningType()).when(volumeVoMock).getProvisioningType(); - Mockito.doReturn(expected.getVolumeType()).when(volumeVoMock).getVolumeType(); + Mockito.doReturn(Volume.Type.valueOf(expected.getVolumeType())).when(volumeVoMock).getVolumeType(); + Mockito.doReturn(ProvisioningType.getProvisioningType(expected.getProvisioningType())).when(volumeVoMock).getProvisioningType(); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Mockito.doReturn(expected.getSize()).when(volumeVoMock).getSize(); Mockito.doReturn(imageFormat).when(volumeVoMock).getFormat(); @@ -832,8 +805,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); Assert.assertEquals(imageFormat.name(), result.getVolumeFormat()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "diskOffering", "provisioningType", "volumeType", "tags", "size", "volumeFormat"), result); } Mockito.verify(presetVariableHelperSpy, Mockito.times(ImageFormat.values().length)).getPresetVariableValueResourceTags(Mockito.anyLong(), @@ -850,11 +821,9 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getId()).when(diskOfferingVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(diskOfferingVoMock).getName(); - GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueDiskOffering(1l); + GenericPresetVariable result = presetVariableHelperSpy.getPresetVariableValueDiskOffering(1L); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("bytesReadBurst", "bytesReadBurstLength", "bytesReadRate", "bytesWriteBurst", "bytesWriteBurstLength", "bytesWriteRate", - "id", "iopsReadBurst", "iopsReadBurstLength", "iopsReadRate", "iopsWriteBurst", "iopsWriteBurstLength", "iopsWriteRate", "name"), result); } @Test @@ -862,7 +831,7 @@ public class PresetVariableHelperTest { Storage expected = getStorageForTests(); Mockito.doReturn(expected).when(presetVariableHelperSpy).getSecondaryStorageForSnapshot(Mockito.anyLong(), Mockito.anyInt()); - Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1l, 2); + Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1L, 2); Assert.assertEquals(expected, result); Mockito.verify(primaryStorageDaoMock, Mockito.never()).findByIdIncludingRemoved(Mockito.anyLong()); @@ -880,16 +849,14 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getId()).when(storagePoolVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(storagePoolVoMock).getName(); - Mockito.doReturn(expected.getScope()).when(storagePoolVoMock).getScope(); + Mockito.doReturn(ScopeType.validateAndGetScopeType(expected.getScope())).when(storagePoolVoMock).getScope(); Mockito.doReturn(storageTagVOListMock).when(storagePoolTagsDaoMock).findStoragePoolTags(Mockito.anyLong()); - Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1l, 2); + Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1L, 2); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getScope(), result.getScope()); Assert.assertEquals(expected.getTags(), result.getTags()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "scope", "tags"), result); } @Test @@ -904,24 +871,22 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getId()).when(storagePoolVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(storagePoolVoMock).getName(); - Mockito.doReturn(expected.getScope()).when(storagePoolVoMock).getScope(); + Mockito.doReturn(ScopeType.validateAndGetScopeType(expected.getScope())).when(storagePoolVoMock).getScope(); Mockito.doReturn(storageTagVOListMock).when(storagePoolTagsDaoMock).findStoragePoolTags(Mockito.anyLong()); - Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1l, 2); + Storage result = presetVariableHelperSpy.getPresetVariableValueStorage(1L, 2); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getScope(), result.getScope()); Assert.assertEquals(new ArrayList<>(), result.getTags()); Assert.assertTrue(result.getIsTagARule()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "isTagARule", "name", "scope", "tags"), result); } @Test public void getSecondaryStorageForSnapshotTestAllTypesAndDoNotBackupSnapshotReturnNull() { presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = false; getQuotaTypesForTests().forEach(type -> { - Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1l, type.getKey()); + Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1L, type.getKey()); Assert.assertNull(result); }); } @@ -930,7 +895,7 @@ public class PresetVariableHelperTest { public void getSecondaryStorageForSnapshotTestAllTypesExceptSnapshotAndBackupSnapshotReturnNull() { presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = true; getQuotaTypesForTests(UsageTypes.SNAPSHOT).forEach(type -> { - Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1l, type.getKey()); + Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1L, type.getKey()); Assert.assertNull(result); }); } @@ -947,10 +912,9 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getName()).when(imageStoreVoMock).getName(); presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = true; - Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1l, UsageTypes.SNAPSHOT); + Storage result = presetVariableHelperSpy.getSecondaryStorageForSnapshot(1L, UsageTypes.SNAPSHOT); assertPresetVariableIdAndName(expected, result); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name"), result); } @Test @@ -991,8 +955,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getOsName(), result.getOsName()); Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "osName", "tags", "size"), result); }); Mockito.verify(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.eq(ResourceObjectType.Template)); @@ -1025,7 +987,7 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getName()).when(snapshotVoMock).getName(); Mockito.doReturn(expected.getSize()).when(snapshotVoMock).getSize(); Mockito.doReturn((short) 3).when(snapshotVoMock).getSnapshotType(); - Mockito.doReturn(1l).when(presetVariableHelperSpy).getSnapshotDataStoreId(Mockito.anyLong(), Mockito.anyLong()); + Mockito.doReturn(1L).when(presetVariableHelperSpy).getSnapshotDataStoreId(Mockito.anyLong(), Mockito.anyLong()); Mockito.doReturn(expected.getStorage()).when(presetVariableHelperSpy).getPresetVariableValueStorage(Mockito.anyLong(), Mockito.anyInt()); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); Mockito.doReturn(hypervisorType).when(snapshotVoMock).getHypervisorType(); @@ -1043,8 +1005,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expectedSize, result.getSize()); Assert.assertEquals(hypervisorType.name(), result.getHypervisorType()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "snapshotType", "storage", "tags", "size", "hypervisorType"), result); } Mockito.verify(presetVariableHelperSpy, Mockito.times(Hypervisor.HypervisorType.values().length)).getPresetVariableValueResourceTags(Mockito.anyLong(), @@ -1056,12 +1016,12 @@ public class PresetVariableHelperTest { public void getSnapshotDataStoreIdTestDoNotBackupSnapshotToSecondaryRetrievePrimaryStorage() { SnapshotDataStoreVO snapshotDataStoreVoMock = Mockito.mock(SnapshotDataStoreVO.class); - Long expected = 1l; + Long expected = 1L; Mockito.doReturn(snapshotDataStoreVoMock).when(snapshotDataStoreDaoMock).findOneBySnapshotAndDatastoreRole(Mockito.anyLong(), Mockito.any(DataStoreRole.class)); Mockito.doReturn(expected).when(snapshotDataStoreVoMock).getDataStoreId(); presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = false; - Long result = presetVariableHelperSpy.getSnapshotDataStoreId(1l, 1l); + Long result = presetVariableHelperSpy.getSnapshotDataStoreId(1L, 1L); Assert.assertEquals(expected, result); @@ -1078,7 +1038,7 @@ public class PresetVariableHelperTest { public void getSnapshotDataStoreIdTestBackupSnapshotToSecondaryRetrieveSecondaryStorage() { SnapshotDataStoreVO snapshotDataStoreVoMock = Mockito.mock(SnapshotDataStoreVO.class); - Long expected = 2l; + Long expected = 2L; ImageStoreVO imageStore = Mockito.mock(ImageStoreVO.class); Mockito.when(imageStoreDaoMock.findById(Mockito.anyLong())).thenReturn(imageStore); Mockito.when(imageStore.getDataCenterId()).thenReturn(1L); @@ -1086,7 +1046,7 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected).when(snapshotDataStoreVoMock).getDataStoreId(); presetVariableHelperSpy.backupSnapshotAfterTakingSnapshot = true; - Long result = presetVariableHelperSpy.getSnapshotDataStoreId(2l, 1L); + Long result = presetVariableHelperSpy.getSnapshotDataStoreId(2L, 1L); Assert.assertEquals(expected, result); @@ -1129,8 +1089,6 @@ public class PresetVariableHelperTest { assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getTag(), result.getTag()); - - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "tag"), result); } @Test @@ -1155,7 +1113,7 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getId()).when(vmSnapshotVoMock).getUuid(); Mockito.doReturn(expected.getName()).when(vmSnapshotVoMock).getName(); Mockito.doReturn(expected.getTags()).when(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.any(ResourceObjectType.class)); - Mockito.doReturn(expected.getVmSnapshotType()).when(vmSnapshotVoMock).getType(); + Mockito.doReturn(VMSnapshot.Type.valueOf(expected.getVmSnapshotType())).when(vmSnapshotVoMock).getType(); Mockito.doReturn(UsageTypes.VM_SNAPSHOT).when(usageVoMock).getUsageType(); @@ -1166,8 +1124,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getTags(), result.getTags()); Assert.assertEquals(expected.getVmSnapshotType(), result.getVmSnapshotType()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "tags", "vmSnapshotType"), result); - Mockito.verify(presetVariableHelperSpy).getPresetVariableValueResourceTags(Mockito.anyLong(), Mockito.eq(ResourceObjectType.VMSnapshot)); } @@ -1200,9 +1156,6 @@ public class PresetVariableHelperTest { if (typeInt == UsageTypes.RUNNING_VM) { Assert.assertEquals(expected.getComputingResources(), result.getComputingResources()); - validateFieldNamesToIncludeInToString(Arrays.asList("computeOffering", "computingResources"), result); - } else { - validateFieldNamesToIncludeInToString(Arrays.asList("computeOffering"), result); } }); } @@ -1228,7 +1181,7 @@ public class PresetVariableHelperTest { @Test public void getDetailByNameTestReturnsValue() { - int expected = Integer.valueOf(getVmDetailsForTests().get(0).getValue()); + int expected = Integer.parseInt(getVmDetailsForTests().get(0).getValue()); int result = presetVariableHelperSpy.getDetailByName(getVmDetailsForTests(), "test_with_value", expected); Assert.assertEquals(expected, result); } @@ -1295,8 +1248,6 @@ public class PresetVariableHelperTest { Assert.assertEquals(expected.getVirtualSize(), result.getVirtualSize()); Assert.assertEquals(expected.getBackupOffering(), result.getBackupOffering()); - validateFieldNamesToIncludeInToString(Arrays.asList("size", "virtualSize", "backupOffering"), result); - Mockito.verify(presetVariableHelperSpy).getPresetVariableValueBackupOffering(Mockito.anyLong()); } @@ -1311,11 +1262,10 @@ public class PresetVariableHelperTest { Mockito.doReturn(expected.getName()).when(backupOfferingVoMock).getName(); Mockito.doReturn(expected.getExternalId()).when(backupOfferingVoMock).getExternalId(); - BackupOffering result = presetVariableHelperSpy.getPresetVariableValueBackupOffering(1l); + BackupOffering result = presetVariableHelperSpy.getPresetVariableValueBackupOffering(1L); assertPresetVariableIdAndName(expected, result); Assert.assertEquals(expected.getExternalId(), result.getExternalId()); - validateFieldNamesToIncludeInToString(Arrays.asList("id", "name", "externalId"), result); } @Test diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/RoleTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/RoleTest.java deleted file mode 100644 index 88265ee4e55..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/RoleTest.java +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class RoleTest { - - @Test - public void setTagsTestAddFieldTagsToCollection() { - Role variable = new Role(); - variable.setType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("type")); - } -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/StorageTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/StorageTest.java deleted file mode 100644 index f36d5c49581..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/StorageTest.java +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class StorageTest { - - @Test - public void setTagsTestAddFieldTagsToCollection() { - Storage variable = new Storage(); - variable.setTags(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tags")); - } - - @Test - public void setScopeTestAddFieldScopeToCollection() { - Storage variable = new Storage(); - variable.setScope(null);; - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("scope")); - } -} diff --git a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ValueTest.java b/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ValueTest.java deleted file mode 100644 index bad33da8836..00000000000 --- a/framework/quota/src/test/java/org/apache/cloudstack/quota/activationrule/presetvariables/ValueTest.java +++ /dev/null @@ -1,175 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.quota.activationrule.presetvariables; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class ValueTest { - - @Test - public void setIdTestAddFieldIdToCollection() { - Value variable = new Value(); - variable.setId(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("id")); - } - - @Test - public void setNameTestAddFieldNameToCollection() { - Value variable = new Value(); - variable.setName(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("name")); - } - - @Test - public void setHostTestAddFieldHostToCollection() { - Value variable = new Value(); - variable.setHost(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("host")); - } - - @Test - public void setOsNameTestAddFieldOsNameToCollection() { - Value variable = new Value(); - variable.setOsName(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("osName")); - } - - @Test - public void setAccountResourcesTestAddFieldAccountResourcesToCollection() { - Value variable = new Value(); - variable.setAccountResources(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("accountResources")); - } - - @Test - public void setTagsTestAddFieldTagsToCollection() { - Value variable = new Value(); - variable.setTags(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tags")); - } - - @Test - public void setTagTestAddFieldTagToCollection() { - Value variable = new Value(); - variable.setTag(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("tag")); - } - - @Test - public void setSizeTestAddFieldSizeToCollection() { - Value variable = new Value(); - variable.setSize(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("size")); - } - - @Test - public void setProvisioningTypeTestAddFieldProvisioningTypeToCollection() { - Value variable = new Value(); - variable.setProvisioningType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("provisioningType")); - } - - @Test - public void setSnapshotTypeTestAddFieldSnapshotTypeToCollection() { - Value variable = new Value(); - variable.setSnapshotType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("snapshotType")); - } - - @Test - public void setVmSnapshotTypeTestAddFieldVmSnapshotTypeToCollection() { - Value variable = new Value(); - variable.setVmSnapshotType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("vmSnapshotType")); - } - - @Test - public void setComputeOfferingTestAddFieldComputeOfferingToCollection() { - Value variable = new Value(); - variable.setComputeOffering(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("computeOffering")); - } - - @Test - public void setTemplateTestAddFieldTemplateToCollection() { - Value variable = new Value(); - variable.setTemplate(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("template")); - } - - @Test - public void setDiskOfferingTestAddFieldDiskOfferingToCollection() { - Value variable = new Value(); - variable.setDiskOffering(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("diskOffering")); - } - - @Test - public void setStorageTestAddFieldStorageToCollection() { - Value variable = new Value(); - variable.setStorage(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("storage")); - } - - @Test - public void setComputingResourcesTestAddFieldComputingResourcesToCollection() { - Value variable = new Value(); - variable.setComputingResources(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("computingResources")); - } - - @Test - public void setVirtualSizeTestAddFieldVirtualSizeToCollection() { - Value variable = new Value(); - variable.setVirtualSize(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("virtualSize")); - } - - @Test - public void setBackupOfferingTestAddFieldBackupOfferingToCollection() { - Value variable = new Value(); - variable.setBackupOffering(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("backupOffering")); - } - - @Test - public void setHypervisorTypeTestAddFieldHypervisorTypeToCollection() { - Value variable = new Value(); - variable.setHypervisorType(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("hypervisorType")); - } - - @Test - public void setVolumeFormatTestAddFieldVolumeFormatToCollection() { - Value variable = new Value(); - variable.setVolumeFormat(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("volumeFormat")); - } - - @Test - public void setStateTestAddFieldStateToCollection() { - Value variable = new Value(); - variable.setState(null); - Assert.assertTrue(variable.fieldNamesToIncludeInToString.contains("state")); - } - -} diff --git a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java index d6d235162ef..4493f1e9074 100644 --- a/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java +++ b/plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java @@ -44,8 +44,10 @@ import org.apache.cloudstack.api.response.ApiDiscoveryResponse; import org.apache.cloudstack.api.response.ApiParameterResponse; import org.apache.cloudstack.api.response.ApiResponseResponse; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.resourcedetail.UserDetailVO; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.reflections.ReflectionUtils; import org.springframework.stereotype.Component; @@ -56,6 +58,7 @@ import com.cloud.serializer.Param; import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.user.User; +import com.cloud.user.UserAccount; import com.cloud.utils.ReflectUtil; import com.cloud.utils.component.ComponentLifecycleBase; import com.cloud.utils.component.PluggableService; @@ -67,6 +70,7 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A List _apiAccessCheckers = null; List _services = null; protected static Map s_apiNameDiscoveryResponseMap = null; + public static final List APIS_ALLOWED_FOR_PASSWORD_CHANGE = Arrays.asList("login", "logout", "updateUser", "listApis"); @Inject AccountService accountService; @@ -287,12 +291,20 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); } - if (role.getRoleType() == RoleType.Admin && role.getId() == RoleType.Admin.getId()) { - logger.info(String.format("Account [%s] is Root Admin, all APIs are allowed.", - ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); + // Limit APIs on first login requiring password change + UserAccount userAccount = accountService.getUserAccountById(user.getId()); + Map userAccDetails = userAccount.getDetails(); + if (MapUtils.isNotEmpty(userAccDetails) && !userAccDetails.containsKey(UserDetailVO.OauthLogin) && + "true".equalsIgnoreCase(userAccDetails.get(UserDetailVO.PasswordChangeRequired))) { + apisAllowed = APIS_ALLOWED_FOR_PASSWORD_CHANGE; } else { - for (APIChecker apiChecker : _apiAccessCheckers) { - apisAllowed = apiChecker.getApisAllowedToUser(role, user, apisAllowed); + if (role.getRoleType() == RoleType.Admin && role.getId() == RoleType.Admin.getId()) { + logger.info(String.format("Account [%s] is Root Admin, all APIs are allowed.", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); + } else { + for (APIChecker apiChecker : _apiAccessCheckers) { + apisAllowed = apiChecker.getApisAllowedToUser(role, user, apisAllowed); + } } } diff --git a/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java b/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java index eea78d8abb9..d33774cad03 100644 --- a/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java +++ b/plugins/api/discovery/src/test/java/org/apache/cloudstack/discovery/ApiDiscoveryTest.java @@ -21,6 +21,8 @@ import com.cloud.user.Account; import com.cloud.user.AccountService; import com.cloud.user.AccountVO; import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.UserAccountVO; import com.cloud.user.UserVO; import org.apache.cloudstack.acl.APIChecker; @@ -29,6 +31,8 @@ import org.apache.cloudstack.acl.RoleService; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleVO; import org.apache.cloudstack.api.response.ApiDiscoveryResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,11 +43,15 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordChangeRequired; +import static org.apache.cloudstack.resourcedetail.UserDetailVO.Setup2FADetail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; @RunWith(MockitoJUnitRunner.class) public class ApiDiscoveryTest { @@ -66,12 +74,17 @@ public class ApiDiscoveryTest { @InjectMocks ApiDiscoveryServiceImpl discoveryServiceSpy; + @Mock + UserAccount mockUserAccount; + @Before public void setup() { discoveryServiceSpy.s_apiNameDiscoveryResponseMap = apiNameDiscoveryResponseMapMock; discoveryServiceSpy._apiAccessCheckers = apiAccessCheckersMock; Mockito.when(discoveryServiceSpy._apiAccessCheckers.iterator()).thenReturn(Arrays.asList(apiCheckerMock).iterator()); + Mockito.when(mockUserAccount.getDetails()).thenReturn(null); + Mockito.when(accountServiceMock.getUserAccountById(anyLong())).thenReturn(mockUserAccount); } private User getTestUser() { @@ -131,4 +144,29 @@ public class ApiDiscoveryTest { Mockito.verify(apiCheckerMock, Mockito.times(1)).getApisAllowedToUser(any(Role.class), any(User.class), anyList()); } + + @Test + public void listApisForUserWithoutEnforcedPwdChange() throws PermissionDeniedException { + RoleVO userRoleVO = new RoleVO(4L, "name", RoleType.User, "description"); + Map userDetails = new HashMap<>(); + userDetails.put(Setup2FADetail, UserAccountVO.Setup2FAstatus.ENABLED.name()); + Mockito.when(mockUserAccount.getDetails()).thenReturn(userDetails); + Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO); + discoveryServiceSpy.listApis(getTestUser(), null); + Mockito.verify(apiCheckerMock, Mockito.times(1)).getApisAllowedToUser(any(Role.class), any(User.class), anyList()); + } + + @Test + public void listApisForUserEnforcedPwdChange() throws PermissionDeniedException { + RoleVO userRoleVO = new RoleVO(4L, "name", RoleType.User, "description"); + Map userDetails = new HashMap<>(); + userDetails.put(PasswordChangeRequired, "true"); + Mockito.when(mockUserAccount.getDetails()).thenReturn(userDetails); + Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount()); + Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO); + Mockito.when(apiNameDiscoveryResponseMapMock.get(Mockito.anyString())).thenReturn(Mockito.mock(ApiDiscoveryResponse.class)); + ListResponse response = (ListResponse) discoveryServiceSpy.listApis(getTestUser(), null); + Assert.assertEquals(4, response.getResponses().size()); + } } diff --git a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java index a350d80d27f..c5b690cfcd4 100644 --- a/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java +++ b/plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java @@ -70,7 +70,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -297,7 +296,8 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co } private Pair restoreVMBackup(VirtualMachine vm, Backup backup) { - List backedVolumesUUIDs = backup.getBackedUpVolumes().stream() + List backedVolumes = backup.getBackedUpVolumes(); + List backedVolumesUUIDs = backedVolumes.stream() .sorted(Comparator.comparingLong(Backup.VolumeInfo::getDeviceId)) .map(Backup.VolumeInfo::getUuid) .collect(Collectors.toList()); @@ -320,6 +320,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co Pair, List> volumePoolsAndPaths = getVolumePoolsAndPaths(restoreVolumes); restoreCommand.setRestoreVolumePools(volumePoolsAndPaths.first()); restoreCommand.setRestoreVolumePaths(volumePoolsAndPaths.second()); + restoreCommand.setBackupFiles(getBackupFiles(backedVolumes)); restoreCommand.setVmExists(vm.getRemoved() == null); restoreCommand.setVmState(vm.getState()); restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value()); @@ -335,6 +336,14 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co return new Pair<>(answer.getResult(), answer.getDetails()); } + private List getBackupFiles(List backedVolumes) { + List backupFiles = new ArrayList<>(); + for (Backup.VolumeInfo backedVolume : backedVolumes) { + backupFiles.add(backedVolume.getPath()); + } + return backupFiles; + } + private Pair, List> getVolumePoolsAndPaths(List volumes) { List volumePools = new ArrayList<>(); List volumePaths = new ArrayList<>(); @@ -373,7 +382,13 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co final StoragePoolVO pool = primaryDataStoreDao.findByUuid(dataStoreUuid); final HostVO hostVO = hostDao.findByIp(hostIp); - LOG.debug("Restoring vm volume {} from backup {} on the NAS Backup Provider", backupVolumeInfo, backup); + Backup.VolumeInfo matchingVolume = getBackedUpVolumeInfo(backup.getBackedUpVolumes(), volume.getUuid()); + if (matchingVolume == null) { + throw new CloudRuntimeException(String.format("Unable to find volume %s in the list of backed up volumes for backup %s, cannot proceed with restore", volume.getUuid(), backup)); + } + Long backedUpVolumeSize = matchingVolume.getSize(); + + LOG.debug("Restoring vm volume {} from backup {} on the NAS Backup Provider", volume, backup); BackupRepository backupRepository = getBackupRepository(backup); VolumeVO restoredVolume = new VolumeVO(Volume.Type.DATADISK, null, backup.getZoneId(), @@ -391,7 +406,7 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co restoredVolume.setPoolType(pool.getPoolType()); restoredVolume.setPath(restoredVolume.getUuid()); restoredVolume.setState(Volume.State.Copying); - restoredVolume.setSize(backupVolumeInfo.getSize()); + restoredVolume.setSize(backedUpVolumeSize); restoredVolume.setDiskOfferingId(diskOffering.getId()); if (pool.getPoolType() != Storage.StoragePoolType.RBD) { restoredVolume.setFormat(Storage.ImageFormat.QCOW2); @@ -411,8 +426,8 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co restoreCommand.setMountOptions(backupRepository.getMountOptions()); restoreCommand.setVmExists(null); restoreCommand.setVmState(vmNameAndState.second()); - restoreCommand.setRestoreVolumeUUID(backupVolumeInfo.getUuid()); restoreCommand.setMountTimeout(NASBackupRestoreMountTimeout.value()); + restoreCommand.setBackupFiles(Collections.singletonList(matchingVolume.getPath())); BackupAnswer answer; try { @@ -442,10 +457,11 @@ public class NASBackupProvider extends AdapterBase implements BackupProvider, Co return backupRepository; } - private Optional getBackedUpVolumeInfo(List backedUpVolumes, String volumeUuid) { + private Backup.VolumeInfo getBackedUpVolumeInfo(List backedUpVolumes, String volumeUuid) { return backedUpVolumes.stream() .filter(v -> v.getUuid().equals(volumeUuid)) - .findFirst(); + .findFirst() + .orElse(null); } @Override diff --git a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java index 66b633e11a9..4cf4bd111ef 100644 --- a/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java +++ b/plugins/backup/networker/src/main/java/org/apache/cloudstack/backup/NetworkerBackupProvider.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.backup; +import com.cloud.agent.AgentManager; import com.cloud.dc.dao.ClusterDao; import com.cloud.host.HostVO; import com.cloud.host.Status; @@ -134,6 +135,9 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid @Inject private DiskOfferingDao diskOfferingDao; + @Inject + private AgentManager agentMgr; + private static String getUrlDomain(String url) throws URISyntaxException { URI uri; try { @@ -251,8 +255,13 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid String nstRegex = "\\bcompleted savetime=([0-9]{10})"; Pattern saveTimePattern = Pattern.compile(nstRegex); + if (host == null) { + LOG.warn("Unable to take backup, host is null"); + return null; + } + try { - Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), 22, + Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), agentMgr.getHostSshPort(host), username, null, password, command, 120000, 120000, 3600000); if (!response.first()) { LOG.error("Backup Script failed on HYPERVISOR {} due to: {}", host, response.second()); @@ -271,9 +280,13 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid return null; } private boolean executeRestoreCommand(HostVO host, String username, String password, String command) { + if (host == null) { + LOG.warn("Unable to restore backup, host is null"); + return false; + } try { - Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), 22, + Pair response = SshHelper.sshExecute(host.getPrivateIpAddress(), agentMgr.getHostSshPort(host), username, null, password, command, 120000, 120000, 3600000); if (!response.first()) { diff --git a/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java b/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java index 224e49f91a3..b98147b5f03 100644 --- a/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java +++ b/plugins/event-bus/rabbitmq/src/main/java/org/apache/cloudstack/mom/rabbitmq/RabbitMQEventBus.java @@ -493,7 +493,7 @@ public class RabbitMQEventBus extends ManagerBase implements EventBus { @Override public synchronized boolean stop() { - if (s_connection.isOpen()) { + if (s_connection != null && s_connection.isOpen()) { for (String subscriberId : s_subscribers.keySet()) { Ternary subscriberDetails = s_subscribers.get(subscriberId); Channel channel = subscriberDetails.second(); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java index fd94013dd50..714e3844b34 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapper.java @@ -72,10 +72,10 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper backedVolumeUUIDs = command.getBackupVolumesUUIDs(); List restoreVolumePools = command.getRestoreVolumePools(); List restoreVolumePaths = command.getRestoreVolumePaths(); - String restoreVolumeUuid = command.getRestoreVolumeUUID(); Integer mountTimeout = command.getMountTimeout() * 1000; int timeout = command.getWait(); KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); + List backupFiles = command.getBackupFiles(); String newVolumeId = null; try { @@ -83,14 +83,15 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper(vmName, command.getVmState()), mountDirectory, timeout); } else if (Boolean.TRUE.equals(vmExists)) { - restoreVolumesOfExistingVM(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backedVolumeUUIDs, backupPath, mountDirectory, timeout); + restoreVolumesOfExistingVM(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backedVolumeUUIDs, backupPath, backupFiles, mountDirectory, timeout); } else { - restoreVolumesOfDestroyedVMs(storagePoolMgr, restoreVolumePools, restoreVolumePaths, vmName, backupPath, mountDirectory, timeout); + restoreVolumesOfDestroyedVMs(storagePoolMgr, restoreVolumePools, restoreVolumePaths, backupPath, backupFiles, mountDirectory, timeout); } } catch (CloudRuntimeException e) { String errorMessage = e.getMessage() != null ? e.getMessage() : ""; @@ -109,19 +110,22 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper restoreVolumePools, List restoreVolumePaths, List backedVolumesUUIDs, - String backupPath, String mountDirectory, int timeout) { + private void restoreVolumesOfExistingVM(KVMStoragePoolManager storagePoolMgr, List restoreVolumePools, + List restoreVolumePaths, List backedVolumesUUIDs, + String backupPath, List backupFiles, String mountDirectory, int timeout) { String diskType = "root"; try { for (int idx = 0; idx < restoreVolumePaths.size(); idx++) { PrimaryDataStoreTO restoreVolumePool = restoreVolumePools.get(idx); String restoreVolumePath = restoreVolumePaths.get(idx); + String backupFile = backupFiles.get(idx); String backupVolumeUuid = backedVolumesUUIDs.get(idx); - Pair bkpPathAndVolUuid = getBackupPath(mountDirectory, null, backupPath, diskType, backupVolumeUuid); + String fullPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType); diskType = "datadisk"; - verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(storagePoolMgr, restoreVolumePool, restoreVolumePath, bkpPathAndVolUuid.first(), timeout)) { - throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); + + verifyBackupFile(fullPath, backupVolumeUuid); + if (!replaceVolumeWithBackup(storagePoolMgr, restoreVolumePool, restoreVolumePath, fullPath, timeout)) { + throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", backupVolumeUuid)); } } } finally { @@ -130,17 +134,20 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper volumePools, List volumePaths, String vmName, String backupPath, String mountDirectory, int timeout) { + private void restoreVolumesOfDestroyedVMs(KVMStoragePoolManager storagePoolMgr, List volumePools, + List volumePaths, String backupPath, List backupFiles, String mountDirectory, int timeout) { String diskType = "root"; try { for (int i = 0; i < volumePaths.size(); i++) { PrimaryDataStoreTO volumePool = volumePools.get(i); String volumePath = volumePaths.get(i); - Pair bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, null); + String backupFile = backupFiles.get(i); + String bkpPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType); + String volumeUuid = volumePath.substring(volumePath.lastIndexOf(File.separator) + 1); diskType = "datadisk"; - verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout)) { - throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); + verifyBackupFile(bkpPath, volumeUuid); + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPath, timeout)) { + throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", volumeUuid)); } } } finally { @@ -149,14 +156,17 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper vmNameAndState, String mountDirectory, int timeout) { - Pair bkpPathAndVolUuid; + String bkpPath; + String volumeUuid; try { - bkpPathAndVolUuid = getBackupPath(mountDirectory, volumePath, backupPath, diskType, volumeUUID); - verifyBackupFile(bkpPathAndVolUuid.first(), bkpPathAndVolUuid.second()); - if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPathAndVolUuid.first(), timeout, true)) { - throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", bkpPathAndVolUuid.second())); + bkpPath = getBackupPath(mountDirectory, backupPath, backupFile, diskType); + volumeUuid = volumePath.substring(volumePath.lastIndexOf(File.separator) + 1); + verifyBackupFile(bkpPath, volumeUuid); + if (!replaceVolumeWithBackup(storagePoolMgr, volumePool, volumePath, bkpPath, timeout, true)) { + throw new CloudRuntimeException(String.format("Unable to restore contents from the backup volume [%s].", volumeUuid)); + } if (VirtualMachine.State.Running.equals(vmNameAndState.second())) { if (!attachVolumeToVm(storagePoolMgr, vmNameAndState.first(), volumePool, volumePath)) { @@ -177,7 +187,7 @@ public class LibvirtRestoreBackupCommandWrapper extends CommandWrapper getBackupPath(String mountDirectory, String volumePath, String backupPath, String diskType, String volumeUuid) { + private String getBackupPath(String mountDirectory, String backupPath, String backupFile, String diskType) { String bkpPath = String.format(FILE_PATH_PLACEHOLDER, mountDirectory, backupPath); - String volUuid = Objects.isNull(volumeUuid) ? volumePath.substring(volumePath.lastIndexOf(File.separator) + 1) : volumeUuid; - String backupFileName = String.format("%s.%s.qcow2", diskType.toLowerCase(Locale.ROOT), volUuid); + String backupFileName = String.format("%s.%s.qcow2", diskType.toLowerCase(Locale.ROOT), backupFile); bkpPath = String.format(FILE_PATH_PLACEHOLDER, bkpPath, backupFileName); - return new Pair<>(bkpPath, volUuid); + return bkpPath; } private boolean checkBackupFileImage(String backupPath) { diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java index 7bcd0bf18e6..d72dc0d8ac3 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtRestoreBackupCommandWrapperTest.java @@ -69,7 +69,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); when(command.getMountTimeout()).thenReturn(30); @@ -112,6 +112,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); when(command.getBackupVolumesUUIDs()).thenReturn(Arrays.asList("volume-123")); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getMountTimeout()).thenReturn(30); try (MockedStatic filesMock = mockStatic(Files.class)) { @@ -149,6 +150,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getMountTimeout()).thenReturn(30); try (MockedStatic filesMock = mockStatic(Files.class)) { @@ -186,7 +188,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); when(command.getMountTimeout()).thenReturn(30); @@ -227,7 +229,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running); lenient().when(command.getMountTimeout()).thenReturn(30); @@ -263,7 +265,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); when(command.getMountTimeout()).thenReturn(30); @@ -309,7 +311,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); when(command.getMountTimeout()).thenReturn(30); @@ -357,7 +359,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); when(command.getMountTimeout()).thenReturn(30); @@ -407,7 +409,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); when(command.getVmState()).thenReturn(VirtualMachine.State.Running); when(command.getMountTimeout()).thenReturn(30); @@ -461,7 +463,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { PrimaryDataStoreTO primaryDataStore = Mockito.mock(PrimaryDataStoreTO.class); when(command.getRestoreVolumePools()).thenReturn(Arrays.asList(primaryDataStore)); lenient().when(command.getRestoreVolumePaths()).thenReturn(Arrays.asList("/var/lib/libvirt/images/volume-123")); - lenient().when(command.getRestoreVolumeUUID()).thenReturn("volume-123"); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123")); lenient().when(command.getVmState()).thenReturn(VirtualMachine.State.Running); lenient().when(command.getMountTimeout()).thenReturn(30); @@ -499,6 +501,7 @@ public class LibvirtRestoreBackupCommandWrapperTest { "/var/lib/libvirt/images/volume-456" )); when(command.getBackupVolumesUUIDs()).thenReturn(Arrays.asList("volume-123", "volume-456")); + when(command.getBackupFiles()).thenReturn(Arrays.asList("volume-123", "volume-456")); when(command.getMountTimeout()).thenReturn(30); try (MockedStatic filesMock = mockStatic(Files.class)) { diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixModifyStoragePoolCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixModifyStoragePoolCommandWrapper.java index 63cb675c614..2878998d622 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixModifyStoragePoolCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixModifyStoragePoolCommandWrapper.java @@ -59,7 +59,7 @@ public final class CitrixModifyStoragePoolCommandWrapper extends CommandWrapper< if (capacity == -1) { final String msg = "Pool capacity is -1! pool: " + pool.getHost() + pool.getPath(); logger.warn(msg); - return new Answer(command, false, msg); + return new ModifyStoragePoolAnswer(command, false, msg); } final Map tInfo = new HashMap(); final ModifyStoragePoolAnswer answer = new ModifyStoragePoolAnswer(command, capacity, available, tInfo); @@ -68,12 +68,12 @@ public final class CitrixModifyStoragePoolCommandWrapper extends CommandWrapper< final String msg = "ModifyStoragePoolCommand add XenAPIException:" + e.toString() + " host:" + citrixResourceBase.getHost().getUuid() + " pool: " + pool.getHost() + pool.getPath(); logger.warn(msg, e); - return new Answer(command, false, msg); + return new ModifyStoragePoolAnswer(command, false, msg); } catch (final Exception e) { final String msg = "ModifyStoragePoolCommand add XenAPIException:" + e.getMessage() + " host:" + citrixResourceBase.getHost().getUuid() + " pool: " + pool.getHost() + pool.getPath(); logger.warn(msg, e); - return new Answer(command, false, msg); + return new ModifyStoragePoolAnswer(command, false, msg); } } else { try { @@ -85,17 +85,17 @@ public final class CitrixModifyStoragePoolCommandWrapper extends CommandWrapper< if (result == null || !result.split("#")[1].equals("0")) { throw new CloudRuntimeException("Unable to remove heartbeat file entry for SR " + srUuid + " due to " + result); } - return new Answer(command, true, "success"); + return new ModifyStoragePoolAnswer(command, true, "success"); } catch (final XenAPIException e) { final String msg = "ModifyStoragePoolCommand remove XenAPIException:" + e.toString() + " host:" + citrixResourceBase.getHost().getUuid() + " pool: " + pool.getHost() + pool.getPath(); logger.warn(msg, e); - return new Answer(command, false, msg); + return new ModifyStoragePoolAnswer(command, false, msg); } catch (final Exception e) { final String msg = "ModifyStoragePoolCommand remove XenAPIException:" + e.getMessage() + " host:" + citrixResourceBase.getHost().getUuid() + " pool: " + pool.getHost() + pool.getPath(); logger.warn(msg, e); - return new Answer(command, false, msg); + return new ModifyStoragePoolAnswer(command, false, msg); } } } diff --git a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java index 2ed65efd1e8..abb94d78679 100644 --- a/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java +++ b/plugins/integrations/prometheus/src/main/java/org/apache/cloudstack/metrics/PrometheusExporterImpl.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.metrics; import java.math.BigDecimal; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -26,6 +27,7 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.commons.lang3.StringUtils; @@ -133,6 +135,8 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp private ResourceCountDao _resourceCountDao; @Inject private HostTagsDao _hostTagsDao; + @Inject + private CAManager caManager; public PrometheusExporterImpl() { super(); @@ -216,6 +220,9 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp } metricsList.add(new ItemHostVM(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), vmDao.listByHostId(host.getId()).size())); + + addSSLCertificateExpirationMetrics(metricsList, zoneName, zoneUuid, host); + final CapacityVO coreCapacity = capacityDao.findByHostIdType(host.getId(), Capacity.CAPACITY_TYPE_CPU_CORE); if (coreCapacity == null && !host.isInMaintenanceStates()){ @@ -253,6 +260,18 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp addHostTagsMetrics(metricsList, dcId, zoneName, zoneUuid, totalHosts, upHosts, downHosts, total, up, down); } + private void addSSLCertificateExpirationMetrics(List metricsList, String zoneName, String zoneUuid, HostVO host) { + if (caManager == null || caManager.getActiveCertificatesMap() == null) { + return; + } + X509Certificate cert = caManager.getActiveCertificatesMap().getOrDefault(host.getPrivateIpAddress(), null); + if (cert == null) { + return; + } + long certExpiryEpoch = cert.getNotAfter().getTime() / 1000; // Convert to epoch seconds + metricsList.add(new ItemHostCertExpiry(zoneName, zoneUuid, host.getName(), host.getUuid(), host.getPrivateIpAddress(), certExpiryEpoch)); + } + private String markTagMaps(HostVO host, Map totalHosts, Map upHosts, Map downHosts) { List hostTagVOS = _hostTagsDao.getHostTags(host.getId()); List hostTags = new ArrayList<>(); @@ -1137,4 +1156,28 @@ public class PrometheusExporterImpl extends ManagerBase implements PrometheusExp return String.format("%s{zone=\"%s\",cpu=\"%d\",memory=\"%d\"} %d", name, zoneName, cpu, memory, total); } } + + class ItemHostCertExpiry extends Item { + String zoneName; + String zoneUuid; + String hostName; + String hostUuid; + String hostIp; + long expiryTimestamp; + + public ItemHostCertExpiry(final String zoneName, final String zoneUuid, final String hostName, final String hostUuid, final String hostIp, final long expiry) { + super("cloudstack_host_cert_expiry_timestamp", "Host certificate expiry timestamp in seconds since epoch", "gauge"); + this.zoneName = zoneName; + this.zoneUuid = zoneUuid; + this.hostName = hostName; + this.hostUuid = hostUuid; + this.hostIp = hostIp; + this.expiryTimestamp = expiry; + } + + @Override + public String toMetricsString() { + return String.format("%s{zone=\"%s\",hostname=\"%s\",ip=\"%s\"} %d", name, zoneName, hostName, hostIp, expiryTimestamp); + } + } } diff --git a/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterImplTest.java b/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterImplTest.java new file mode 100644 index 00000000000..40490c46f56 --- /dev/null +++ b/plugins/integrations/prometheus/src/test/java/org/apache/cloudstack/metrics/PrometheusExporterImplTest.java @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.metrics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class PrometheusExporterImplTest { + + private static final String TEST_ZONE_NAME = "zone1"; + private static final String TEST_ZONE_UUID = "zone-uuid-1"; + private static final String TEST_HOST_NAME = "host1"; + private static final String TEST_HOST_UUID = "host-uuid-1"; + private static final String TEST_HOST_IP = "192.168.1.10"; + private static final long CERT_EXPIRY_TIME = 1735689600000L; // 2025-01-01 00:00:00 UTC + private static final long CERT_EXPIRY_EPOCH = CERT_EXPIRY_TIME / 1000; + + @Test + public void testItemHostCertExpiryFormat() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + String expected = String.format( + "cloudstack_host_cert_expiry_timestamp{zone=\"%s\",hostname=\"%s\",ip=\"%s\"} %d", + TEST_ZONE_NAME, + TEST_HOST_NAME, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + assertEquals("Certificate expiry metric format should match expected format", expected, metricsString); + } + + @Test + public void testItemHostCertExpiryContainsCorrectMetricName() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + assertTrue("Metric should contain correct metric name", + metricsString.contains("cloudstack_host_cert_expiry_timestamp")); + } + + @Test + public void testItemHostCertExpiryContainsAllLabels() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + assertTrue("Metric should contain zone label", metricsString.contains("zone=\"" + TEST_ZONE_NAME + "\"")); + assertTrue("Metric should contain hostname label", metricsString.contains("hostname=\"" + TEST_HOST_NAME + "\"")); + assertTrue("Metric should contain ip label", metricsString.contains("ip=\"" + TEST_HOST_IP + "\"")); + } + + @Test + public void testItemHostCertExpiryContainsTimestampValue() { + PrometheusExporterImpl exporter = new PrometheusExporterImpl(); + PrometheusExporterImpl.ItemHostCertExpiry item = exporter.new ItemHostCertExpiry( + TEST_ZONE_NAME, + TEST_ZONE_UUID, + TEST_HOST_NAME, + TEST_HOST_UUID, + TEST_HOST_IP, + CERT_EXPIRY_EPOCH + ); + + String metricsString = item.toMetricsString(); + assertTrue("Metric should contain correct timestamp value", + metricsString.endsWith(" " + CERT_EXPIRY_EPOCH)); + } +} diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index 4dffb405d1a..d3714f14834 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -131,7 +131,7 @@ public class MockAccountManager extends ManagerBase implements AccountManager { } @Override - public User createUser(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5, String arg6, Long arg7, String arg8) { + public User createUser(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5, String arg6, Long arg7, String arg8, boolean arg9) { // TODO Auto-generated method stub return null; } diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java index f9a1d10d352..57f71c4ea80 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java @@ -34,6 +34,8 @@ import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.cloudstack.resourcedetail.UserDetailVO; +import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; @@ -74,6 +76,9 @@ public class OauthLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent @Inject ApiServerService _apiServer; + @Inject + UserDetailsDao userDetailsDao; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -157,8 +162,10 @@ public class OauthLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent if (userAccount != null && User.Source.SAML2 == userAccount.getSource()) { throw new CloudAuthenticationException("User is not allowed CloudStack login"); } - return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, userAccount.getUsername(), null, domainId, domain, remoteAddress, params), + serializedResponse = ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, userAccount.getUsername(), null, domainId, domain, remoteAddress, params), responseType); + userDetailsDao.addDetail(userAccount.getId(), UserDetailVO.OauthLogin, "true", false); + return serializedResponse; } catch (final CloudAuthenticationException ex) { ApiServlet.invalidateHttpSession(session, "fall through to API key,"); String msg = String.format("%s", ex.getMessage() != null ? @@ -177,12 +184,8 @@ public class OauthLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent protected Long getDomainIdFromParams(Map params, StringBuilder auditTrailSb, String responseType) { String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID); - - if (domainIdArr == null) { - domainIdArr = (String[])params.get(ApiConstants.DOMAIN__ID); - } Long domainId = null; - if ((domainIdArr != null) && (domainIdArr.length > 0)) { + if (domainIdArr != null && domainIdArr.length > 0) { try { //check if UUID is passed in for domain domainId = _apiServer.fetchDomainId(domainIdArr[0]); diff --git a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java index bfd47922142..584f2463754 100644 --- a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java @@ -158,11 +158,17 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent String domainPath = null; if (params.containsKey(ApiConstants.IDP_ID)) { - idpId = ((String[])params.get(ApiConstants.IDP_ID))[0]; + String[] idpIds = (String[])params.get(ApiConstants.IDP_ID); + if (idpIds != null && idpIds.length > 0) { + idpId = idpIds[0]; + } } if (params.containsKey(ApiConstants.DOMAIN)) { - domainPath = ((String[])params.get(ApiConstants.DOMAIN))[0]; + String[] domainPaths = (String[])params.get(ApiConstants.DOMAIN); + if (domainPaths != null && domainPaths.length > 0) { + domainPath = domainPaths[0]; + } } if (domainPath != null && !domainPath.isEmpty()) { diff --git a/pom.xml b/pom.xml index dc7d0685f72..17935c52692 100644 --- a/pom.xml +++ b/pom.xml @@ -1087,6 +1087,10 @@ utils/testsmallfileinactive **/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker .github/workflows/dependabot.yaml + .gitattributes + .github/workflows/*.md + .github/workflows/*.lock.ya?ml + .github/aw/** diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 95aca28b53f..4a6a1180363 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -116,9 +116,11 @@ import org.apache.cloudstack.framework.messagebus.MessageBus; import org.apache.cloudstack.framework.messagebus.MessageDispatcher; import org.apache.cloudstack.framework.messagebus.MessageHandler; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.resourcedetail.UserDetailVO; import org.apache.cloudstack.user.UserPasswordResetManager; import org.apache.cloudstack.utils.identity.ManagementServerNode; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.EnumUtils; import org.apache.http.ConnectionClosedException; import org.apache.http.HttpException; @@ -194,6 +196,7 @@ import com.cloud.utils.net.NetUtils; import com.google.gson.reflect.TypeToken; import static com.cloud.user.AccountManagerImpl.apiKeyAccess; +import static org.apache.cloudstack.api.ApiConstants.PASSWORD_CHANGE_REQUIRED; import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; @Component @@ -1227,6 +1230,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer if (ApiConstants.MANAGEMENT_SERVER_ID.equalsIgnoreCase(attrName)) { response.setManagementServerId(attrObj.toString()); } + if (PASSWORD_CHANGE_REQUIRED.equalsIgnoreCase(attrName) && attrObj instanceof Boolean) { + response.setPasswordChangeRequired((Boolean) attrObj); + } } } response.setResponseName("loginresponse"); @@ -1327,6 +1333,13 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer final String sessionKey = Base64.encodeBase64URLSafeString(sessionKeyBytes); session.setAttribute(ApiConstants.SESSIONKEY, sessionKey); + Map userAccDetails = userAcct.getDetails(); + if (MapUtils.isNotEmpty(userAccDetails)) { + String needPwdChangeStr = userAccDetails.get(UserDetailVO.PasswordChangeRequired); + if ("true".equalsIgnoreCase(needPwdChangeStr)) { + session.setAttribute(PASSWORD_CHANGE_REQUIRED, true); + } + } return createLoginResponse(session); } throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials"); diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index 158df224071..dd99b4e13ca 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -38,6 +38,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.cloudstack.api.APICommand; +import com.cloud.api.auth.DefaultForgotPasswordAPIAuthenticatorCmd; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ApiServerService; @@ -200,7 +201,6 @@ public class ApiServlet extends HttpServlet { LOGGER.warn(message); } }); - } void processRequestInContext(final HttpServletRequest req, final HttpServletResponse resp) { @@ -273,7 +273,6 @@ public class ApiServlet extends HttpServlet { } if (command != null && !command.equals(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME)) { - APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command); if (apiAuthenticator != null) { auditTrailSb.append("command="); @@ -309,7 +308,9 @@ public class ApiServlet extends HttpServlet { } catch (ServerApiException e) { httpResponseCode = e.getErrorCode().getHttpCode(); responseString = e.getMessage(); - LOGGER.debug("Authentication failure: " + e.getMessage()); + if (!DefaultForgotPasswordAPIAuthenticatorCmd.APINAME.equalsIgnoreCase(command) || StringUtils.isNotBlank(username)) { + LOGGER.debug("Authentication failure: {}", e.getMessage()); + } } if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) { @@ -390,7 +391,7 @@ public class ApiServlet extends HttpServlet { } } - if (! requestChecksoutAsSane(resp, auditTrailSb, responseType, params, session, command, userId, account, accountObj)) + if (!requestChecksoutAsSane(resp, auditTrailSb, responseType, params, session, command, userId, account, accountObj)) return; } else { CallContext.register(accountMgr.getSystemUser(), accountMgr.getSystemAccount()); @@ -422,7 +423,6 @@ public class ApiServlet extends HttpServlet { apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials and/or request signature", params, responseType); HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.JSONcontentType.value()); - } } catch (final ServerApiException se) { final String serializedResponseText = apiServer.getSerializedApiError(se, params, responseType); @@ -653,6 +653,9 @@ public class ApiServlet extends HttpServlet { if (LOGGER.isTraceEnabled()) { LOGGER.trace(msg); } + if (session == null) { + return; + } session.invalidate(); } catch (final IllegalStateException ise) { if (LOGGER.isTraceEnabled()) { diff --git a/server/src/main/java/com/cloud/api/auth/DefaultForgotPasswordAPIAuthenticatorCmd.java b/server/src/main/java/com/cloud/api/auth/DefaultForgotPasswordAPIAuthenticatorCmd.java index 1e90b43c5e8..46a9dd9bfe3 100644 --- a/server/src/main/java/com/cloud/api/auth/DefaultForgotPasswordAPIAuthenticatorCmd.java +++ b/server/src/main/java/com/cloud/api/auth/DefaultForgotPasswordAPIAuthenticatorCmd.java @@ -44,13 +44,13 @@ import java.net.InetAddress; import java.util.List; import java.util.Map; -@APICommand(name = "forgotPassword", +@APICommand(name = DefaultForgotPasswordAPIAuthenticatorCmd.APINAME, description = "Sends an email to the user with a token to reset the password using resetPassword command.", since = "4.20.0.0", requestHasSensitiveInfo = true, responseObject = SuccessResponse.class) public class DefaultForgotPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { - + public static final String APINAME = "forgotPassword"; ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -108,10 +108,12 @@ public class DefaultForgotPasswordAPIAuthenticatorCmd extends BaseCmd implements if (userDomain != null) { domainId = userDomain.getId(); } else { + logger.debug("Unable to find the domain from the path {}", domain); throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find the domain from the path %s", domain)); } final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId); if (userAccount != null && List.of(User.Source.SAML2, User.Source.OAUTH2, User.Source.LDAP).contains(userAccount.getSource())) { + logger.debug("Forgot Password is not allowed for the user {} from source {}", username[0], userAccount.getSource()); throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Forgot Password is not allowed for this user"); } boolean success = _apiServer.forgotPassword(userAccount, userDomain); diff --git a/server/src/main/java/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java b/server/src/main/java/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java index c9b03a85f4c..4514ed83178 100644 --- a/server/src/main/java/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java +++ b/server/src/main/java/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java @@ -34,6 +34,8 @@ import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.cloudstack.resourcedetail.UserDetailVO; +import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao; import org.jetbrains.annotations.Nullable; import javax.inject.Inject; @@ -47,7 +49,6 @@ import java.net.InetAddress; @APICommand(name = "login", description = "Logs a user into the CloudStack. A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { - ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// @@ -66,6 +67,9 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe @Inject ApiServerService _apiServer; + @Inject + UserDetailsDao userDetailsDao; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -107,17 +111,13 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe if (HTTPMethod.valueOf(req.getMethod()) != HTTPMethod.POST) { throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "Please use HTTP POST to authenticate using this API"); } + // FIXME: ported from ApiServlet, refactor and cleanup final String[] username = (String[])params.get(ApiConstants.USERNAME); final String[] password = (String[])params.get(ApiConstants.PASSWORD); - String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID); - - if (domainIdArr == null) { - domainIdArr = (String[])params.get(ApiConstants.DOMAIN__ID); - } - final String[] domainName = (String[])params.get(ApiConstants.DOMAIN); + final String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID); Long domainId = null; - if ((domainIdArr != null) && (domainIdArr.length > 0)) { + if (domainIdArr != null && domainIdArr.length > 0) { try { //check if UUID is passed in for domain domainId = _apiServer.fetchDomainId(domainIdArr[0]); @@ -135,6 +135,7 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe } String domain = null; + final String[] domainName = (String[])params.get(ApiConstants.DOMAIN); domain = getDomainName(auditTrailSb, domainName, domain); String serializedResponse = null; @@ -151,8 +152,10 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe if (userAccount != null && User.Source.SAML2 == userAccount.getSource()) { throw new CloudAuthenticationException("User is not allowed CloudStack login"); } - return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params), + serializedResponse = ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params), responseType); + userDetailsDao.removeDetail(userAccount.getId(), UserDetailVO.OauthLogin); + return serializedResponse; } catch (final CloudAuthenticationException ex) { ApiServlet.invalidateHttpSession(session, "fall through to API key,"); // TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401) diff --git a/server/src/main/java/com/cloud/api/auth/DefaultResetPasswordAPIAuthenticatorCmd.java b/server/src/main/java/com/cloud/api/auth/DefaultResetPasswordAPIAuthenticatorCmd.java index 077efdee087..810b5ebefcf 100644 --- a/server/src/main/java/com/cloud/api/auth/DefaultResetPasswordAPIAuthenticatorCmd.java +++ b/server/src/main/java/com/cloud/api/auth/DefaultResetPasswordAPIAuthenticatorCmd.java @@ -53,7 +53,6 @@ import java.util.Map; responseObject = SuccessResponse.class) public class DefaultResetPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { - ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index ee397ee2a5b..0ce4b913a02 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -107,6 +107,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.ValidatedConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.dao.ConfigurationGroupDao; import org.apache.cloudstack.framework.config.dao.ConfigurationSubGroupDao; @@ -764,6 +765,12 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati throw new InvalidParameterValueException(validationMsg); } + ConfigKey configKey = _configDepot.get(name); + if (configKey instanceof ValidatedConfigKey) { + ValidatedConfigKey validatedConfigKey = (ValidatedConfigKey) configKey; + validatedConfigKey.validateValue(value); + } + // If scope of the parameter is given then it needs to be updated in the // corresponding details table, // if scope is mentioned as global or not mentioned then it is normal diff --git a/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java b/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java index 3bd5fcba8e9..efda72555e2 100644 --- a/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java +++ b/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java @@ -272,7 +272,12 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements } } - sshConnection = new Connection(agentIp, 22); + int port = uri.getPort(); + if (port <= 0) { + port = AgentManager.KVMHostDiscoverySshPort.valueIn(clusterId); + } + + sshConnection = new Connection(agentIp, port); sshConnection.connect(null, 60000, 60000); @@ -380,6 +385,9 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements Map hostDetails = connectedHost.getDetails(); hostDetails.put("password", password); hostDetails.put("username", username); + if (uri.getPort() > 0) { + hostDetails.put(Host.HOST_SSH_PORT, String.valueOf(uri.getPort())); + } _hostDao.saveDetails(connectedHost); return resources; } catch (DiscoveredWithErrorException e) { diff --git a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java index 6df31c6fa53..621b110486b 100755 --- a/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/main/java/com/cloud/resource/ResourceManagerImpl.java @@ -851,7 +851,6 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, _clusterDetailsDao.persist(cluster_cpu_detail); _clusterDetailsDao.persist(cluster_memory_detail); } - } uri = validatedHostUrl(url, hypervisorType); @@ -945,7 +944,6 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, hosts.add(host); } discoverer.postDiscovery(hosts, _nodeId); - } logger.info("server resources successfully discovered by " + discoverer.getName()); return hosts; @@ -3149,15 +3147,26 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, private HostVO getNewHost(StartupCommand[] startupCommands) { StartupCommand startupCommand = startupCommands[0]; - HostVO host = findHostByGuid(startupCommand.getGuid()); + String fullGuid = startupCommand.getGuid(); + logger.debug(String.format("Trying to find Host by guid %s", fullGuid)); + HostVO host = findHostByGuid(fullGuid); if (host != null) { + logger.debug(String.format("Found Host by guid %s: %s", fullGuid, host)); return host; } - host = findHostByGuid(startupCommand.getGuidWithoutResource()); + String guidPrefix = startupCommand.getGuidWithoutResource(); + logger.debug(String.format("Trying to find Host by guid prefix %s", guidPrefix)); + host = findHostByGuidPrefix(guidPrefix); - return host; // even when host == null! + if (host != null) { + logger.debug(String.format("Found Host by guid prefix %s: %s", guidPrefix, host)); + return host; + } + + logger.debug(String.format("Could not find Host by guid %s", fullGuid)); + return null; } protected HostVO createHostVO(final StartupCommand[] cmds, final ServerResource resource, final Map details, List hostTags, @@ -3862,7 +3871,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, */ protected void connectAndRestartAgentOnHost(HostVO host, String username, String password, String privateKey) { final com.trilead.ssh2.Connection connection = SSHCmdHelper.acquireAuthorizedConnection( - host.getPrivateIpAddress(), 22, username, password, privateKey); + host.getPrivateIpAddress(), _agentMgr.getHostSshPort(host), username, password, privateKey); if (connection == null) { throw new CloudRuntimeException(String.format("SSH to agent is enabled, but failed to connect to %s via IP address [%s].", host, host.getPrivateIpAddress())); } @@ -4209,6 +4218,15 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, public HostVO findHostByGuid(final String guid) { final QueryBuilder sc = QueryBuilder.create(HostVO.class); sc.and(sc.entity().getGuid(), Op.EQ, guid); + sc.and(sc.entity().getRemoved(), Op.NULL); + return sc.find(); + } + + @Override + public HostVO findHostByGuidPrefix(String guid) { + final QueryBuilder sc = QueryBuilder.create(HostVO.class); + sc.and(sc.entity().getGuid(), Op.LIKE, guid + "%"); + sc.and(sc.entity().getRemoved(), Op.NULL); return sc.find(); } @@ -4216,6 +4234,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager, public HostVO findHostByName(final String name) { final QueryBuilder sc = QueryBuilder.create(HostVO.class); sc.and(sc.entity().getName(), Op.EQ, name); + sc.and(sc.entity().getRemoved(), Op.NULL); return sc.find(); } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index bd850094c37..b595d01127c 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -43,6 +43,7 @@ import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.network.vpc.VpcVO; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.affinity.AffinityGroupProcessor; @@ -2651,12 +2652,21 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } if (associatedNetworkId != null) { - _accountMgr.checkAccess(caller, null, false, networkDao.findById(associatedNetworkId)); - sc.setParameters("associatedNetworkIdEq", associatedNetworkId); + NetworkVO associatedNetwork = networkDao.findById(associatedNetworkId); + + if (associatedNetwork != null) { + _accountMgr.checkAccess(caller, null, false, associatedNetwork); + sc.setParameters("associatedNetworkIdEq", associatedNetworkId); + } } + if (vpcId != null) { - _accountMgr.checkAccess(caller, null, false, _vpcDao.findById(vpcId)); - sc.setParameters("vpcId", vpcId); + VpcVO vpc = _vpcDao.findById(vpcId); + + if (vpc != null) { + _accountMgr.checkAccess(caller, null, false, vpc); + sc.setParameters("vpcId", vpcId); + } } addrs = _publicIpAddressDao.search(sc, searchFilter); // Allocated @@ -2673,12 +2683,15 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe } if (associatedNetworkId != null) { NetworkVO guestNetwork = networkDao.findById(associatedNetworkId); - if (zoneId == null) { - zoneId = guestNetwork.getDataCenterId(); - } else if (zoneId != guestNetwork.getDataCenterId()) { - throw new InvalidParameterValueException("Please specify a valid associated network id in the specified zone."); + + if (guestNetwork != null) { + if (zoneId == null) { + zoneId = guestNetwork.getDataCenterId(); + } else if (zoneId != guestNetwork.getDataCenterId()) { + throw new InvalidParameterValueException("Please specify a valid associated network id in the specified zone."); + } + owner = _accountDao.findById(guestNetwork.getAccountId()); } - owner = _accountDao.findById(guestNetwork.getAccountId()); } List dcList = new ArrayList<>(); if (zoneId == null){ diff --git a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java index a6714109395..67f7128e864 100755 --- a/server/src/main/java/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/main/java/com/cloud/template/TemplateManagerImpl.java @@ -221,7 +221,6 @@ import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; -import com.google.common.base.Joiner; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -1395,9 +1394,16 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, else { vmInstanceVOList = _vmInstanceDao.listNonExpungedByTemplate(templateId); } - if(!cmd.isForced() && CollectionUtils.isNotEmpty(vmInstanceVOList)) { - final String message = String.format("Unable to delete Template: %s because Instance: [%s] are using it.", template, Joiner.on(",").join(vmInstanceVOList)); - logger.warn(message); + if (!cmd.isForced() && CollectionUtils.isNotEmpty(vmInstanceVOList)) { + String message = String.format("Unable to delete template [%s] because there are [%d] VM instances using it.", template, vmInstanceVOList.size()); + String instancesListMessage = String.format(" Instances list: [%s].", StringUtils.join(vmInstanceVOList, ",")); + + logger.warn("{}{}", message, instancesListMessage); + + if (_accountMgr.isRootAdmin(caller.getAccountId())) { + message += instancesListMessage; + } + throw new InvalidParameterValueException(message); } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index e7395b69131..7a0fb8248f7 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.user; +import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordChangeRequired; + import java.net.InetAddress; import java.net.URLEncoder; import java.security.NoSuchAlgorithmException; @@ -87,7 +89,6 @@ import org.apache.cloudstack.webhook.WebhookHelper; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -180,6 +181,7 @@ import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.UuidUtils; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; @@ -227,7 +229,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Inject private InstanceGroupDao _vmGroupDao; @Inject - private UserAccountDao _userAccountDao; + private UserAccountDao userAccountDao; @Inject private VolumeDao _volumeDao; @Inject @@ -591,11 +593,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (acct == null) { return false; //account is deleted or does not exist } - if ((isRootAdmin(accountId)) || (isDomainAdmin(accountId)) || (isResourceDomainAdmin(accountId))) { - return true; - } else if (acct.getType() == Account.Type.READ_ONLY_ADMIN) { - return true; - } + return (isRootAdmin(accountId)) || (isDomainAdmin(accountId)) || (isResourceDomainAdmin(accountId)) || (acct.getType() == Account.Type.READ_ONLY_ADMIN); } return false; @@ -650,10 +648,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public boolean isNormalUser(long accountId) { AccountVO acct = _accountDao.findById(accountId); - if (acct != null && acct.getType() == Account.Type.NORMAL) { - return true; - } - return false; + return acct != null && acct.getType() == Account.Type.NORMAL; } @Override @@ -684,10 +679,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (account == null) { return false; //account is deleted or does not exist } - if (isRootAdmin(accountId) || (account.getType() == Account.Type.ADMIN)) { - return true; - } - return false; + return isRootAdmin(accountId) || (account.getType() == Account.Type.ADMIN); } @Override @@ -718,7 +710,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M for (ControlledEntity entity : entities) { if (ownerId == null) { ownerId = entity.getAccountId(); - } else if (ownerId.longValue() != entity.getAccountId()) { + } else if (! ownerId.equals(entity.getAccountId())) { throw new PermissionDeniedException("Entity " + entity + " and entity " + prevEntity + " belong to different accounts"); } prevEntity = entity; @@ -744,7 +736,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M domainId = account != null ? account.getDomainId() : -1; } if (entity.getAccountId() != -1 && domainId != -1 && !(entity instanceof VirtualMachineTemplate) - && !(entity instanceof Network && accessType != null && (accessType == AccessType.UseEntry || accessType == AccessType.OperateEntry)) + && !(entity instanceof Network && (accessType == AccessType.UseEntry || accessType == AccessType.OperateEntry)) && !(entity instanceof AffinityGroup) && !(entity instanceof VirtualRouter) && !(entity instanceof DnsServer)) { List toBeChecked = domains.get(entity.getDomainId()); @@ -828,7 +820,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // Currently just for resource domain admin List dcList = _dcDao.findZonesByDomainId(account.getDomainId()); - if (dcList != null && dcList.size() != 0) { + if (CollectionUtils.isNotEmpty(dcList)) { return dcList.get(0).getId(); } else { throw new CloudRuntimeException("Failed to find any private zone for Resource domain admin."); @@ -843,23 +835,23 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public void doInTransactionWithoutResult(TransactionStatus status) { UserAccountVO user = null; - user = _userAccountDao.lockRow(id, true); + user = userAccountDao.lockRow(id, true); user.setLoginAttempts(attempts); if (toDisable) { user.setState(State.DISABLED.toString()); } - _userAccountDao.update(id, user); + userAccountDao.update(id, user); } }); } catch (Exception e) { - logger.error("Failed to update login attempts for user {}", () -> _userAccountDao.findById(id)); + logger.error("Failed to update login attempts for user {}", () -> userAccountDao.findById(id)); } } private boolean doSetUserStatus(long userId, State state) { UserVO userForUpdate = _userDao.createForUpdate(); userForUpdate.setState(state); - return _userDao.update(Long.valueOf(userId), userForUpdate); + return _userDao.update(userId, userForUpdate); } @Override @@ -868,7 +860,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M AccountVO acctForUpdate = _accountDao.createForUpdate(); acctForUpdate.setState(State.ENABLED); acctForUpdate.setNeedsCleanup(false); - success = _accountDao.update(Long.valueOf(accountId), acctForUpdate); + success = _accountDao.update(accountId, acctForUpdate); return success; } @@ -881,7 +873,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } else if (account.getState().equals(State.ENABLED)) { AccountVO acctForUpdate = _accountDao.createForUpdate(); acctForUpdate.setState(State.LOCKED); - success = _accountDao.update(Long.valueOf(accountId), acctForUpdate); + success = _accountDao.update(accountId, acctForUpdate); } else { if (logger.isInfoEnabled()) { logger.info("Attempting to lock a non-enabled account {}, current state is {}, locking failed.", account, account.getState()); @@ -995,7 +987,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } // Destroy VM Snapshots - List vmSnapshots = _vmSnapshotDao.listByAccountId(Long.valueOf(accountId)); + List vmSnapshots = _vmSnapshotDao.listByAccountId(accountId); for (VMSnapshot vmSnapshot : vmSnapshots) { try { _vmSnapshotMgr.deleteVMSnapshot(vmSnapshot.getId()); @@ -1017,8 +1009,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M try { _vmMgr.destroyVm(vm.getId(), false); } catch (Exception e) { - e.printStackTrace(); - logger.warn("Failed destroying instance {} as part of account deletion.", vm); + logger.warn("Failed destroying instance {} as part of account deletion.", vm, e); } } // no need to catch exception at this place as expunging vm @@ -1076,7 +1067,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M logger.debug("Deleting networks for account {}", account); List networks = _networkDao.listByOwner(accountId); if (networks != null) { - Collections.sort(networks, new Comparator<>() { + networks.sort(new Comparator<>() { @Override public int compare(NetworkVO network1, NetworkVO network2) { if (network1.getGuestType() != network2.getGuestType() && Network.GuestType.Isolated.equals(network2.getGuestType())) { @@ -1247,7 +1238,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } else { AccountVO acctForUpdate = _accountDao.createForUpdate(); acctForUpdate.setState(State.DISABLED); - success = _accountDao.update(Long.valueOf(accountId), acctForUpdate); + success = _accountDao.update(accountId, acctForUpdate); if (success) { boolean disableAccountResult = false; @@ -1341,11 +1332,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // Check permissions checkAccess(getCurrentCallingAccount(), domain); - if (!userAllowMultipleAccounts.valueInScope(ConfigKey.Scope.Domain, domainId) && !_userAccountDao.validateUsernameInDomain(userName, domainId)) { + if (!userAllowMultipleAccounts.valueInScope(ConfigKey.Scope.Domain, domainId) && !userAccountDao.validateUsernameInDomain(userName, domainId)) { throw new InvalidParameterValueException(String.format("The user %s already exists in domain %s", userName, domain)); } - if (networkDomain != null && networkDomain.length() > 0) { + if (StringUtils.isNotEmpty(networkDomain)) { if (!NetUtils.verifyDomainName(networkDomain)) { throw new InvalidParameterValueException( "Invalid network domain. Total length shouldn't exceed 190 chars. Each domain label must be between 1 and 63 characters long, can contain ASCII letters 'a' through 'z', the digits '0' through '9', " @@ -1397,7 +1388,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M CallContext.current().putContextParameter(User.class, userId); // check success - return _userAccountDao.findById(userId); + return userAccountDao.findById(userId); } /* @@ -1511,12 +1502,24 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override @ActionEvent(eventType = EventTypes.EVENT_USER_CREATE, eventDescription = "creating User") public UserVO createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID, - User.Source source) { + User.Source source) { + return createUser(userName, password, firstName, lastName, email, timeZone, accountName, domainId, userUUID, source, false); + } + + + @ActionEvent(eventType = EventTypes.EVENT_USER_CREATE, eventDescription = "creating User") + public UserVO createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID, + User.Source source, boolean isPasswordChangeRequired) { // default domain to ROOT if not specified if (domainId == null) { domainId = Domain.ROOT_DOMAIN; } + if (isPasswordChangeRequired && (source == User.Source.SAML2 || source == User.Source.SAML2DISABLED || source == User.Source.LDAP)) { + logger.warn("Enforcing password change is not permitted for source [{}].", source); + throw new InvalidParameterValueException("CloudStack does not support enforcing password change for SAML or LDAP users."); + } + Domain domain = _domainMgr.getDomain(domainId); if (domain == null) { throw new CloudRuntimeException("The domain " + domainId + " does not exist; unable to create user"); @@ -1535,7 +1538,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M throw new PermissionDeniedException(String.format("Account: %s is a system account, can't add a user to it", account)); } - if (!userAllowMultipleAccounts.valueInScope(ConfigKey.Scope.Domain, domainId) && !_userAccountDao.validateUsernameInDomain(userName, domainId)) { + if (!userAllowMultipleAccounts.valueInScope(ConfigKey.Scope.Domain, domainId) && !userAccountDao.validateUsernameInDomain(userName, domainId)) { throw new CloudRuntimeException("The user " + userName + " already exists in domain " + domainId); } List duplicatedUsers = _userDao.findUsersByName(userName); @@ -1547,14 +1550,21 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M verifyCallerPrivilegeForUserOrAccountOperations(account); UserVO user; user = createUser(account.getId(), userName, password, firstName, lastName, email, timeZone, userUUID, source); + if (isPasswordChangeRequired) { + long callerAccountId = CallContext.current().getCallingAccountId(); + if ((isRootAdmin(callerAccountId) || isDomainAdmin(callerAccountId))) { + _userDetailsDao.addDetail(user.getId(), PasswordChangeRequired, "true", false); + } + } return user; } @Override @ActionEvent(eventType = EventTypes.EVENT_USER_CREATE, eventDescription = "creating User") - public UserVO createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID) { + public UserVO createUser(String userName, String password, String firstName, String lastName, String email, + String timeZone, String accountName, Long domainId, String userUUID, boolean isPasswordChangeRequired) { - return createUser(userName, password, firstName, lastName, email, timeZone, accountName, domainId, userUUID, User.Source.UNKNOWN); + return createUser(userName, password, firstName, lastName, email, timeZone, accountName, domainId, userUUID, User.Source.UNKNOWN, isPasswordChangeRequired); } @Override @@ -1588,8 +1598,39 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (mandate2FA != null && mandate2FA) { user.setUser2faEnabled(true); } + validateAndUpdatePasswordChangeRequired(caller, updateUserCmd, user, account); _userDao.update(user.getId(), user); - return _userAccountDao.findById(user.getId()); + return userAccountDao.findById(user.getId()); + } + + private void validateAndUpdatePasswordChangeRequired(User caller, UpdateUserCmd updateUserCmd, UserVO user, Account account) { + if (updateUserCmd.isPasswordChangeRequired()) { + if (user.getState() != State.ENABLED || account.getState() != State.ENABLED) { + throw new CloudRuntimeException("CloudStack does not support enforcing password change for locked/disabled User or Account."); + } + + User.Source userSource = user.getSource(); + if (userSource == User.Source.SAML2 || userSource == User.Source.SAML2DISABLED || userSource == User.Source.LDAP) { + logger.warn("Enforcing password change is not permitted for source [{}].", user.getSource()); + throw new InvalidParameterValueException("CloudStack does not support enforcing password change for SAML or LDAP users."); + } + } + + boolean isCallerSameAsUser = user.getId() == caller.getId(); + boolean isPasswordResetRequired = updateUserCmd.isPasswordChangeRequired() && !isCallerSameAsUser; + // Admins only can enforce passwordChangeRequired for user + if (isRootAdmin(caller.getAccountId()) || isDomainAdmin(caller.getAccountId())) { + if (isPasswordResetRequired) { + _userDetailsDao.addDetail(user.getId(), PasswordChangeRequired, "true", false); + } + } + + if (StringUtils.isNotBlank(updateUserCmd.getPassword())) { + // Remove passwordChangeRequired if user updating own pwd or admin has not enforced it + if (isCallerSameAsUser || !isPasswordResetRequired) { + _userDetailsDao.removeDetail(user.getId(), PasswordChangeRequired); + } + } } @Override @@ -1871,10 +1912,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (isApiKeyBlank && isSecretKeyBlank) { return; } - Pair apiKeyOwner = _accountDao.findUserAccountByApiKey(apiKey); + UserAccount apiKeyOwner = userAccountDao.getUserByApiKey(apiKey); if (apiKeyOwner != null) { - User userThatHasTheProvidedApiKey = apiKeyOwner.first(); - if (userThatHasTheProvidedApiKey.getId() != user.getId()) { + if (apiKeyOwner.getId() != user.getId()) { throw new InvalidParameterValueException(String.format("The API key [%s] already exists in the system. Please provide a unique key.", apiKey)); } } @@ -1962,7 +2002,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M CallContext.current().putContextParameter(User.class, user.getUuid()); // user successfully disabled - return _userAccountDao.findById(userId); + return userAccountDao.findById(userId); } else { throw new CloudRuntimeException(String.format("Unable to disable user %s", user)); } @@ -2016,7 +2056,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M CallContext.current().putContextParameter(User.class, user.getUuid()); - return _userAccountDao.findById(userId); + return userAccountDao.findById(userId); } else { throw new CloudRuntimeException(String.format("Unable to enable user %s", user)); } @@ -2057,7 +2097,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M boolean success; if (user.getState().equals(State.LOCKED)) { // already locked...no-op - return _userAccountDao.findById(userId); + return userAccountDao.findById(userId); } else if (user.getState().equals(State.ENABLED)) { success = doSetUserStatus(user.getId(), State.LOCKED); @@ -2084,7 +2124,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M CallContext.current().putContextParameter(User.class, user.getUuid()); - return _userAccountDao.findById(userId); + return userAccountDao.findById(userId); } else { throw new CloudRuntimeException(String.format("Unable to lock user %s", user)); } @@ -2612,7 +2652,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return owner; } else if (!isAdmin(caller.getId()) && accountName != null && domainId != null) { - if (!accountName.equals(caller.getAccountName()) || domainId.longValue() != caller.getDomainId()) { + if (!accountName.equals(caller.getAccountName()) || domainId != caller.getDomainId()) { throw new PermissionDeniedException("Can't create/list resources for account " + accountName + " in domain " + domainId + ", permission denied"); } else { return caller; @@ -2637,12 +2677,12 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public UserAccount getActiveUserAccount(String username, Long domainId) { - return _userAccountDao.getUserAccount(username, domainId); + return userAccountDao.getUserAccount(username, domainId); } @Override public List getActiveUserAccountByEmail(String email, Long domainId) { - List userAccountByEmail = _userAccountDao.getUserAccountByEmail(email, domainId); + List userAccountByEmail = userAccountDao.getUserAccountByEmail(email, domainId); List userAccounts = userAccountByEmail.stream() .map(userAccountVO -> (UserAccount) userAccountVO) .collect(Collectors.toList()); @@ -2686,7 +2726,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M public void markUserRegistered(long userId) { UserVO userForUpdate = _userDao.createForUpdate(); userForUpdate.setRegistered(true); - _userDao.update(Long.valueOf(userId), userForUpdate); + _userDao.update(userId, userForUpdate); } @Override @@ -2741,7 +2781,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M throw new CloudRuntimeException(String.format("Failed to create account name %s in domain id=%s", accountName, _domainMgr.getDomain(domainId))); } - Long accountId = account.getId(); + long accountId = account.getId(); if (details != null) { _accountDetailsDao.persist(accountId, details); @@ -2790,7 +2830,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public void logoutUser(long userId) { - UserAccount userAcct = _userAccountDao.findById(userId); + UserAccount userAcct = userAccountDao.findById(userId); if (userAcct != null) { ActionEventUtils.onActionEvent(userId, userAcct.getAccountId(), userAcct.getDomainId(), EventTypes.EVENT_USER_LOGOUT, "user has logged out", userId, ApiCommandResourceType.User.toString()); } // else log some kind of error event? This likely means the user doesn't exist, or has been deleted... @@ -2832,11 +2872,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M final Boolean ApiSourceCidrChecksEnabled = ApiServiceConfiguration.ApiSourceCidrChecksEnabled.value(); if (ApiSourceCidrChecksEnabled) { - logger.debug("CIDRs from which account '" + account.toString() + "' is allowed to perform API calls: " + accessAllowedCidrs); + logger.debug("CIDRs from which account '{}' is allowed to perform API calls: {}", account.toString(), accessAllowedCidrs); // Block when is not in the list of allowed IPs if (!NetUtils.isIpInCidrList(loginIpAddress, accessAllowedCidrs.split(","))) { - logger.warn("Request by account '" + account.toString() + "' was denied since " + loginIpAddress.toString().replace("/", "") + " does not match " + accessAllowedCidrs); + logger.warn("Request by account '{}' was denied since {} does not match {}", account.toString(), loginIpAddress.toString().replace("/", ""), accessAllowedCidrs); throw new CloudAuthenticationException("Failed to authenticate user '" + username + "' in domain '" + domain.getPath() + "' from ip " + loginIpAddress.toString().replace("/", "") + "; please provide valid credentials"); } @@ -2850,6 +2890,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M logger.debug(String.format("User: %s in domain %d has successfully logged in, auth time duration - %d ms", username, domainId, validUserLastAuthTimeDurationInMs)); } + user.setDetails(_userDetailsDao.listDetailsKeyPairs(user.getId())); + return user; } else { if (logger.isDebugEnabled()) { @@ -2868,6 +2910,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M try { Thread.sleep(waitTimeDurationInMs); } catch (final InterruptedException e) { + // ignored } } @@ -2879,7 +2922,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (logger.isDebugEnabled()) { logger.debug("Attempting to log in user: " + username + " in domain " + domainId); } - UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId); + UserAccount userAccount = userAccountDao.getUserAccount(username, domainId); boolean authenticated = false; HashSet actionsOnFailedAuthenticaion = new HashSet<>(); @@ -2909,11 +2952,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (authenticated) { Domain domain = _domainMgr.getDomain(domainId); - String domainName = null; - if (domain != null) { - domainName = domain.getName(); - } - userAccount = _userAccountDao.getUserAccount(username, domainId); + userAccount = userAccountDao.getUserAccount(username, domainId); if (!userAccount.getState().equalsIgnoreCase(Account.State.ENABLED.toString()) || !userAccount.getAccountState().equalsIgnoreCase(Account.State.ENABLED.toString())) { if (logger.isInfoEnabled()) { @@ -2973,11 +3012,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // - build a request string with sorted params, make sure it's all lowercase // - sign the request, verify the signature is the same - List parameterNames = new ArrayList<>(); - for (Object paramNameObj : requestParameters.keySet()) { - parameterNames.add((String)paramNameObj); // put the name in a list that we'll sort later - } + // put the name in a list that we'll sort later + List parameterNames = new ArrayList<>(requestParameters.keySet()); Collections.sort(parameterNames); @@ -3009,7 +3046,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (unsignedRequestBuffer.length() != 0) { unsignedRequestBuffer.append("&"); } - unsignedRequestBuffer.append(paramName).append("=").append(URLEncoder.encode(paramValue, "UTF-8")); + unsignedRequestBuffer.append(paramName).append("=").append(URLEncoder.encode(paramValue, com.cloud.utils.StringUtils.getPreferredCharset())); } } @@ -3032,7 +3069,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (!equalSig) { logger.info("User signature: " + signature + " is not equaled to computed signature: " + computedSignature); } else { - user = _userAccountDao.getUserAccount(username, domainId); + user = userAccountDao.getUserAccount(username, domainId); } } catch (Exception ex) { logger.error("Exception authenticating user", ex); @@ -3060,7 +3097,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public Pair findUserByApiKey(String apiKey) { - return _accountDao.findUserAccountByApiKey(apiKey); + UserAccount userAccount = userAccountDao.getUserByApiKey(apiKey); + if (userAccount != null) { + User user = _userDao.getUser(userAccount.getId()); + Account account = _accountDao.findById(userAccount.getAccountId()); + return new Pair<>(user, account); + } else { + return null; + } } @Override @@ -3194,14 +3238,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M UserVO updatedUser = _userDao.createForUpdate(); String encodedKey; - Pair userAcct; + UserAccount userAcct; int retryLimit = 10; do { // FIXME: what algorithm should we use for API keys? KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1"); SecretKey key = generator.generateKey(); encodedKey = Base64.encodeBase64URLSafeString(key.getEncoded()); - userAcct = _accountDao.findUserAccountByApiKey(encodedKey); + userAcct = userAccountDao.getUserByApiKey(encodedKey); retryLimit--; } while ((userAcct != null) && (retryLimit >= 0)); @@ -3212,7 +3256,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M _userDao.update(userId, updatedUser); return encodedKey; } catch (NoSuchAlgorithmException ex) { - logger.error("error generating secret key for user {}", _userAccountDao.findById(userId), ex); + logger.error("error generating secret key for user {}", userAccountDao.findById(userId), ex); } return null; } @@ -3239,7 +3283,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M _userDao.update(userId, updatedUser); return encodedKey; } catch (NoSuchAlgorithmException ex) { - logger.error("error generating secret key for user {}", _userAccountDao.findById(userId), ex); + logger.error("error generating secret key for user {}", userAccountDao.findById(userId), ex); } return null; } @@ -3450,12 +3494,12 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public UserAccount getUserByApiKey(String apiKey) { - return _userAccountDao.getUserByApiKey(apiKey); + return userAccountDao.getUserByApiKey(apiKey); } @Override public List listAclGroupsByAccount(Long accountId) { - if (_querySelectors == null || _querySelectors.size() == 0) { + if (CollectionUtils.isEmpty(_querySelectors)) { return new ArrayList<>(); } @@ -3510,7 +3554,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public UserAccount getUserAccountById(Long userId) { - UserAccount userAccount = _userAccountDao.findById(userId); + UserAccount userAccount = userAccountDao.findById(userId); Map details = _userDetailsDao.listDetailsKeyPairs(userId); userAccount.setDetails(details); @@ -3699,7 +3743,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } protected UserTwoFactorAuthenticationSetupResponse enableTwoFactorAuthentication(Long userId, String providerName) { - UserAccountVO userAccount = _userAccountDao.findById(userId); + UserAccountVO userAccount = userAccountDao.findById(userId); UserVO userVO = _userDao.findById(userId); Long domainId = userAccount.getDomainId(); if (Boolean.FALSE.equals(enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.FALSE.equals(mandateUserTwoFactorAuthentication.valueIn(domainId))) { @@ -3791,11 +3835,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (userDetailVO != null) { _userDetailsDao.remove(userDetailVO.getId()); } - UserAccountVO userAccountVO = _userAccountDao.findById(user.getId()); + UserAccountVO userAccountVO = userAccountDao.findById(user.getId()); userAccountVO.setUser2faEnabled(false); userAccountVO.setUser2faProvider(null); userAccountVO.setKeyFor2fa(null); - _userAccountDao.update(user.getId(), userAccountVO); + userAccountDao.update(user.getId(), userAccountVO); return userAccountVO; }); } diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index f2c17283086..ed8391fe0c6 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -1728,10 +1728,10 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { if (StringUtils.isEmpty(name)) { throw new CloudRuntimeException("Invalid backup provider name provided"); } - if (!backupProvidersMap.containsKey(name)) { - throw new CloudRuntimeException("Failed to find backup provider by the name: " + name); - } - return backupProvidersMap.get(name); + if (!backupProvidersMap.containsKey(name)) { + throw new CloudRuntimeException("Failed to find backup provider by the name: " + name); + } + return backupProvidersMap.get(name); } @Override diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java index 2e0780e7fe8..beecf90d2b8 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelper.java @@ -139,23 +139,23 @@ public class HeuristicRuleHelper { * @param presetVariables used for injecting in the JS interpreter. */ protected void injectPresetVariables(JsInterpreter jsInterpreter, PresetVariables presetVariables) { - jsInterpreter.injectVariable("secondaryStorages", presetVariables.getSecondaryStorages().toString()); + jsInterpreter.injectVariable("secondaryStorages", presetVariables.getSecondaryStorages()); if (presetVariables.getTemplate() != null) { - jsInterpreter.injectVariable("template", presetVariables.getTemplate().toString()); - jsInterpreter.injectVariable("iso", presetVariables.getTemplate().toString()); + jsInterpreter.injectVariable("template", presetVariables.getTemplate()); + jsInterpreter.injectVariable("iso", presetVariables.getTemplate()); } if (presetVariables.getSnapshot() != null) { - jsInterpreter.injectVariable("snapshot", presetVariables.getSnapshot().toString()); + jsInterpreter.injectVariable("snapshot", presetVariables.getSnapshot()); } if (presetVariables.getVolume() != null) { - jsInterpreter.injectVariable("volume", presetVariables.getVolume().toString()); + jsInterpreter.injectVariable("volume", presetVariables.getVolume()); } if (presetVariables.getAccount() != null) { - jsInterpreter.injectVariable("account", presetVariables.getAccount().toString()); + jsInterpreter.injectVariable("account", presetVariables.getAccount()); } } @@ -185,8 +185,8 @@ public class HeuristicRuleHelper { Template template = new Template(); template.setName(templateVO.getName()); - template.setFormat(templateVO.getFormat()); - template.setHypervisorType(templateVO.getHypervisorType()); + template.setFormat(templateVO.getFormat().toString()); + template.setHypervisorType(templateVO.getHypervisorType().toString()); return template; } @@ -195,7 +195,7 @@ public class HeuristicRuleHelper { Volume volumePresetVariable = new Volume(); volumePresetVariable.setName(volumeVO.getName()); - volumePresetVariable.setFormat(volumeVO.getFormat()); + volumePresetVariable.setFormat(volumeVO.getFormat().toString()); volumePresetVariable.setSize(volumeVO.getSize()); return volumePresetVariable; @@ -206,7 +206,7 @@ public class HeuristicRuleHelper { snapshot.setName(snapshotInfo.getName()); snapshot.setSize(snapshotInfo.getSize()); - snapshot.setHypervisorType(snapshotInfo.getHypervisorType()); + snapshot.setHypervisorType(snapshotInfo.getHypervisorType().toString()); return snapshot; } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Account.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Account.java index 67750e8ec5c..f4652d87e7b 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Account.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Account.java @@ -27,7 +27,6 @@ public class Account extends GenericHeuristicPresetVariable { public void setId(String id) { this.id = id; - fieldNamesToIncludeInToString.add("id"); } public Domain getDomain() { @@ -36,6 +35,5 @@ public class Account extends GenericHeuristicPresetVariable { public void setDomain(Domain domain) { this.domain = domain; - fieldNamesToIncludeInToString.add("domain"); } } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Domain.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Domain.java index 704cbf4373e..0b01604e673 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Domain.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Domain.java @@ -25,6 +25,5 @@ public class Domain extends GenericHeuristicPresetVariable { public void setId(String id) { this.id = id; - fieldNamesToIncludeInToString.add("id"); } } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariable.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariable.java index b85b7763eee..56c7d48e68a 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariable.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariable.java @@ -16,15 +16,11 @@ // under the License. package org.apache.cloudstack.storage.heuristics.presetvariables; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; - -import java.util.HashSet; -import java.util.Set; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; public class GenericHeuristicPresetVariable { - protected transient Set fieldNamesToIncludeInToString = new HashSet<>(); - private String name; public String getName() { @@ -33,15 +29,10 @@ public class GenericHeuristicPresetVariable { public void setName(String name) { this.name = name; - fieldNamesToIncludeInToString.add("name"); } - /*** - * Converts the preset variable into a valid JSON object that will be injected into the JS interpreter. - * This method should not be overridden or changed. - */ @Override - public final String toString() { - return ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, fieldNamesToIncludeInToString.toArray(new String[0])); + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); } } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/SecondaryStorage.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/SecondaryStorage.java index ad7058d8336..e3a8dac43c6 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/SecondaryStorage.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/SecondaryStorage.java @@ -32,7 +32,6 @@ public class SecondaryStorage extends GenericHeuristicPresetVariable { public void setId(String id) { this.id = id; - fieldNamesToIncludeInToString.add("id"); } public Long getUsedDiskSize() { @@ -41,7 +40,6 @@ public class SecondaryStorage extends GenericHeuristicPresetVariable { public void setUsedDiskSize(Long usedDiskSize) { this.usedDiskSize = usedDiskSize; - fieldNamesToIncludeInToString.add("usedDiskSize"); } public Long getTotalDiskSize() { @@ -50,7 +48,6 @@ public class SecondaryStorage extends GenericHeuristicPresetVariable { public void setTotalDiskSize(Long totalDiskSize) { this.totalDiskSize = totalDiskSize; - fieldNamesToIncludeInToString.add("totalDiskSize"); } public String getProtocol() { @@ -59,6 +56,5 @@ public class SecondaryStorage extends GenericHeuristicPresetVariable { public void setProtocol(String protocol) { this.protocol = protocol; - fieldNamesToIncludeInToString.add("protocol"); } } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Snapshot.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Snapshot.java index 34acd394dbe..404db3cdebc 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Snapshot.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Snapshot.java @@ -16,13 +16,11 @@ // under the License. package org.apache.cloudstack.storage.heuristics.presetvariables; -import com.cloud.hypervisor.Hypervisor; - public class Snapshot extends GenericHeuristicPresetVariable { private Long size; - private Hypervisor.HypervisorType hypervisorType; + private String hypervisorType; public Long getSize() { return size; @@ -30,15 +28,13 @@ public class Snapshot extends GenericHeuristicPresetVariable { public void setSize(Long size) { this.size = size; - fieldNamesToIncludeInToString.add("size"); } - public Hypervisor.HypervisorType getHypervisorType() { + public String getHypervisorType() { return hypervisorType; } - public void setHypervisorType(Hypervisor.HypervisorType hypervisorType) { + public void setHypervisorType(String hypervisorType) { this.hypervisorType = hypervisorType; - fieldNamesToIncludeInToString.add("hypervisorType"); } } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Template.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Template.java index 297c95fad9a..c6df349010f 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Template.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Template.java @@ -16,41 +16,35 @@ // under the License. package org.apache.cloudstack.storage.heuristics.presetvariables; -import com.cloud.hypervisor.Hypervisor; -import com.cloud.storage.Storage; - public class Template extends GenericHeuristicPresetVariable { - private Hypervisor.HypervisorType hypervisorType; + private String hypervisorType; - private Storage.ImageFormat format; + private String format; - private Storage.TemplateType templateType; + private String templateType; - public Hypervisor.HypervisorType getHypervisorType() { + public String getHypervisorType() { return hypervisorType; } - public void setHypervisorType(Hypervisor.HypervisorType hypervisorType) { + public void setHypervisorType(String hypervisorType) { this.hypervisorType = hypervisorType; - fieldNamesToIncludeInToString.add("hypervisorType"); } - public Storage.ImageFormat getFormat() { + public String getFormat() { return format; } - public void setFormat(Storage.ImageFormat format) { + public void setFormat(String format) { this.format = format; - fieldNamesToIncludeInToString.add("format"); } - public Storage.TemplateType getTemplateType() { + public String getTemplateType() { return templateType; } - public void setTemplateType(Storage.TemplateType templateType) { + public void setTemplateType(String templateType) { this.templateType = templateType; - fieldNamesToIncludeInToString.add("templateType"); } } diff --git a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Volume.java b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Volume.java index 4e5e81b117f..8f571a57209 100644 --- a/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Volume.java +++ b/server/src/main/java/org/apache/cloudstack/storage/heuristics/presetvariables/Volume.java @@ -16,13 +16,11 @@ // under the License. package org.apache.cloudstack.storage.heuristics.presetvariables; -import com.cloud.storage.Storage; - public class Volume extends GenericHeuristicPresetVariable { private Long size; - private Storage.ImageFormat format; + private String format; public Long getSize() { return size; @@ -30,15 +28,13 @@ public class Volume extends GenericHeuristicPresetVariable { public void setSize(Long size) { this.size = size; - fieldNamesToIncludeInToString.add("size"); } - public Storage.ImageFormat getFormat() { + public String getFormat() { return format; } - public void setFormat(Storage.ImageFormat format) { + public void setFormat(String format) { this.format = format; - fieldNamesToIncludeInToString.add("format"); } } diff --git a/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java b/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java index c62bca8eca4..d6b5dbb18f9 100644 --- a/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/user/UserPasswordResetManagerImpl.java @@ -50,6 +50,7 @@ import java.util.Set; import java.util.UUID; import static org.apache.cloudstack.config.ApiServiceConfiguration.ManagementServerAddresses; +import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordChangeRequired; import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetToken; import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetTokenExpiryDate; @@ -265,6 +266,8 @@ public class UserPasswordResetManagerImpl extends ManagerBase implements UserPas userDetailsDao.removeDetail(userAccount.getId(), PasswordResetToken); userDetailsDao.removeDetail(userAccount.getId(), PasswordResetTokenExpiryDate); + // remove password change required if user reset password + userDetailsDao.removeDetail(userAccount.getId(), PasswordChangeRequired); userDao.persist(user); } diff --git a/server/src/test/java/com/cloud/api/ApiServerTest.java b/server/src/test/java/com/cloud/api/ApiServerTest.java index dedd6e02ec5..2caf6bf9fae 100644 --- a/server/src/test/java/com/cloud/api/ApiServerTest.java +++ b/server/src/test/java/com/cloud/api/ApiServerTest.java @@ -17,11 +17,21 @@ package com.cloud.api; import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.DomainManager; import com.cloud.user.User; import com.cloud.user.UserAccount; +import com.cloud.user.UserVO; import com.cloud.utils.exception.CloudRuntimeException; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.response.LoginCmdResponse; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.resourcedetail.UserDetailVO; import org.apache.cloudstack.user.UserPasswordResetManager; import org.junit.AfterClass; import org.junit.Assert; @@ -35,10 +45,22 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import java.lang.reflect.Field; +import java.net.InetAddress; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import static org.apache.cloudstack.api.ApiConstants.PASSWORD_CHANGE_REQUIRED; import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; + +import javax.servlet.http.HttpSession; @RunWith(MockitoJUnitRunner.class) public class ApiServerTest { @@ -49,6 +71,15 @@ public class ApiServerTest { @Mock UserPasswordResetManager userPasswordResetManager; + @Mock + DomainManager domainManager; + + @Mock + AccountManager accountManager; + + @Mock + HttpSession session; + @BeforeClass public static void beforeClass() throws Exception { overrideDefaultConfigValue(UserPasswordResetEnabled, "_value", true); @@ -96,8 +127,8 @@ public class ApiServerTest { @Test public void testForgotPasswordSuccess() { - UserAccount userAccount = Mockito.mock(UserAccount.class); - Domain domain = Mockito.mock(Domain.class); + UserAccount userAccount = mock(UserAccount.class); + Domain domain = mock(Domain.class); Mockito.when(userAccount.getEmail()).thenReturn("test@test.com"); Mockito.when(userAccount.getState()).thenReturn("ENABLED"); @@ -110,8 +141,8 @@ public class ApiServerTest { @Test(expected = CloudRuntimeException.class) public void testForgotPasswordFailureNoEmail() { - UserAccount userAccount = Mockito.mock(UserAccount.class); - Domain domain = Mockito.mock(Domain.class); + UserAccount userAccount = mock(UserAccount.class); + Domain domain = mock(Domain.class); Mockito.when(userAccount.getEmail()).thenReturn(""); apiServer.forgotPassword(userAccount, domain); @@ -119,8 +150,8 @@ public class ApiServerTest { @Test(expected = CloudRuntimeException.class) public void testForgotPasswordFailureDisabledUser() { - UserAccount userAccount = Mockito.mock(UserAccount.class); - Domain domain = Mockito.mock(Domain.class); + UserAccount userAccount = mock(UserAccount.class); + Domain domain = mock(Domain.class); Mockito.when(userAccount.getEmail()).thenReturn("test@test.com"); Mockito.when(userAccount.getState()).thenReturn("DISABLED"); @@ -129,8 +160,8 @@ public class ApiServerTest { @Test(expected = CloudRuntimeException.class) public void testForgotPasswordFailureDisabledAccount() { - UserAccount userAccount = Mockito.mock(UserAccount.class); - Domain domain = Mockito.mock(Domain.class); + UserAccount userAccount = mock(UserAccount.class); + Domain domain = mock(Domain.class); Mockito.when(userAccount.getEmail()).thenReturn("test@test.com"); Mockito.when(userAccount.getState()).thenReturn("ENABLED"); @@ -140,8 +171,8 @@ public class ApiServerTest { @Test(expected = CloudRuntimeException.class) public void testForgotPasswordFailureInactiveDomain() { - UserAccount userAccount = Mockito.mock(UserAccount.class); - Domain domain = Mockito.mock(Domain.class); + UserAccount userAccount = mock(UserAccount.class); + Domain domain = mock(Domain.class); Mockito.when(userAccount.getEmail()).thenReturn("test@test.com"); Mockito.when(userAccount.getState()).thenReturn("ENABLED"); @@ -153,8 +184,8 @@ public class ApiServerTest { @Test public void testVerifyApiKeyAccessAllowed() { Long domainId = 1L; - User user = Mockito.mock(User.class); - Account account = Mockito.mock(Account.class); + User user = mock(User.class); + Account account = mock(Account.class); Mockito.when(user.getApiKeyAccess()).thenReturn(true); Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); @@ -176,4 +207,73 @@ public class ApiServerTest { Mockito.when(account.getApiKeyAccess()).thenReturn(null); Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account)); } + + @Test + public void testLoginUserSuccess() throws Exception { + String username = "user"; + String password = "password"; + Long domainId = 1L; + String domainPath = "/"; + InetAddress loginIp = InetAddress.getByName("127.0.0.1"); + Map requestParams = new HashMap<>(); + + DomainVO domain = mock(DomainVO.class); + Mockito.when(domain.getId()).thenReturn(domainId); + Mockito.when(domain.getUuid()).thenReturn("domain-uuid"); + + Mockito.when(domainManager.findDomainByIdOrPath(domainId, domainPath)).thenReturn(domain); + Mockito.when(domainManager.getDomain(domainId)).thenReturn(domain); + + UserAccount userAccount = mock(UserAccount.class); + Mockito.when(userAccount.getId()).thenReturn(100L); + Mockito.when(userAccount.getAccountId()).thenReturn(200L); + Mockito.when(userAccount.getUsername()).thenReturn(username); + Mockito.when(userAccount.getFirstname()).thenReturn("First"); + Mockito.when(userAccount.getLastname()).thenReturn("Last"); + Mockito.when(userAccount.getTimezone()).thenReturn("UTC"); + Mockito.when(userAccount.getRegistrationToken()).thenReturn("token"); + Mockito.when(userAccount.isRegistered()).thenReturn(true); + Mockito.when(userAccount.getDomainId()).thenReturn(domainId); + Map userAccDetails = new HashMap<>(); + userAccDetails.put(UserDetailVO.PasswordChangeRequired, "true"); + Mockito.when(userAccount.getDetails()).thenReturn(userAccDetails); + + Mockito.when(accountManager.authenticateUser(username, password, domainId, loginIp, requestParams)).thenReturn(userAccount); + Mockito.when(accountManager.clearUserTwoFactorAuthenticationInSetupStateOnLogin(userAccount)).thenReturn(userAccount); + + Account account = mock(Account.class); + Mockito.when(account.getAccountName()).thenReturn("account"); + Mockito.when(account.getDomainId()).thenReturn(domainId); + Mockito.when(account.getType()).thenReturn(Account.Type.NORMAL); + Mockito.when(account.getType()).thenReturn(Account.Type.NORMAL); + Mockito.when(accountManager.getAccount(200L)).thenReturn(account); + + UserVO userVO = mock(UserVO.class); + Mockito.when(userVO.getUuid()).thenReturn("user-uuid"); + Mockito.when(accountManager.getActiveUser(100L)).thenReturn(userVO); + + Mockito.when(session.getAttributeNames()).thenReturn(Collections.enumeration(List.of(PASSWORD_CHANGE_REQUIRED))); + Mockito.when(session.getAttribute(PASSWORD_CHANGE_REQUIRED)).thenReturn(Boolean.TRUE); + + ResponseObject response = apiServer.loginUser(session, username, password, domainId, domainPath, loginIp, requestParams); + Assert.assertNotNull(response); + Assert.assertTrue(response instanceof LoginCmdResponse); + Mockito.verify(session).setAttribute(eq("userid"), eq(100L)); + Mockito.verify(session).setAttribute(eq(ApiConstants.SESSIONKEY), anyString()); + } + + @Test(expected = CloudAuthenticationException.class) + public void testLoginUserDomainNotFound() throws Exception { + Mockito.when(domainManager.findDomainByIdOrPath(anyLong(), anyString())).thenReturn(null); + apiServer.loginUser(session, "user", "pass", 1L, "/", null, null); + } + + @Test(expected = CloudAuthenticationException.class) + public void testLoginUserAuthFailed() throws Exception { + DomainVO domain = mock(DomainVO.class); + Mockito.when(domain.getId()).thenReturn(1L); + Mockito.when(domainManager.findDomainByIdOrPath(anyLong(), anyString())).thenReturn(domain); + Mockito.when(accountManager.authenticateUser(anyString(), anyString(), anyLong(), any(), any())).thenReturn(null); + apiServer.loginUser(session, "user", "pass", 1L, "/", null, null); + } } diff --git a/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java b/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java index 1408a815322..6373c3277d0 100755 --- a/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java +++ b/server/src/test/java/com/cloud/resource/MockResourceManagerImpl.java @@ -474,6 +474,11 @@ public class MockResourceManagerImpl extends ManagerBase implements ResourceMana return null; } + @Override + public HostVO findHostByGuidPrefix(String guid) { + return null; + } + /* (non-Javadoc) * @see com.cloud.resource.ResourceManager#findHostByName(java.lang.String) */ diff --git a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java index 47472408404..3937a595069 100644 --- a/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java +++ b/server/src/test/java/com/cloud/resource/ResourceManagerImplTest.java @@ -387,12 +387,14 @@ public class ResourceManagerImplTest { @Test public void testConnectAndRestartAgentOnHost() { + when(agentManager.getHostSshPort(any())).thenReturn(22); resourceManager.connectAndRestartAgentOnHost(host, hostUsername, hostPassword, hostPrivateKey); } @Test public void testHandleAgentSSHEnabledNotConnectedAgent() { when(host.getStatus()).thenReturn(Status.Disconnected); + when(agentManager.getHostSshPort(any())).thenReturn(22); resourceManager.handleAgentIfNotConnected(host, false); verify(resourceManager).getHostCredentials(eq(host)); verify(resourceManager).connectAndRestartAgentOnHost(eq(host), eq(hostUsername), eq(hostPassword), eq(hostPrivateKey)); diff --git a/server/src/test/java/com/cloud/user/AccountManagetImplTestBase.java b/server/src/test/java/com/cloud/user/AccountManagentImplTestBase.java similarity index 98% rename from server/src/test/java/com/cloud/user/AccountManagetImplTestBase.java rename to server/src/test/java/com/cloud/user/AccountManagentImplTestBase.java index 90e27790966..8c790b78da0 100644 --- a/server/src/test/java/com/cloud/user/AccountManagetImplTestBase.java +++ b/server/src/test/java/com/cloud/user/AccountManagentImplTestBase.java @@ -85,7 +85,7 @@ import java.util.HashMap; import java.util.Map; @RunWith(MockitoJUnitRunner.class) -public class AccountManagetImplTestBase { +public class AccountManagentImplTestBase { @Mock AccountDao _accountDao; @@ -100,7 +100,7 @@ public class AccountManagetImplTestBase { @Mock InstanceGroupDao _vmGroupDao; @Mock - UserAccountDao userAccountDaoMock; + UserAccountDao userAccountDao; @Mock VolumeDao _volumeDao; @Mock @@ -213,9 +213,6 @@ public class AccountManagetImplTestBase { @Mock RoutedIpv4Manager routedIpv4Manager; - @Mock - Account accountMock; - @Before public void setup() { accountManagerImpl.setUserAuthenticators(Arrays.asList(userAuthenticator)); @@ -231,7 +228,6 @@ public class AccountManagetImplTestBase { @Test public void test() { - return; } public static Map getInheritedFields(Class type) { diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index c33cb334e77..e5d09ba9141 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -23,6 +23,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -47,13 +48,11 @@ import org.apache.cloudstack.webhook.WebhookHelper; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import com.cloud.acl.DomainChecker; @@ -76,8 +75,7 @@ import com.cloud.vm.UserVmVO; import com.cloud.vm.VMInstanceVO; import com.cloud.vm.snapshot.VMSnapshotVO; -@RunWith(MockitoJUnitRunner.class) -public class AccountManagerImplTest extends AccountManagetImplTestBase { +public class AccountManagerImplTest extends AccountManagentImplTestBase { @Mock private UserVmManagerImpl _vmMgr; @@ -100,11 +98,11 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Mock private UpdateAccountCmd UpdateAccountCmdMock; - private long userVoIdMock = 111l; + private final long userVoIdMock = 111L; @Mock private UserVO userVoMock; - private long accountMockId = 100l; + private final long accountMockId = 100L; @Mock private Account accountMock; @@ -156,7 +154,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Test public void disableAccountNotexisting() throws ConcurrentOperationException, ResourceUnavailableException { - Mockito.when(_accountDao.findById(42l)).thenReturn(null); + Mockito.when(_accountDao.findById(42L)).thenReturn(null); Assert.assertTrue(accountManagerImpl.disableAccount(42)); } @@ -164,7 +162,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { public void disableAccountDisabled() throws ConcurrentOperationException, ResourceUnavailableException { AccountVO disabledAccount = new AccountVO(); disabledAccount.setState(State.DISABLED); - Mockito.when(_accountDao.findById(42l)).thenReturn(disabledAccount); + Mockito.when(_accountDao.findById(42L)).thenReturn(disabledAccount); Assert.assertTrue(accountManagerImpl.disableAccount(42)); } @@ -172,22 +170,22 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { public void disableAccount() throws ConcurrentOperationException, ResourceUnavailableException { AccountVO account = new AccountVO(); account.setState(State.ENABLED); - Mockito.when(_accountDao.findById(42l)).thenReturn(account); + Mockito.when(_accountDao.findById(42L)).thenReturn(account); Mockito.when(_accountDao.createForUpdate()).thenReturn(new AccountVO()); - Mockito.when(_accountDao.update(Mockito.eq(42l), Mockito.any(AccountVO.class))).thenReturn(true); - Mockito.when(_vmDao.listByAccountId(42l)).thenReturn(Arrays.asList(Mockito.mock(VMInstanceVO.class))); + Mockito.when(_accountDao.update(Mockito.eq(42L), Mockito.any(AccountVO.class))).thenReturn(true); + Mockito.when(_vmDao.listByAccountId(42L)).thenReturn(Arrays.asList(Mockito.mock(VMInstanceVO.class))); Assert.assertTrue(accountManagerImpl.disableAccount(42)); - Mockito.verify(_accountDao, Mockito.atLeastOnce()).update(Mockito.eq(42l), Mockito.any(AccountVO.class)); + Mockito.verify(_accountDao, Mockito.atLeastOnce()).update(Mockito.eq(42L), Mockito.any(AccountVO.class)); } @Test public void deleteUserAccount() { AccountVO account = new AccountVO(); - account.setId(42l); + account.setId(42L); DomainVO domain = new DomainVO(); - Mockito.when(_accountDao.findById(42l)).thenReturn(account); + Mockito.when(_accountDao.findById(42L)).thenReturn(account); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); - Mockito.when(_accountDao.remove(42l)).thenReturn(true); + Mockito.when(_accountDao.remove(42L)).thenReturn(true); Mockito.when(_configMgr.releaseAccountSpecificVirtualRanges(account)).thenReturn(true); Mockito.lenient().when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domain); Mockito.lenient().when(securityChecker.checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class))).thenReturn(true); @@ -196,7 +194,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { List sshkeyList = new ArrayList(); SSHKeyPairVO sshkey = new SSHKeyPairVO(); - sshkey.setId(1l); + sshkey.setId(1L); sshkeyList.add(sshkey); Mockito.when(_sshKeyPairDao.listKeyPairs(Mockito.anyLong(), Mockito.anyLong())).thenReturn(sshkeyList); Mockito.when(_sshKeyPairDao.remove(Mockito.anyLong())).thenReturn(true); @@ -205,30 +203,30 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.doNothing().when(accountManagerImpl).deleteWebhooksForAccount(Mockito.anyLong()); Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations((Account) any()); - Assert.assertTrue(accountManagerImpl.deleteUserAccount(42l)); + Assert.assertTrue(accountManagerImpl.deleteUserAccount(42L)); // assert that this was a clean delete - Mockito.verify(_accountDao, Mockito.never()).markForCleanup(Mockito.eq(42l)); + Mockito.verify(_accountDao, Mockito.never()).markForCleanup(Mockito.eq(42L)); } @Test public void deleteUserAccountCleanup() { AccountVO account = new AccountVO(); - account.setId(42l); + account.setId(42L); DomainVO domain = new DomainVO(); - Mockito.when(_accountDao.findById(42l)).thenReturn(account); + Mockito.when(_accountDao.findById(42L)).thenReturn(account); Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.isNull(), Mockito.anyBoolean(), Mockito.any(Account.class)); - Mockito.when(_accountDao.remove(42l)).thenReturn(true); + Mockito.when(_accountDao.remove(42L)).thenReturn(true); Mockito.when(_configMgr.releaseAccountSpecificVirtualRanges(account)).thenReturn(true); - Mockito.when(_userVmDao.listByAccountId(42l)).thenReturn(Arrays.asList(Mockito.mock(UserVmVO.class))); + Mockito.when(_userVmDao.listByAccountId(42L)).thenReturn(Arrays.asList(Mockito.mock(UserVmVO.class))); Mockito.when(_vmMgr.expunge(Mockito.any(UserVmVO.class))).thenReturn(false); Mockito.lenient().when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domain); Mockito.lenient().when(securityChecker.checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class))).thenReturn(true); Mockito.doNothing().when(accountManagerImpl).deleteWebhooksForAccount(Mockito.anyLong()); Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations((Account) any()); - Assert.assertTrue(accountManagerImpl.deleteUserAccount(42l)); + Assert.assertTrue(accountManagerImpl.deleteUserAccount(42L)); // assert that this was NOT a clean delete - Mockito.verify(_accountDao, Mockito.atLeastOnce()).markForCleanup(Mockito.eq(42l)); + Mockito.verify(_accountDao, Mockito.atLeastOnce()).markForCleanup(Mockito.eq(42L)); } @Test (expected = InvalidParameterValueException.class) @@ -311,7 +309,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { UserAccountVO userAccountVO = new UserAccountVO(); userAccountVO.setSource(User.Source.UNKNOWN); userAccountVO.setState(Account.State.DISABLED.toString()); - Mockito.when(userAccountDaoMock.getUserAccount("test", 1L)).thenReturn(userAccountVO); + Mockito.when(userAccountDao.getUserAccount("test", 1L)).thenReturn(userAccountVO); Mockito.when(userAuthenticator.authenticate("test", "fail", 1L, new HashMap<>())).thenReturn(failureAuthenticationPair); Mockito.lenient().when(userAuthenticator.authenticate("test", null, 1L, new HashMap<>())).thenReturn(successAuthenticationPair); Mockito.lenient().when(userAuthenticator.authenticate("test", "", 1L, new HashMap<>())).thenReturn(successAuthenticationPair); @@ -340,7 +338,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { CallContext.register(callingUser, callingAccount); // Calling account is user account i.e normal account Mockito.when(_listkeyscmd.getID()).thenReturn(1L); Mockito.when(accountManagerImpl.getActiveUser(1L)).thenReturn(userVoMock); - Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO); + Mockito.when(userAccountDao.findById(1L)).thenReturn(userAccountVO); Mockito.when(userAccountVO.getAccountId()).thenReturn(1L); Mockito.lenient().when(accountManagerImpl.getAccount(Mockito.anyLong())).thenReturn(accountMock); // Queried account - admin account @@ -358,7 +356,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { CallContext.register(callingUser, callingAccount); Mockito.when(_listkeyscmd.getID()).thenReturn(2L); Mockito.when(accountManagerImpl.getActiveUser(2L)).thenReturn(userVoMock); - Mockito.when(userAccountDaoMock.findById(2L)).thenReturn(userAccountVO); + Mockito.when(userAccountDao.findById(2L)).thenReturn(userAccountVO); Mockito.when(userAccountVO.getAccountId()).thenReturn(2L); Mockito.when(userDetailsDaoMock.listDetailsKeyPairs(Mockito.anyLong())).thenReturn(null); @@ -432,6 +430,44 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { prepareMockAndExecuteUpdateUserTest(1); } + @Test(expected = CloudRuntimeException.class) + public void updateUserTestPwdChangeDisabledUser() { + Mockito.when(userVoMock.getState()).thenReturn(State.DISABLED); + updateUserPwdChange(); + } + + @Test(expected = CloudRuntimeException.class) + public void updateUserTestPwdChangeLockedUser() { + Mockito.when(userVoMock.getState()).thenReturn(State.LOCKED); + updateUserPwdChange(); + } + + @Test(expected = CloudRuntimeException.class) + public void updateUserTestPwdChangeDisabledAccount() { + Mockito.when(userVoMock.getState()).thenReturn(State.ENABLED); + Mockito.when(accountMock.getState()).thenReturn(State.LOCKED); + updateUserPwdChange(); + } + + @Test + public void testUpdateUserTestPwdChange() { + Mockito.when(userVoMock.getState()).thenReturn(State.ENABLED); + Mockito.when(accountMock.getState()).thenReturn(State.ENABLED); + updateUserPwdChange(); + } + + private void updateUserPwdChange() { + Mockito.doReturn(true).when(UpdateUserCmdMock).isPasswordChangeRequired(); + Mockito.when(userVoMock.getAccountId()).thenReturn(10L); + Mockito.doReturn(accountMock).when(accountManagerImpl).getAccount(10L); + Mockito.when(accountMock.getAccountId()).thenReturn(10L); + Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(10L); + Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(accountMock))).thenReturn(RoleType.User); + Mockito.when(callingUser.getAccountId()).thenReturn(1L); + Mockito.doReturn(true).when(accountManagerImpl).isRootAdmin(1L); + prepareMockAndExecuteUpdateUserTest(0); + } + private void prepareMockAndExecuteUpdateUserTest(int numberOfExpectedCallsForSetEmailAndSetTimeZone) { Mockito.doReturn("password").when(UpdateUserCmdMock).getPassword(); Mockito.doReturn("newpassword").when(UpdateUserCmdMock).getCurrentPassword(); @@ -445,14 +481,14 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.doNothing().when(accountManagerImpl).validateUserPasswordAndUpdateIfNeeded(Mockito.anyString(), Mockito.eq(userVoMock), Mockito.anyString(), Mockito.eq(false)); Mockito.doReturn(true).when(userDaoMock).update(Mockito.anyLong(), Mockito.eq(userVoMock)); - Mockito.doReturn(Mockito.mock(UserAccountVO.class)).when(userAccountDaoMock).findById(Mockito.anyLong()); + Mockito.doReturn(Mockito.mock(UserAccountVO.class)).when(userAccountDao).findById(Mockito.anyLong()); Mockito.doNothing().when(accountManagerImpl).checkAccess(nullable(User.class), nullable(Account.class)); accountManagerImpl.updateUser(UpdateUserCmdMock); Mockito.lenient().doNothing().when(accountManagerImpl).checkRoleEscalation(accountMock, accountMock); - InOrder inOrder = Mockito.inOrder(userVoMock, accountManagerImpl, userDaoMock, userAccountDaoMock); + InOrder inOrder = Mockito.inOrder(userVoMock, accountManagerImpl, userDaoMock, userAccountDao); inOrder.verify(accountManagerImpl).retrieveAndValidateUser(UpdateUserCmdMock); inOrder.verify(accountManagerImpl).retrieveAndValidateAccount(userVoMock); @@ -467,7 +503,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { inOrder.verify(userVoMock, Mockito.times(numberOfExpectedCallsForSetEmailAndSetTimeZone)).setTimezone(Mockito.anyString()); inOrder.verify(userDaoMock).update(Mockito.anyLong(), Mockito.eq(userVoMock)); - inOrder.verify(userAccountDaoMock).findById(Mockito.anyLong()); + inOrder.verify(userAccountDao).findById(Mockito.anyLong()); } @Test(expected = InvalidParameterValueException.class) @@ -490,7 +526,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { public void validateAndUpdatApiAndSecretKeyIfNeededTestNoKeys() { accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); - Mockito.verify(_accountDao, Mockito.times(0)).findUserAccountByApiKey(Mockito.anyString()); + Mockito.verify(userAccountDao, Mockito.times(0)).getUserByApiKey(Mockito.anyString()); } @Test(expected = InvalidParameterValueException.class) @@ -516,10 +552,9 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.doReturn(1L).when(userVoMock).getId(); User otherUserMock = Mockito.mock(User.class); - Mockito.doReturn(2L).when(otherUserMock).getId(); - Pair pairUserAccountMock = new Pair(otherUserMock, Mockito.mock(Account.class)); - Mockito.doReturn(pairUserAccountMock).when(_accountDao).findUserAccountByApiKey(apiKey); + UserAccount UserAccountMock = Mockito.mock(UserAccount.class); + Mockito.doReturn(UserAccountMock).when(userAccountDao).getUserByApiKey(apiKey); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); } @@ -532,17 +567,13 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { String secretKey = "secretKey"; Mockito.doReturn(secretKey).when(UpdateUserCmdMock).getSecretKey(); - Mockito.doReturn(1L).when(userVoMock).getId(); - User otherUserMock = Mockito.mock(User.class); - Mockito.doReturn(1L).when(otherUserMock).getId(); - Pair pairUserAccountMock = new Pair(otherUserMock, Mockito.mock(Account.class)); - Mockito.doReturn(pairUserAccountMock).when(_accountDao).findUserAccountByApiKey(apiKey); + Mockito.doReturn(null).when(userAccountDao).getUserByApiKey(apiKey); accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock); - Mockito.verify(_accountDao).findUserAccountByApiKey(apiKey); + Mockito.verify(userAccountDao).getUserByApiKey(apiKey); Mockito.verify(userVoMock).setApiKey(apiKey); Mockito.verify(userVoMock).setSecretKey(secretKey); } @@ -696,18 +727,18 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Test(expected = InvalidParameterValueException.class) public void validateAndUpdateUsernameIfNeededTestDuplicatedUserSameDomainThisUser() { - long domanIdCurrentUser = 22l; + long domanIdCurrentUser = 22L; String userName = "username"; Mockito.doReturn(userName).when(UpdateUserCmdMock).getUsername(); Mockito.lenient().doReturn(userName).when(userVoMock).getUsername(); Mockito.doReturn(domanIdCurrentUser).when(accountMock).getDomainId(); - long userVoDuplicatedMockId = 67l; + long userVoDuplicatedMockId = 67L; UserVO userVoDuplicatedMock = Mockito.mock(UserVO.class); Mockito.doReturn(userVoDuplicatedMockId).when(userVoDuplicatedMock).getId(); - long accountIdUserDuplicated = 98l; + long accountIdUserDuplicated = 98L; Mockito.doReturn(accountIdUserDuplicated).when(userVoDuplicatedMock).getAccountId(); @@ -731,24 +762,24 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Test public void validateAndUpdateUsernameIfNeededTestDuplicatedUserButInDifferentDomains() { - long domanIdCurrentUser = 22l; + long domanIdCurrentUser = 22L; String userName = "username"; Mockito.doReturn(userName).when(UpdateUserCmdMock).getUsername(); Mockito.lenient().doReturn(userName).when(userVoMock).getUsername(); Mockito.doReturn(domanIdCurrentUser).when(accountMock).getDomainId(); - long userVoDuplicatedMockId = 67l; + long userVoDuplicatedMockId = 67L; UserVO userVoDuplicatedMock = Mockito.mock(UserVO.class); Mockito.lenient().doReturn(userName).when(userVoDuplicatedMock).getUsername(); Mockito.doReturn(userVoDuplicatedMockId).when(userVoDuplicatedMock).getId(); - long accountIdUserDuplicated = 98l; + long accountIdUserDuplicated = 98L; Mockito.doReturn(accountIdUserDuplicated).when(userVoDuplicatedMock).getAccountId(); Account accountUserDuplicatedMock = Mockito.mock(AccountVO.class); Mockito.lenient().doReturn(accountIdUserDuplicated).when(accountUserDuplicatedMock).getId(); - Mockito.doReturn(45l).when(accountUserDuplicatedMock).getDomainId(); + Mockito.doReturn(45L).when(accountUserDuplicatedMock).getDomainId(); List usersWithSameUserName = new ArrayList<>(); usersWithSameUserName.add(userVoMock); @@ -766,7 +797,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Test public void validateAndUpdateUsernameIfNeededTestNoDuplicatedUserNames() { - long domanIdCurrentUser = 22l; + long domanIdCurrentUser = 22L; String userName = "username"; Mockito.doReturn(userName).when(UpdateUserCmdMock).getUsername(); @@ -964,7 +995,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Test public void validateCurrentPasswordTestUserAuthenticatedWithProvidedCurrentPasswordViaFirstAuthenticator() { AccountVO accountVoMock = Mockito.mock(AccountVO.class); - long domainId = 14l; + long domainId = 14L; Mockito.doReturn(domainId).when(accountVoMock).getDomainId(); Mockito.doReturn(accountVoMock).when(_accountDao).findById(accountMockId); @@ -993,7 +1024,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { @Test public void validateCurrentPasswordTestUserAuthenticatedWithProvidedCurrentPasswordViaSecondAuthenticator() { AccountVO accountVoMock = Mockito.mock(AccountVO.class); - long domainId = 14l; + long domainId = 14L; Mockito.doReturn(domainId).when(accountVoMock).getDomainId(); Mockito.doReturn(accountVoMock).when(_accountDao).findById(accountMockId); @@ -1054,7 +1085,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { UserAccountVO userAccount = Mockito.mock(UserAccountVO.class); UserVO userVO = Mockito.mock(UserVO.class); - Mockito.when(userAccountDaoMock.findById(userId)).thenReturn(userAccount); + Mockito.when(userAccountDao.findById(userId)).thenReturn(userAccount); Mockito.when(userDaoMock.findById(userId)).thenReturn(userVO); Mockito.when(userAccount.getDomainId()).thenReturn(1L); @@ -1073,7 +1104,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { UserAccountVO userAccount = Mockito.mock(UserAccountVO.class); UserVO userVO = Mockito.mock(UserVO.class); - Mockito.when(userAccountDaoMock.findById(userId)).thenReturn(userAccount); + Mockito.when(userAccountDao.findById(userId)).thenReturn(userAccount); Mockito.when(userDaoMock.findById(userId)).thenReturn(userVO); Mockito.when(userAccount.getDomainId()).thenReturn(1L); @@ -1102,7 +1133,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { UserAccountVO userAccount = Mockito.mock(UserAccountVO.class); UserVO userVO = Mockito.mock(UserVO.class); - Mockito.when(userAccountDaoMock.findById(userId)).thenReturn(userAccount); + Mockito.when(userAccountDao.findById(userId)).thenReturn(userAccount); Mockito.when(userDaoMock.findById(userId)).thenReturn(userVO); Mockito.when(userAccount.getDomainId()).thenReturn(1L); @@ -1208,7 +1239,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.when(callingUser.getId()).thenReturn(1L); CallContext.register(callingUser, callingAccount); // Calling account is user account i.e normal account Mockito.lenient().when(_accountService.getActiveAccountById(1L)).thenReturn(accountMock); - Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO); + Mockito.when(userAccountDao.findById(1L)).thenReturn(userAccountVO); Mockito.when(userDaoMock.findById(1L)).thenReturn(userVoMock); Mockito.when(userAccountVO.getDomainId()).thenReturn(1L); Mockito.when(enableUserTwoFactorAuthenticationMock.valueIn(1L)).thenReturn(true); @@ -1234,7 +1265,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { List userAccountVOList = new ArrayList<>(); UserAccountVO userAccountVO = new UserAccountVO(); userAccountVOList.add(userAccountVO); - Mockito.when(userAccountDaoMock.getUserAccountByEmail(email, domainId)).thenReturn(userAccountVOList); + Mockito.when(userAccountDao.getUserAccountByEmail(email, domainId)).thenReturn(userAccountVOList); List userAccounts = accountManagerImpl.getActiveUserAccountByEmail(email, domainId); Assert.assertEquals(userAccountVOList.size(), userAccounts.size()); Assert.assertEquals(userAccountVOList.get(0), userAccounts.get(0)); @@ -1409,7 +1440,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.when(user.getUser2faProvider()).thenReturn(null); UserAccount result = accountManagerImpl.clearUserTwoFactorAuthenticationInSetupStateOnLogin(user); Assert.assertSame(user, result); - Mockito.verifyNoInteractions(userDetailsDaoMock, userAccountDaoMock); + Mockito.verifyNoInteractions(userDetailsDaoMock, userAccountDao); } @Test @@ -1423,7 +1454,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { UserAccount result = accountManagerImpl.clearUserTwoFactorAuthenticationInSetupStateOnLogin(user); Assert.assertSame(user, result); Mockito.verify(userDetailsDaoMock).findDetail(1L, UserDetailVO.Setup2FADetail); - Mockito.verifyNoMoreInteractions(userDetailsDaoMock, userAccountDaoMock); + Mockito.verifyNoMoreInteractions(userDetailsDaoMock, userAccountDao); } @Test @@ -1436,16 +1467,16 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { UserAccountVO userAccountVO = new UserAccountVO(); userAccountVO.setId(1L); Mockito.when(userDetailsDaoMock.findDetail(1L, UserDetailVO.Setup2FADetail)).thenReturn(userDetail); - Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO); + Mockito.when(userAccountDao.findById(any())).thenReturn(userAccountVO); UserAccount result = accountManagerImpl.clearUserTwoFactorAuthenticationInSetupStateOnLogin(user); Assert.assertNotNull(result); Assert.assertFalse(result.isUser2faEnabled()); Assert.assertNull(result.getUser2faProvider()); Mockito.verify(userDetailsDaoMock).findDetail(1L, UserDetailVO.Setup2FADetail); Mockito.verify(userDetailsDaoMock).remove(Mockito.anyLong()); - Mockito.verify(userAccountDaoMock).findById(1L); + Mockito.verify(userAccountDao).findById(1L); ArgumentCaptor captor = ArgumentCaptor.forClass(UserAccountVO.class); - Mockito.verify(userAccountDaoMock).update(Mockito.eq(1L), captor.capture()); + Mockito.verify(userAccountDao).update(Mockito.eq(1L), captor.capture()); UserAccountVO updatedUser = captor.getValue(); Assert.assertFalse(updatedUser.isUser2faEnabled()); Assert.assertNull(updatedUser.getUser2faProvider()); @@ -1592,4 +1623,104 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock); } + + @Test(expected = InvalidParameterValueException.class) + public void testPasswordChangeRequiredWithSamlThrowsException() { + accountManagerImpl.createUser( + "user", "pass", "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.SAML2, true + ); + } + + @Test(expected = InvalidParameterValueException.class) + public void testPasswordChangeRequiredWithLdapSourceThrows() { + accountManagerImpl.createUser( + "user", "pass", "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.LDAP, true); + } + + @Test(expected = CloudRuntimeException.class) + public void testDomainNotFound() { + Mockito.when(_domainMgr.getDomain(1L)).thenReturn(null); + accountManagerImpl.createUser( + "user", "pass", "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.UNKNOWN, false); + } + + @Test(expected = CloudRuntimeException.class) + public void testCreateUserInactiveDomain() { + Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Inactive); + Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock); + accountManagerImpl.createUser( + "user", "pass", "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.NATIVE, false); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateUserCheckAccess() { + Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active); + Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + accountManagerImpl.createUser( + "user", "pass", "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.NATIVE, false); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateUserMissingOrProjectAccount() { + Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active); + Mockito.when(_accountDao.findEnabledAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(accountMock); + Mockito.when(accountMock.getType()).thenReturn(Account.Type.PROJECT); + Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock); + Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); + accountManagerImpl.createUser( + "user", "pass", "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.NATIVE, false); + } + + @Test + public void testCreateUserSuccess() { + Account rootAdminAccount = Mockito.mock(Account.class); + Mockito.when(rootAdminAccount.getId()).thenReturn(1L); + Mockito.when(accountManagerImpl.isRootAdmin(1L)).thenReturn(true); + User callingUser = Mockito.mock(User.class); + CallContext.register(callingUser, rootAdminAccount); + + String newPassword = "newPassword"; + configureUserMockAuthenticators(newPassword); + Mockito.doNothing().when(accountManagerImpl).checkAccess(any(Account.class), any(Domain.class)); + Mockito.doReturn(accountMock).when(accountManagerImpl).getAccount(Mockito.anyLong()); + Mockito.doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong()); + Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock); + Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active); + + Mockito.when(_accountDao.findEnabledAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(accountMock); + Mockito.when(accountMock.getId()).thenReturn(10L); + Mockito.when(accountMock.getType()).thenReturn(Account.Type.NORMAL); + + Mockito.when(userAccountDao.validateUsernameInDomain(Mockito.anyString(), Mockito.anyLong())).thenReturn(true); + Mockito.when(userDaoMock.findUsersByName(Mockito.anyString())).thenReturn(Collections.emptyList()); + UserVO createdUser = new UserVO(); + String userMockUUID = "userMockUUID"; + createdUser.setUuid(userMockUUID); + Mockito.when(userDaoMock.persist(Mockito.any(UserVO.class))).thenReturn(createdUser); + UserVO userResultVO = accountManagerImpl.createUser( + "user", newPassword, "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.NATIVE, false + ); + Assert.assertNotNull(userResultVO); + UserVO userResultPasswordChangeVO = accountManagerImpl.createUser( + "user", newPassword, "fn", "ln", "e@mail.com", + "UTC", "acct", 1L, null, + User.Source.NATIVE, true + ); + Assert.assertNotNull(userResultVO); + } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java index 76056333a2a..d1b02e434a2 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplVolumeDeleteEventTest.java @@ -63,7 +63,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) -public class AccountManagerImplVolumeDeleteEventTest extends AccountManagetImplTestBase { +public class AccountManagerImplVolumeDeleteEventTest extends AccountManagentImplTestBase { private static final Long ACCOUNT_ID = 1l; private static final String VOLUME_UUID = "vol-111111"; diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java index 272e79fea49..032e947fdce 100644 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java +++ b/server/src/test/java/org/apache/cloudstack/storage/heuristics/HeuristicRuleHelperTest.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.storage.heuristics; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.Storage; import com.cloud.storage.VMTemplateVO; import com.cloud.storage.VolumeVO; import com.cloud.utils.exception.CloudRuntimeException; @@ -29,6 +31,7 @@ import org.apache.cloudstack.storage.heuristics.presetvariables.PresetVariables; import org.apache.cloudstack.utils.jsinterpreter.JsInterpreter; import org.apache.logging.log4j.Logger; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -68,6 +71,19 @@ public class HeuristicRuleHelperTest { @InjectMocks HeuristicRuleHelper heuristicRuleHelperSpy = new HeuristicRuleHelper(); + @Before + public void setUp() { + Mockito.doReturn("template-name").when(vmTemplateVOMock).getName(); + Mockito.doReturn(Storage.ImageFormat.QCOW2).when(vmTemplateVOMock).getFormat(); + Mockito.doReturn(Hypervisor.HypervisorType.KVM).when(vmTemplateVOMock).getHypervisorType(); + Mockito.doReturn("snapshot-name").when(snapshotInfoMock).getName(); + Mockito.doReturn(1024L).when(snapshotInfoMock).getSize(); + Mockito.doReturn(Hypervisor.HypervisorType.VMware).when(snapshotInfoMock).getHypervisorType(); + Mockito.doReturn("volume-name").when(volumeVOMock).getName(); + Mockito.doReturn(Storage.ImageFormat.RAW).when(volumeVOMock).getFormat(); + Mockito.doReturn(2048L).when(volumeVOMock).getSize(); + } + @Test public void getImageStoreIfThereIsHeuristicRuleTestZoneDoesNotHaveHeuristicRuleShouldReturnNull() { Long zoneId = 1L; diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/AccountTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/AccountTest.java deleted file mode 100644 index f7610438b78..00000000000 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/AccountTest.java +++ /dev/null @@ -1,46 +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.storage.heuristics.presetvariables; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class AccountTest { - - @Test - public void toStringTestReturnsValidJson() { - Account variable = new Account(); - variable.setName("test name"); - variable.setId("test id"); - - Domain domainVariable = new Domain(); - domainVariable.setId("domain id"); - domainVariable.setName("domain name"); - variable.setDomain(domainVariable); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "id", "domain"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/DomainTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/DomainTest.java deleted file mode 100644 index a1ec6854ecc..00000000000 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/DomainTest.java +++ /dev/null @@ -1,41 +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.storage.heuristics.presetvariables; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class DomainTest { - - @Test - public void toStringTestReturnsValidJson() { - Domain variable = new Domain(); - variable.setName("test name"); - variable.setId("test id"); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "id"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariableTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariableTest.java deleted file mode 100644 index cd295e92caf..00000000000 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/GenericHeuristicPresetVariableTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.apache.cloudstack.storage.heuristics.presetvariables; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class GenericHeuristicPresetVariableTest { - - @Test - public void toStringTestReturnsValidJson() { - GenericHeuristicPresetVariable variable = new GenericHeuristicPresetVariable(); - variable.setName("test name"); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/SecondaryStorageTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/SecondaryStorageTest.java deleted file mode 100644 index a09386789cf..00000000000 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/SecondaryStorageTest.java +++ /dev/null @@ -1,45 +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.storage.heuristics.presetvariables; - -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class SecondaryStorageTest { - - @Test - public void toStringTestReturnsValidJson() { - SecondaryStorage variable = new SecondaryStorage(); - variable.setName("test name"); - variable.setId("test id"); - variable.setProtocol("test protocol"); - variable.setUsedDiskSize(1L); - variable.setTotalDiskSize(2L); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "id", - "protocol", "usedDiskSize", "totalDiskSize"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/SnapshotTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/SnapshotTest.java deleted file mode 100644 index b8476cd8e46..00000000000 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/SnapshotTest.java +++ /dev/null @@ -1,44 +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.storage.heuristics.presetvariables; - -import com.cloud.hypervisor.Hypervisor; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class SnapshotTest { - - @Test - public void toStringTestReturnsValidJson() { - Snapshot variable = new Snapshot(); - variable.setName("test name"); - variable.setSize(1L); - variable.setHypervisorType(Hypervisor.HypervisorType.KVM); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "size", - "hypervisorType"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/TemplateTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/TemplateTest.java deleted file mode 100644 index 2c1582befb2..00000000000 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/TemplateTest.java +++ /dev/null @@ -1,46 +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.storage.heuristics.presetvariables; - -import com.cloud.hypervisor.Hypervisor; -import com.cloud.storage.Storage; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class TemplateTest { - - @Test - public void toStringTestReturnsValidJson() { - Template variable = new Template(); - variable.setName("test name"); - variable.setTemplateType(Storage.TemplateType.USER); - variable.setHypervisorType(Hypervisor.HypervisorType.KVM); - variable.setFormat(Storage.ImageFormat.QCOW2); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "templateType", - "hypervisorType", "format"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/VolumeTest.java b/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/VolumeTest.java deleted file mode 100644 index e74ddc93ec5..00000000000 --- a/server/src/test/java/org/apache/cloudstack/storage/heuristics/presetvariables/VolumeTest.java +++ /dev/null @@ -1,44 +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.storage.heuristics.presetvariables; - -import com.cloud.storage.Storage; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class VolumeTest { - - @Test - public void toStringTestReturnsValidJson() { - Volume variable = new Volume(); - variable.setName("test name"); - variable.setFormat(Storage.ImageFormat.QCOW2); - variable.setSize(1L); - - String expected = ReflectionToStringBuilderUtils.reflectOnlySelectedFields(variable, "name", "format", - "size"); - String result = variable.toString(); - - Assert.assertEquals(expected, result); - } - -} diff --git a/server/src/test/java/org/apache/cloudstack/user/UserPasswordResetManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/user/UserPasswordResetManagerImplTest.java index 17092e6311d..5e274b06be2 100644 --- a/server/src/test/java/org/apache/cloudstack/user/UserPasswordResetManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/user/UserPasswordResetManagerImplTest.java @@ -16,7 +16,11 @@ // under the License. package org.apache.cloudstack.user; +import com.cloud.user.AccountManager; import com.cloud.user.UserAccount; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserDao; + import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.resourcedetail.UserDetailVO; @@ -45,6 +49,12 @@ public class UserPasswordResetManagerImplTest { @Mock private UserDetailsDao userDetailsDao; + @Mock + AccountManager accountManager; + + @Mock + UserDao userDao; + @Test public void testGetMessageBody() { ConfigKey passwordResetMailTemplate = Mockito.mock(ConfigKey.class); @@ -147,4 +157,21 @@ public class UserPasswordResetManagerImplTest { Assert.assertFalse(passwordReset.validateExistingToken(userAccount)); } + + @Test + public void testResetPassword() { + UserAccount userAccount = Mockito.mock(UserAccount.class); + UserVO userVO = Mockito.mock(UserVO.class); + long userId = 1L; + String newPassword = "newPassword"; + Mockito.when(userAccount.getId()).thenReturn(userId); + Mockito.when(userDao.getUser(userId)).thenReturn(userVO); + passwordReset.resetPassword(userAccount, newPassword); + Mockito.verify(userDao).getUser(userId); + Mockito.verify(accountManager).validateUserPasswordAndUpdateIfNeeded(newPassword, userVO, "", true); + Mockito.verify(userDetailsDao).removeDetail(userId, PasswordResetToken); + Mockito.verify(userDetailsDao).removeDetail(userId, PasswordResetTokenExpiryDate); + Mockito.verify(userDetailsDao).removeDetail(userId, UserDetailVO.PasswordChangeRequired); + Mockito.verify(userDao).persist(userVO); + } } diff --git a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java index abddc3aa1bf..4ee6d28e329 100644 --- a/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java +++ b/services/secondary-storage/server/src/main/java/org/apache/cloudstack/storage/template/UploadManagerImpl.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.storage.template; +import static com.cloud.utils.NumbersUtil.toHumanReadableSize; + import java.io.File; import java.io.IOException; import java.net.URI; @@ -32,11 +34,11 @@ import java.util.concurrent.Executors; import javax.naming.ConfigurationException; -import com.cloud.agent.api.Answer; - -import com.cloud.agent.api.ConvertSnapshotCommand; import org.apache.cloudstack.storage.resource.SecondaryStorageResource; +import org.apache.commons.lang3.StringUtils; +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.ConvertSnapshotCommand; import com.cloud.agent.api.storage.CreateEntityDownloadURLAnswer; import com.cloud.agent.api.storage.CreateEntityDownloadURLCommand; import com.cloud.agent.api.storage.DeleteEntityDownloadURLCommand; @@ -51,15 +53,18 @@ import com.cloud.storage.template.FtpTemplateUploader; import com.cloud.storage.template.TemplateUploader; import com.cloud.storage.template.TemplateUploader.Status; import com.cloud.storage.template.TemplateUploader.UploadCompleteCallback; +import com.cloud.utils.FileUtil; import com.cloud.utils.NumbersUtil; +import com.cloud.utils.UuidUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; -import static com.cloud.utils.NumbersUtil.toHumanReadableSize; - public class UploadManagerImpl extends ManagerBase implements UploadManager { + protected static final String EXTRACT_USERDATA_DIR = "userdata"; + protected static final String BASE_EXTRACT_PATH = String.format("/var/www/html/%s/", EXTRACT_USERDATA_DIR); + public class Completion implements UploadCompleteCallback { private final String jobId; @@ -269,7 +274,7 @@ public class UploadManagerImpl extends ManagerBase implements UploadManager { return new CreateEntityDownloadURLAnswer(errorString, CreateEntityDownloadURLAnswer.RESULT_FAILURE); } // Create the directory structure so that its visible under apache server root - String extractDir = "/var/www/html/userdata/"; + String extractDir = BASE_EXTRACT_PATH; extractDir = extractDir + cmd.getFilepathInExtractURL() + File.separator; Script command = new Script("/bin/su", logger); command.add("-s"); @@ -333,12 +338,20 @@ public class UploadManagerImpl extends ManagerBase implements UploadManager { String extractUrl = cmd.getExtractUrl(); String result; if (extractUrl != null) { - command.add("unlink /var/www/html/userdata/" + extractUrl.substring(extractUrl.lastIndexOf(File.separator) + 1)); + URI uri = URI.create(extractUrl); + String uriPath = uri.getPath(); + String marker = String.format("/%s/", EXTRACT_USERDATA_DIR); + String linkPath = uriPath.startsWith(marker) + ? uriPath.substring(marker.length()) + : uriPath.substring(uriPath.indexOf(marker) + marker.length()); + command.add("unlink " + BASE_EXTRACT_PATH + linkPath); result = command.execute(); if (result != null) { // FIXME - Ideally should bail out if you can't delete symlink. Not doing it right now. // This is because the ssvm might already be destroyed and the symlinks do not exist. logger.warn("Error in deleting symlink :" + result); + } else { + deleteEntitySymlinkRootDirectoryIfNeeded(cmd, linkPath); } } @@ -371,6 +384,30 @@ public class UploadManagerImpl extends ManagerBase implements UploadManager { return new Answer(cmd, true, ""); } + protected void deleteEntitySymlinkRootDirectoryIfNeeded(DeleteEntityDownloadURLCommand cmd, String linkPath) { + if (StringUtils.isEmpty(linkPath)) { + return; + } + String[] parts = linkPath.split("/"); + if (parts.length == 0) { + return; + } + String rootDir = parts[0]; + if (StringUtils.isEmpty(rootDir) || !UuidUtils.isUuid(rootDir)) { + return; + } + logger.info("Deleting symlink root directory: {} for {}", rootDir, cmd.getExtractUrl()); + Path rootDirPath = Path.of(BASE_EXTRACT_PATH, rootDir); + String failMsg = "Failed to delete symlink root directory: {} for {}"; + try { + if (!FileUtil.deleteRecursively(rootDirPath)) { + logger.warn(failMsg, rootDir, cmd.getExtractUrl()); + } + } catch (IOException e) { + logger.warn(failMsg, rootDir, cmd.getExtractUrl(), e); + } + } + private String getInstallPath(String jobId) { // TODO Auto-generated method stub return null; diff --git a/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java b/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java new file mode 100644 index 00000000000..9038098e235 --- /dev/null +++ b/services/secondary-storage/server/src/test/java/org/apache/cloudstack/storage/template/UploadManagerImplTest.java @@ -0,0 +1,85 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.storage.template; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import java.nio.file.Path; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.agent.api.storage.DeleteEntityDownloadURLCommand; +import com.cloud.utils.FileUtil; + +@RunWith(MockitoJUnitRunner.class) +public class UploadManagerImplTest { + + @InjectMocks + UploadManagerImpl uploadManager; + + MockedStatic fileUtilMock; + + @Before + public void setup() { + fileUtilMock = mockStatic(FileUtil.class, Mockito.CALLS_REAL_METHODS); + fileUtilMock.when(() -> FileUtil.deleteRecursively(any(Path.class))).thenReturn(true); + } + + @After + public void tearDown() { + fileUtilMock.close(); + } + + @Test + public void doesNotDeleteWhenLinkPathIsEmpty() { + String emptyLinkPath = ""; + uploadManager.deleteEntitySymlinkRootDirectoryIfNeeded(mock(DeleteEntityDownloadURLCommand.class), emptyLinkPath); + fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), never()); + } + + @Test + public void doesNotDeleteWhenRootDirIsNotUuid() { + String invalidLinkPath = "invalidRootDir/file"; + uploadManager.deleteEntitySymlinkRootDirectoryIfNeeded(mock(DeleteEntityDownloadURLCommand.class), invalidLinkPath); + fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), never()); + } + + @Test + public void deletesSymlinkRootDirectoryWhenValidUuid() { + String validLinkPath = "123e4567-e89b-12d3-a456-426614174000/file"; + uploadManager.deleteEntitySymlinkRootDirectoryIfNeeded(mock(DeleteEntityDownloadURLCommand.class), validLinkPath); + fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), times(1)); + } + + @Test + public void deletesSymlinkRootDirectoryWhenNoFile() { + String validLinkPath = "123e4567-e89b-12d3-a456-426614174000"; + uploadManager.deleteEntitySymlinkRootDirectoryIfNeeded(mock(DeleteEntityDownloadURLCommand.class), validLinkPath); + fileUtilMock.verify(() -> FileUtil.deleteRecursively(any(Path.class)), times(1)); + } +} diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 0379aaab169..f09fe22ea7c 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -540,6 +540,8 @@ "label.change.ipaddress": "Change IP address for NIC", "label.change.disk.offering": "Change disk offering", "label.change.offering.for.volume": "Change disk offering for the volume", +"label.change.password.onlogin": "User must change password at next login", +"label.change.password.reset": "Force password reset", "label.change.service.offering": "Change service offering", "label.character": "Character", "label.checksum": "Checksum", @@ -3173,6 +3175,8 @@ "message.change.offering.for.volume.failed": "Change offering for the volume failed", "message.change.offering.for.volume.processing": "Changing offering for the volume...", "message.change.password": "Please change your password.", +"message.change.password.required": "You are required to change your password.", +"message.change.password.reset": "Force password reset on next login.", "message.change.scope.failed": "Scope change failed", "message.change.scope.processing": "Scope change in progress", "message.change.service.offering.sharedfs.failed": "Failed to change service offering for the Shared FileSystem.", @@ -3423,6 +3427,7 @@ "message.error.apply.tungsten.tag": "Applying Tag failed", "message.error.binaries.iso.url": "Please enter binaries ISO URL.", "message.error.bucket": "Please enter bucket", +"message.error.change.password": "Failed to change password.", "message.error.cidr": "CIDR is required", "message.error.cidr.or.cidrsize": "CIDR or cidr size is required", "message.error.cloudian.console": "Single-Sign-On failed for Cloudian management console. Please ask your administrator to fix integration issues.", @@ -3486,6 +3491,7 @@ "message.error.netmask": "Please enter Netmask.", "message.error.network.offering": "Please select Network offering.", "message.error.new.password": "Please enter new password.", +"message.error.newpassword.sameascurrent": "New password cannot be the same as the current password.", "message.error.nexus1000v.ipaddress": "Please enter Nexus 1000v IP address.", "message.error.nexus1000v.password": "Please enter Nexus 1000v password.", "message.error.nexus1000v.username": "Please enter Nexus 1000v username.", @@ -3730,6 +3736,7 @@ "message.please.confirm.remove.user.data": "Please confirm that you want to remove this User Data", "message.please.enter.valid.value": "Please enter a valid value.", "message.please.enter.value": "Please enter values.", +"message.please.login.new.password": "Please log in again with your new password", "message.please.wait.while.autoscale.vmgroup.is.being.created": "Please wait while your AutoScaling Group is being created; this may take a while...", "message.please.wait.while.zone.is.being.created": "Please wait while your Zone is being created; this may take a while...", "message.pod.dedicated": "Pod dedicated.", diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index d6f44cb6c78..66dd6b3db9e 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -49,7 +49,7 @@