blob: c9e74fdbc04304c1f2745ba74eb3c12d5376d832 [file] [log] [blame]
Sergey Kolekonovba203982016-12-21 18:32:17 +04001package com.mirantis.mk
2
3/**
4 *
5 * Openstack functions
6 *
7 */
8
9/**
Tomáš Kukrálc3964e52017-02-22 14:07:37 +010010 * Convert maps
11 *
12 */
13
14@NonCPS def entries(m) {
15 return m.collect {k, v -> [k, v]}
16}
17
18/**
Sergey Kolekonovba203982016-12-21 18:32:17 +040019 * Install OpenStack service clients in isolated environment
20 *
21 * @param path Path where virtualenv is created
22 * @param version Version of the OpenStack clients
23 */
24
Tomáš Kukrálbee0b992017-08-10 16:50:40 +020025def setupOpenstackVirtualenv(path, version = 'latest') {
iberezovskiyd4240b52017-02-20 17:18:28 +040026 def python = new com.mirantis.mk.Python()
Vasyl Saienko030fc182017-07-12 14:54:42 +030027 python.setupDocutilsVirtualenv(path)
Sergey Kolekonovba203982016-12-21 18:32:17 +040028
29 def openstack_kilo_packages = [
Jakub Josef268bc842017-10-10 14:36:17 +020030 //XXX: hack to fix https://bugs.launchpad.net/ubuntu/+source/python-pip/+bug/1635463
31 'cliff==2.8',
Sergey Kolekonovba203982016-12-21 18:32:17 +040032 'python-cinderclient>=1.3.1,<1.4.0',
33 'python-glanceclient>=0.19.0,<0.20.0',
34 'python-heatclient>=0.6.0,<0.7.0',
35 'python-keystoneclient>=1.6.0,<1.7.0',
36 'python-neutronclient>=2.2.6,<2.3.0',
37 'python-novaclient>=2.19.0,<2.20.0',
38 'python-swiftclient>=2.5.0,<2.6.0',
Jakub Josefbd927322017-05-30 13:20:27 +000039 'python-openstackclient>=1.7.0,<1.8.0',
Sergey Kolekonovba203982016-12-21 18:32:17 +040040 'oslo.config>=2.2.0,<2.3.0',
41 'oslo.i18n>=2.3.0,<2.4.0',
42 'oslo.serialization>=1.8.0,<1.9.0',
43 'oslo.utils>=1.4.0,<1.5.0',
Jakub Josef60280212017-08-10 19:01:19 +020044 'docutils'
Sergey Kolekonovba203982016-12-21 18:32:17 +040045 ]
46
Tomáš Kukrál381a8c92017-06-21 09:01:52 +020047 def openstack_latest_packages = [
Jakub Josef268bc842017-10-10 14:36:17 +020048 //XXX: hack to fix https://bugs.launchpad.net/ubuntu/+source/python-pip/+bug/1635463
49 'cliff==2.8',
Vasyl Saienko36a019d2018-05-30 09:51:18 +030050 // NOTE(vsaienko): cmd2 is dependency for cliff, since we don't using upper-contstraints
51 // we have to pin cmd2 < 0.9.0 as later versions are not compatible with python2.
52 // TODO(vsaienko): use upper-constraints here, as in requirements we set only lowest library
53 // versions.
54 'cmd2<0.9.0;python_version=="2.7"',
55 'cmd2>=0.9.1;python_version=="3.4"',
56 'cmd2>=0.9.1;python_version=="3.5"',
Tomáš Kukrál381a8c92017-06-21 09:01:52 +020057 'python-openstackclient',
58 'python-heatclient',
Jakub Josef60280212017-08-10 19:01:19 +020059 'docutils'
Tomáš Kukrálc6a94c62017-06-19 14:44:24 +020060 ]
Sergey Kolekonovba203982016-12-21 18:32:17 +040061
Tomáš Kukrálc6a94c62017-06-19 14:44:24 +020062 if (version == 'kilo') {
Sergey Kolekonovba203982016-12-21 18:32:17 +040063 requirements = openstack_kilo_packages
Tomáš Kukrálc6a94c62017-06-19 14:44:24 +020064 } else if (version == 'liberty') {
Sergey Kolekonovba203982016-12-21 18:32:17 +040065 requirements = openstack_kilo_packages
Tomáš Kukrálc6a94c62017-06-19 14:44:24 +020066 } else if (version == 'mitaka') {
Sergey Kolekonovba203982016-12-21 18:32:17 +040067 requirements = openstack_kilo_packages
Tomáš Kukrál381a8c92017-06-21 09:01:52 +020068 } else {
Sergey Kolekonovba203982016-12-21 18:32:17 +040069 requirements = openstack_latest_packages
70 }
Tomáš Kukrálbee0b992017-08-10 16:50:40 +020071 python.setupVirtualenv(path, 'python2', requirements, null, true)
Sergey Kolekonovba203982016-12-21 18:32:17 +040072}
73
74/**
75 * create connection to OpenStack API endpoint
76 *
Jakub Josef6c963762018-01-18 16:02:22 +010077 * @param path Path to created venv
Sergey Kolekonovba203982016-12-21 18:32:17 +040078 * @param url OpenStack API endpoint address
79 * @param credentialsId Credentials to the OpenStack API
80 * @param project OpenStack project to connect to
81 */
Jakub Josef6c963762018-01-18 16:02:22 +010082def createOpenstackEnv(path, url, credentialsId, project, project_domain="default",
Tomáš Kukrál381a8c92017-06-21 09:01:52 +020083 project_id="", user_domain="default", api_ver="2", cacert="/etc/ssl/certs/ca-certificates.crt") {
iberezovskiyd4240b52017-02-20 17:18:28 +040084 def common = new com.mirantis.mk.Common()
Jakub Josef6c963762018-01-18 16:02:22 +010085 rcFile = "${path}/keystonerc"
Sergey Kolekonovba203982016-12-21 18:32:17 +040086 creds = common.getPasswordCredentials(credentialsId)
Alexander Tivelkovf89a1882017-01-11 13:29:35 +030087 rc = """set +x
88export OS_USERNAME=${creds.username}
Ales Komarek0e558ee2016-12-23 13:02:55 +010089export OS_PASSWORD=${creds.password.toString()}
90export OS_TENANT_NAME=${project}
91export OS_AUTH_URL=${url}
92export OS_AUTH_STRATEGY=keystone
kairat_kushaev0a26bf72017-05-18 13:20:09 +040093export OS_PROJECT_NAME=${project}
Jakub Josefbd927322017-05-30 13:20:27 +000094export OS_PROJECT_ID=${project_id}
kairat_kushaev0a26bf72017-05-18 13:20:09 +040095export OS_PROJECT_DOMAIN_ID=${project_domain}
Jakub Josefbd927322017-05-30 13:20:27 +000096export OS_USER_DOMAIN_NAME=${user_domain}
Kirill Mashchenko234708f2017-07-20 17:00:01 +030097export OS_IDENTITY_API_VERSION=${api_ver}
Tomáš Kukrál381a8c92017-06-21 09:01:52 +020098export OS_CACERT=${cacert}
Alexander Tivelkovf89a1882017-01-11 13:29:35 +030099set -x
Ales Komarek0e558ee2016-12-23 13:02:55 +0100100"""
101 writeFile file: rcFile, text: rc
102 return rcFile
Sergey Kolekonovba203982016-12-21 18:32:17 +0400103}
104
105/**
106 * Run command with OpenStack env params and optional python env
107 *
108 * @param cmd Command to be executed
109 * @param env Environmental parameters with endpoint credentials
110 * @param path Optional path to virtualenv with specific clients
111 */
112def runOpenstackCommand(cmd, venv, path = null) {
iberezovskiyd4240b52017-02-20 17:18:28 +0400113 def python = new com.mirantis.mk.Python()
Sergey Kolekonovba203982016-12-21 18:32:17 +0400114 openstackCmd = ". ${venv}; ${cmd}"
115 if (path) {
116 output = python.runVirtualenvCommand(path, openstackCmd)
117 }
118 else {
119 echo("[Command]: ${openstackCmd}")
120 output = sh (
121 script: openstackCmd,
122 returnStdout: true
123 ).trim()
124 }
125 return output
126}
127
128/**
129 * Get OpenStack Keystone token for current credentials
130 *
131 * @param env Connection parameters for OpenStack API endpoint
132 * @param path Optional path to the custom virtualenv
133 */
134def getKeystoneToken(client, path = null) {
iberezovskiyd4240b52017-02-20 17:18:28 +0400135 def python = new com.mirantis.mk.Python()
Jakub Josefbd927322017-05-30 13:20:27 +0000136 cmd = "openstack token issue"
Sergey Kolekonovba203982016-12-21 18:32:17 +0400137 outputTable = runOpenstackCommand(cmd, client, path)
Ales Komareke11e8792016-12-28 09:42:25 +0100138 output = python.parseTextTable(outputTable, 'item', 'prettytable', path)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400139 return output
140}
141
142/**
Ales Komarek51b7b152017-06-27 11:14:50 +0200143 * Create OpenStack environment file
Sergey Kolekonovba203982016-12-21 18:32:17 +0400144 *
145 * @param env Connection parameters for OpenStack API endpoint
146 * @param path Optional path to the custom virtualenv
147 */
148def createHeatEnv(file, environment = [], original_file = null) {
149 if (original_file) {
150 envString = readFile file: original_file
Tomáš Kukrál03029442017-02-21 17:14:29 +0100151 } else {
Sergey Kolekonovba203982016-12-21 18:32:17 +0400152 envString = "parameters:\n"
153 }
Tomáš Kukrál03029442017-02-21 17:14:29 +0100154
Tomáš Kukrálc3964e52017-02-22 14:07:37 +0100155 p = entries(environment)
Tomáš Kukrálb1fe9642017-02-22 11:21:17 +0100156 for (int i = 0; i < p.size(); i++) {
157 envString = "${envString} ${p.get(i)[0]}: ${p.get(i)[1]}\n"
Sergey Kolekonovba203982016-12-21 18:32:17 +0400158 }
Tomáš Kukrál03029442017-02-21 17:14:29 +0100159
Tomáš Kukrále19ddea2017-02-21 11:09:40 +0100160 echo("writing to env file:\n${envString}")
Sergey Kolekonovba203982016-12-21 18:32:17 +0400161 writeFile file: file, text: envString
162}
163
164/**
165 * Create new OpenStack Heat stack
166 *
167 * @param env Connection parameters for OpenStack API endpoint
168 * @param template HOT template for the new Heat stack
169 * @param environment Environmentale parameters of the new Heat stack
170 * @param name Name of the new Heat stack
171 * @param path Optional path to the custom virtualenv
172 */
Tomáš Kukrála1152742017-08-22 16:21:50 +0200173def createHeatStack(client, name, template, params = [], environment = null, path = null, action="create") {
iberezovskiyd4240b52017-02-20 17:18:28 +0400174 def python = new com.mirantis.mk.Python()
Jakub Josef0a898762017-08-11 16:27:44 +0200175 def templateFile = "${env.WORKSPACE}/template/template/${template}.hot"
176 def envFile
177 def envSource
Sergey Kolekonovba203982016-12-21 18:32:17 +0400178 if (environment) {
Tomáš Kukrála1152742017-08-22 16:21:50 +0200179 envFile = "${env.WORKSPACE}/template/env/${name}.env"
180 if (environment.contains("/")) {
181 //init() returns all elements but the last in a collection.
182 def envPath = environment.tokenize("/").init().join("/")
183 if (envPath) {
184 envFile = "${env.WORKSPACE}/template/env/${envPath}/${name}.env"
185 }
Ales Komarek51b7b152017-06-27 11:14:50 +0200186 }
Tomáš Kukrála1152742017-08-22 16:21:50 +0200187 envSource = "${env.WORKSPACE}/template/env/${environment}.env"
Sergey Kolekonovba203982016-12-21 18:32:17 +0400188 createHeatEnv(envFile, params, envSource)
Jakub Josef9a59aeb2017-08-11 15:50:20 +0200189 } else {
Sergey Kolekonovba203982016-12-21 18:32:17 +0400190 envFile = "${env.WORKSPACE}/template/${name}.env"
191 createHeatEnv(envFile, params)
192 }
Tomáš Kukrála1152742017-08-22 16:21:50 +0200193
Mykyta Karpincf44f812017-08-28 14:45:21 +0300194 def cmd
195 def waitState
196
Tomáš Kukrála1152742017-08-22 16:21:50 +0200197 if (action == "create") {
Mykyta Karpincf44f812017-08-28 14:45:21 +0300198 cmd = "heat stack-create -f ${templateFile} -e ${envFile} ${name}"
199 waitState = "CREATE_COMPLETE"
Tomáš Kukrála1152742017-08-22 16:21:50 +0200200 } else {
Mykyta Karpincf44f812017-08-28 14:45:21 +0300201 cmd = "heat stack-update -f ${templateFile} -e ${envFile} ${name}"
202 waitState = "UPDATE_COMPLETE"
Tomáš Kukrála1152742017-08-22 16:21:50 +0200203 }
204
Sergey Kolekonovba203982016-12-21 18:32:17 +0400205 dir("${env.WORKSPACE}/template/template") {
206 outputTable = runOpenstackCommand(cmd, client, path)
207 }
Tomáš Kukrála1152742017-08-22 16:21:50 +0200208
Ales Komareke11e8792016-12-28 09:42:25 +0100209 output = python.parseTextTable(outputTable, 'item', 'prettytable', path)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400210
Jakub Josef9a59aeb2017-08-11 15:50:20 +0200211 def heatStatusCheckerCount = 1
Jakub Josefe0337272017-09-12 14:15:27 +0200212 while (heatStatusCheckerCount <= 250) {
Sergey Kolekonovba203982016-12-21 18:32:17 +0400213 status = getHeatStackStatus(client, name, path)
Jakub Josef9a59aeb2017-08-11 15:50:20 +0200214 echo("[Heat Stack] Status: ${status}, Check: ${heatStatusCheckerCount}")
Ales Komarekdce8b472016-12-30 16:56:07 +0100215
Tomáš Kukrála1152742017-08-22 16:21:50 +0200216 if (status.contains('CREATE_FAILED')) {
Sergey Kolekonovba203982016-12-21 18:32:17 +0400217 info = getHeatStackInfo(client, name, path)
218 throw new Exception(info.stack_status_reason)
Tomáš Kukrála1152742017-08-22 16:21:50 +0200219
220 } else if (status.contains(waitState)) {
Sergey Kolekonovba203982016-12-21 18:32:17 +0400221 info = getHeatStackInfo(client, name, path)
222 echo(info.stack_status_reason)
223 break
224 }
Tomáš Kukrála1152742017-08-22 16:21:50 +0200225
Jakub Josefe0337272017-09-12 14:15:27 +0200226 sleep(30)
Jakub Josef9a59aeb2017-08-11 15:50:20 +0200227 heatStatusCheckerCount++
Sergey Kolekonovba203982016-12-21 18:32:17 +0400228 }
229 echo("[Heat Stack] Status: ${status}")
230}
231
232/**
Jakub Josefdb4baf22017-05-10 15:16:09 +0200233 * Returns list of stacks for stack name filter
234 *
235 * @param client Connection parameters for OpenStack API endpoint
236 * @param filter Stack name filter
237 * @param path Optional path to the custom virtualenv
238 */
239def getStacksForNameContains(client, filter, path = null){
Jakub Josef6465fca2017-05-10 16:09:20 +0200240 cmd = 'heat stack-list | awk \'NR>3 {print $4}\' | sed \'$ d\' | grep ' + filter + '|| true'
Jakub Josefdb4baf22017-05-10 15:16:09 +0200241 return runOpenstackCommand(cmd, client, path).trim().tokenize("\n")
242}
243
244
245/**
Jakub Josef5e238a22017-04-19 16:35:15 +0200246 * Get list of stack names with given stack status
247 *
Jakub Josefdb4baf22017-05-10 15:16:09 +0200248 * @param client Connection parameters for OpenStack API endpoint
Jakub Josef5e238a22017-04-19 16:35:15 +0200249 * @param status Stack status
250 * @param path Optional path to the custom virtualenv
251 */
252 def getStacksWithStatus(client, status, path = null) {
253 cmd = 'heat stack-list -f stack_status='+status+' | awk \'NR>3 {print $4}\' | sed \'$ d\''
254 return runOpenstackCommand(cmd, client, path).trim().tokenize("\n")
255 }
256
257/**
Sergey Kolekonovba203982016-12-21 18:32:17 +0400258 * Get life cycle status for existing OpenStack Heat stack
259 *
260 * @param env Connection parameters for OpenStack API endpoint
261 * @param name Name of the managed Heat stack instance
262 * @param path Optional path to the custom virtualenv
263 */
264def getHeatStackStatus(client, name, path = null) {
265 cmd = 'heat stack-list | awk -v stack='+name+' \'{if ($4==stack) print $6}\''
266 return runOpenstackCommand(cmd, client, path)
267}
268
269/**
270 * Get info about existing OpenStack Heat stack
271 *
272 * @param env Connection parameters for OpenStack API endpoint
273 * @param name Name of the managed Heat stack instance
274 * @param path Optional path to the custom virtualenv
275 */
276def getHeatStackInfo(env, name, path = null) {
iberezovskiyd4240b52017-02-20 17:18:28 +0400277 def python = new com.mirantis.mk.Python()
Sergey Kolekonovba203982016-12-21 18:32:17 +0400278 cmd = "heat stack-show ${name}"
279 outputTable = runOpenstackCommand(cmd, env, path)
Ales Komareke11e8792016-12-28 09:42:25 +0100280 output = python.parseTextTable(outputTable, 'item', 'prettytable', path)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400281 return output
282}
283
284/**
285 * Get existing OpenStack Heat stack output parameter
286 *
287 * @param env Connection parameters for OpenStack API endpoint
288 * @param name Name of the managed Heat stack
289 * @param parameter Name of the output parameter
290 * @param path Optional path to the custom virtualenv
291 */
292def getHeatStackOutputParam(env, name, outputParam, path = null) {
Vasyl Saienkoea4b2812017-07-10 10:36:03 +0000293 cmd = "heat output-show ${name} ${outputParam}"
Sergey Kolekonovba203982016-12-21 18:32:17 +0400294 output = runOpenstackCommand(cmd, env, path)
Ales Komarekeedc2222017-01-03 10:10:03 +0100295 echo("${cmd}: ${output}")
Vasyl Saienko2a1c2de2017-07-11 11:41:53 +0300296 // NOTE(vsaienko) heatclient 1.5.1 returns output in "", while later
297 // versions returns string without "".
298 // TODO Use openstack 'stack output show' when all jobs using at least Mitaka heatclient
299 return "${output}".replaceAll('"', '')
Sergey Kolekonovba203982016-12-21 18:32:17 +0400300}
301
302/**
303 * List all resources from existing OpenStack Heat stack
304 *
305 * @param env Connection parameters for OpenStack API endpoint
306 * @param name Name of the managed Heat stack instance
307 * @param path Optional path to the custom virtualenv
Mykyta Karpin72306362018-02-08 16:40:43 +0200308 * @param depth Optional depth of stack for listing resources,
309 * 0 - do not list nested resources
Sergey Kolekonovba203982016-12-21 18:32:17 +0400310 */
Mykyta Karpin72306362018-02-08 16:40:43 +0200311def getHeatStackResources(env, name, path = null, depth = 0) {
iberezovskiyd4240b52017-02-20 17:18:28 +0400312 def python = new com.mirantis.mk.Python()
Mykyta Karpin72306362018-02-08 16:40:43 +0200313 cmd = "heat resource-list --nested-depth ${depth} ${name}"
Sergey Kolekonovba203982016-12-21 18:32:17 +0400314 outputTable = runOpenstackCommand(cmd, env, path)
Ales Komareke11e8792016-12-28 09:42:25 +0100315 output = python.parseTextTable(outputTable, 'list', 'prettytable', path)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400316 return output
317}
318
319/**
320 * Get info about resource from existing OpenStack Heat stack
321 *
322 * @param env Connection parameters for OpenStack API endpoint
323 * @param name Name of the managed Heat stack instance
324 * @param path Optional path to the custom virtualenv
325 */
326def getHeatStackResourceInfo(env, name, resource, path = null) {
iberezovskiyd4240b52017-02-20 17:18:28 +0400327 def python = new com.mirantis.mk.Python()
Sergey Kolekonovba203982016-12-21 18:32:17 +0400328 cmd = "heat resource-show ${name} ${resource}"
329 outputTable = runOpenstackCommand(cmd, env, path)
Ales Komareke11e8792016-12-28 09:42:25 +0100330 output = python.parseTextTable(outputTable, 'item', 'prettytable', path)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400331 return output
332}
333
334/**
335 * Update existing OpenStack Heat stack
336 *
337 * @param env Connection parameters for OpenStack API endpoint
338 * @param name Name of the managed Heat stack instance
339 * @param path Optional path to the custom virtualenv
340 */
341def updateHeatStack(env, name, path = null) {
iberezovskiyd4240b52017-02-20 17:18:28 +0400342 def python = new com.mirantis.mk.Python()
Sergey Kolekonovba203982016-12-21 18:32:17 +0400343 cmd = "heat stack-update ${name}"
344 outputTable = runOpenstackCommand(cmd, env, path)
Ales Komareke11e8792016-12-28 09:42:25 +0100345 output = python.parseTextTable(outputTable, 'item', 'prettytable', path)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400346 return output
347}
348
349/**
350 * Delete existing OpenStack Heat stack
351 *
352 * @param env Connection parameters for OpenStack API endpoint
353 * @param name Name of the managed Heat stack instance
354 * @param path Optional path to the custom virtualenv
355 */
356def deleteHeatStack(env, name, path = null) {
357 cmd = "heat stack-delete ${name}"
358 outputTable = runOpenstackCommand(cmd, env, path)
359}
360
361/**
Mykyta Karpin72306362018-02-08 16:40:43 +0200362 * Return hashmap of hashes server_id:server_name of servers from OpenStack Heat stack
Sergey Kolekonovba203982016-12-21 18:32:17 +0400363 *
364 * @param env Connection parameters for OpenStack API endpoint
365 * @param name Name of the managed Heat stack instance
366 * @param path Optional path to the custom virtualenv
367 */
368def getHeatStackServers(env, name, path = null) {
Mykyta Karpin72306362018-02-08 16:40:43 +0200369 // set depth to 1000 to ensure all nested resources are shown
370 resources = getHeatStackResources(env, name, path, 1000)
371 servers = [:]
Sergey Kolekonovba203982016-12-21 18:32:17 +0400372 for (resource in resources) {
373 if (resource.resource_type == 'OS::Nova::Server') {
Mykyta Karpin67978112018-02-22 11:16:45 +0200374 server = getHeatStackResourceInfo(env, resource.stack_name, resource.resource_name, path)
Mykyta Karpin72306362018-02-08 16:40:43 +0200375 servers[server.attributes.id] = server.attributes.name
Sergey Kolekonovba203982016-12-21 18:32:17 +0400376 }
377 }
378 echo("[Stack ${name}] Servers: ${servers}")
379 return servers
380}
Jiri Broulikf8f96942018-02-15 10:03:42 +0100381
382/**
Mykyta Karpin8306a9d2018-07-27 11:34:10 +0300383 * Delete nova key pair
384 *
385 * @param env Connection parameters for OpenStack API endpoint
386 * @param name Name of the key pair to delete
387 * @param path Optional path to the custom virtualenv
388 */
389def deleteKeyPair(env, name, path = null) {
390 def common = new com.mirantis.mk.Common()
391 common.infoMsg("Removing key pair ${name}")
392 def cmd = "openstack keypair delete ${name}"
393 runOpenstackCommand(cmd, env, path)
394}
395
396/**
397 * Get nova key pair
398 *
399 * @param env Connection parameters for OpenStack API endpoint
400 * @param name Name of the key pair to show
401 * @param path Optional path to the custom virtualenv
402 */
403
404def getKeyPair(env, name, path = null) {
405 def common = new com.mirantis.mk.Common()
406 def cmd = "openstack keypair show ${name}"
407 def outputTable
408 try {
409 outputTable = runOpenstackCommand(cmd, env, path)
410 } catch (Exception e) {
411 common.infoMsg("Key pair ${name} not found")
412 }
413 return outputTable
414}
415
416/**
Jiri Broulikf8f96942018-02-15 10:03:42 +0100417 * Stops all services that contain specific string (for example nova,heat, etc.)
418 * @param env Salt Connection object or pepperEnv
419 * @param probe single node on which to list service names
420 * @param target all targeted nodes
421 * @param services lists of type of services to be stopped
Jiri Broulikf6daac62018-03-08 13:17:53 +0100422 * @param confirm enable/disable manual service stop confirmation
Jiri Broulikf8f96942018-02-15 10:03:42 +0100423 * @return output of salt commands
424 */
Jiri Broulik27e83052018-03-06 11:37:29 +0100425def stopServices(env, probe, target, services=[], confirm=false) {
Jiri Broulikf8f96942018-02-15 10:03:42 +0100426 def salt = new com.mirantis.mk.Salt()
Jiri Broulikf6daac62018-03-08 13:17:53 +0100427 def common = new com.mirantis.mk.Common()
Jiri Broulikf8f96942018-02-15 10:03:42 +0100428 for (s in services) {
Dmitry Ukovd72cd2a2018-09-04 17:31:46 +0400429 def outputServicesStr = salt.getReturnValues(salt.cmdRun(env, probe, "service --status-all | grep ${s} | awk \'{print \$4}\'"))
Jiri Broulikf6daac62018-03-08 13:17:53 +0100430 def servicesList = outputServicesStr.tokenize("\n").init()
Jiri Broulik27e83052018-03-06 11:37:29 +0100431 if (confirm) {
Jiri Broulikf6daac62018-03-08 13:17:53 +0100432 if (servicesList) {
433 try {
434 input message: "Click PROCEED to stop ${servicesList}. Otherwise click ABORT to skip stopping them."
435 for (name in servicesList) {
436 if (!name.contains('Salt command')) {
Dmitry Ukovd72cd2a2018-09-04 17:31:46 +0400437 salt.runSaltProcessStep(env, target, 'service.stop', ["${name}"])
Jiri Broulikf6daac62018-03-08 13:17:53 +0100438 }
439 }
440 } catch (Exception er) {
441 common.infoMsg("skipping stopping ${servicesList} services")
442 }
443 }
444 } else {
445 if (servicesList) {
Jiri Broulik27e83052018-03-06 11:37:29 +0100446 for (name in servicesList) {
447 if (!name.contains('Salt command')) {
Dmitry Ukovd72cd2a2018-09-04 17:31:46 +0400448 salt.runSaltProcessStep(env, target, 'service.stop', ["${name}"])
Jiri Broulik27e83052018-03-06 11:37:29 +0100449 }
450 }
Jiri Broulikf8f96942018-02-15 10:03:42 +0100451 }
452 }
453 }
454}
455
456/**
Vasyl Saienko4129e102018-09-03 10:15:52 +0300457 * Return intersection of globally installed services and those are
458 * defined on specific target according to theirs priorities.
459 *
460 * @param env Salt Connection object or env
461 * @param target The target node to get list of apps for.
462**/
463def getOpenStackUpgradeServices(env, target){
464 def salt = new com.mirantis.mk.Salt()
465 def common = new com.mirantis.mk.Common()
466
467 def global_apps = salt.getConfig(env, 'I@salt:master:enabled:true', 'orchestration.upgrade.applications')
468 def node_apps = salt.getPillar(env, target, '__reclass__:applications')['return'][0].values()[0]
469 def node_sorted_apps = []
470 if ( !global_apps['return'][0].values()[0].isEmpty() ) {
471 Map<String,Integer> _sorted_apps = [:]
472 for (k in global_apps['return'][0].values()[0].keySet()) {
473 if (k in node_apps) {
474 _sorted_apps[k] = global_apps['return'][0].values()[0][k].values()[0].toInteger()
475 }
476 }
477 node_sorted_apps = common.SortMapByValueAsc(_sorted_apps).keySet()
478 common.infoMsg("Applications are placed in following order:"+node_sorted_apps)
479 } else {
480 common.errorMsg("No applications found.")
481 }
482
483 return node_sorted_apps
484}
485
486
487/**
488 * Run specified upgrade phase for all services on given node.
489 *
490 * @param env Salt Connection object or env
491 * @param target The target node to run states on.
492 * @param phase The phase name to run.
493**/
494def runOpenStackUpgradePhase(env, target, phase){
495 def salt = new com.mirantis.mk.Salt()
496 def common = new com.mirantis.mk.Common()
497
498 services = getOpenStackUpgradeServices(env, target)
499 def st
500
501 for (service in services){
502 st = "${service}.upgrade.${phase}".trim()
503 common.infoMsg("Running ${phase} for service ${st} on ${target}")
504 salt.enforceState(env, target, st)
505 }
506}
507
508
509/**
510 * Run OpenStack states on specified node.
511 *
512 * @param env Salt Connection object or env
513 * @param target The target node to run states on.
514**/
515def applyOpenstackAppsStates(env, target){
516 def salt = new com.mirantis.mk.Salt()
517 def common = new com.mirantis.mk.Common()
518
519 services = getOpenStackUpgradeServices(env, target)
520 def st
521
522 for (service in services){
523 st = "${service}".trim()
524 common.infoMsg("Running ${st} on ${target}")
525 salt.enforceState(env, target, st)
526 }
527}
528
529/**
Martin Polreich65864b02018-12-05 10:42:50 +0100530 * Verifies Galera database
531 *
532 * This function checks for Galera master, tests connection and if reachable, it obtains the result
533 * of Salt mysql.status function. The result is then parsed, validated and outputed to the user.
534 *
535 * @param env Salt Connection object or pepperEnv
Martin Polreichddfe51c2019-01-21 14:31:00 +0100536 * @param slave Boolean value to enable slave checking (if master in unreachable)
537 * @param checkTimeSync Boolean value to enable time sync check
Martin Polreich65864b02018-12-05 10:42:50 +0100538 * @return resultCode int values used to determine exit status in the calling function
539 */
Martin Polreichddfe51c2019-01-21 14:31:00 +0100540def verifyGaleraStatus(env, slave=false, checkTimeSync=false) {
Martin Polreich65864b02018-12-05 10:42:50 +0100541 def salt = new com.mirantis.mk.Salt()
542 def common = new com.mirantis.mk.Common()
543 def out = ""
544 def status = "unknown"
Martin Polreich47c09d12018-12-21 16:42:06 +0100545 def testNode = ""
546 if (!slave) {
547 try {
548 galeraMaster = salt.getMinions(env, "I@galera:master")
549 common.infoMsg("Current Galera master is: ${galeraMaster}")
550 salt.minionsReachable(env, "I@salt:master", "I@galera:master")
551 testNode = "I@galera:master"
552 } catch (Exception e) {
553 common.errorMsg('Galera master is not reachable.')
554 return 128
555 }
556 } else {
557 try {
558 galeraMinions = salt.getMinions(env, "I@galera:slave")
559 common.infoMsg("Testing Galera slave minions: ${galeraMinions}")
560 } catch (Exception e) {
561 common.errorMsg("Cannot obtain Galera slave minions list.")
562 return 129
563 }
564 for (minion in galeraMinions) {
565 try {
566 salt.minionsReachable(env, "I@salt:master", minion)
567 testNode = minion
568 break
569 } catch (Exception e) {
570 common.warningMsg("Slave '${minion}' is not reachable.")
571 }
572 }
573 }
574 if (!testNode) {
575 common.errorMsg("No Galera slave was reachable.")
576 return 130
Martin Polreich65864b02018-12-05 10:42:50 +0100577 }
Martin Polreichddfe51c2019-01-21 14:31:00 +0100578 if (checkTimeSync && !salt.checkClusterTimeSync(env, "I@galera:master or I@galera:slave")) {
579 common.errorMsg("Time in cluster is desynchronized or it couldn't be detemined. You should fix this issue manually before proceeding.")
580 return 131
581 }
Martin Polreich65864b02018-12-05 10:42:50 +0100582 try {
Martin Polreich47c09d12018-12-21 16:42:06 +0100583 out = salt.cmdRun(env, "I@salt:master", "salt -C '${testNode}' mysql.status")
Martin Polreich65864b02018-12-05 10:42:50 +0100584 } catch (Exception e) {
585 common.errorMsg('Could not determine mysql status.')
586 return 256
587 }
588 if (out) {
589 try {
Martin Polreich47c09d12018-12-21 16:42:06 +0100590 status = validateAndPrintGaleraStatusReport(env, out, testNode)
Martin Polreich65864b02018-12-05 10:42:50 +0100591 } catch (Exception e) {
592 common.errorMsg('Could not parse the mysql status output. Check it manually.')
593 return 1
594 }
595 } else {
596 common.errorMsg("Mysql status response unrecognized or is empty. Response: ${out}")
597 return 1024
598 }
599 if (status == "OK") {
600 common.infoMsg("No errors found - MySQL status is ${status}.")
601 return 0
602 } else if (status == "unknown") {
603 common.warningMsg('MySQL status cannot be detemined')
604 return 1
605 } else {
606 common.errorMsg("Errors found.")
607 return 2
608 }
609}
610
611/** Validates and prints result of verifyGaleraStatus function
612@param env Salt Connection object or pepperEnv
613@param out Output of the mysql.status Salt function
614@return status "OK", "ERROR" or "uknown" depending on result of validation
615*/
616
Martin Polreich47c09d12018-12-21 16:42:06 +0100617def validateAndPrintGaleraStatusReport(env, out, minion) {
Martin Polreich65864b02018-12-05 10:42:50 +0100618 def salt = new com.mirantis.mk.Salt()
619 def common = new com.mirantis.mk.Common()
Martin Polreich47c09d12018-12-21 16:42:06 +0100620 if (minion == "I@galera:master") {
621 role = "master"
622 } else {
623 role = "slave"
624 }
Martin Polreich614d4222019-01-17 16:20:24 +0100625 sizeOut = salt.getReturnValues(salt.getPillar(env, minion, "galera:${role}:members"))
Martin Polreich65864b02018-12-05 10:42:50 +0100626 expected_cluster_size = sizeOut.size()
627 outlist = out['return'][0]
628 resultString = outlist.get(outlist.keySet()[0]).replace("\n ", " ").replace(" ", "").replace("Salt command execution success", "").replace("----------", "").replace(": \n", ": no value\n")
629 resultYaml = readYaml text: resultString
630 parameters = [
631 wsrep_cluster_status: [title: 'Cluster status', expectedValues: ['Primary'], description: ''],
632 wsrep_cluster_size: [title: 'Current cluster size', expectedValues: [expected_cluster_size], description: ''],
Martin Polreich47c09d12018-12-21 16:42:06 +0100633 wsrep_ready: [title: 'Node status', expectedValues: ['ON', true], description: ''],
634 wsrep_local_state_comment: [title: 'Node status comment', expectedValues: ['Joining', 'Waiting on SST', 'Joined', 'Synced', 'Donor'], description: ''],
635 wsrep_connected: [title: 'Node connectivity', expectedValues: ['ON', true], description: ''],
Martin Polreich65864b02018-12-05 10:42:50 +0100636 wsrep_local_recv_queue_avg: [title: 'Average size of local reveived queue', expectedThreshold: [warn: 0.5, error: 1.0], description: '(Value above 0 means that the node cannot apply write-sets as fast as it receives them, which can lead to replication throttling)'],
637 wsrep_local_send_queue_avg: [title: 'Average size of local send queue', expectedThreshold: [warn: 0.5, error: 1.0], description: '(Value above 0 indicate replication throttling or network throughput issues, such as a bottleneck on the network link.)']
638 ]
Martin Polreich65864b02018-12-05 10:42:50 +0100639 for (key in parameters.keySet()) {
640 value = resultYaml[key]
641 parameters.get(key) << [actualValue: value]
642 }
643 for (key in parameters.keySet()) {
644 param = parameters.get(key)
645 if (key == 'wsrep_local_recv_queue_avg' || key == 'wsrep_local_send_queue_avg') {
646 if (param.get('actualValue') > param.get('expectedThreshold').get('error')) {
647 param << [match: 'error']
648 } else if (param.get('actualValue') > param.get('expectedThreshold').get('warn')) {
649 param << [match: 'warn']
650 } else {
651 param << [match: 'ok']
652 }
653 } else {
654 for (expValue in param.get('expectedValues')) {
655 if (expValue == param.get('actualValue')) {
656 param << [match: 'ok']
657 break
658 } else {
659 param << [match: 'error']
660 }
661 }
662 }
663 }
664 cluster_info_report = []
665 cluster_warning_report = []
666 cluster_error_report = []
667 for (key in parameters.keySet()) {
668 param = parameters.get(key)
669 if (param.containsKey('expectedThreshold')) {
670 expValues = "below ${param.get('expectedThreshold').get('warn')}"
671 } else {
672 if (param.get('expectedValues').size() > 1) {
673 expValues = param.get('expectedValues').join(' or ')
674 } else {
675 expValues = param.get('expectedValues')[0]
676 }
677 }
678 reportString = "${param.title}: ${param.actualValue} (Expected: ${expValues}) ${param.description}"
679 if (param.get('match').equals('ok')) {
680 cluster_info_report.add("[OK ] ${reportString}")
681 } else if (param.get('match').equals('warn')) {
682 cluster_warning_report.add("[WARNING] ${reportString}")
683 } else {
684 cluster_error_report.add("[ ERROR] ${reportString})")
685 }
686 }
687 common.infoMsg("CLUSTER STATUS REPORT: ${cluster_info_report.size()} expected values, ${cluster_warning_report.size()} warnings and ${cluster_error_report.size()} error found:")
688 if (cluster_info_report.size() > 0) {
689 common.infoMsg(cluster_info_report.join('\n'))
690 }
691 if (cluster_warning_report.size() > 0) {
692 common.warningMsg(cluster_warning_report.join('\n'))
693 }
694 if (cluster_error_report.size() > 0) {
695 common.errorMsg(cluster_error_report.join('\n'))
696 return "ERROR"
697 } else {
698 return "OK"
699 }
700}
701
Martin Polreich47c09d12018-12-21 16:42:06 +0100702def getGaleraLastShutdownNode(env) {
703 def salt = new com.mirantis.mk.Salt()
704 def common = new com.mirantis.mk.Common()
705 members = ''
706 lastNode = [ip: '', seqno: -2]
707 try {
708 members = salt.getReturnValues(salt.getPillar(env, "I@galera:master", "galera:master:members"))
709 } catch (Exception er) {
710 common.errorMsg('Could not retrieve members list')
711 return 'I@galera:master'
712 }
713 if (members) {
714 for (member in members) {
715 try {
716 salt.minionsReachable(env, 'I@salt:master', "S@${member.host}")
717 out = salt.getReturnValues(salt.cmdRun(env, "S@${member.host}", 'cat /var/lib/mysql/grastate.dat | grep "seqno" | cut -d ":" -f2', true, null, false))
718 seqno = out.tokenize('\n')[0].trim()
719 if (seqno.isNumber()) {
720 seqno = seqno.toInteger()
721 } else {
722 seqno = -2
723 }
724 highestSeqno = lastNode.get('seqno')
725 if (seqno > highestSeqno) {
726 lastNode << [ip: "${member.host}", seqno: seqno]
727 }
728 } catch (Exception er) {
729 common.warningMsg("Could not determine 'seqno' value for node ${member.host} ")
730 }
731 }
732 }
733 if (lastNode.get('ip') != '') {
734 return "S@${lastNode.ip}"
735 } else {
736 return "I@galera:master"
737 }
738}
739
Martin Polreich65864b02018-12-05 10:42:50 +0100740/**
Jiri Broulikf8f96942018-02-15 10:03:42 +0100741 * Restores Galera database
742 * @param env Salt Connection object or pepperEnv
743 * @return output of salt commands
744 */
Martin Polreichdb692582019-04-25 14:32:36 +0200745def restoreGaleraDb(env) {
Jiri Broulikf8f96942018-02-15 10:03:42 +0100746 def salt = new com.mirantis.mk.Salt()
747 def common = new com.mirantis.mk.Common()
748 try {
749 salt.runSaltProcessStep(env, 'I@galera:slave', 'service.stop', ['mysql'])
750 } catch (Exception er) {
751 common.warningMsg('Mysql service already stopped')
752 }
753 try {
754 salt.runSaltProcessStep(env, 'I@galera:master', 'service.stop', ['mysql'])
755 } catch (Exception er) {
756 common.warningMsg('Mysql service already stopped')
757 }
Martin Polreich47c09d12018-12-21 16:42:06 +0100758 lastNodeTarget = getGaleraLastShutdownNode(env)
Jiri Broulikf8f96942018-02-15 10:03:42 +0100759 try {
760 salt.cmdRun(env, 'I@galera:slave', "rm /var/lib/mysql/ib_logfile*")
761 } catch (Exception er) {
762 common.warningMsg('Files are not present')
763 }
764 try {
Martin Polreich47c09d12018-12-21 16:42:06 +0100765 salt.cmdRun(env, 'I@galera:slave', "rm /var/lib/mysql/grastate.dat")
766 } catch (Exception er) {
767 common.warningMsg('Files are not present')
768 }
769 try {
770 salt.cmdRun(env, lastNodeTarget, "mkdir /root/mysql/mysql.bak")
Jiri Broulikf8f96942018-02-15 10:03:42 +0100771 } catch (Exception er) {
772 common.warningMsg('Directory already exists')
773 }
774 try {
Martin Polreich47c09d12018-12-21 16:42:06 +0100775 salt.cmdRun(env, lastNodeTarget, "rm -rf /root/mysql/mysql.bak/*")
Jiri Broulikf8f96942018-02-15 10:03:42 +0100776 } catch (Exception er) {
777 common.warningMsg('Directory already empty')
778 }
779 try {
Martin Polreich47c09d12018-12-21 16:42:06 +0100780 salt.cmdRun(env, lastNodeTarget, "mv /var/lib/mysql/* /root/mysql/mysql.bak")
Jiri Broulikf8f96942018-02-15 10:03:42 +0100781 } catch (Exception er) {
782 common.warningMsg('Files were already moved')
783 }
784 try {
Martin Polreich47c09d12018-12-21 16:42:06 +0100785 salt.runSaltProcessStep(env, lastNodeTarget, 'file.remove', ["/var/lib/mysql/.galera_bootstrap"])
Jiri Broulikf8f96942018-02-15 10:03:42 +0100786 } catch (Exception er) {
787 common.warningMsg('File is not present')
788 }
Martin Polreich47c09d12018-12-21 16:42:06 +0100789 salt.cmdRun(env, lastNodeTarget, "sed -i '/gcomm/c\\wsrep_cluster_address=\"gcomm://\"' /etc/mysql/my.cnf")
790 def backup_dir = salt.getReturnValues(salt.getPillar(env, lastNodeTarget, 'xtrabackup:client:backup_dir'))
Jiri Broulikf8f96942018-02-15 10:03:42 +0100791 if(backup_dir == null || backup_dir.isEmpty()) { backup_dir='/var/backups/mysql/xtrabackup' }
Martin Polreich47c09d12018-12-21 16:42:06 +0100792 salt.runSaltProcessStep(env, lastNodeTarget, 'file.remove', ["${backup_dir}/dbrestored"])
Jiri Broulikf8f96942018-02-15 10:03:42 +0100793 salt.cmdRun(env, 'I@xtrabackup:client', "su root -c 'salt-call state.sls xtrabackup'")
Martin Polreich47c09d12018-12-21 16:42:06 +0100794 salt.runSaltProcessStep(env, lastNodeTarget, 'service.start', ['mysql'])
Jiri Broulikf8f96942018-02-15 10:03:42 +0100795
796 // wait until mysql service on galera master is up
Jiri Broulik22b04572018-02-16 12:02:41 +0100797 try {
Martin Polreich47c09d12018-12-21 16:42:06 +0100798 salt.commandStatus(env, lastNodeTarget, 'service mysql status', 'running')
Jiri Broulik22b04572018-02-16 12:02:41 +0100799 } catch (Exception er) {
800 input message: "Database is not running please fix it first and only then click on PROCEED."
801 }
Jiri Broulikf8f96942018-02-15 10:03:42 +0100802
Martin Polreich47c09d12018-12-21 16:42:06 +0100803 salt.runSaltProcessStep(env, "I@galera:master and not ${lastNodeTarget}", 'service.start', ['mysql'])
804 salt.runSaltProcessStep(env, "I@galera:slave and not ${lastNodeTarget}", 'service.start', ['mysql'])
Martin Polreich65864b02018-12-05 10:42:50 +0100805}