blob: 9077df73159f41eb1501dc9012b2a2e3df4ac78a [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()
34
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)
83 }
84
Filip Pytloun794ad952017-03-03 10:39:26 +010085 if (HEAT_STACK_REUSE.toBoolean() == false) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +010086 stage('Launch new Heat stack') {
87 envParams = [
88 'instance_zone': HEAT_STACK_ZONE,
89 'public_net': HEAT_STACK_PUBLIC_NET
90 ]
91 openstack.createHeatStack(openstackCloud, HEAT_STACK_NAME, HEAT_STACK_TEMPLATE, envParams, HEAT_STACK_ENVIRONMENT, openstackEnv)
92 }
93 }
94
95 stage('Connect to Salt master') {
96 def saltMasterPort
97 try {
98 saltMasterPort = SALT_MASTER_PORT
99 } catch (MissingPropertyException e) {
Filip Pytloun2ef26132017-03-10 09:44:37 +0100100 saltMasterPort = 6969
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100101 }
102 saltMasterHost = openstack.getHeatStackOutputParam(openstackCloud, HEAT_STACK_NAME, 'salt_master_ip', openstackEnv)
Jakub Josefbde0d442017-04-07 16:32:58 +0200103 currentBuild.description = "${HEAT_STACK_NAME}: ${saltMasterHost}"
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100104 saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
105 saltMaster = salt.connection(saltMasterUrl, SALT_MASTER_CREDENTIALS)
106 }
107
108 //
109 // Install
110 //
111
112 stage('Install core infra') {
113 // salt.master, reclass
114 // refresh_pillar
115 // sync_all
116 // linux,openssh,salt.minion.ntp
117
118 orchestrate.installFoundationInfra(saltMaster)
119 orchestrate.validateFoundationInfra(saltMaster)
120 }
121
122 stage("Deploy GlusterFS") {
123 salt.enforceState(saltMaster, 'I@glusterfs:server', 'glusterfs.server.service', true)
Filip Pytloun97e6fff2017-03-30 16:56:11 +0200124 retry(2) {
125 salt.enforceState(saltMaster, 'ci01*', 'glusterfs.server.setup', true)
126 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100127 sleep(5)
128 salt.enforceState(saltMaster, 'I@glusterfs:client', 'glusterfs.client', true)
Filip Pytloun5555b452017-04-19 12:41:44 +0200129
130 timeout(5) {
131 println "Waiting for GlusterFS volumes to get mounted.."
132 salt.cmdRun(saltMaster, 'I@glusterfs:client', 'while true; do systemctl -a|grep "GlusterFS File System"|grep -v mounted >/dev/null || break; done')
133 }
Jakub Joseffafd6592017-03-27 18:53:17 +0200134 print common.prettyPrint(salt.cmdRun(saltMaster, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"'))
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100135 }
136
137 stage("Deploy GlusterFS") {
138 salt.enforceState(saltMaster, 'I@haproxy:proxy', 'haproxy,keepalived')
139 }
140
141 stage("Setup Docker Swarm") {
142 salt.enforceState(saltMaster, 'I@docker:host', 'docker.host', true)
143 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.swarm', true)
144 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'salt', true)
145 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.flush')
146 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.update')
147 salt.enforceState(saltMaster, 'I@docker:swarm', 'docker.swarm', true)
Jakub Joseffafd6592017-03-27 18:53:17 +0200148 print common.prettyPrint(salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'docker node ls'))
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100149 }
150
Ilya Kharin04c09982017-03-30 14:46:20 +0400151 stage("Configure OSS services") {
152 salt.enforceState(saltMaster, 'I@devops_portal:config', 'devops_portal.config')
Ilya Kharin7a18c322017-04-24 18:49:34 +0400153 salt.enforceState(saltMaster, 'I@rundeck:server', 'rundeck.server')
Ilya Kharin04c09982017-03-30 14:46:20 +0400154 }
155
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100156 stage("Deploy Docker services") {
Filip Pytloun65b928d2017-04-18 17:19:30 +0200157 retry(3) {
Filip Pytlound6d18502017-04-13 15:35:07 +0200158 sleep(5)
159 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
160 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100161 }
162
163 stage("Configure CI/CD services") {
Filip Pytloun29d0bc12017-03-10 14:39:26 +0100164 salt.syncAll(saltMaster, '*')
165
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100166 // Aptly
Filip Pytloun6cde7882017-03-28 17:22:18 +0200167 timeout(10) {
168 println "Waiting for Aptly to come up.."
Filip Pytloun25c1adb2017-03-28 18:06:01 +0200169 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 +0200170 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100171 salt.enforceState(saltMaster, 'I@aptly:server', 'aptly', true)
172
Filip Pytloun03983812017-03-28 13:07:34 +0200173 // OpenLDAP
174 timeout(10) {
175 println "Waiting for OpenLDAP to come up.."
176 salt.cmdRun(saltMaster, 'I@openldap:client', 'while true; do curl -svf ldap://172.16.10.254 >/dev/null && break; done')
177 }
178 salt.enforceState(saltMaster, 'I@openldap:client', 'openldap', true)
179
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100180 // Gerrit
181 timeout(10) {
182 println "Waiting for Gerrit to come up.."
183 salt.cmdRun(saltMaster, 'I@gerrit:client', 'while true; do curl -svf 172.16.10.254:8080 >/dev/null && break; done')
184 }
Filip Pytloun0a4a15e2017-03-28 19:06:00 +0200185 retry(3) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100186 // Needs to run twice to pass __virtual__ method of gerrit module
187 // after installation of dependencies
188 try {
189 salt.enforceState(saltMaster, 'I@gerrit:client', 'gerrit', true)
190 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100191 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100192 salt.cmdRun(saltMaster, 'I@gerrit:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
193 sleep(5)
194 throw e
195 }
196 }
197
198 // Jenkins
199 timeout(10) {
200 println "Waiting for Jenkins to come up.."
201 salt.cmdRun(saltMaster, 'I@jenkins:client', 'while true; do curl -svf 172.16.10.254:8081 >/dev/null && break; done')
202 }
203 retry(2) {
204 // Same for jenkins
205 try {
206 salt.enforceState(saltMaster, 'I@jenkins:client', 'jenkins', true)
207 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100208 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100209 salt.cmdRun(saltMaster, 'I@jenkins:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
210 sleep(5)
211 throw e
212 }
213 }
Ilya Kharin7a18c322017-04-24 18:49:34 +0400214
215 // Rundeck
216 timeout(10) {
217 println "Waiting for Rundeck to come up.."
218 salt.cmdRun(saltMaster, 'I@rundeck:client', 'while true; do curl -svf 172.16.10.254:4440 >/dev/null && break; done')
219 }
220 retry(2) {
221 // Same for Rundeck
222 try {
Ilya Kharinb4ea57e2017-04-25 18:59:31 +0400223 salt.enforceState(saltMaster, 'I@rundeck:client', 'rundeck.client', true)
Ilya Kharin7a18c322017-04-24 18:49:34 +0400224 } catch (Exception e) {
225 common.infoMsg("Restarting Salt minion")
226 salt.cmdRun(saltMaster, 'I@rundeck:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
227 sleep(5)
228 throw e
229 }
230 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100231 }
232
233 stage("Finalize") {
234 //
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100235 // Deploy user's ssh key
236 //
Filip Pytloun0da421f2017-03-03 18:50:45 +0100237 def adminUser
238 def authorizedKeysFile
Filip Pytlounbfa918a2017-03-04 10:01:30 +0100239 def adminUserCmdOut = salt.cmdRun(saltMaster, 'I@salt:master', "[ -d /home/ubuntu ] && echo 'ubuntu user exists'")
240 if (adminUserCmdOut =~ /ubuntu user exists/) {
Filip Pytloun0da421f2017-03-03 18:50:45 +0100241 adminUser = "ubuntu"
242 authorizedKeysFile = "/home/ubuntu/.ssh/authorized_keys"
243 } else {
244 adminUser = "root"
245 authorizedKeysFile = "/root/.ssh/authorized_keys"
246 }
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100247
Filip Pytloun0da421f2017-03-03 18:50:45 +0100248 if (sshPubKey) {
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100249 println "Deploying provided ssh key at ${authorizedKeysFile}"
Filip Pytloun4a847d62017-03-03 15:54:56 +0100250 salt.cmdRun(saltMaster, '*', "echo '${sshPubKey}' | tee -a ${authorizedKeysFile}")
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100251 }
252
253 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100254 // Generate docs
255 //
256 try {
Filip Pytloun64ff0792017-03-07 16:47:46 +0100257 try {
258 // Run sphinx state to install sphinx-build needed in
259 // upcomming orchestrate
260 salt.enforceState(saltMaster, 'I@sphinx:server', 'sphinx')
261 } catch (Throwable e) {
262 true
263 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100264 retry(3) {
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100265 // TODO: fix salt.orchestrateSystem
266 // print salt.orchestrateSystem(saltMaster, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
Filip Pytlounc17161d2017-03-03 09:50:54 +0100267 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 +0200268 print common.prettyPrint(out)
Filip Pytlounc17161d2017-03-03 09:50:54 +0100269 if (out =~ /Command execution failed/) {
270 throw new Exception("Command execution failed")
271 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100272 }
273 } catch (Throwable e) {
274 // We don't want sphinx docs to ruin whole build, so possible
275 // errors are just ignored here
276 true
277 }
278 salt.enforceState(saltMaster, 'I@nginx:server', 'nginx')
279
Filip Pytlounbd619272017-03-22 12:21:01 +0100280 def failedSvc = salt.cmdRun(saltMaster, '*', """systemctl --failed | grep -E 'loaded[ \t]+failed' && echo 'Command execution failed'""")
Jakub Joseffafd6592017-03-27 18:53:17 +0200281 print common.prettyPrint(failedSvc)
Filip Pytlounbd619272017-03-22 12:21:01 +0100282 if (failedSvc =~ /Command execution failed/) {
283 common.errorMsg("Some services are not running. Environment may not be fully functional!")
284 }
285
Filip Pytlound9427392017-03-04 13:58:08 +0100286 common.successMsg("""
Filip Pytloun794ad952017-03-03 10:39:26 +0100287 ============================================================
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100288 Your CI/CD lab has been deployed and you can enjoy it:
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100289 Use sshuttle to connect to your private subnet:
290
Filip Pytloun85464052017-03-03 16:31:43 +0100291 sshuttle -r ${adminUser}@${saltMasterHost} 172.16.10.0/24
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100292
293 And visit services running at 172.16.10.254 (vip address):
294
Ilya Kharin04c09982017-03-30 14:46:20 +0400295 9600 HAProxy statistics
296 8080 Gerrit
297 8081 Jenkins
Filip Pytlound0d700d2017-03-29 11:15:42 +0200298 8089 LDAP administration
Ilya Kharin04c09982017-03-30 14:46:20 +0400299 4440 Rundeck
300 8084 DevOps Portal
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100301 8091 Docker swarm visualizer
302 8090 Reclass-generated documentation
303
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100304 If you provided SSH_PUBLIC_KEY, you can use it to login,
305 otherwise you need to get private key connected to this
306 heat template.
307
308 DON'T FORGET TO TERMINATE YOUR STACK WHEN YOU DON'T NEED IT!
Filip Pytloun85464052017-03-03 16:31:43 +0100309 ============================================================""")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100310 }
311 } catch (Throwable e) {
312 // If there was an error or exception thrown, the build failed
313 currentBuild.result = "FAILURE"
314 throw e
315 } finally {
316 // Cleanup
Filip Pytloun794ad952017-03-03 10:39:26 +0100317 if (HEAT_STACK_DELETE.toBoolean() == true) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100318 stage('Trigger cleanup job') {
Filip Pytloun592773c2017-03-03 14:17:00 +0100319 build job: 'deploy-heat-cleanup', parameters: [[$class: 'StringParameterValue', name: 'HEAT_STACK_NAME', value: HEAT_STACK_NAME]]
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100320 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +0100321 }
Filip Pytloun23741982017-02-27 17:43:00 +0100322 }
Filip Pytlounf6e877f2017-02-28 19:38:16 +0100323 }
Filip Pytloun0a07f702017-02-24 18:26:18 +0100324}