blob: 6705f9bae4159f79ad9eb487ef64727ddb825c93 [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']) {
Jakub Josef78c3f8b2017-05-10 15:45:29 +020084 if (env.BUILD_USER_ID && !env.BUILD_USER_ID.equals("jenkins")) {
85 def existingStacks = openstack.getStacksForNameContains(openstackCloud, "${env.BUILD_USER_ID}-${JOB_NAME}", openstackEnv)
86 if(existingStacks.size() >= _MAX_PERMITTED_STACKS){
87 throw new Exception("You cannot create new stack, you already have ${_MAX_PERMITTED_STACKS} stacks of this type (${JOB_NAME}). \nStack names: ${existingStacks}")
88 }
Jakub Josef458913d2017-05-10 15:37:56 +020089 }
90 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +010091 }
92
Filip Pytloun794ad952017-03-03 10:39:26 +010093 if (HEAT_STACK_REUSE.toBoolean() == false) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +010094 stage('Launch new Heat stack') {
95 envParams = [
96 'instance_zone': HEAT_STACK_ZONE,
97 'public_net': HEAT_STACK_PUBLIC_NET
98 ]
99 openstack.createHeatStack(openstackCloud, HEAT_STACK_NAME, HEAT_STACK_TEMPLATE, envParams, HEAT_STACK_ENVIRONMENT, openstackEnv)
100 }
101 }
102
103 stage('Connect to Salt master') {
104 def saltMasterPort
105 try {
106 saltMasterPort = SALT_MASTER_PORT
107 } catch (MissingPropertyException e) {
Filip Pytloun2ef26132017-03-10 09:44:37 +0100108 saltMasterPort = 6969
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100109 }
110 saltMasterHost = openstack.getHeatStackOutputParam(openstackCloud, HEAT_STACK_NAME, 'salt_master_ip', openstackEnv)
Jakub Josefbde0d442017-04-07 16:32:58 +0200111 currentBuild.description = "${HEAT_STACK_NAME}: ${saltMasterHost}"
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100112 saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
113 saltMaster = salt.connection(saltMasterUrl, SALT_MASTER_CREDENTIALS)
114 }
115
116 //
117 // Install
118 //
119
120 stage('Install core infra') {
121 // salt.master, reclass
122 // refresh_pillar
123 // sync_all
124 // linux,openssh,salt.minion.ntp
125
126 orchestrate.installFoundationInfra(saltMaster)
127 orchestrate.validateFoundationInfra(saltMaster)
128 }
129
130 stage("Deploy GlusterFS") {
131 salt.enforceState(saltMaster, 'I@glusterfs:server', 'glusterfs.server.service', true)
Filip Pytloun97e6fff2017-03-30 16:56:11 +0200132 retry(2) {
133 salt.enforceState(saltMaster, 'ci01*', 'glusterfs.server.setup', true)
134 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100135 sleep(5)
136 salt.enforceState(saltMaster, 'I@glusterfs:client', 'glusterfs.client', true)
Filip Pytloun5555b452017-04-19 12:41:44 +0200137
138 timeout(5) {
139 println "Waiting for GlusterFS volumes to get mounted.."
140 salt.cmdRun(saltMaster, 'I@glusterfs:client', 'while true; do systemctl -a|grep "GlusterFS File System"|grep -v mounted >/dev/null || break; done')
141 }
Jakub Joseffafd6592017-03-27 18:53:17 +0200142 print common.prettyPrint(salt.cmdRun(saltMaster, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"'))
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100143 }
144
145 stage("Deploy GlusterFS") {
146 salt.enforceState(saltMaster, 'I@haproxy:proxy', 'haproxy,keepalived')
147 }
148
149 stage("Setup Docker Swarm") {
150 salt.enforceState(saltMaster, 'I@docker:host', 'docker.host', true)
151 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.swarm', true)
152 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'salt', true)
153 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.flush')
154 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.update')
155 salt.enforceState(saltMaster, 'I@docker:swarm', 'docker.swarm', true)
Jakub Joseffafd6592017-03-27 18:53:17 +0200156 print common.prettyPrint(salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'docker node ls'))
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100157 }
158
Ilya Kharin04c09982017-03-30 14:46:20 +0400159 stage("Configure OSS services") {
160 salt.enforceState(saltMaster, 'I@devops_portal:config', 'devops_portal.config')
Ilya Kharin7a18c322017-04-24 18:49:34 +0400161 salt.enforceState(saltMaster, 'I@rundeck:server', 'rundeck.server')
Ilya Kharin04c09982017-03-30 14:46:20 +0400162 }
163
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100164 stage("Deploy Docker services") {
Filip Pytloun65b928d2017-04-18 17:19:30 +0200165 retry(3) {
Filip Pytlound6d18502017-04-13 15:35:07 +0200166 sleep(5)
167 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
168 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100169 }
170
171 stage("Configure CI/CD services") {
Filip Pytloun29d0bc12017-03-10 14:39:26 +0100172 salt.syncAll(saltMaster, '*')
173
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100174 // Aptly
Filip Pytloun6cde7882017-03-28 17:22:18 +0200175 timeout(10) {
176 println "Waiting for Aptly to come up.."
Filip Pytloun25c1adb2017-03-28 18:06:01 +0200177 salt.cmdRun(saltMaster, 'I@aptly:server', 'while true; do curl -svf http://172.16.10.254:8084/api/version >/dev/null && break; done')
Filip Pytloun6cde7882017-03-28 17:22:18 +0200178 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100179 salt.enforceState(saltMaster, 'I@aptly:server', 'aptly', true)
180
Filip Pytloun03983812017-03-28 13:07:34 +0200181 // OpenLDAP
182 timeout(10) {
183 println "Waiting for OpenLDAP to come up.."
184 salt.cmdRun(saltMaster, 'I@openldap:client', 'while true; do curl -svf ldap://172.16.10.254 >/dev/null && break; done')
185 }
186 salt.enforceState(saltMaster, 'I@openldap:client', 'openldap', true)
187
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100188 // Gerrit
189 timeout(10) {
190 println "Waiting for Gerrit to come up.."
191 salt.cmdRun(saltMaster, 'I@gerrit:client', 'while true; do curl -svf 172.16.10.254:8080 >/dev/null && break; done')
192 }
Filip Pytloun0a4a15e2017-03-28 19:06:00 +0200193 retry(3) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100194 // Needs to run twice to pass __virtual__ method of gerrit module
195 // after installation of dependencies
196 try {
197 salt.enforceState(saltMaster, 'I@gerrit:client', 'gerrit', true)
198 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100199 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100200 salt.cmdRun(saltMaster, 'I@gerrit:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
201 sleep(5)
202 throw e
203 }
204 }
205
206 // Jenkins
207 timeout(10) {
208 println "Waiting for Jenkins to come up.."
209 salt.cmdRun(saltMaster, 'I@jenkins:client', 'while true; do curl -svf 172.16.10.254:8081 >/dev/null && break; done')
210 }
211 retry(2) {
212 // Same for jenkins
213 try {
214 salt.enforceState(saltMaster, 'I@jenkins:client', 'jenkins', true)
215 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100216 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100217 salt.cmdRun(saltMaster, 'I@jenkins:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
218 sleep(5)
219 throw e
220 }
221 }
Ilya Kharin7a18c322017-04-24 18:49:34 +0400222
223 // Rundeck
224 timeout(10) {
225 println "Waiting for Rundeck to come up.."
226 salt.cmdRun(saltMaster, 'I@rundeck:client', 'while true; do curl -svf 172.16.10.254:4440 >/dev/null && break; done')
227 }
228 retry(2) {
229 // Same for Rundeck
230 try {
Ilya Kharinb4ea57e2017-04-25 18:59:31 +0400231 salt.enforceState(saltMaster, 'I@rundeck:client', 'rundeck.client', true)
Ilya Kharin7a18c322017-04-24 18:49:34 +0400232 } catch (Exception e) {
233 common.infoMsg("Restarting Salt minion")
234 salt.cmdRun(saltMaster, 'I@rundeck:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
235 sleep(5)
236 throw e
237 }
238 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100239 }
240
241 stage("Finalize") {
242 //
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100243 // Deploy user's ssh key
244 //
Filip Pytloun0da421f2017-03-03 18:50:45 +0100245 def adminUser
246 def authorizedKeysFile
Filip Pytlounbfa918a2017-03-04 10:01:30 +0100247 def adminUserCmdOut = salt.cmdRun(saltMaster, 'I@salt:master', "[ -d /home/ubuntu ] && echo 'ubuntu user exists'")
248 if (adminUserCmdOut =~ /ubuntu user exists/) {
Filip Pytloun0da421f2017-03-03 18:50:45 +0100249 adminUser = "ubuntu"
250 authorizedKeysFile = "/home/ubuntu/.ssh/authorized_keys"
251 } else {
252 adminUser = "root"
253 authorizedKeysFile = "/root/.ssh/authorized_keys"
254 }
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100255
Filip Pytloun0da421f2017-03-03 18:50:45 +0100256 if (sshPubKey) {
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100257 println "Deploying provided ssh key at ${authorizedKeysFile}"
Filip Pytloun4a847d62017-03-03 15:54:56 +0100258 salt.cmdRun(saltMaster, '*', "echo '${sshPubKey}' | tee -a ${authorizedKeysFile}")
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100259 }
260
261 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100262 // Generate docs
263 //
264 try {
Filip Pytloun64ff0792017-03-07 16:47:46 +0100265 try {
266 // Run sphinx state to install sphinx-build needed in
267 // upcomming orchestrate
268 salt.enforceState(saltMaster, 'I@sphinx:server', 'sphinx')
269 } catch (Throwable e) {
270 true
271 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100272 retry(3) {
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100273 // TODO: fix salt.orchestrateSystem
274 // print salt.orchestrateSystem(saltMaster, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
Filip Pytlounc17161d2017-03-03 09:50:54 +0100275 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 +0200276 print common.prettyPrint(out)
Filip Pytlounc17161d2017-03-03 09:50:54 +0100277 if (out =~ /Command execution failed/) {
278 throw new Exception("Command execution failed")
279 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100280 }
281 } catch (Throwable e) {
282 // We don't want sphinx docs to ruin whole build, so possible
283 // errors are just ignored here
284 true
285 }
286 salt.enforceState(saltMaster, 'I@nginx:server', 'nginx')
287
Filip Pytlounbd619272017-03-22 12:21:01 +0100288 def failedSvc = salt.cmdRun(saltMaster, '*', """systemctl --failed | grep -E 'loaded[ \t]+failed' && echo 'Command execution failed'""")
Jakub Joseffafd6592017-03-27 18:53:17 +0200289 print common.prettyPrint(failedSvc)
Filip Pytlounbd619272017-03-22 12:21:01 +0100290 if (failedSvc =~ /Command execution failed/) {
291 common.errorMsg("Some services are not running. Environment may not be fully functional!")
292 }
293
Filip Pytlound9427392017-03-04 13:58:08 +0100294 common.successMsg("""
Filip Pytloun794ad952017-03-03 10:39:26 +0100295 ============================================================
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100296 Your CI/CD lab has been deployed and you can enjoy it:
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100297 Use sshuttle to connect to your private subnet:
298
Filip Pytloun85464052017-03-03 16:31:43 +0100299 sshuttle -r ${adminUser}@${saltMasterHost} 172.16.10.0/24
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100300
301 And visit services running at 172.16.10.254 (vip address):
302
Ilya Kharin04c09982017-03-30 14:46:20 +0400303 9600 HAProxy statistics
304 8080 Gerrit
305 8081 Jenkins
Filip Pytlound0d700d2017-03-29 11:15:42 +0200306 8089 LDAP administration
Ilya Kharin04c09982017-03-30 14:46:20 +0400307 4440 Rundeck
308 8084 DevOps Portal
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100309 8091 Docker swarm visualizer
310 8090 Reclass-generated documentation
311
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100312 If you provided SSH_PUBLIC_KEY, you can use it to login,
313 otherwise you need to get private key connected to this
314 heat template.
315
316 DON'T FORGET TO TERMINATE YOUR STACK WHEN YOU DON'T NEED IT!
Filip Pytloun85464052017-03-03 16:31:43 +0100317 ============================================================""")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100318 }
319 } catch (Throwable e) {
320 // If there was an error or exception thrown, the build failed
321 currentBuild.result = "FAILURE"
322 throw e
323 } finally {
324 // Cleanup
Filip Pytloun794ad952017-03-03 10:39:26 +0100325 if (HEAT_STACK_DELETE.toBoolean() == true) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100326 stage('Trigger cleanup job') {
Filip Pytloun592773c2017-03-03 14:17:00 +0100327 build job: 'deploy-heat-cleanup', parameters: [[$class: 'StringParameterValue', name: 'HEAT_STACK_NAME', value: HEAT_STACK_NAME]]
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100328 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +0100329 }
Filip Pytloun23741982017-02-27 17:43:00 +0100330 }
Filip Pytlounf6e877f2017-02-28 19:38:16 +0100331 }
Filip Pytloun0a07f702017-02-24 18:26:18 +0100332}