blob: 08b7527e7b5cf7db63da94be4ae0645ba2c91fa2 [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)
103 saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
104 saltMaster = salt.connection(saltMasterUrl, SALT_MASTER_CREDENTIALS)
105 }
106
107 //
108 // Install
109 //
110
111 stage('Install core infra') {
112 // salt.master, reclass
113 // refresh_pillar
114 // sync_all
115 // linux,openssh,salt.minion.ntp
116
117 orchestrate.installFoundationInfra(saltMaster)
118 orchestrate.validateFoundationInfra(saltMaster)
119 }
120
121 stage("Deploy GlusterFS") {
122 salt.enforceState(saltMaster, 'I@glusterfs:server', 'glusterfs.server.service', true)
123 salt.enforceState(saltMaster, 'ci01*', 'glusterfs.server.setup', true)
124 sleep(5)
125 salt.enforceState(saltMaster, 'I@glusterfs:client', 'glusterfs.client', true)
126 print salt.cmdRun(saltMaster, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"')
127 }
128
129 stage("Deploy GlusterFS") {
130 salt.enforceState(saltMaster, 'I@haproxy:proxy', 'haproxy,keepalived')
131 }
132
133 stage("Setup Docker Swarm") {
134 salt.enforceState(saltMaster, 'I@docker:host', 'docker.host', true)
135 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.swarm', true)
136 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'salt', true)
137 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.flush')
138 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.update')
139 salt.enforceState(saltMaster, 'I@docker:swarm', 'docker.swarm', true)
140 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'docker node ls')
141 }
142
143 stage("Deploy Docker services") {
144 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
145
146 // XXX: Hack to fix dependency of gerrit on mysql
147 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', "docker service rm gerrit; sleep 5; rm -rf /srv/volumes/gerrit/*")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100148
149 timeout(10) {
150 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'apt-get install -y mysql-client')
151 println "Waiting for MySQL to come up.."
152 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'while true; do mysql -h172.16.10.254 -ppassword -e"show status;" >/dev/null && break; done')
153 }
154 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
155 // ---- cut here (end of hack) ----
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100156 }
157
158 stage("Configure CI/CD services") {
Filip Pytloun29d0bc12017-03-10 14:39:26 +0100159 salt.syncAll(saltMaster, '*')
160
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100161 // Aptly
162 salt.enforceState(saltMaster, 'I@aptly:server', 'aptly', true)
163
164 // Gerrit
165 timeout(10) {
166 println "Waiting for Gerrit to come up.."
167 salt.cmdRun(saltMaster, 'I@gerrit:client', 'while true; do curl -svf 172.16.10.254:8080 >/dev/null && break; done')
168 }
169 retry(2) {
170 // Needs to run twice to pass __virtual__ method of gerrit module
171 // after installation of dependencies
172 try {
173 salt.enforceState(saltMaster, 'I@gerrit:client', 'gerrit', true)
174 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100175 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100176 salt.cmdRun(saltMaster, 'I@gerrit:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
177 sleep(5)
178 throw e
179 }
180 }
181
182 // Jenkins
183 timeout(10) {
184 println "Waiting for Jenkins to come up.."
185 salt.cmdRun(saltMaster, 'I@jenkins:client', 'while true; do curl -svf 172.16.10.254:8081 >/dev/null && break; done')
186 }
187 retry(2) {
188 // Same for jenkins
189 try {
190 salt.enforceState(saltMaster, 'I@jenkins:client', 'jenkins', true)
191 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100192 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100193 salt.cmdRun(saltMaster, 'I@jenkins:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
194 sleep(5)
195 throw e
196 }
197 }
198 }
199
200 stage("Finalize") {
201 //
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100202 // Deploy user's ssh key
203 //
Filip Pytloun0da421f2017-03-03 18:50:45 +0100204 def adminUser
205 def authorizedKeysFile
Filip Pytlounbfa918a2017-03-04 10:01:30 +0100206 def adminUserCmdOut = salt.cmdRun(saltMaster, 'I@salt:master', "[ -d /home/ubuntu ] && echo 'ubuntu user exists'")
207 if (adminUserCmdOut =~ /ubuntu user exists/) {
Filip Pytloun0da421f2017-03-03 18:50:45 +0100208 adminUser = "ubuntu"
209 authorizedKeysFile = "/home/ubuntu/.ssh/authorized_keys"
210 } else {
211 adminUser = "root"
212 authorizedKeysFile = "/root/.ssh/authorized_keys"
213 }
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100214
Filip Pytloun0da421f2017-03-03 18:50:45 +0100215 if (sshPubKey) {
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100216 println "Deploying provided ssh key at ${authorizedKeysFile}"
Filip Pytloun4a847d62017-03-03 15:54:56 +0100217 salt.cmdRun(saltMaster, '*', "echo '${sshPubKey}' | tee -a ${authorizedKeysFile}")
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100218 }
219
220 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100221 // Generate docs
222 //
223 try {
Filip Pytloun64ff0792017-03-07 16:47:46 +0100224 try {
225 // Run sphinx state to install sphinx-build needed in
226 // upcomming orchestrate
227 salt.enforceState(saltMaster, 'I@sphinx:server', 'sphinx')
228 } catch (Throwable e) {
229 true
230 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100231 retry(3) {
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100232 // TODO: fix salt.orchestrateSystem
233 // print salt.orchestrateSystem(saltMaster, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
Filip Pytlounc17161d2017-03-03 09:50:54 +0100234 def out = salt.cmdRun(saltMaster, 'I@salt:master', 'salt-run state.orchestrate sphinx.orch.generate_doc || echo "Command execution failed"')
235 print out
236 if (out =~ /Command execution failed/) {
237 throw new Exception("Command execution failed")
238 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100239 }
240 } catch (Throwable e) {
241 // We don't want sphinx docs to ruin whole build, so possible
242 // errors are just ignored here
243 true
244 }
245 salt.enforceState(saltMaster, 'I@nginx:server', 'nginx')
246
Filip Pytlounbd619272017-03-22 12:21:01 +0100247 def failedSvc = salt.cmdRun(saltMaster, '*', """systemctl --failed | grep -E 'loaded[ \t]+failed' && echo 'Command execution failed'""")
248 print failedSvc
249 if (failedSvc =~ /Command execution failed/) {
250 common.errorMsg("Some services are not running. Environment may not be fully functional!")
251 }
252
Filip Pytlound9427392017-03-04 13:58:08 +0100253 common.successMsg("""
Filip Pytloun794ad952017-03-03 10:39:26 +0100254 ============================================================
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100255 Your CI/CD lab has been deployed and you can enjoy it:
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100256 Use sshuttle to connect to your private subnet:
257
Filip Pytloun85464052017-03-03 16:31:43 +0100258 sshuttle -r ${adminUser}@${saltMasterHost} 172.16.10.0/24
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100259
260 And visit services running at 172.16.10.254 (vip address):
261
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100262 9600 haproxy stats
263 8080 gerrit
264 8081 jenkins
265 8091 Docker swarm visualizer
266 8090 Reclass-generated documentation
267
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100268 If you provided SSH_PUBLIC_KEY, you can use it to login,
269 otherwise you need to get private key connected to this
270 heat template.
271
272 DON'T FORGET TO TERMINATE YOUR STACK WHEN YOU DON'T NEED IT!
Filip Pytloun85464052017-03-03 16:31:43 +0100273 ============================================================""")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100274 }
275 } catch (Throwable e) {
276 // If there was an error or exception thrown, the build failed
277 currentBuild.result = "FAILURE"
278 throw e
279 } finally {
280 // Cleanup
Filip Pytloun794ad952017-03-03 10:39:26 +0100281 if (HEAT_STACK_DELETE.toBoolean() == true) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100282 stage('Trigger cleanup job') {
Filip Pytloun592773c2017-03-03 14:17:00 +0100283 build job: 'deploy-heat-cleanup', parameters: [[$class: 'StringParameterValue', name: 'HEAT_STACK_NAME', value: HEAT_STACK_NAME]]
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100284 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +0100285 }
Filip Pytloun23741982017-02-27 17:43:00 +0100286 }
Filip Pytlounf6e877f2017-02-28 19:38:16 +0100287 }
Filip Pytloun0a07f702017-02-24 18:26:18 +0100288}