blob: 11fc5865ecce68a657c32d06e03c02f008ecf6ca [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 Pytloun8fe1f7e2017-03-01 18:45:09 +010035def waitForServices(saltMaster) {
36 retry(30) {
37 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'""")
38 for (int a = 0; a < out['return'].size(); a++) {
39 def entry = out['return'].get(a)
40 for (int i = 0; i < entry.size(); i++) {
41 def node = entry.get(i)
42 if (node) {
43 if (node.value =~ /Some services are not running/) {
44 sleep(10)
45 throw new Exception("$node.key: $node.value")
46 } else {
47 print out
48 }
49 }
50 }
51 }
52 }
53}
54
Filip Pytlounbfce09d2017-03-01 19:00:43 +010055timestamps {
56 node {
57 try {
58 // connection objects
59 def openstackCloud
60 def saltMaster
Filip Pytloun0a07f702017-02-24 18:26:18 +010061
Filip Pytlounbfce09d2017-03-01 19:00:43 +010062 // value defaults
63 def openstackVersion = OPENSTACK_API_CLIENT ? OPENSTACK_API_CLIENT : 'liberty'
64 def openstackEnv = "${env.WORKSPACE}/venv"
Filip Pytloun0a07f702017-02-24 18:26:18 +010065
Filip Pytloun3eefd3d2017-03-03 14:13:41 +010066 try {
67 sshPubKey = SSH_PUBLIC_KEY
68 } catch (MissingPropertyException e) {
69 sshPubKey = false
70 }
71
Filip Pytloun794ad952017-03-03 10:39:26 +010072 if (HEAT_STACK_REUSE.toBoolean() == true && HEAT_STACK_NAME == '') {
73 error("If you want to reuse existing stack you need to provide it's name")
74 }
75
76 if (HEAT_STACK_REUSE.toBoolean() == false) {
77 // Don't allow to set custom heat stack name
78 wrap([$class: 'BuildUser']) {
Tomáš Kukrál24d7fe62017-03-03 10:57:11 +010079 if (env.BUILD_USER_ID) {
80 HEAT_STACK_NAME = "${env.BUILD_USER_ID}-${JOB_NAME}-${BUILD_NUMBER}"
81 } else {
82 HEAT_STACK_NAME = "jenkins-${JOB_NAME}-${BUILD_NUMBER}"
83 }
Filip Pytloun794ad952017-03-03 10:39:26 +010084 currentBuild.description = HEAT_STACK_NAME
85 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +010086 }
Filip Pytloun5b0954b2017-03-01 10:10:18 +010087
Filip Pytloun3d045f82017-03-01 09:44:52 +010088 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010089 // Bootstrap
Filip Pytloun3d045f82017-03-01 09:44:52 +010090 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +010091
92 stage ('Download Heat templates') {
93 git.checkoutGitRepository('template', HEAT_TEMPLATE_URL, HEAT_TEMPLATE_BRANCH, HEAT_TEMPLATE_CREDENTIALS)
Filip Pytloun3d045f82017-03-01 09:44:52 +010094 }
Filip Pytloun3d045f82017-03-01 09:44:52 +010095
Filip Pytlounbfce09d2017-03-01 19:00:43 +010096 stage('Install OpenStack CLI') {
97 openstack.setupOpenstackVirtualenv(openstackEnv, openstackVersion)
98 }
Filip Pytloun64123cd2017-03-01 11:26:17 +010099
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100100 stage('Connect to OpenStack cloud') {
101 openstackCloud = openstack.createOpenstackEnv(OPENSTACK_API_URL, OPENSTACK_API_CREDENTIALS, OPENSTACK_API_PROJECT)
102 openstack.getKeystoneToken(openstackCloud, openstackEnv)
103 }
104
Filip Pytloun794ad952017-03-03 10:39:26 +0100105 if (HEAT_STACK_REUSE.toBoolean() == false) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100106 stage('Launch new Heat stack') {
107 envParams = [
108 'instance_zone': HEAT_STACK_ZONE,
109 'public_net': HEAT_STACK_PUBLIC_NET
110 ]
111 openstack.createHeatStack(openstackCloud, HEAT_STACK_NAME, HEAT_STACK_TEMPLATE, envParams, HEAT_STACK_ENVIRONMENT, openstackEnv)
112 }
113 }
114
115 stage('Connect to Salt master') {
116 def saltMasterPort
117 try {
118 saltMasterPort = SALT_MASTER_PORT
119 } catch (MissingPropertyException e) {
120 saltMasterPort = 8000
121 }
122 saltMasterHost = openstack.getHeatStackOutputParam(openstackCloud, HEAT_STACK_NAME, 'salt_master_ip', openstackEnv)
123 saltMasterUrl = "http://${saltMasterHost}:${saltMasterPort}"
124 saltMaster = salt.connection(saltMasterUrl, SALT_MASTER_CREDENTIALS)
125 }
126
127 //
128 // Install
129 //
130
131 stage('Install core infra') {
132 // salt.master, reclass
133 // refresh_pillar
134 // sync_all
135 // linux,openssh,salt.minion.ntp
136
137 orchestrate.installFoundationInfra(saltMaster)
138 orchestrate.validateFoundationInfra(saltMaster)
139 }
140
141 stage("Deploy GlusterFS") {
142 salt.enforceState(saltMaster, 'I@glusterfs:server', 'glusterfs.server.service', true)
143 salt.enforceState(saltMaster, 'ci01*', 'glusterfs.server.setup', true)
144 sleep(5)
145 salt.enforceState(saltMaster, 'I@glusterfs:client', 'glusterfs.client', true)
146 print salt.cmdRun(saltMaster, 'I@glusterfs:client', 'mount|grep fuse.glusterfs || echo "Command failed"')
147 }
148
149 stage("Deploy GlusterFS") {
150 salt.enforceState(saltMaster, 'I@haproxy:proxy', 'haproxy,keepalived')
151 }
152
153 stage("Setup Docker Swarm") {
154 salt.enforceState(saltMaster, 'I@docker:host', 'docker.host', true)
155 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.swarm', true)
156 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'salt', true)
157 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.flush')
158 salt.runSaltProcessStep(saltMaster, 'I@docker:swarm:role:master', 'mine.update')
159 salt.enforceState(saltMaster, 'I@docker:swarm', 'docker.swarm', true)
160 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'docker node ls')
161 }
162
163 stage("Deploy Docker services") {
164 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
165
166 // XXX: Hack to fix dependency of gerrit on mysql
167 print salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', "docker service rm gerrit; sleep 5; rm -rf /srv/volumes/gerrit/*")
168 waitForServices(saltMaster)
169
170 timeout(10) {
171 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'apt-get install -y mysql-client')
172 println "Waiting for MySQL to come up.."
173 salt.cmdRun(saltMaster, 'I@docker:swarm:role:master', 'while true; do mysql -h172.16.10.254 -ppassword -e"show status;" >/dev/null && break; done')
174 }
175 salt.enforceState(saltMaster, 'I@docker:swarm:role:master', 'docker.client')
176 // ---- cut here (end of hack) ----
177
178 waitForServices(saltMaster)
179 }
180
181 stage("Configure CI/CD services") {
182 // Aptly
183 salt.enforceState(saltMaster, 'I@aptly:server', 'aptly', true)
184
185 // Gerrit
186 timeout(10) {
187 println "Waiting for Gerrit to come up.."
188 salt.cmdRun(saltMaster, 'I@gerrit:client', 'while true; do curl -svf 172.16.10.254:8080 >/dev/null && break; done')
189 }
190 retry(2) {
191 // Needs to run twice to pass __virtual__ method of gerrit module
192 // after installation of dependencies
193 try {
194 salt.enforceState(saltMaster, 'I@gerrit:client', 'gerrit', true)
195 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100196 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100197 salt.cmdRun(saltMaster, 'I@gerrit:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
198 sleep(5)
199 throw e
200 }
201 }
202
203 // Jenkins
204 timeout(10) {
205 println "Waiting for Jenkins to come up.."
206 salt.cmdRun(saltMaster, 'I@jenkins:client', 'while true; do curl -svf 172.16.10.254:8081 >/dev/null && break; done')
207 }
208 retry(2) {
209 // Same for jenkins
210 try {
211 salt.enforceState(saltMaster, 'I@jenkins:client', 'jenkins', true)
212 } catch (Exception e) {
Filip Pytloun85464052017-03-03 16:31:43 +0100213 common.infoMsg("Restarting Salt minion")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100214 salt.cmdRun(saltMaster, 'I@jenkins:client', "exec 0>&-; exec 1>&-; exec 2>&-; nohup /bin/sh -c 'salt-call --local service.restart salt-minion' &")
215 sleep(5)
216 throw e
217 }
218 }
219 }
220
221 stage("Finalize") {
222 //
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100223 // Deploy user's ssh key
224 //
Filip Pytloun0da421f2017-03-03 18:50:45 +0100225 def adminUser
226 def authorizedKeysFile
Filip Pytlounbfa918a2017-03-04 10:01:30 +0100227 def adminUserCmdOut = salt.cmdRun(saltMaster, 'I@salt:master', "[ -d /home/ubuntu ] && echo 'ubuntu user exists'")
228 if (adminUserCmdOut =~ /ubuntu user exists/) {
Filip Pytloun0da421f2017-03-03 18:50:45 +0100229 adminUser = "ubuntu"
230 authorizedKeysFile = "/home/ubuntu/.ssh/authorized_keys"
231 } else {
232 adminUser = "root"
233 authorizedKeysFile = "/root/.ssh/authorized_keys"
234 }
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100235
Filip Pytloun0da421f2017-03-03 18:50:45 +0100236 if (sshPubKey) {
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100237 println "Deploying provided ssh key at ${authorizedKeysFile}"
Filip Pytloun4a847d62017-03-03 15:54:56 +0100238 salt.cmdRun(saltMaster, '*', "echo '${sshPubKey}' | tee -a ${authorizedKeysFile}")
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100239 }
240
241 //
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100242 // Generate docs
243 //
244 try {
Filip Pytloun64ff0792017-03-07 16:47:46 +0100245 try {
246 // Run sphinx state to install sphinx-build needed in
247 // upcomming orchestrate
248 salt.enforceState(saltMaster, 'I@sphinx:server', 'sphinx')
249 } catch (Throwable e) {
250 true
251 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100252 retry(3) {
Filip Pytloun27e8fa02017-03-01 20:02:46 +0100253 // TODO: fix salt.orchestrateSystem
254 // print salt.orchestrateSystem(saltMaster, ['expression': '*', 'type': 'compound'], 'sphinx.orch.generate_doc')
Filip Pytlounc17161d2017-03-03 09:50:54 +0100255 def out = salt.cmdRun(saltMaster, 'I@salt:master', 'salt-run state.orchestrate sphinx.orch.generate_doc || echo "Command execution failed"')
256 print out
257 if (out =~ /Command execution failed/) {
258 throw new Exception("Command execution failed")
259 }
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100260 }
261 } catch (Throwable e) {
262 // We don't want sphinx docs to ruin whole build, so possible
263 // errors are just ignored here
264 true
265 }
266 salt.enforceState(saltMaster, 'I@nginx:server', 'nginx')
267
Filip Pytlound9427392017-03-04 13:58:08 +0100268 common.successMsg("""
Filip Pytloun794ad952017-03-03 10:39:26 +0100269 ============================================================
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100270 Your CI/CD lab has been deployed and you can enjoy it:
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100271 Use sshuttle to connect to your private subnet:
272
Filip Pytloun85464052017-03-03 16:31:43 +0100273 sshuttle -r ${adminUser}@${saltMasterHost} 172.16.10.0/24
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100274
275 And visit services running at 172.16.10.254 (vip address):
276
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100277 9600 haproxy stats
278 8080 gerrit
279 8081 jenkins
280 8091 Docker swarm visualizer
281 8090 Reclass-generated documentation
282
Filip Pytloun3eefd3d2017-03-03 14:13:41 +0100283 If you provided SSH_PUBLIC_KEY, you can use it to login,
284 otherwise you need to get private key connected to this
285 heat template.
286
287 DON'T FORGET TO TERMINATE YOUR STACK WHEN YOU DON'T NEED IT!
Filip Pytloun85464052017-03-03 16:31:43 +0100288 ============================================================""")
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100289 }
290 } catch (Throwable e) {
291 // If there was an error or exception thrown, the build failed
292 currentBuild.result = "FAILURE"
293 throw e
294 } finally {
295 // Cleanup
Filip Pytloun794ad952017-03-03 10:39:26 +0100296 if (HEAT_STACK_DELETE.toBoolean() == true) {
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100297 stage('Trigger cleanup job') {
Filip Pytloun592773c2017-03-03 14:17:00 +0100298 build job: 'deploy-heat-cleanup', parameters: [[$class: 'StringParameterValue', name: 'HEAT_STACK_NAME', value: HEAT_STACK_NAME]]
Filip Pytlounbfce09d2017-03-01 19:00:43 +0100299 }
Filip Pytlounfd6726a2017-02-28 19:31:16 +0100300 }
Filip Pytloun23741982017-02-27 17:43:00 +0100301 }
Filip Pytlounf6e877f2017-02-28 19:38:16 +0100302 }
Filip Pytloun0a07f702017-02-24 18:26:18 +0100303}