blob: d67c01deaa1ad7dc2ec20242d32eac9606168591 [file] [log] [blame]
Filip Pytloun0a07f702017-02-24 18:26:18 +01001/**
2 *
3 * Launch heat stack with CI/CD lab infrastructure
4 *
5 * Expected parameters:
6 * HEAT_TEMPLATE_URL URL to git repo with Heat templates
7 * HEAT_TEMPLATE_CREDENTIALS Credentials to the Heat templates repo
8 * HEAT_TEMPLATE_BRANCH Heat templates repo branch
9 * HEAT_STACK_NAME Heat stack name
10 * HEAT_STACK_TEMPLATE Heat stack HOT template
11 * HEAT_STACK_ENVIRONMENT Heat stack environmental parameters
12 * HEAT_STACK_ZONE Heat stack availability zone
13 * HEAT_STACK_PUBLIC_NET Heat stack floating IP pool
14 * HEAT_STACK_DELETE Delete Heat stack when finished (bool)
15 * HEAT_STACK_CLEANUP_JOB Name of job for deleting Heat stack
16 * HEAT_STACK_REUSE Reuse Heat stack (don't create one)
17 *
18 * SALT_MASTER_CREDENTIALS Credentials to the Salt API
Filip Pytloune32fda82017-02-24 18:26:18 +010019 * SALT_MASTER_PORT Port of salt-api, defaults to 8000
Filip Pytloun0a07f702017-02-24 18:26:18 +010020 *
21 * OPENSTACK_API_URL OpenStack API address
22 * OPENSTACK_API_CREDENTIALS Credentials to the OpenStack API
23 * OPENSTACK_API_PROJECT OpenStack project to connect to
24 * OPENSTACK_API_CLIENT Versions of OpenStack python clients
25 * OPENSTACK_API_VERSION Version of the OpenStack API (2/3)
26 *
27 */
28
Filip Pytlounad2b36b2017-03-04 20:33:41 +010029common = new com.mirantis.mk.Common()
Filip Pytloun0a07f702017-02-24 18:26:18 +010030git = new com.mirantis.mk.Git()
31openstack = new com.mirantis.mk.Openstack()
32salt = new com.mirantis.mk.Salt()
33orchestrate = new com.mirantis.mk.Orchestrate()
Jakub Josef458913d2017-05-10 15:37:56 +020034_MAX_PERMITTED_STACKS = 2
Filip Pytlounbfce09d2017-03-01 19:00:43 +010035timestamps {
36 node {
37 try {
38 // connection objects
39 def openstackCloud
40 def saltMaster
Filip Pytloun0a07f702017-02-24 18:26:18 +010041
Filip Pytlounbfce09d2017-03-01 19:00:43 +010042 // value defaults
43 def openstackVersion = OPENSTACK_API_CLIENT ? OPENSTACK_API_CLIENT : 'liberty'
44 def openstackEnv = "${env.WORKSPACE}/venv"
Filip Pytloun0a07f702017-02-24 18:26:18 +010045
Filip Pytloun3eefd3d2017-03-03 14:13:41 +010046 try {
47 sshPubKey = SSH_PUBLIC_KEY
48 } catch (MissingPropertyException e) {
49 sshPubKey = false
50 }
51
Filip Pytloun794ad952017-03-03 10:39:26 +010052 if (HEAT_STACK_REUSE.toBoolean() == true && HEAT_STACK_NAME == '') {
53 error("If you want to reuse existing stack you need to provide it's name")
54 }
55
56 if (HEAT_STACK_REUSE.toBoolean() == false) {
57 // Don't allow to set custom heat stack name
58 wrap([$class: 'BuildUser']) {
Tomáš Kukrál24d7fe62017-03-03 10:57:11 +010059 if (env.BUILD_USER_ID) {
60 HEAT_STACK_NAME = "${env.BUILD_USER_ID}-${JOB_NAME}-${BUILD_NUMBER}"
61 } else {
62 HEAT_STACK_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"
63 }
Filip Pytloun794ad952017-03-03 10:39:26 +010064 currentBuild.description = HEAT_STACK_NAME
65 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +010066 }
Filip Pytloun5b0954b2017-03-01 10:10:18 +010067
Filip Pytloun3d045f82017-03-01 09:44:52 +010068 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010069 // Bootstrap
Filip Pytloun3d045f82017-03-01 09:44:52 +010070 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010071
72 stage ('Download Heat templates') {
73 git.checkoutGitRepository('template', HEAT_TEMPLATE_URL, HEAT_TEMPLATE_BRANCH, HEAT_TEMPLATE_CREDENTIALS)
Filip Pytloun3d045f82017-03-01 09:44:52 +010074 }
Filip Pytloun3d045f82017-03-01 09:44:52 +010075
Filip Pytlounbfce09d2017-03-01 19:00:43 +010076 stage('Install OpenStack CLI') {
77 openstack.setupOpenstackVirtualenv(openstackEnv, openstackVersion)
78 }
Filip Pytloun64123cd2017-03-01 11:26:17 +010079
Filip Pytlounbfce09d2017-03-01 19:00:43 +010080 stage('Connect to OpenStack cloud') {
81 openstackCloud = openstack.createOpenstackEnv(OPENSTACK_API_URL, OPENSTACK_API_CREDENTIALS, OPENSTACK_API_PROJECT)
82 openstack.getKeystoneToken(openstackCloud, openstackEnv)
Jakub Josef458913d2017-05-10 15:37:56 +020083 wrap([$class: 'BuildUser']) {
Tomáš Kukrálab2f3702017-05-11 09:17:43 +020084 if (env.BUILD_USER_ID && !env.BUILD_USER_ID.equals("jenkins") && !HEAT_STACK_REUSE.toBoolean()) {
Jakub Josef78c3f8b2017-05-10 15:45:29 +020085 def existingStacks = openstack.getStacksForNameContains(openstackCloud, "${env.BUILD_USER_ID}-${JOB_NAME}", openstackEnv)
86 if(existingStacks.size() >= _MAX_PERMITTED_STACKS){
Jakub Josef124403a2017-05-10 15:58:06 +020087 HEAT_STACK_DELETE = "false"
Jakub Josef78c3f8b2017-05-10 15:45:29 +020088 throw new Exception("You cannot create new stack, you already have ${_MAX_PERMITTED_STACKS} stacks of this type (${JOB_NAME}). \nStack names: ${existingStacks}")
89 }
Jakub Josef458913d2017-05-10 15:37:56 +020090 }
91 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +010092 }
93
Filip Pytloun794ad952017-03-03 10:39:26 +010094 if (HEAT_STACK_REUSE.toBoolean() == false) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +010095 stage('Launch new Heat stack') {
96 envParams = [
97 'instance_zone': HEAT_STACK_ZONE,
98 'public_net': HEAT_STACK_PUBLIC_NET
99 ]
100 openstack.createHeatStack(openstackCloud, HEAT_STACK_NAME, HEAT_STACK_TEMPLATE, envParams, HEAT_STACK_ENVIRONMENT, openstackEnv)
101 }
102 }
103
104 stage('Connect to Salt master') {
105 def saltMasterPort
106 try {
107 saltMasterPort = SALT_MASTER_PORT
108 } catch (MissingPropertyException e) {
Filip Pytloun2ef26132017-03-10 09:44:37 +0100109 saltMasterPort = 6969
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100110 }
111 saltMasterHost = openstack.getHeatStackOutputParam(openstackCloud, HEAT_STACK_NAME, 'salt_master_ip', openstackEnv)
Jakub Josefbde0d442017-04-07 16:32:58 +0200112 currentBuild.description = "${HEAT_STACK_NAME}: ${saltMasterHost}"
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100113 saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
114 saltMaster = salt.connection(saltMasterUrl, SALT_MASTER_CREDENTIALS)
115 }
116
117 //
118 // Install
119 //
120
121 stage('Install core infra') {
122 // salt.master, reclass
123 // refresh_pillar
124 // sync_all
125 // linux,openssh,salt.minion.ntp
126
127 orchestrate.installFoundationInfra(saltMaster)
128 orchestrate.validateFoundationInfra(saltMaster)
129 }
130
131 stage("Deploy GlusterFS") {
132 salt.enforceState(saltMaster, 'I@glusterfs:server', 'glusterfs.server.service', true)
Filip Pytloun97e6fff2017-03-30 16:56:11 +0200133 retry(2) {
134 salt.enforceState(saltMaster, 'ci01*', 'glusterfs.server.setup', true)
135 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100136 sleep(5)
137 salt.enforceState(saltMaster, 'I@glusterfs:client', 'glusterfs.client', true)
Filip Pytloun5555b452017-04-19 12:41:44 +0200138
139 timeout(5) {
140 println "Waiting for GlusterFS volumes to get mounted.."
141 salt.cmdRun(saltMaster, 'I@glusterfs:client', 'while true; do systemctl -a|grep "GlusterFS File System"|grep -v mounted >/dev/null || break; done')
142 }
Jakub Joseffafd6592017-03-27 18:53:17 +0200143 print common.prettyPrint(salt.cmdRun(saltMaster, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"'))
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100144 }
145
146 stage("Deploy GlusterFS") {
147 salt.enforceState(saltMaster, 'I@haproxy:proxy', 'haproxy,keepalived')
148 }
149
150 stage("Setup Docker Swarm") {
151 salt.enforceState(saltMaster, 'I@docker:host', 'docker.host', true)
152 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.swarm', true)
153 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'salt', true)
154 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.flush')
155 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.update')
156 salt.enforceState(saltMaster, 'I@docker:swarm', 'docker.swarm', true)
Jakub Joseffafd6592017-03-27 18:53:17 +0200157 print common.prettyPrint(salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'docker node ls'))
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100158 }
159
Ilya Kharin04c09982017-03-30 14:46:20 +0400160 stage("Configure OSS services") {
161 salt.enforceState(saltMaster, 'I@devops_portal:config', 'devops_portal.config')
Ilya Kharin7a18c322017-04-24 18:49:34 +0400162 salt.enforceState(saltMaster, 'I@rundeck:server', 'rundeck.server')
Ilya Kharin04c09982017-03-30 14:46:20 +0400163 }
164
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100165 stage("Deploy Docker services") {
Filip Pytloun65b928d2017-04-18 17:19:30 +0200166 retry(3) {
Filip Pytlound6d18502017-04-13 15:35:07 +0200167 sleep(5)
168 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
169 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100170 }
171
172 stage("Configure CI/CD services") {
Filip Pytloun29d0bc12017-03-10 14:39:26 +0100173 salt.syncAll(saltMaster, '*')
174
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100175 // Aptly
Filip Pytloun6cde7882017-03-28 17:22:18 +0200176 timeout(10) {
177 println "Waiting for Aptly to come up.."
Filip Pytlounb1ddf322017-05-12 16:18:31 +0200178 retry(2) {
179 // XXX: retry to workaround magical VALUE_TRIMMED
180 // response from salt master + to give slow cloud some
181 // more time to settle down
182 salt.cmdRun(saltMaster, 'I@aptly:server', 'while true; do curl -sf http://172.16.10.254:8084/api/version >/dev/null && break; done')
183 }
Filip Pytloun6cde7882017-03-28 17:22:18 +0200184 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100185 salt.enforceState(saltMaster, 'I@aptly:server', 'aptly', true)
186
Filip Pytloun03983812017-03-28 13:07:34 +0200187 // OpenLDAP
188 timeout(10) {
189 println "Waiting for OpenLDAP to come up.."
Filip Pytlounb1ddf322017-05-12 16:18:31 +0200190 salt.cmdRun(saltMaster, 'I@openldap:client', 'while true; do curl -sf ldap://172.16.10.254 >/dev/null && break; done')
Filip Pytloun03983812017-03-28 13:07:34 +0200191 }
192 salt.enforceState(saltMaster, 'I@openldap:client', 'openldap', true)
193
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100194 // Gerrit
195 timeout(10) {
196 println "Waiting for Gerrit to come up.."
Filip Pytlounb1ddf322017-05-12 16:18:31 +0200197 salt.cmdRun(saltMaster, 'I@gerrit:client', 'while true; do curl -sf 172.16.10.254:8080 >/dev/null && break; done')
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100198 }
Filip Pytlounb1ddf322017-05-12 16:18:31 +0200199 salt.enforceState(saltMaster, 'I@gerrit:client', 'gerrit', true)
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100200
201 // Jenkins
202 timeout(10) {
203 println "Waiting for Jenkins to come up.."
Filip Pytlounb1ddf322017-05-12 16:18:31 +0200204 salt.cmdRun(saltMaster, 'I@jenkins:client', 'while true; do curl -sf 172.16.10.254:8081 >/dev/null && break; done')
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100205 }
Filip Pytloun45d40742017-05-12 18:19:44 +0200206 retry(2) {
207 // XXX: needs retry as first run installs python-jenkins
208 // thus make jenkins modules available for second run
209 salt.enforceState(saltMaster, 'I@jenkins:client', 'jenkins', true)
210 }
Ilya Kharin7a18c322017-04-24 18:49:34 +0400211
Volodymyr Stoiko75e341e2017-05-30 01:45:21 +0300212 // Postgres client - initialize OSS services databases
213 timeout(300){
214 println "Waiting for postgresql database to come up.."
215 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'while true; do if docker service logs postgresql_db | grep "ready to accept"; then break; else sleep 5; fi; done')
216 }
217 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'postgresql.client')
218
219 // Setup postgres database with integration between
220 // Pushkin notification service and Security Monkey security audit service
221 timeout(10) {
222 println "Waiting for Pushkin to come up.."
223 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'while true; do curl -sf 172.16.10.254:8887/apps >/dev/null && break; done')
224 }
225 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'postgresql.client', true)
226
Ilya Kharin7a18c322017-04-24 18:49:34 +0400227 // Rundeck
228 timeout(10) {
229 println "Waiting for Rundeck to come up.."
Filip Pytlounb1ddf322017-05-12 16:18:31 +0200230 salt.cmdRun(saltMaster, 'I@rundeck:client', 'while true; do curl -sf 172.16.10.254:4440 >/dev/null && break; done')
Ilya Kharin7a18c322017-04-24 18:49:34 +0400231 }
Filip Pytlounb1ddf322017-05-12 16:18:31 +0200232 salt.enforceState(saltMaster, 'I@rundeck:client', 'rundeck.client', true)
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100233 }
234
235 stage("Finalize") {
236 //
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100237 // Deploy user's ssh key
238 //
Filip Pytloun0da421f2017-03-03 18:50:45 +0100239 def adminUser
240 def authorizedKeysFile
Filip Pytloun9935af02017-05-15 18:09:17 +0200241 def adminUserCmdOut = salt.cmdRun(saltMaster, 'I@salt:master', "[ ! -d /home/ubuntu ] || echo 'ubuntu user exists'")
Filip Pytlounbfa918a2017-03-04 10:01:30 +0100242 if (adminUserCmdOut =~ /ubuntu user exists/) {
Filip Pytloun0da421f2017-03-03 18:50:45 +0100243 adminUser = "ubuntu"
244 authorizedKeysFile = "/home/ubuntu/.ssh/authorized_keys"
245 } else {
246 adminUser = "root"
247 authorizedKeysFile = "/root/.ssh/authorized_keys"
248 }
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100249
Filip Pytloun0da421f2017-03-03 18:50:45 +0100250 if (sshPubKey) {
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100251 println "Deploying provided ssh key at ${authorizedKeysFile}"
Filip Pytloun4a847d62017-03-03 15:54:56 +0100252 salt.cmdRun(saltMaster, '*', "echo '${sshPubKey}' | tee -a ${authorizedKeysFile}")
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100253 }
254
255 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100256 // Generate docs
257 //
258 try {
Filip Pytloun64ff0792017-03-07 16:47:46 +0100259 try {
260 // Run sphinx state to install sphinx-build needed in
261 // upcomming orchestrate
262 salt.enforceState(saltMaster, 'I@sphinx:server', 'sphinx')
263 } catch (Throwable e) {
264 true
265 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100266 retry(3) {
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100267 // TODO: fix salt.orchestrateSystem
268 // print salt.orchestrateSystem(saltMaster, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
Filip Pytlounc17161d2017-03-03 09:50:54 +0100269 def out = salt.cmdRun(saltMaster, 'I@salt:master', 'salt-run state.orchestrate sphinx.orch.generate_doc || echo "Command execution failed"')
Jakub Joseffafd6592017-03-27 18:53:17 +0200270 print common.prettyPrint(out)
Filip Pytlounc17161d2017-03-03 09:50:54 +0100271 if (out =~ /Command execution failed/) {
272 throw new Exception("Command execution failed")
273 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100274 }
275 } catch (Throwable e) {
276 // We don't want sphinx docs to ruin whole build, so possible
277 // errors are just ignored here
278 true
279 }
280 salt.enforceState(saltMaster, 'I@nginx:server', 'nginx')
281
Filip Pytlouna4000402017-05-16 10:12:02 +0200282 def failedSvc = salt.cmdRun(saltMaster, '*', """systemctl --failed | grep -E 'loaded[ \t]+failed' && echo 'Command execution failed' || true""")
Jakub Joseffafd6592017-03-27 18:53:17 +0200283 print common.prettyPrint(failedSvc)
Filip Pytlounbd619272017-03-22 12:21:01 +0100284 if (failedSvc =~ /Command execution failed/) {
285 common.errorMsg("Some services are not running. Environment may not be fully functional!")
286 }
287
Filip Pytlound9427392017-03-04 13:58:08 +0100288 common.successMsg("""
Filip Pytloun794ad952017-03-03 10:39:26 +0100289 ============================================================
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100290 Your CI/CD lab has been deployed and you can enjoy it:
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100291 Use sshuttle to connect to your private subnet:
292
Filip Pytloun85464052017-03-03 16:31:43 +0100293 sshuttle -r ${adminUser}@${saltMasterHost} 172.16.10.0/24
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100294
295 And visit services running at 172.16.10.254 (vip address):
296
Ilya Kharin04c09982017-03-30 14:46:20 +0400297 9600 HAProxy statistics
298 8080 Gerrit
299 8081 Jenkins
Filip Pytlound0d700d2017-03-29 11:15:42 +0200300 8089 LDAP administration
Ilya Kharin04c09982017-03-30 14:46:20 +0400301 4440 Rundeck
302 8084 DevOps Portal
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100303 8091 Docker swarm visualizer
304 8090 Reclass-generated documentation
305
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100306 If you provided SSH_PUBLIC_KEY, you can use it to login,
307 otherwise you need to get private key connected to this
308 heat template.
309
310 DON'T FORGET TO TERMINATE YOUR STACK WHEN YOU DON'T NEED IT!
Filip Pytloun85464052017-03-03 16:31:43 +0100311 ============================================================""")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100312 }
313 } catch (Throwable e) {
314 // If there was an error or exception thrown, the build failed
315 currentBuild.result = "FAILURE"
316 throw e
317 } finally {
318 // Cleanup
Filip Pytloun794ad952017-03-03 10:39:26 +0100319 if (HEAT_STACK_DELETE.toBoolean() == true) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100320 stage('Trigger cleanup job') {
Filip Pytloun592773c2017-03-03 14:17:00 +0100321 build job: 'deploy-heat-cleanup', parameters: [[$class: 'StringParameterValue', name: 'HEAT_STACK_NAME', value: HEAT_STACK_NAME]]
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100322 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +0100323 }
Filip Pytloun23741982017-02-27 17:43:00 +0100324 }
Filip Pytlounf6e877f2017-02-28 19:38:16 +0100325 }
Filip Pytloun0a07f702017-02-24 18:26:18 +0100326}