blob: 8726bd45d689553c6d0194fc202f63babc38d6a4 [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
29git = new com.mirantis.mk.Git()
30openstack = new com.mirantis.mk.Openstack()
31salt = new com.mirantis.mk.Salt()
32orchestrate = new com.mirantis.mk.Orchestrate()
33
Filip Pytloun8fe1f7e2017-03-01 18:45:09 +010034def waitForServices(saltMaster) {
35 retry(30) {
36 out = salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', """/bin/bash -c 'docker service ls | grep -E "0/[0-9]+"' && echo 'Some services are not running'""")
37 for (int a = 0; a < out['return'].size(); a++) {
38 def entry = out['return'].get(a)
39 for (int i = 0; i < entry.size(); i++) {
40 def node = entry.get(i)
41 if (node) {
42 if (node.value =~ /Some services are not running/) {
43 sleep(10)
44 throw new Exception("$node.key: $node.value")
45 } else {
46 print out
47 }
48 }
49 }
50 }
51 }
52}
53
Filip Pytlounbfce09d2017-03-01 19:00:43 +010054timestamps {
55 node {
56 try {
57 // connection objects
58 def openstackCloud
59 def saltMaster
Filip Pytloun0a07f702017-02-24 18:26:18 +010060
Filip Pytlounbfce09d2017-03-01 19:00:43 +010061 // value defaults
62 def openstackVersion = OPENSTACK_API_CLIENT ? OPENSTACK_API_CLIENT : 'liberty'
63 def openstackEnv = "${env.WORKSPACE}/venv"
Filip Pytloun0a07f702017-02-24 18:26:18 +010064
Filip Pytloun794ad952017-03-03 10:39:26 +010065 if (HEAT_STACK_REUSE.toBoolean() == true && HEAT_STACK_NAME == '') {
66 error("If you want to reuse existing stack you need to provide it's name")
67 }
68
69 if (HEAT_STACK_REUSE.toBoolean() == false) {
70 // Don't allow to set custom heat stack name
71 wrap([$class: 'BuildUser']) {
Tomáš Kukrál24d7fe62017-03-03 10:57:11 +010072 if (env.BUILD_USER_ID) {
73 HEAT_STACK_NAME = "${env.BUILD_USER_ID}-${JOB_NAME}-${BUILD_NUMBER}"
74 } else {
75 HEAT_STACK_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"
76 }
Filip Pytloun794ad952017-03-03 10:39:26 +010077 currentBuild.description = HEAT_STACK_NAME
78 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +010079 }
Filip Pytloun5b0954b2017-03-01 10:10:18 +010080
Filip Pytloun3d045f82017-03-01 09:44:52 +010081 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010082 // Bootstrap
Filip Pytloun3d045f82017-03-01 09:44:52 +010083 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010084
85 stage ('Download Heat templates') {
86 git.checkoutGitRepository('template', HEAT_TEMPLATE_URL, HEAT_TEMPLATE_BRANCH, HEAT_TEMPLATE_CREDENTIALS)
Filip Pytloun3d045f82017-03-01 09:44:52 +010087 }
Filip Pytloun3d045f82017-03-01 09:44:52 +010088
Filip Pytlounbfce09d2017-03-01 19:00:43 +010089 stage('Install OpenStack CLI') {
90 openstack.setupOpenstackVirtualenv(openstackEnv, openstackVersion)
91 }
Filip Pytloun64123cd2017-03-01 11:26:17 +010092
Filip Pytlounbfce09d2017-03-01 19:00:43 +010093 stage('Connect to OpenStack cloud') {
94 openstackCloud = openstack.createOpenstackEnv(OPENSTACK_API_URL, OPENSTACK_API_CREDENTIALS, OPENSTACK_API_PROJECT)
95 openstack.getKeystoneToken(openstackCloud, openstackEnv)
96 }
97
Filip Pytloun794ad952017-03-03 10:39:26 +010098 if (HEAT_STACK_REUSE.toBoolean() == false) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +010099 stage('Launch new Heat stack') {
100 envParams = [
101 'instance_zone': HEAT_STACK_ZONE,
102 'public_net': HEAT_STACK_PUBLIC_NET
103 ]
104 openstack.createHeatStack(openstackCloud, HEAT_STACK_NAME, HEAT_STACK_TEMPLATE, envParams, HEAT_STACK_ENVIRONMENT, openstackEnv)
105 }
106 }
107
108 stage('Connect to Salt master') {
109 def saltMasterPort
110 try {
111 saltMasterPort = SALT_MASTER_PORT
112 } catch (MissingPropertyException e) {
113 saltMasterPort = 8000
114 }
115 saltMasterHost = openstack.getHeatStackOutputParam(openstackCloud, HEAT_STACK_NAME, 'salt_master_ip', openstackEnv)
116 saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
117 saltMaster = salt.connection(saltMasterUrl, SALT_MASTER_CREDENTIALS)
118 }
119
120 //
121 // Install
122 //
123
124 stage('Install core infra') {
125 // salt.master, reclass
126 // refresh_pillar
127 // sync_all
128 // linux,openssh,salt.minion.ntp
129
130 orchestrate.installFoundationInfra(saltMaster)
131 orchestrate.validateFoundationInfra(saltMaster)
132 }
133
134 stage("Deploy GlusterFS") {
135 salt.enforceState(saltMaster, 'I@glusterfs:server', 'glusterfs.server.service', true)
136 salt.enforceState(saltMaster, 'ci01*', 'glusterfs.server.setup', true)
137 sleep(5)
138 salt.enforceState(saltMaster, 'I@glusterfs:client', 'glusterfs.client', true)
139 print salt.cmdRun(saltMaster, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"')
140 }
141
142 stage("Deploy GlusterFS") {
143 salt.enforceState(saltMaster, 'I@haproxy:proxy', 'haproxy,keepalived')
144 }
145
146 stage("Setup Docker Swarm") {
147 salt.enforceState(saltMaster, 'I@docker:host', 'docker.host', true)
148 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.swarm', true)
149 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'salt', true)
150 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.flush')
151 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.update')
152 salt.enforceState(saltMaster, 'I@docker:swarm', 'docker.swarm', true)
153 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'docker node ls')
154 }
155
156 stage("Deploy Docker services") {
157 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
158
159 // XXX: Hack to fix dependency of gerrit on mysql
160 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', "docker service rm gerrit; sleep 5; rm -rf /srv/volumes/gerrit/*")
161 waitForServices(saltMaster)
162
163 timeout(10) {
164 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'apt-get install -y mysql-client')
165 println "Waiting for MySQL to come up.."
166 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'while true; do mysql -h172.16.10.254 -ppassword -e"show status;" >/dev/null && break; done')
167 }
168 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
169 // ---- cut here (end of hack) ----
170
171 waitForServices(saltMaster)
172 }
173
174 stage("Configure CI/CD services") {
175 // Aptly
176 salt.enforceState(saltMaster, 'I@aptly:server', 'aptly', true)
177
178 // Gerrit
179 timeout(10) {
180 println "Waiting for Gerrit to come up.."
181 salt.cmdRun(saltMaster, 'I@gerrit:client', 'while true; do curl -svf 172.16.10.254:8080 >/dev/null && break; done')
182 }
183 retry(2) {
184 // Needs to run twice to pass __virtual__ method of gerrit module
185 // after installation of dependencies
186 try {
187 salt.enforceState(saltMaster, 'I@gerrit:client', 'gerrit', true)
188 } catch (Exception e) {
189 print "Restarting Salt minion"
190 salt.cmdRun(saltMaster, 'I@gerrit:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
191 sleep(5)
192 throw e
193 }
194 }
195
196 // Jenkins
197 timeout(10) {
198 println "Waiting for Jenkins to come up.."
199 salt.cmdRun(saltMaster, 'I@jenkins:client', 'while true; do curl -svf 172.16.10.254:8081 >/dev/null && break; done')
200 }
201 retry(2) {
202 // Same for jenkins
203 try {
204 salt.enforceState(saltMaster, 'I@jenkins:client', 'jenkins', true)
205 } catch (Exception e) {
206 print "Restarting Salt minion"
207 salt.cmdRun(saltMaster, 'I@jenkins:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
208 sleep(5)
209 throw e
210 }
211 }
212 }
213
214 stage("Finalize") {
215 //
216 // Generate docs
217 //
218 try {
219 retry(3) {
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100220 // TODO: fix salt.orchestrateSystem
221 // print salt.orchestrateSystem(saltMaster, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
Filip Pytlounc17161d2017-03-03 09:50:54 +0100222 def out = salt.cmdRun(saltMaster, 'I@salt:master', 'salt-run state.orchestrate sphinx.orch.generate_doc || echo "Command execution failed"')
223 print out
224 if (out =~ /Command execution failed/) {
225 throw new Exception("Command execution failed")
226 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100227 }
228 } catch (Throwable e) {
229 // We don't want sphinx docs to ruin whole build, so possible
230 // errors are just ignored here
231 true
232 }
233 salt.enforceState(saltMaster, 'I@nginx:server', 'nginx')
234
Filip Pytloun794ad952017-03-03 10:39:26 +0100235 print """
236 ============================================================
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100237 Your CI/CD lab has been deployed and you can enjoy it:
238 Use sshuttle -r ubuntu@${saltMasterHost} 172.16.10.0/24
239 to connect to your private subnet and visit services
240 running at 172.16.10.254 (vip address):
241 9600 haproxy stats
242 8080 gerrit
243 8081 jenkins
244 8091 Docker swarm visualizer
245 8090 Reclass-generated documentation
246
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100247 Don't forget to terminate your stack when you don't need it!
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100248 ============================================================"""
249 }
250 } catch (Throwable e) {
251 // If there was an error or exception thrown, the build failed
252 currentBuild.result = "FAILURE"
253 throw e
254 } finally {
255 // Cleanup
Filip Pytloun794ad952017-03-03 10:39:26 +0100256 if (HEAT_STACK_DELETE.toBoolean() == true) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100257 stage('Trigger cleanup job') {
258 build job: 'deploy_heat_cleanup', parameters: [[$class: 'StringParameterValue', name: 'HEAT_STACK_NAME', value: HEAT_STACK_NAME]]
259 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +0100260 }
Filip Pytloun23741982017-02-27 17:43:00 +0100261 }
Filip Pytlounf6e877f2017-02-28 19:38:16 +0100262 }
Filip Pytloun0a07f702017-02-24 18:26:18 +0100263}