blob: 8a7a90d5d0d394ce4f62490a41e87550022d1cac [file] [log] [blame]
/**
*
* Launch heat stack with CI/CD lab infrastructure
*
* Expected parameters:
* HEAT_TEMPLATE_URL URL to git repo with Heat templates
* HEAT_TEMPLATE_CREDENTIALS Credentials to the Heat templates repo
* HEAT_TEMPLATE_BRANCH Heat templates repo branch
* HEAT_STACK_NAME Heat stack name
* HEAT_STACK_TEMPLATE Heat stack HOT template
* HEAT_STACK_ENVIRONMENT Heat stack environmental parameters
* HEAT_STACK_ZONE Heat stack availability zone
* HEAT_STACK_PUBLIC_NET Heat stack floating IP pool
* HEAT_STACK_DELETE Delete Heat stack when finished (bool)
* HEAT_STACK_CLEANUP_JOB Name of job for deleting Heat stack
* HEAT_STACK_REUSE Reuse Heat stack (don't create one)
*
* SALT_MASTER_CREDENTIALS Credentials to the Salt API
* SALT_MASTER_PORT Port of salt-api, defaults to 8000
*
* OPENSTACK_API_URL OpenStack API address
* OPENSTACK_API_CREDENTIALS Credentials to the OpenStack API
* OPENSTACK_API_PROJECT OpenStack project to connect to
* OPENSTACK_API_CLIENT Versions of OpenStack python clients
* OPENSTACK_API_VERSION Version of the OpenStack API (2/3)
*
*/
common = new com.mirantis.mk.Common()
git = new com.mirantis.mk.Git()
openstack = new com.mirantis.mk.Openstack()
salt = new com.mirantis.mk.Salt()
orchestrate = new com.mirantis.mk.Orchestrate()
def python = new com.mirantis.mk.Python()
def pepperEnv = "pepperEnv"
_MAX_PERMITTED_STACKS = 2
timeout(time: 12, unit: 'HOURS') {
node {
try {
// connection objects
def openstackCloud
// value defaults
def openstackVersion = OPENSTACK_API_CLIENT ? OPENSTACK_API_CLIENT : 'liberty'
def openstackEnv = "${env.WORKSPACE}/venv"
try {
sshPubKey = SSH_PUBLIC_KEY
} catch (MissingPropertyException e) {
sshPubKey = false
}
if (HEAT_STACK_REUSE.toBoolean() == true && HEAT_STACK_NAME == '') {
error("If you want to reuse existing stack you need to provide it's name")
}
if (HEAT_STACK_REUSE.toBoolean() == false) {
// Don't allow to set custom heat stack name
wrap([$class: 'BuildUser']) {
if (env.BUILD_USER_ID) {
HEAT_STACK_NAME = "${env.BUILD_USER_ID}-${JOB_NAME}-${BUILD_NUMBER}"
} else {
HEAT_STACK_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"
}
currentBuild.description = HEAT_STACK_NAME
}
}
//
// Bootstrap
//
stage ('Download Heat templates') {
git.checkoutGitRepository('template', HEAT_TEMPLATE_URL, HEAT_TEMPLATE_BRANCH, HEAT_TEMPLATE_CREDENTIALS)
}
stage('Install OpenStack CLI') {
openstack.setupOpenstackVirtualenv(openstackEnv, openstackVersion)
}
stage('Connect to OpenStack cloud') {
openstackCloud = openstack.createOpenstackEnv(openstackEnv,
OPENSTACK_API_URL, OPENSTACK_API_CREDENTIALS,
OPENSTACK_API_PROJECT, OPENSTACK_API_PROJECT_DOMAIN,
OPENSTACK_API_PROJECT_ID, OPENSTACK_API_USER_DOMAIN,
OPENSTACK_API_VERSION)
openstack.getKeystoneToken(openstackCloud, openstackEnv)
wrap([$class: 'BuildUser']) {
if (env.BUILD_USER_ID && !env.BUILD_USER_ID.equals("jenkins") && !HEAT_STACK_REUSE.toBoolean()) {
def existingStacks = openstack.getStacksForNameContains(openstackCloud, "${env.BUILD_USER_ID}-${JOB_NAME}", openstackEnv)
if(existingStacks.size() >= _MAX_PERMITTED_STACKS){
HEAT_STACK_DELETE = "false"
throw new Exception("You cannot create new stack, you already have ${_MAX_PERMITTED_STACKS} stacks of this type (${JOB_NAME}). \nStack names: ${existingStacks}")
}
}
}
}
if (HEAT_STACK_REUSE.toBoolean() == false) {
stage('Launch new Heat stack') {
envParams = [
'cluster_zone': HEAT_STACK_ZONE,
'cluster_public_net': HEAT_STACK_PUBLIC_NET
]
openstack.createHeatStack(openstackCloud, HEAT_STACK_NAME, HEAT_STACK_TEMPLATE, envParams, HEAT_STACK_ENVIRONMENT, openstackEnv)
}
}
stage('Connect to Salt master') {
def saltMasterPort
try {
saltMasterPort = SALT_MASTER_PORT
} catch (MissingPropertyException e) {
saltMasterPort = 6969
}
saltMasterHost = openstack.getHeatStackOutputParam(openstackCloud, HEAT_STACK_NAME, 'salt_master_ip', openstackEnv)
currentBuild.description = "${HEAT_STACK_NAME}: ${saltMasterHost}"
saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
python.setupPepperVirtualenv(pepperEnv, saltMasterUrl, SALT_MASTER_CREDENTIALS)
}
//
// Install
//
stage('Install core infra') {
// salt.master, reclass
// refresh_pillar
// sync_all
// linux,openssh,salt.minion.ntp
orchestrate.installFoundationInfra(pepperEnv)
orchestrate.validateFoundationInfra(pepperEnv)
}
stage("Deploy GlusterFS") {
salt.enforceState(pepperEnv, 'I@glusterfs:server', 'glusterfs.server.service', true)
retry(2) {
salt.enforceState(pepperEnv, 'ci01*', 'glusterfs.server.setup', true)
}
sleep(5)
salt.enforceState(pepperEnv, 'I@glusterfs:client', 'glusterfs.client', true)
timeout(5) {
println "Waiting for GlusterFS volumes to get mounted.."
salt.cmdRun(pepperEnv, 'I@glusterfs:client', 'while true; do systemctl -a|grep "GlusterFS File System"|grep -v mounted >/dev/null || break; done')
}
print common.prettyPrint(salt.cmdRun(pepperEnv, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"'))
}
stage("Deploy GlusterFS") {
salt.enforceState(pepperEnv, 'I@haproxy:proxy', 'haproxy,keepalived')
}
stage("Setup Docker Swarm") {
salt.enforceState(pepperEnv, 'I@docker:host', 'docker.host', true)
salt.enforceState(pepperEnv, 'I@docker:swarm:role:master', 'docker.swarm', true)
salt.enforceState(pepperEnv, 'I@docker:swarm:role:master', 'salt', true)
salt.runSaltProcessStep(pepperEnv, 'I@docker:swarm:role:master', 'mine.flush')
salt.runSaltProcessStep(pepperEnv, 'I@docker:swarm:role:master', 'mine.update')
salt.enforceState(pepperEnv, 'I@docker:swarm', 'docker.swarm', true)
print common.prettyPrint(salt.cmdRun(pepperEnv, 'I@docker:swarm:role:master', 'docker node ls'))
}
stage("Configure OSS services") {
salt.enforceState(pepperEnv, 'I@devops_portal:config', 'devops_portal.config')
salt.enforceState(pepperEnv, 'I@rundeck:server', 'rundeck.server')
}
stage("Deploy Docker services") {
// We need /etc/aptly-publisher.yaml to be present before
// services are deployed
// XXX: for some weird unknown reason, refresh_pillar is
// required to execute here
salt.runSaltProcessStep(pepperEnv, 'I@aptly:publisher', 'saltutil.refresh_pillar', [], null, true)
salt.enforceState(pepperEnv, 'I@aptly:publisher', 'aptly.publisher', true)
retry(3) {
sleep(5)
salt.enforceState(pepperEnv, 'I@docker:swarm:role:master', 'docker.client')
}
// XXX: Workaround to have `/var/lib/jenkins` on all
// nodes where are jenkins_slave services are created.
salt.cmdRun(pepperEnv, 'I@docker:swarm', "mkdir -p /var/lib/jenkins")
}
stage("Configure CI/CD services") {
salt.syncAll(pepperEnv, '*')
// Aptly
timeout(10) {
println "Waiting for Aptly to come up.."
retry(2) {
// XXX: retry to workaround magical VALUE_TRIMMED
// response from salt master + to give slow cloud some
// more time to settle down
salt.cmdRun(pepperEnv, 'I@aptly:server', 'while true; do curl -sf http://apt.mcp.mirantis.net:8081/api/version >/dev/null && break; done')
}
}
salt.enforceState(pepperEnv, 'I@aptly:server', 'aptly', true)
// OpenLDAP
timeout(10) {
println "Waiting for OpenLDAP to come up.."
salt.cmdRun(pepperEnv, 'I@openldap:client', 'while true; do curl -sf ldap://172.16.10.254 >/dev/null && break; done')
}
salt.enforceState(pepperEnv, 'I@openldap:client', 'openldap', true)
// Gerrit
timeout(10) {
println "Waiting for Gerrit to come up.."
salt.cmdRun(pepperEnv, 'I@gerrit:client', 'while true; do curl -sf 172.16.10.254:8080 >/dev/null && break; done')
}
salt.enforceState(pepperEnv, 'I@gerrit:client', 'gerrit', true)
// Jenkins
timeout(10) {
println "Waiting for Jenkins to come up.."
salt.cmdRun(pepperEnv, 'I@jenkins:client', 'while true; do curl -sf 172.16.10.254:8081 >/dev/null && break; done')
}
retry(2) {
// XXX: needs retry as first run installs python-jenkins
// thus make jenkins modules available for second run
salt.enforceState(pepperEnv, 'I@jenkins:client', 'jenkins', true)
}
// Postgres client - initialize OSS services databases
timeout(300){
println "Waiting for postgresql database to come up.."
salt.cmdRun(pepperEnv, 'I@postgresql:client', 'while true; do if docker service logs postgresql_postgresql-db | grep "ready to accept"; then break; else sleep 5; fi; done')
}
// XXX: first run usually fails on some inserts, but we need to create databases at first
salt.enforceState(pepperEnv, 'I@postgresql:client', 'postgresql.client', true, false)
// Setup postgres database with integration between
// Pushkin notification service and Security Monkey security audit service
timeout(10) {
println "Waiting for Pushkin to come up.."
salt.cmdRun(pepperEnv, 'I@postgresql:client', 'while true; do curl -sf 172.16.10.254:8887/apps >/dev/null && break; done')
}
salt.enforceState(pepperEnv, 'I@postgresql:client', 'postgresql.client', true)
// Rundeck
timeout(10) {
println "Waiting for Rundeck to come up.."
salt.cmdRun(pepperEnv, 'I@rundeck:client', 'while true; do curl -sf 172.16.10.254:4440 >/dev/null && break; done')
}
salt.enforceState(pepperEnv, 'I@rundeck:client', 'rundeck.client', true)
// Elasticsearch
timeout(10) {
println 'Waiting for Elasticsearch to come up..'
salt.cmdRun(pepperEnv, 'I@elasticsearch:client', 'while true; do curl -sf 172.16.10.254:9200 >/dev/null && break; done')
}
retry(3){
sleep(10)
// XXX: first run sometimes fails on update indexes, so we need to wait
salt.enforceState(pepperEnv, 'I@elasticsearch:client', 'elasticsearch.client', true)
}
}
stage("Finalize") {
//
// Deploy user's ssh key
//
def adminUser
def authorizedKeysFile
def adminUserCmdOut = salt.cmdRun(pepperEnv, 'I@salt:master', "[ ! -d /home/ubuntu ] || echo 'ubuntu user exists'")
if (adminUserCmdOut =~ /ubuntu user exists/) {
adminUser = "ubuntu"
authorizedKeysFile = "/home/ubuntu/.ssh/authorized_keys"
} else {
adminUser = "root"
authorizedKeysFile = "/root/.ssh/authorized_keys"
}
if (sshPubKey) {
println "Deploying provided ssh key at ${authorizedKeysFile}"
salt.cmdRun(pepperEnv, '*', "echo '${sshPubKey}' | tee -a ${authorizedKeysFile}")
}
//
// Generate docs
//
try {
try {
// Run sphinx state to install sphinx-build needed in
// upcomming orchestrate
salt.enforceState(pepperEnv, 'I@sphinx:server', 'sphinx')
} catch (Throwable e) {
true
}
retry(3) {
// TODO: fix salt.orchestrateSystem
// print salt.orchestrateSystem(pepperEnv, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
def out = salt.cmdRun(pepperEnv, 'I@salt:master', 'salt-run state.orchestrate sphinx.orch.generate_doc || echo "Command execution failed"')
print common.prettyPrint(out)
if (out =~ /Command execution failed/) {
throw new Exception("Command execution failed")
}
}
} catch (Throwable e) {
// We don't want sphinx docs to ruin whole build, so possible
// errors are just ignored here
true
}
salt.enforceState(pepperEnv, 'I@nginx:server', 'nginx')
def failedSvc = salt.cmdRun(pepperEnv, '*', """systemctl --failed | grep -E 'loaded[ \t]+failed' && echo 'Command execution failed' || true""")
if (failedSvc =~ /Command execution failed/) {
common.errorMsg("Some services are not running. Environment may not be fully functional!")
}
common.successMsg("""
============================================================
Your CI/CD lab has been deployed and you can enjoy it:
Use sshuttle to connect to your private subnet:
sshuttle -r ${adminUser}@${saltMasterHost} 172.16.10.0/24
And visit services running at 172.16.10.254 (vip address):
9600 HAProxy statistics
8080 Gerrit
8081 Jenkins
8089 LDAP administration
4440 Rundeck
8084 DevOps Portal
8091 Docker swarm visualizer
8090 Reclass-generated documentation
If you provided SSH_PUBLIC_KEY, you can use it to login,
otherwise you need to get private key connected to this
heat template.
DON'T FORGET TO TERMINATE YOUR STACK WHEN YOU DON'T NEED IT!
============================================================""")
}
} catch (Throwable e) {
// If there was an error or exception thrown, the build failed
currentBuild.result = "FAILURE"
currentBuild.description = currentBuild.description ? e.message + " " + currentBuild.description : e.message
throw e
} finally {
// Cleanup
if (HEAT_STACK_DELETE.toBoolean() == true) {
stage('Trigger cleanup job') {
build(job: 'deploy-stack-cleanup', parameters: [
[$class: 'StringParameterValue', name: 'STACK_NAME', value: HEAT_STACK_NAME],
[$class: 'StringParameterValue', name: 'OPENSTACK_API_PROJECT', value: OPENSTACK_API_PROJECT],
])
}
}
}
}
}