Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions private-cicd/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Local overrides with secrets (templates *.example stay tracked)
config/qa.yaml
marvin/zones/*.cfg
!marvin/zones/*.cfg.example
292 changes: 292 additions & 0 deletions private-cicd/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -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]]
]
Comment on lines +188 to +198
)
}
}
}

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.'
}
}
}
60 changes: 60 additions & 0 deletions private-cicd/README.md
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 27 additions & 0 deletions private-cicd/config/build-fast.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading