From 4e3f2052377f3402d97b0de99e8786512b470440 Mon Sep 17 00:00:00 2001 From: "Jain, Rajiv" Date: Tue, 19 May 2026 16:47:43 +0530 Subject: [PATCH] CSTACKEX-175: adding initial changes for CICD related workflows --- private-cicd/.gitignore | 4 + private-cicd/Jenkinsfile | 292 ++++++++++++++++++ private-cicd/README.md | 60 ++++ private-cicd/config/build-fast.yaml | 27 ++ private-cicd/config/defaults.yaml | 25 ++ private-cicd/config/marvin.yaml | 26 ++ private-cicd/config/qa.yaml.example | 11 + private-cicd/docker/Dockerfile.agent | 17 + private-cicd/docs/BRANCH-STRATEGY.md | 25 ++ private-cicd/docs/FOLDER-LAYOUT.md | 37 +++ private-cicd/docs/IMPLEMENTATION-PHASES.md | 95 ++++++ private-cicd/marvin/README.md | 10 + private-cicd/marvin/bundles.txt | 5 + private-cicd/marvin/zones/README.md | 8 + .../marvin/zones/ontap-simulator.cfg.example | 12 + .../scripts/install-build-deps-ubuntu.sh | 32 ++ private-cicd/scripts/marvin-run.sh | 18 ++ private-cicd/scripts/mvn-full.sh | 29 ++ private-cicd/scripts/mvn-ontap-fast.sh | 46 +++ .../scripts/setup-ipmitool-wrapper.sh | 21 ++ private-cicd/scripts/validate-local.sh | 119 +++++++ test/integration/plugins/ontap/README.md | 10 + 22 files changed, 929 insertions(+) create mode 100644 private-cicd/.gitignore create mode 100644 private-cicd/Jenkinsfile create mode 100644 private-cicd/README.md create mode 100644 private-cicd/config/build-fast.yaml create mode 100644 private-cicd/config/defaults.yaml create mode 100644 private-cicd/config/marvin.yaml create mode 100644 private-cicd/config/qa.yaml.example create mode 100644 private-cicd/docker/Dockerfile.agent create mode 100644 private-cicd/docs/BRANCH-STRATEGY.md create mode 100644 private-cicd/docs/FOLDER-LAYOUT.md create mode 100644 private-cicd/docs/IMPLEMENTATION-PHASES.md create mode 100644 private-cicd/marvin/README.md create mode 100644 private-cicd/marvin/bundles.txt create mode 100644 private-cicd/marvin/zones/README.md create mode 100644 private-cicd/marvin/zones/ontap-simulator.cfg.example create mode 100755 private-cicd/scripts/install-build-deps-ubuntu.sh create mode 100755 private-cicd/scripts/marvin-run.sh create mode 100755 private-cicd/scripts/mvn-full.sh create mode 100755 private-cicd/scripts/mvn-ontap-fast.sh create mode 100755 private-cicd/scripts/setup-ipmitool-wrapper.sh create mode 100755 private-cicd/scripts/validate-local.sh create mode 100644 test/integration/plugins/ontap/README.md diff --git a/private-cicd/.gitignore b/private-cicd/.gitignore new file mode 100644 index 000000000000..7b3edb1df303 --- /dev/null +++ b/private-cicd/.gitignore @@ -0,0 +1,4 @@ +# Local overrides with secrets (templates *.example stay tracked) +config/qa.yaml +marvin/zones/*.cfg +!marvin/zones/*.cfg.example diff --git a/private-cicd/Jenkinsfile b/private-cicd/Jenkinsfile new file mode 100644 index 000000000000..95d3c64209fe --- /dev/null +++ b/private-cicd/Jenkinsfile @@ -0,0 +1,292 @@ +/* + * Private downstream Jenkins pipeline for Apache CloudStack. + * Not for inclusion in apache/cloudstack upstream. + * + * Rollout: Phase -1 = preflight (validate private-cicd only). Phase 1 = build-only. Phases 2–3 = marvin / delivery — see private-cicd/docs/IMPLEMENTATION-PHASES.md + * + * Usage: + * - Monorepo: Script Path = private-cicd/Jenkinsfile ; leave CLONE_SEPARATE = false. + * - CI-only repo: set CLONE_SEPARATE = true and point CLOUDSTACK_* at your fork/tag. + */ + +pipeline { + agent any + + options { + buildDiscarder(logRotator(numToKeepStr: '30')) + timestamps() + timeout(time: 4, unit: 'HOURS') + } + + parameters { + choice( + name: 'PIPELINE_PHASE', + choices: ['build-ontap-fast', 'build-only', 'preflight', 'marvin', 'delivery'], + description: 'build-ontap-fast = ONTAP plugin -pl -am test. preflight = Phase -1. build-only = full Maven. marvin / delivery reserved; see private-cicd/docs/IMPLEMENTATION-PHASES.md' + ) + string( + name: 'CONFIG_DEFAULTS_FILE', + defaultValue: 'config/defaults.yaml', + description: 'YAML path relative to CICD_ROOT (e.g. config/defaults.yaml or config/team-b.yaml).' + ) + string( + name: 'CONFIG_PROFILE', + defaultValue: '', + description: 'Optional: key under profiles in the defaults YAML (e.g. example-lts-branch). Blank = use only top-level cloudstack: from the file.' + ) + booleanParam( + name: 'CLONE_SEPARATE', + defaultValue: false, + description: 'If true, clone CloudStack into cloudstack-src/ (use when this job repo is CI-only). If false, expect CloudStack pom.xml at the workspace root (multibranch on your fork). Branch/url below apply only when this is true.' + ) + string( + name: 'CLOUDSTACK_GIT_URL', + defaultValue: '', + description: 'Override Git URL for CloudStack. Leave blank to use config/defaults.yaml (after optional CONFIG_PROFILE).' + ) + string( + name: 'CLOUDSTACK_GIT_BRANCH', + defaultValue: '', + description: 'Override branch or tag for clone. Leave blank to use config/defaults.yaml (after optional CONFIG_PROFILE). Ignored when CLONE_SEPARATE is false.' + ) + booleanParam( + name: 'ENABLE_NOREDIST', + defaultValue: false, + description: 'If true, run third-party non-OSS install script then mvn -Dnoredist (see README / legal for your org).' + ) + booleanParam( + name: 'SKIP_TESTS', + defaultValue: true, + description: 'If true, Maven uses -DskipTests=true (faster CI).' + ) + booleanParam( + name: 'INSTALL_APT_DEPS', + defaultValue: true, + description: 'If true, run private-cicd/scripts/install-build-deps-ubuntu.sh (requires sudo on agent).' + ) + booleanParam( + name: 'SETUP_IPMITOOL_WRAPPER', + defaultValue: false, + description: 'If true, install CloudStack-style ipmitool wrapper (requires sudo).' + ) + } + + environment { + MAVEN_OPTS = '-Xmx3072m -XX:MaxMetaspaceSize=512m' + } + + stages { + stage('Phase gate') { + steps { + script { + switch (params.PIPELINE_PHASE) { + case 'preflight': + case 'build-ontap-fast': + case 'build-only': + break + case 'marvin': + case 'delivery': + error("PIPELINE_PHASE='${params.PIPELINE_PHASE}' is not implemented yet. Use preflight, build-ontap-fast, or build-only. See private-cicd/docs/IMPLEMENTATION-PHASES.md") + default: + error("PIPELINE_PHASE='${params.PIPELINE_PHASE}' is not supported.") + } + } + } + } + + stage('Resolve CloudStack directory') { + steps { + script { + if (fileExists("${env.WORKSPACE}/private-cicd/scripts/install-build-deps-ubuntu.sh")) { + env.CICD_SCRIPT_DIR = "${env.WORKSPACE}/private-cicd/scripts" + env.CICD_ROOT = "${env.WORKSPACE}/private-cicd" + } else if (fileExists("${env.WORKSPACE}/scripts/install-build-deps-ubuntu.sh")) { + env.CICD_SCRIPT_DIR = "${env.WORKSPACE}/scripts" + env.CICD_ROOT = env.WORKSPACE + } else { + error('Cannot find install-build-deps-ubuntu.sh (expected private-cicd/scripts or scripts/).') + } + if (params.CLONE_SEPARATE) { + env.CLOUDSTACK_DIR = "${env.WORKSPACE}/cloudstack-src" + } else { + env.CLOUDSTACK_DIR = env.WORKSPACE + } + + def rel = params.CONFIG_DEFAULTS_FILE?.trim() + if (!rel) { + rel = 'config/defaults.yaml' + } + rel = rel.replaceAll('^/+', '') + if (rel.contains('..')) { + error('CONFIG_DEFAULTS_FILE must stay under CICD_ROOT (no .. segments).') + } + def cfgPath = "${env.CICD_ROOT}/${rel}" + echo "Loading CI defaults from: ${cfgPath}" + def baseCfg = [cloudstack: [git_url: 'https://github.com/apache/cloudstack.git', branch: 'main']] + if (fileExists(cfgPath)) { + baseCfg = readYaml(file: cfgPath) ?: baseCfg + } else { + echo "No ${cfgPath}; using built-in CloudStack URL/branch defaults." + } + + def cs = new LinkedHashMap((baseCfg.cloudstack ?: [:]) as Map) + def profileName = params.CONFIG_PROFILE?.trim() + if (profileName && baseCfg.profiles instanceof Map && baseCfg.profiles[profileName]) { + def overlay = baseCfg.profiles[profileName].cloudstack + if (overlay instanceof Map) { + cs.putAll(overlay as Map) + } + } + + def urlOverride = params.CLOUDSTACK_GIT_URL?.trim() + def branchOverride = params.CLOUDSTACK_GIT_BRANCH?.trim() + env.EFFECTIVE_CLOUDSTACK_GIT_URL = urlOverride ?: (cs.git_url as String ?: 'https://github.com/apache/cloudstack.git') + env.EFFECTIVE_CLOUDSTACK_GIT_BRANCH = branchOverride ?: (cs.branch as String ?: 'main') + + echo "CICD_ROOT=${env.CICD_ROOT} CICD_SCRIPT_DIR=${env.CICD_SCRIPT_DIR} CLOUDSTACK_DIR=${env.CLOUDSTACK_DIR}" + echo "CONFIG_PROFILE=${profileName ?: '(none)'} EFFECTIVE_CLOUDSTACK_GIT_URL=${env.EFFECTIVE_CLOUDSTACK_GIT_URL} EFFECTIVE_CLOUDSTACK_GIT_BRANCH=${env.EFFECTIVE_CLOUDSTACK_GIT_BRANCH}" + } + } + } + + stage('Phase -1: validate private-cicd') { + when { + expression { return params.PIPELINE_PHASE == 'preflight' } + } + steps { + sh """CICD_ROOT='${env.CICD_ROOT}' bash '${env.CICD_SCRIPT_DIR}/validate-local.sh'""" + } + } + + stage('Clone CloudStack') { + when { + allOf { + expression { return params.PIPELINE_PHASE in ['build-only', 'build-ontap-fast'] } + expression { return params.CLONE_SEPARATE } + } + } + steps { + sh 'rm -rf cloudstack-src && mkdir cloudstack-src' + dir('cloudstack-src') { + checkout( + [ + $class : 'GitSCM', + branches : [[name: "*/${env.EFFECTIVE_CLOUDSTACK_GIT_BRANCH}"]], + doGenerateSubmoduleConfigurations: false, + extensions : [], + submoduleCfg : [], + userRemoteConfigs : [[url: env.EFFECTIVE_CLOUDSTACK_GIT_URL]] + ] + ) + } + } + } + + stage('Validate tree') { + when { + expression { return params.PIPELINE_PHASE in ['build-only', 'build-ontap-fast'] } + } + steps { + script { + def pom = "${env.CLOUDSTACK_DIR}/pom.xml" + if (!fileExists(pom)) { + error("Missing ${pom}. Enable CLONE_SEPARATE or check out CloudStack with Script Path private-cicd/Jenkinsfile at repo root.") + } + } + } + } + + stage('Install OS build dependencies') { + when { + allOf { + expression { return params.PIPELINE_PHASE in ['build-only', 'build-ontap-fast'] } + expression { return params.INSTALL_APT_DEPS } + } + } + steps { + sh """bash '${env.CICD_SCRIPT_DIR}/install-build-deps-ubuntu.sh'""" + } + } + + stage('Optional ipmitool wrapper') { + when { + allOf { + expression { return params.PIPELINE_PHASE in ['build-only', 'build-ontap-fast'] } + expression { return params.SETUP_IPMITOOL_WRAPPER } + } + } + steps { + sh """bash '${env.CICD_SCRIPT_DIR}/setup-ipmitool-wrapper.sh'""" + } + } + + stage('Non-OSS (noredist)') { + when { + allOf { + expression { return params.PIPELINE_PHASE == 'build-only' } + expression { return params.ENABLE_NOREDIST } + } + } + steps { + dir("${env.CLOUDSTACK_DIR}") { + sh ''' + set -e + rm -rf nonoss + git clone https://github.com/shapeblue/cloudstack-nonoss.git nonoss + cd nonoss && bash -x install-non-oss.sh + cd .. + rm -rf nonoss + ''' + } + } + } + + stage('Phase 1a: ONTAP fast build') { + when { + expression { return params.PIPELINE_PHASE == 'build-ontap-fast' } + } + steps { + dir("${env.CLOUDSTACK_DIR}") { + script { + def skip = params.SKIP_TESTS ? 'true' : 'false' + sh """SKIP_TESTS='${skip}' CLOUDSTACK_DIR='${env.CLOUDSTACK_DIR}' bash '${env.CICD_SCRIPT_DIR}/mvn-ontap-fast.sh'""" + } + } + } + } + + stage('Phase 1: Maven build') { + when { + expression { return params.PIPELINE_PHASE == 'build-only' } + } + steps { + dir("${env.CLOUDSTACK_DIR}") { + script { + def skip = params.SKIP_TESTS ? '-DskipTests=true' : '' + def noredist = params.ENABLE_NOREDIST ? '-Dnoredist' : '' + sh "mvn -B -P developer,systemvm -Dsimulator ${noredist} clean install ${skip} -T\$(nproc)" + } + } + } + } + } + + post { + always { + script { + if (params.PIPELINE_PHASE in ['build-only', 'build-ontap-fast']) { + dir("${env.CLOUDSTACK_DIR}") { + def junitGlob = '**/target/surefire-reports/*.xml' + if (params.PIPELINE_PHASE == 'build-ontap-fast') { + junitGlob = 'plugins/storage/volume/ontap/target/surefire-reports/*.xml' + } + junit testResults: junitGlob, allowEmptyResults: true + } + } + } + } + failure { + echo 'Pipeline failed — see console output.' + } + } +} diff --git a/private-cicd/README.md b/private-cicd/README.md new file mode 100644 index 000000000000..d1029cc47c49 --- /dev/null +++ b/private-cicd/README.md @@ -0,0 +1,60 @@ +# Private CI/CD (downstream only) + +This directory is **not** part of Apache CloudStack upstream. Do **not** include it in pull requests to `apache/cloudstack`. + +## Option A (default): committed on the NetApp fork + +`private-cicd/` is **tracked on integration branches** (e.g. `dev_branch`, `netapp/main`). Jenkins uses the same checkout as CloudStack. + +- **Script Path:** `private-cicd/Jenkinsfile` +- **Clone CloudStack separately:** leave unchecked (`CLONE_SEPARATE = false`) +- **Upstream PRs:** use branches without `private-cicd/` commits — see [`docs/BRANCH-STRATEGY.md`](docs/BRANCH-STRATEGY.md) +- **Layout:** [`docs/FOLDER-LAYOUT.md`](docs/FOLDER-LAYOUT.md) + +## Rollout phases + +| Phase | `PIPELINE_PHASE` | Status | +|-------|------------------|--------| +| **-1 — Preflight** | `preflight` | Implemented | +| **1a — ONTAP fast** | `build-ontap-fast` | Implemented (`scripts/mvn-ontap-fast.sh`) | +| **1 — Full build** | `build-only` | Implemented | +| **2 — Marvin** | `marvin` | Stub — see `config/marvin.yaml`, `scripts/marvin-run.sh` | +| **3 — CD** | `delivery` | Planned | + +## Quick start + +```bash +# Validate CI tree only (no Maven) +./private-cicd/scripts/validate-local.sh + +# ONTAP plugin compile + JUnit (from repo root; only ontap *Test.java, not whole tree) +CLOUDSTACK_DIR=$PWD SKIP_TESTS=false ./private-cicd/scripts/mvn-ontap-fast.sh + +# Full build (long) +CLOUDSTACK_DIR=$PWD SKIP_TESTS=true ./private-cicd/scripts/mvn-full.sh +``` + +Do not use `mvn -pl :cloud-plugin-storage-volume-ontap -am test` alone — `-am test` runs upstream module tests (e.g. `engine/schema`), which may call `sudo mount` and block on `Password:`. + +## Configuration + +| File | Purpose | +|------|---------| +| [`config/defaults.yaml`](config/defaults.yaml) | Git URL, branch, profiles | +| [`config/build-fast.yaml`](config/build-fast.yaml) | ONTAP `-pl -am` settings | +| [`config/marvin.yaml`](config/marvin.yaml) | Marvin phase (Phase 2) | +| [`config/qa.yaml.example`](config/qa.yaml.example) | Copy to `qa.yaml` for local overrides (gitignored) | + +## Marvin tests + +- Product tests: `test/integration/plugins/ontap/` +- CI bundles: [`marvin/bundles.txt`](marvin/bundles.txt) +- Zone templates: [`marvin/zones/`](marvin/zones/) + +## Alternative: separate CI repository + +Copy this tree to a dedicated repo and set `CLONE_SEPARATE = true` in Jenkins. See historical note in git history if you migrate from Option A. + +## License + +Scripts and the Jenkinsfile are for internal use only; not submitted to the ASF as part of CloudStack. diff --git a/private-cicd/config/build-fast.yaml b/private-cicd/config/build-fast.yaml new file mode 100644 index 000000000000..c0cead66c995 --- /dev/null +++ b/private-cicd/config/build-fast.yaml @@ -0,0 +1,27 @@ +# Fast build profile: ONTAP storage plugin only (Maven reactor -pl -am). +# Used by PIPELINE_PHASE=build-ontap-fast and scripts/mvn-ontap-fast.sh. + +ontap: + maven_artifact: cloud-plugin-storage-volume-ontap + maven_pl: ":cloud-plugin-storage-volume-ontap" + plugin_path: plugins/storage/volume/ontap + +maven: + profiles: developer + skip_tests: false + extra_args: "" + # Two-step: (-am -DskipTests install) then (-pl ontap test). Never use "-am test" + # or upstream modules (e.g. engine/schema) run sudo-mount tests and hang on Password: + steps: + - install_deps_skip_tests + - test_ontap_only + +# Paths that trigger a full build when changed (git diff); optional in Jenkins later. +change_detection: + full_build_paths: + - client/ + - engine/ + - api/ + - framework/ + - plugins/storage/volume/default/ + - pom.xml diff --git a/private-cicd/config/defaults.yaml b/private-cicd/config/defaults.yaml new file mode 100644 index 000000000000..bd158ea0b426 --- /dev/null +++ b/private-cicd/config/defaults.yaml @@ -0,0 +1,25 @@ +# Private CI defaults — safe to extend with new repositories or profiles. +# Jenkins resolves: parameter override > profile overlay > values here. +# +# Extension ideas (not all wired in Jenkins yet): +# nonoss: { git_url: "...", branch: "..." } +# +# Phase 2 (Marvin) — reserved; Jenkinsfile will read when implemented: +# marvin: +# python_version: "3.10" +# zone_config: setup/dev/advdualzone.cfg +# +# Phase 3 (CD) — reserved: +# delivery: +# artifact_repo_url: https://artifacts.example.com/cloudstack + +cloudstack: + git_url: https://github.com/apache/cloudstack.git + branch: main + +# Optional named presets. Select via Jenkins parameter CONFIG_PROFILE. +profiles: + example-lts-branch: + cloudstack: + branch: "4.19" + git_url: https://github.com/apache/cloudstack.git diff --git a/private-cicd/config/marvin.yaml b/private-cicd/config/marvin.yaml new file mode 100644 index 000000000000..d371d53982bd --- /dev/null +++ b/private-cicd/config/marvin.yaml @@ -0,0 +1,26 @@ +# Marvin / simulator integration (Phase 2). Jenkins reads this when PIPELINE_PHASE=marvin. + +python_version: "3.10" + +# Zone config under private-cicd/marvin/zones/ (lab secrets stay in Jenkins credentials). +zone_config: marvin/zones/ontap-simulator.cfg.example + +# Test paths relative to CloudStack test/integration/ (see marvin/bundles.txt). +default_bundle: ontap-smoke + +bundles: + ontap-smoke: + - plugins/ontap/test_ontap_smoke.py + ontap-all: + - plugins/ontap/ + +maven: + # Marvin needs a built management server; run full build before marvin phase. + require_full_build: true + profiles: developer,systemvm + simulator: true + +mysql: + database: cloud + user: cloud + # password: set via Jenkins credential / env MARVIN_DB_PASSWORD diff --git a/private-cicd/config/qa.yaml.example b/private-cicd/config/qa.yaml.example new file mode 100644 index 000000000000..a9d59c384752 --- /dev/null +++ b/private-cicd/config/qa.yaml.example @@ -0,0 +1,11 @@ +# Copy to config/qa.yaml and customize (qa.yaml may be gitignored locally if it contains secrets). +# Select in Jenkins via CONFIG_DEFAULTS_FILE=config/qa.yaml + +cloudstack: + git_url: https://github.com/NetApp/netapp-cloudstack.git + branch: dev_branch + +profiles: + nightly: + cloudstack: + branch: main diff --git a/private-cicd/docker/Dockerfile.agent b/private-cicd/docker/Dockerfile.agent new file mode 100644 index 000000000000..2efc195df5b2 --- /dev/null +++ b/private-cicd/docker/Dockerfile.agent @@ -0,0 +1,17 @@ +# Jenkins (or other CI) build agent image — private downstream use only. +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl git openssh-client \ + openjdk-17-jdk-headless maven \ + python3 python3-pip python3-venv \ + && rm -rf /var/lib/apt/lists/* + +# Optional: run private-cicd/scripts/install-build-deps-ubuntu.sh at image build time +COPY scripts/install-build-deps-ubuntu.sh /tmp/install-build-deps-ubuntu.sh +RUN chmod +x /tmp/install-build-deps-ubuntu.sh && /tmp/install-build-deps-ubuntu.sh && rm -f /tmp/install-build-deps-ubuntu.sh + +WORKDIR /workspace diff --git a/private-cicd/docs/BRANCH-STRATEGY.md b/private-cicd/docs/BRANCH-STRATEGY.md new file mode 100644 index 000000000000..2826cb3a9a4a --- /dev/null +++ b/private-cicd/docs/BRANCH-STRATEGY.md @@ -0,0 +1,25 @@ +# Branch strategy (Option A — private-cicd on the fork) + +`private-cicd/` is **committed** on NetApp integration branches (e.g. `dev_branch`, `netapp/main`). It is **not** included in pull requests to `apache/cloudstack`. + +## Recommended workflow + +1. **Integration branch** (`dev_branch`, `netapp/main`) — contains `private-cicd/` and ONTAP plugin work. +2. **Upstream PR branch** — create from `apache/cloudstack` (or rebase) **without** `private-cicd/` commits. +3. **Jenkins** — multibranch on the fork; **Script Path** = `private-cicd/Jenkinsfile`; `CLONE_SEPARATE` = false. + +## Before opening an Apache PR + +```bash +# Ensure private-cicd is not in the commits you push upstream +git log --oneline apache/main..HEAD -- private-cicd/ +``` + +If those commits appear, recreate the PR branch from upstream and cherry-pick only product changes. + +## Files that may stay local only + +- `config/qa.yaml` (copy from `config/qa.yaml.example`) +- `marvin/zones/*.cfg` (non-`.example` configs with secrets) + +Optional: add `config/qa.yaml` and `marvin/zones/*.cfg` to `.gitignore` while keeping `private-cicd/` tracked. diff --git a/private-cicd/docs/FOLDER-LAYOUT.md b/private-cicd/docs/FOLDER-LAYOUT.md new file mode 100644 index 000000000000..cb64a460b5b6 --- /dev/null +++ b/private-cicd/docs/FOLDER-LAYOUT.md @@ -0,0 +1,37 @@ +# private-cicd folder layout + +```text +private-cicd/ +├── Jenkinsfile # Main pipeline (preflight, build-only, build-ontap-fast, …) +├── README.md +├── config/ +│ ├── defaults.yaml # Git URL, profiles +│ ├── build-fast.yaml # ONTAP -pl -am settings +│ ├── marvin.yaml # Phase 2 Marvin settings +│ └── qa.yaml.example # Team override template +├── docker/ +│ └── Dockerfile.agent # Optional Jenkins agent image +├── docs/ +│ ├── BRANCH-STRATEGY.md # Option A: fork vs upstream PRs +│ ├── FOLDER-LAYOUT.md # This file +│ └── IMPLEMENTATION-PHASES.md +├── marvin/ +│ ├── bundles.txt # Named test lists +│ ├── README.md +│ └── zones/ # Zone cfg templates (secrets not committed) +├── scripts/ +│ ├── install-build-deps-ubuntu.sh +│ ├── setup-ipmitool-wrapper.sh +│ ├── validate-local.sh +│ ├── mvn-ontap-fast.sh # ONTAP compile + JUnit +│ ├── mvn-full.sh # Full mvn install +│ └── marvin-run.sh # Phase 2 (stub) +└── test/ # (none — Marvin tests under CloudStack test/integration/plugins/ontap/) +``` + +CloudStack product paths used by CI: + +| Path | Role | +|------|------| +| `plugins/storage/volume/ontap/` | ONTAP plugin source + JUnit | +| `test/integration/plugins/ontap/` | Marvin tests | diff --git a/private-cicd/docs/IMPLEMENTATION-PHASES.md b/private-cicd/docs/IMPLEMENTATION-PHASES.md new file mode 100644 index 000000000000..b09df41d285b --- /dev/null +++ b/private-cicd/docs/IMPLEMENTATION-PHASES.md @@ -0,0 +1,95 @@ +# Private CI/CD — three-phase rollout + +This file is under `private-cicd/` only (not for Apache CloudStack upstream). + +## Phase -1 — Preflight (`preflight`) + +**Goal:** Validate the private CI tree itself (shell syntax, YAML) on the **same** Jenkins agent you will use for builds — **no** CloudStack `pom.xml`, **no** Maven, **no** clone (unless you later add optional checks). + +**Implemented:** + +- Jenkins: set `PIPELINE_PHASE` to **`preflight`**. After `Resolve CloudStack directory`, the job runs `scripts/validate-local.sh` with `CICD_ROOT` pointing at this tree. +- Locally: `./private-cicd/scripts/validate-local.sh` (same checks). + +**Agent needs:** `bash`; for YAML parsing, one of Ruby (stdlib `yaml`), Python with PyYAML, or `yq`. If none are present, the script skips YAML with a message (non-fatal); install a parser on agents for strict validation. + +**How to validate behaviour:** Run a Jenkins job with `preflight` and confirm the **Phase -1: validate private-cicd** stage is green and no Maven stages run. Then run `build-only` for Phase 1. + +## Phase 1a — ONTAP fast build (`build-ontap-fast`) + +**Goal:** Fast PR feedback — compile and JUnit for `cloud-plugin-storage-volume-ontap` only (`-pl -am test`). + +**Implemented:** + +- `scripts/mvn-ontap-fast.sh`, `config/build-fast.yaml` +- Jenkins stage **Phase 1a: ONTAP fast build** +- JUnit glob: `plugins/storage/volume/ontap/target/surefire-reports/*.xml` + +**Local:** + +```bash +CLOUDSTACK_DIR=$PWD SKIP_TESTS=false ./private-cicd/scripts/mvn-ontap-fast.sh +``` + +**Note:** Does not replace full build before Marvin or release; run `build-only` on merge/nightly. + +**Important:** `mvn-ontap-fast.sh` uses two Maven invocations: `(-am -DskipTests install)` then `(-pl ontap test)`. A single `mvn -pl ontap -am test` runs upstream tests (e.g. `engine/schema` / `SystemVmTemplateRegistrationTest`) and can hang on `sudo` `Password:`. + +## Phase 1 — Build only (full) + +**Goal:** Prove a reproducible compile on your Jenkins agents (same intent as upstream `.github/workflows/build.yml`, without living in `.github/`). + +**Implemented:** + +- Declarative pipeline: `PIPELINE_PHASE` = **build-only**. +- Optional fast path: **build-ontap-fast** (default first choice in Jenkins parameter list). +- Checkout modes: multibranch workspace vs separate clone (`CLONE_SEPARATE`). +- Config-driven Git URL/branch via `config/defaults.yaml` + optional `CONFIG_PROFILE`. +- OS packages script, optional ipmitool wrapper, optional noredist, Maven `developer,systemvm` + `simulator`, JUnit collection from Surefire. +- Local checks: `scripts/validate-local.sh`. +- Optional agent image: `docker/Dockerfile.agent`. + +**Your checklist to “done” for Phase 1:** + +1. Jenkins: Pipeline + Git + Pipeline Utility Steps (`readYaml`); agent with JDK 17, Maven, RAM/disk. +2. One successful run with your real `defaults.yaml` / profile and `SKIP_TESTS=true`, then decide on `SKIP_TESTS=false`. +3. Optional: bake deps into the Docker agent and disable `INSTALL_APT_DEPS` on agents without sudo. + +## Phase 2 — Marvin / simulator integration (next) + +**Goal:** Run a subset (or matrix) of `test/integration/` against management server in simulator mode, aligned with upstream `ci.yml` patterns. + +**Planned work (not implemented yet):** + +- MySQL service, DB deploy goals, Marvin wheel install, Jacoco (optional), `nosetests` with xUnit output. +- Jenkins **matrix** or parallel branches for test bundles; timeouts and log archival (`MarvinLogs`). +- Extend `config/defaults.yaml` (or a dedicated `config/marvin.yaml`) for Python version, test lists, `MAVEN_OPTS`, zone config paths. +- Either new stages in a second `Jenkinsfile` (e.g. `Jenkinsfile.marvin`) or the same repo with `PIPELINE_PHASE=marvin` wired to those stages. + +**Prerequisite:** Phase 1 green on the same (or larger) agent class. + +## Phase 3 — CD (delivery / deploy) + +**Goal:** Promote built artifacts (DEB/RPM, images, internal packages) to repositories and optionally to environments with approvals. + +**Planned work (not implemented yet):** + +- Package or image build stage; push to internal registry/artifact server (credentials via Jenkins). +- Promotion model: dev → staging → prod; manual `input` or external approval gate. +- Rollback notes and config per environment (separate YAML or Jenkins credentials). + +**Prerequisite:** Stable artifact identity from Phase 1 (and usually test confidence from Phase 2). + +--- + +## Jenkins parameter `PIPELINE_PHASE` + +| Value | Behavior | +|------------|----------| +| `build-ontap-fast` | Phase 1a: ONTAP `-pl -am test` via `mvn-ontap-fast.sh`. | +| `build-only` | Phase 1: full `mvn clean install`. | +| `preflight` | Phase -1: runs `validate-local.sh` only; skips Maven and CloudStack tree validation. | +| `marvin` | Fails fast until Phase 2 is implemented (reserved). | +| `delivery` | Fails fast until Phase 3 is implemented (reserved). | + +This keeps one job definition while you extend the repo over time. diff --git a/private-cicd/marvin/README.md b/private-cicd/marvin/README.md new file mode 100644 index 000000000000..629024d120da --- /dev/null +++ b/private-cicd/marvin/README.md @@ -0,0 +1,10 @@ +# Marvin assets (private CI) + +- **`zones/`** — Zone / simulator configuration templates. Copy `*.example` to `*.cfg` locally or inject paths via Jenkins; do not commit secrets. +- **`bundles.txt`** — Named test lists for Jenkins matrix jobs (`bundle-name=path1,path2`). + +Product Marvin tests live under CloudStack: + +`test/integration/plugins/ontap/` + +Those tests can be submitted upstream when appropriate; this directory stays downstream-only. diff --git a/private-cicd/marvin/bundles.txt b/private-cicd/marvin/bundles.txt new file mode 100644 index 000000000000..aabf68f3bde1 --- /dev/null +++ b/private-cicd/marvin/bundles.txt @@ -0,0 +1,5 @@ +# Named Marvin test bundles (paths relative to test/integration/). +# Used by scripts/marvin-run.sh and config/marvin.yaml. + +ontap-smoke=plugins/ontap/test_ontap_smoke.py +ontap-all=plugins/ontap/ diff --git a/private-cicd/marvin/zones/README.md b/private-cicd/marvin/zones/README.md new file mode 100644 index 000000000000..6f359fa9f72a --- /dev/null +++ b/private-cicd/marvin/zones/README.md @@ -0,0 +1,8 @@ +# Zone configuration + +Place simulator or lab zone files here, for example: + +- `ontap-simulator.cfg` — used by Jenkins Marvin jobs (not committed if it contains secrets) +- `ontap-simulator.cfg.example` — template without credentials + +Reference upstream samples under `setup/dev/` in the CloudStack tree when authoring configs. diff --git a/private-cicd/marvin/zones/ontap-simulator.cfg.example b/private-cicd/marvin/zones/ontap-simulator.cfg.example new file mode 100644 index 000000000000..cb5b6deeebda --- /dev/null +++ b/private-cicd/marvin/zones/ontap-simulator.cfg.example @@ -0,0 +1,12 @@ +# Example zone stub for ONTAP Marvin jobs. Copy to ontap-simulator.cfg and align with setup/dev/*.cfg. +# Do not commit real credentials. + +[cloud] +name = CloudStack CI ONTAP +dns1 = 8.8.8.8 +dns2 = 8.8.4.4 +internaldns1 = 8.8.8.8 +internaldns2 = 8.8.4.4 + +# Extend with zone, pod, cluster, primary storage, and ONTAP provider settings +# per your simulator or lab layout. diff --git a/private-cicd/scripts/install-build-deps-ubuntu.sh b/private-cicd/scripts/install-build-deps-ubuntu.sh new file mode 100755 index 000000000000..c6a1b29f4703 --- /dev/null +++ b/private-cicd/scripts/install-build-deps-ubuntu.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Private downstream build dependencies (Ubuntu/Debian). +# Aligns loosely with Apache CloudStack GitHub Actions build workflow. + +set -euo pipefail + +if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + SUDO="sudo" +else + SUDO="" +fi + +$SUDO apt-get update +$SUDO apt-get install -y \ + git \ + uuid-runtime \ + genisoimage \ + netcat-openbsd \ + ipmitool \ + build-essential \ + libgcrypt20 \ + libgpg-error-dev \ + libgpg-error0 \ + libopenipmi0 \ + libpython3-dev \ + libssl-dev \ + libffi-dev \ + python3-openssl \ + python3-dev \ + python3-setuptools \ + wget \ + unzip diff --git a/private-cicd/scripts/marvin-run.sh b/private-cicd/scripts/marvin-run.sh new file mode 100755 index 000000000000..19c53e42c03a --- /dev/null +++ b/private-cicd/scripts/marvin-run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Run Marvin integration tests (Phase 2 stub). Requires prior full Maven build + Marvin install. +# See private-cicd/docs/IMPLEMENTATION-PHASES.md and config/marvin.yaml. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CICD_ROOT="${CICD_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" +CLOUDSTACK_DIR="${CLOUDSTACK_DIR:?Set CLOUDSTACK_DIR}" + +BUNDLE="${MARVIN_BUNDLE:-ontap-smoke}" +BUNDLES_FILE="${CICD_ROOT}/marvin/bundles.txt" + +echo "==> Marvin run (bundle=$BUNDLE) — not fully wired yet." +echo " CloudStack: $CLOUDSTACK_DIR" +echo " Bundles file: $BUNDLES_FILE" +echo " Implement: MS simulator start, DB deploy, nosetests per config/marvin.yaml" +exit 1 diff --git a/private-cicd/scripts/mvn-full.sh b/private-cicd/scripts/mvn-full.sh new file mode 100755 index 000000000000..70d07d0c3c0b --- /dev/null +++ b/private-cicd/scripts/mvn-full.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Full CloudStack Maven build (Phase 1). Mirrors private-cicd/Jenkinsfile build-only stage. + +set -euo pipefail + +CLOUDSTACK_DIR="${CLOUDSTACK_DIR:?Set CLOUDSTACK_DIR to the CloudStack repo root}" + +SKIP_TESTS="${SKIP_TESTS:-false}" +ENABLE_NOREDIST="${ENABLE_NOREDIST:-false}" +THREADS="${MAVEN_THREADS:-$(nproc)}" + +skip_flag="" +if [[ "$SKIP_TESTS" == "true" ]]; then + skip_flag="-DskipTests=true" +fi + +noredist_flag="" +if [[ "$ENABLE_NOREDIST" == "true" ]]; then + noredist_flag="-Dnoredist" +fi + +echo "==> Full build in $CLOUDSTACK_DIR" +cd "$CLOUDSTACK_DIR" + +exec mvn -B -P developer,systemvm -Dsimulator \ + ${noredist_flag} \ + clean install \ + ${skip_flag} \ + -T"${THREADS}" diff --git a/private-cicd/scripts/mvn-ontap-fast.sh b/private-cicd/scripts/mvn-ontap-fast.sh new file mode 100755 index 000000000000..c96773ff785c --- /dev/null +++ b/private-cicd/scripts/mvn-ontap-fast.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Compile and run JUnit for the ONTAP volume plugin only (-pl -am). Downstream CI only. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CICD_ROOT="${CICD_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" +CLOUDSTACK_DIR="${CLOUDSTACK_DIR:-$(cd "$CICD_ROOT/.." && pwd)}" + +if [[ ! -f "$CLOUDSTACK_DIR/pom.xml" ]]; then + echo "CLOUDSTACK_DIR must contain pom.xml (got: $CLOUDSTACK_DIR)" >&2 + exit 1 +fi + +SKIP_TESTS="${SKIP_TESTS:-false}" +EXTRA="${MAVEN_EXTRA_ARGS:-}" + +skip_flag="" +if [[ "$SKIP_TESTS" == "true" ]]; then + skip_flag="-DskipTests=true" +fi + +echo "==> ONTAP fast build in $CLOUDSTACK_DIR" +cd "$CLOUDSTACK_DIR" + +# Step 1: compile/install plugin dependencies (-am) without running their tests. +# Using "mvn … -am test" would run the entire upstream reactor (e.g. engine/schema +# SystemVmTemplateRegistrationTest), which can invoke "sudo mount" and hang on Password: +echo "==> Step 1/2: install dependencies (skipTests)" +mvn -B -P developer \ + -pl :cloud-plugin-storage-volume-ontap -am \ + -DskipTests=true \ + $EXTRA \ + install + +if [[ "$SKIP_TESTS" == "true" ]]; then + echo "==> Step 2/2: skipped (SKIP_TESTS=true)" + exit 0 +fi + +# Step 2: run tests only on the ONTAP plugin module (no -am). +echo "==> Step 2/2: ONTAP plugin tests only" +exec mvn -B -P developer \ + -pl :cloud-plugin-storage-volume-ontap \ + $EXTRA \ + test diff --git a/private-cicd/scripts/setup-ipmitool-wrapper.sh b/private-cicd/scripts/setup-ipmitool-wrapper.sh new file mode 100755 index 000000000000..ab8244ecace0 --- /dev/null +++ b/private-cicd/scripts/setup-ipmitool-wrapper.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Optional CloudStack-style ipmitool wrapper (matches upstream CI expectations). +set -euo pipefail + +if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + SUDO="sudo" +else + SUDO="" +fi + +$SUDO mkdir -p /usr/share/cloudstack-common +if [[ ! -f /usr/share/cloudstack-common/ipmitool ]]; then + $SUDO cp /usr/bin/ipmitool /usr/share/cloudstack-common/ipmitool + $SUDO chmod 755 /usr/share/cloudstack-common/ipmitool +fi + +$SUDO tee /usr/bin/ipmitool > /dev/null << 'EOF' +#!/bin/bash +/usr/share/cloudstack-common/ipmitool -C3 "$@" +EOF +$SUDO chmod 755 /usr/bin/ipmitool diff --git a/private-cicd/scripts/validate-local.sh b/private-cicd/scripts/validate-local.sh new file mode 100755 index 000000000000..514e35021c09 --- /dev/null +++ b/private-cicd/scripts/validate-local.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Local validation for private-cicd only — no CloudStack source required. +# Safe to keep out of upstream Apache PR scope (entire tree under private-cicd/). + +set -euo pipefail + +usage() { + cat << 'EOF' +Usage: validate-local.sh [--with-docker] + + Validates shell scripts (bash -n) and YAML under this repo's private-cicd + (or standalone CI repo) root — independent of CloudStack merge scope. + + --with-docker Also run: docker build -f docker/Dockerfile.agent . + (requires Docker; run from CICD_ROOT or set CICD_ROOT). + +Environment: + CICD_ROOT Root containing scripts/, config/, docker/ (default: inferred). + +Exit status: 0 if all checks pass, non-zero otherwise. +EOF +} + +WITH_DOCKER=false +for arg in "$@"; do + case "$arg" in + -h|--help) usage; exit 0 ;; + --with-docker) WITH_DOCKER=true ;; + *) echo "Unknown option: $arg" >&2; usage >&2; exit 2 ;; + esac +done + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -n "${CICD_ROOT:-}" ]]; then + CICD_ROOT="$(cd "$CICD_ROOT" && pwd)" +else + CICD_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +fi + +echo "==> CICD_ROOT=$CICD_ROOT" + +failures=0 + +run_check() { + local name="$1" + shift + echo "==> $name" + if "$@"; then + echo " OK" + else + echo " FAILED" >&2 + failures=$((failures + 1)) + fi +} + +# --- Shell: bash -n on all scripts/*.sh (no CloudStack tree required) +while IFS= read -r -d '' f; do + run_check "bash -n: ${f#$CICD_ROOT/}" bash -n "$f" +done < <(find "$CICD_ROOT/scripts" -maxdepth 1 -type f -name '*.sh' -print0 2>/dev/null || true) + +if [[ ! -d "$CICD_ROOT/scripts" ]]; then + echo "No scripts directory at $CICD_ROOT/scripts" >&2 + failures=$((failures + 1)) +fi + +# --- YAML under config/ +yaml_ok=false +if command -v ruby >/dev/null 2>&1 && ruby -ryaml -e 'true' >/dev/null 2>&1; then + yaml_check() { ruby -ryaml -e "YAML.load_file(ARGV[0])" "$1"; } + yaml_ok=true +elif command -v python3 >/dev/null 2>&1; then + if python3 -c 'import yaml' >/dev/null 2>&1; then + yaml_check() { python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" "$1"; } + yaml_ok=true + fi +elif command -v yq >/dev/null 2>&1; then + yaml_check() { yq e '.' "$1" >/dev/null; } + yaml_ok=true +fi + +if [[ -d "$CICD_ROOT/config" ]]; then + shopt -s nullglob + yfiles=("$CICD_ROOT"/config/*.yaml "$CICD_ROOT"/config/*.yml) + shopt -u nullglob + if [[ ${#yfiles[@]} -eq 0 ]]; then + echo "==> YAML: no *.yaml in $CICD_ROOT/config (skipped)" + elif [[ "$yaml_ok" == true ]]; then + for f in "${yfiles[@]}"; do + run_check "YAML: ${f#$CICD_ROOT/}" yaml_check "$f" + done + else + echo "==> YAML: skipped (install Ruby+psych, PyYAML, or yq to validate)" >&2 + fi +else + echo "==> YAML: no config directory (skipped)" +fi + +# --- Docker (optional) +if [[ "$WITH_DOCKER" == true ]]; then + if ! command -v docker >/dev/null 2>&1; then + echo "==> docker: not installed, skipping" >&2 + failures=$((failures + 1)) + else + df="$CICD_ROOT/docker/Dockerfile.agent" + if [[ ! -f "$df" ]]; then + echo "==> docker: missing $df" >&2 + failures=$((failures + 1)) + else + run_check "docker build (agent image)" docker build -f "$df" -t cloudstack-private-cicd-agent:validate "$CICD_ROOT" + fi + fi +fi + +if [[ "$failures" -ne 0 ]]; then + echo "==> Done with $failures failure(s)." >&2 + exit 1 +fi + +echo "==> All checks passed." diff --git a/test/integration/plugins/ontap/README.md b/test/integration/plugins/ontap/README.md new file mode 100644 index 000000000000..e1a2940b19dd --- /dev/null +++ b/test/integration/plugins/ontap/README.md @@ -0,0 +1,10 @@ +# ONTAP plugin — Marvin integration tests + +Add Marvin tests here (e.g. `test_ontap_smoke.py`). They can be included in Apache upstream PRs when ready. + +CI wiring: + +- Bundles: `private-cicd/marvin/bundles.txt` +- Zone config: `private-cicd/marvin/zones/` (downstream only) + +Follow patterns in `test/integration/plugins/solidfire/` and `test/integration/plugins/linstor/`.