blob: b63c58aa82f997a8fd0961f0d6bc0f79d5be8370 [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 Pytloun3eefd3d2017-03-03 14:13:41 +010065 try {
66 sshPubKey = SSH_PUBLIC_KEY
67 } catch (MissingPropertyException e) {
68 sshPubKey = false
69 }
70
Filip Pytloun794ad952017-03-03 10:39:26 +010071 if (HEAT_STACK_REUSE.toBoolean() == true && HEAT_STACK_NAME == '') {
72 error("If you want to reuse existing stack you need to provide it's name")
73 }
74
75 if (HEAT_STACK_REUSE.toBoolean() == false) {
76 // Don't allow to set custom heat stack name
77 wrap([$class: 'BuildUser']) {
Tomáš Kukrál24d7fe62017-03-03 10:57:11 +010078 if (env.BUILD_USER_ID) {
79 HEAT_STACK_NAME = "${env.BUILD_USER_ID}-${JOB_NAME}-${BUILD_NUMBER}"
80 } else {
81 HEAT_STACK_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"
82 }
Filip Pytloun794ad952017-03-03 10:39:26 +010083 currentBuild.description = HEAT_STACK_NAME
84 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +010085 }
Filip Pytloun5b0954b2017-03-01 10:10:18 +010086
Filip Pytloun3d045f82017-03-01 09:44:52 +010087 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010088 // Bootstrap
Filip Pytloun3d045f82017-03-01 09:44:52 +010089 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010090
91 stage ('Download Heat templates') {
92 git.checkoutGitRepository('template', HEAT_TEMPLATE_URL, HEAT_TEMPLATE_BRANCH, HEAT_TEMPLATE_CREDENTIALS)
Filip Pytloun3d045f82017-03-01 09:44:52 +010093 }
Filip Pytloun3d045f82017-03-01 09:44:52 +010094
Filip Pytlounbfce09d2017-03-01 19:00:43 +010095 stage('Install OpenStack CLI') {
96 openstack.setupOpenstackVirtualenv(openstackEnv, openstackVersion)
97 }
Filip Pytloun64123cd2017-03-01 11:26:17 +010098
Filip Pytlounbfce09d2017-03-01 19:00:43 +010099 stage('Connect to OpenStack cloud') {
100 openstackCloud = openstack.createOpenstackEnv(OPENSTACK_API_URL, OPENSTACK_API_CREDENTIALS, OPENSTACK_API_PROJECT)
101 openstack.getKeystoneToken(openstackCloud, openstackEnv)
102 }
103
Filip Pytloun794ad952017-03-03 10:39:26 +0100104 if (HEAT_STACK_REUSE.toBoolean() == false) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100105 stage('Launch new Heat stack') {
106 envParams = [
107 'instance_zone': HEAT_STACK_ZONE,
108 'public_net': HEAT_STACK_PUBLIC_NET
109 ]
110 openstack.createHeatStack(openstackCloud, HEAT_STACK_NAME, HEAT_STACK_TEMPLATE, envParams, HEAT_STACK_ENVIRONMENT, openstackEnv)
111 }
112 }
113
114 stage('Connect to Salt master') {
115 def saltMasterPort
116 try {
117 saltMasterPort = SALT_MASTER_PORT
118 } catch (MissingPropertyException e) {
119 saltMasterPort = 8000
120 }
121 saltMasterHost = openstack.getHeatStackOutputParam(openstackCloud, HEAT_STACK_NAME, 'salt_master_ip', openstackEnv)
122 saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
123 saltMaster = salt.connection(saltMasterUrl, SALT_MASTER_CREDENTIALS)
124 }
125
126 //
127 // Install
128 //
129
130 stage('Install core infra') {
131 // salt.master, reclass
132 // refresh_pillar
133 // sync_all
134 // linux,openssh,salt.minion.ntp
135
136 orchestrate.installFoundationInfra(saltMaster)
137 orchestrate.validateFoundationInfra(saltMaster)
138 }
139
140 stage("Deploy GlusterFS") {
141 salt.enforceState(saltMaster, 'I@glusterfs:server', 'glusterfs.server.service', true)
142 salt.enforceState(saltMaster, 'ci01*', 'glusterfs.server.setup', true)
143 sleep(5)
144 salt.enforceState(saltMaster, 'I@glusterfs:client', 'glusterfs.client', true)
145 print salt.cmdRun(saltMaster, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"')
146 }
147
148 stage("Deploy GlusterFS") {
149 salt.enforceState(saltMaster, 'I@haproxy:proxy', 'haproxy,keepalived')
150 }
151
152 stage("Setup Docker Swarm") {
153 salt.enforceState(saltMaster, 'I@docker:host', 'docker.host', true)
154 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.swarm', true)
155 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'salt', true)
156 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.flush')
157 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.update')
158 salt.enforceState(saltMaster, 'I@docker:swarm', 'docker.swarm', true)
159 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'docker node ls')
160 }
161
162 stage("Deploy Docker services") {
163 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
164
165 // XXX: Hack to fix dependency of gerrit on mysql
166 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', "docker service rm gerrit; sleep 5; rm -rf /srv/volumes/gerrit/*")
167 waitForServices(saltMaster)
168
169 timeout(10) {
170 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'apt-get install -y mysql-client')
171 println "Waiting for MySQL to come up.."
172 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'while true; do mysql -h172.16.10.254 -ppassword -e"show status;" >/dev/null && break; done')
173 }
174 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
175 // ---- cut here (end of hack) ----
176
177 waitForServices(saltMaster)
178 }
179
180 stage("Configure CI/CD services") {
181 // Aptly
182 salt.enforceState(saltMaster, 'I@aptly:server', 'aptly', true)
183
184 // Gerrit
185 timeout(10) {
186 println "Waiting for Gerrit to come up.."
187 salt.cmdRun(saltMaster, 'I@gerrit:client', 'while true; do curl -svf 172.16.10.254:8080 >/dev/null && break; done')
188 }
189 retry(2) {
190 // Needs to run twice to pass __virtual__ method of gerrit module
191 // after installation of dependencies
192 try {
193 salt.enforceState(saltMaster, 'I@gerrit:client', 'gerrit', true)
194 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100195 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100196 salt.cmdRun(saltMaster, 'I@gerrit:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
197 sleep(5)
198 throw e
199 }
200 }
201
202 // Jenkins
203 timeout(10) {
204 println "Waiting for Jenkins to come up.."
205 salt.cmdRun(saltMaster, 'I@jenkins:client', 'while true; do curl -svf 172.16.10.254:8081 >/dev/null && break; done')
206 }
207 retry(2) {
208 // Same for jenkins
209 try {
210 salt.enforceState(saltMaster, 'I@jenkins:client', 'jenkins', true)
211 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100212 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100213 salt.cmdRun(saltMaster, 'I@jenkins:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
214 sleep(5)
215 throw e
216 }
217 }
218 }
219
220 stage("Finalize") {
221 //
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100222 // Deploy user's ssh key
223 //
Filip Pytloun0da421f2017-03-03 18:50:45 +0100224 def adminUser
225 def authorizedKeysFile
Filip Pytlounbfa918a2017-03-04 10:01:30 +0100226 def adminUserCmdOut = salt.cmdRun(saltMaster, 'I@salt:master', "[ -d /home/ubuntu ] && echo 'ubuntu user exists'")
227 if (adminUserCmdOut =~ /ubuntu user exists/) {
Filip Pytloun0da421f2017-03-03 18:50:45 +0100228 adminUser = "ubuntu"
229 authorizedKeysFile = "/home/ubuntu/.ssh/authorized_keys"
230 } else {
231 adminUser = "root"
232 authorizedKeysFile = "/root/.ssh/authorized_keys"
233 }
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100234
Filip Pytloun0da421f2017-03-03 18:50:45 +0100235 if (sshPubKey) {
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100236 println "Deploying provided ssh key at ${authorizedKeysFile}"
Filip Pytloun4a847d62017-03-03 15:54:56 +0100237 salt.cmdRun(saltMaster, '*', "echo '${sshPubKey}' | tee -a ${authorizedKeysFile}")
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100238 }
239
240 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100241 // Generate docs
242 //
243 try {
244 retry(3) {
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100245 // TODO: fix salt.orchestrateSystem
246 // print salt.orchestrateSystem(saltMaster, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
Filip Pytlounc17161d2017-03-03 09:50:54 +0100247 def out = salt.cmdRun(saltMaster, 'I@salt:master', 'salt-run state.orchestrate sphinx.orch.generate_doc || echo "Command execution failed"')
248 print out
249 if (out =~ /Command execution failed/) {
250 throw new Exception("Command execution failed")
251 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100252 }
253 } catch (Throwable e) {
254 // We don't want sphinx docs to ruin whole build, so possible
255 // errors are just ignored here
256 true
257 }
258 salt.enforceState(saltMaster, 'I@nginx:server', 'nginx')
259
Filip Pytloun85464052017-03-03 16:31:43 +0100260 successMsg("""
Filip Pytloun794ad952017-03-03 10:39:26 +0100261 ============================================================
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100262 Your CI/CD lab has been deployed and you can enjoy it:
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100263 Use sshuttle to connect to your private subnet:
264
Filip Pytloun85464052017-03-03 16:31:43 +0100265 sshuttle -r ${adminUser}@${saltMasterHost} 172.16.10.0/24
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100266
267 And visit services running at 172.16.10.254 (vip address):
268
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100269 9600 haproxy stats
270 8080 gerrit
271 8081 jenkins
272 8091 Docker swarm visualizer
273 8090 Reclass-generated documentation
274
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100275 If you provided SSH_PUBLIC_KEY, you can use it to login,
276 otherwise you need to get private key connected to this
277 heat template.
278
279 DON'T FORGET TO TERMINATE YOUR STACK WHEN YOU DON'T NEED IT!
Filip Pytloun85464052017-03-03 16:31:43 +0100280 ============================================================""")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100281 }
282 } catch (Throwable e) {
283 // If there was an error or exception thrown, the build failed
284 currentBuild.result = "FAILURE"
285 throw e
286 } finally {
287 // Cleanup
Filip Pytloun794ad952017-03-03 10:39:26 +0100288 if (HEAT_STACK_DELETE.toBoolean() == true) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100289 stage('Trigger cleanup job') {
Filip Pytloun592773c2017-03-03 14:17:00 +0100290 build job: 'deploy-heat-cleanup', parameters: [[$class: 'StringParameterValue', name: 'HEAT_STACK_NAME', value: HEAT_STACK_NAME]]
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100291 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +0100292 }
Filip Pytloun23741982017-02-27 17:43:00 +0100293 }
Filip Pytlounf6e877f2017-02-28 19:38:16 +0100294 }
Filip Pytloun0a07f702017-02-24 18:26:18 +0100295}