blob: 6a054bbfdd656e24406ed2abcb28349b2bda1647 [file] [log] [blame]
Sergey Kolekonovba203982016-12-21 18:32:17 +04001package com.mirantis.mk
2
3/**
4 *
5 * Python functions
6 *
7 */
8
9/**
10 * Install python virtualenv
11 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000012 * @param path Path to virtualenv
13 * @param python Version of Python (python/python3)
14 * @param reqs Environment requirements in list format
15 * @param reqs_path Environment requirements path in str format
Sergey Kolekonovba203982016-12-21 18:32:17 +040016 */
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000017def setupVirtualenv(path, python = 'python2', reqs = [], reqs_path = null, clean = false, useSystemPackages = false) {
Tomáš Kukrál69c25452017-07-27 14:59:40 +020018 def common = new com.mirantis.mk.Common()
19
Jakub Josef87a8a3c2018-01-26 12:11:11 +010020 def offlineDeployment = env.getEnvironment().containsKey("OFFLINE_DEPLOYMENT") && env["OFFLINE_DEPLOYMENT"].toBoolean()
Mykyta Karpin1e4bfc92017-11-01 14:38:25 +020021 def virtualenv_cmd = "virtualenv ${path} --python ${python}"
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000022 if (useSystemPackages) {
Jakub Josef996f4ef2017-10-24 13:20:43 +020023 virtualenv_cmd += " --system-site-packages"
24 }
Tomáš Kukrál69c25452017-07-27 14:59:40 +020025 if (clean) {
26 common.infoMsg("Cleaning venv directory " + path)
27 sh("rm -rf \"${path}\"")
28 }
29
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000030 if (offlineDeployment) {
31 virtualenv_cmd += " --no-download"
Jakub Josef87a8a3c2018-01-26 12:11:11 +010032 }
Tomáš Kukrál69c25452017-07-27 14:59:40 +020033 common.infoMsg("[Python ${path}] Setup ${python} environment")
Sergey Kolekonovba203982016-12-21 18:32:17 +040034 sh(returnStdout: true, script: virtualenv_cmd)
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000035 if (!offlineDeployment) {
36 try {
37 runVirtualenvCommand(path, "pip install -U setuptools pip")
38 } catch (Exception e) {
39 common.warningMsg("Setuptools and pip cannot be updated, you might be offline but OFFLINE_DEPLOYMENT global property not initialized!")
40 }
Yuriy Taraday67352e92017-10-12 10:54:23 +000041 }
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000042 if (reqs_path == null) {
Vladislav Naumov11103862017-07-19 17:02:39 +030043 def args = ""
44 for (req in reqs) {
45 args = args + "${req}\n"
46 }
47 writeFile file: "${path}/requirements.txt", text: args
48 reqs_path = "${path}/requirements.txt"
Sergey Kolekonovba203982016-12-21 18:32:17 +040049 }
Jakub Josefa2491ad2018-01-15 16:26:27 +010050 runVirtualenvCommand(path, "pip install -r ${reqs_path}", true)
Sergey Kolekonovba203982016-12-21 18:32:17 +040051}
52
53/**
54 * Run command in specific python virtualenv
55 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000056 * @param path Path to virtualenv
57 * @param cmd Command to be executed
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010058 * @param silent dont print any messages (optional, default false)
Sergey Kolekonovba203982016-12-21 18:32:17 +040059 */
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000060def runVirtualenvCommand(path, cmd, silent = false) {
Tomáš Kukrál69c25452017-07-27 14:59:40 +020061 def common = new com.mirantis.mk.Common()
62
Jakub Josef5feeee42018-01-08 15:50:36 +010063 virtualenv_cmd = "set +x; . ${path}/bin/activate; ${cmd}"
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000064 if (!silent) {
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010065 common.infoMsg("[Python ${path}] Run command ${cmd}")
66 }
Sergey Kolekonovba203982016-12-21 18:32:17 +040067 output = sh(
68 returnStdout: true,
69 script: virtualenv_cmd
70 ).trim()
71 return output
72}
73
Ales Komarekd874d482016-12-26 10:33:29 +010074/**
75 * Install docutils in isolated environment
76 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000077 * @param path Path where virtualenv is created
Ales Komarekd874d482016-12-26 10:33:29 +010078 */
79def setupDocutilsVirtualenv(path) {
80 requirements = [
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000081 'docutils',
Ales Komarekd874d482016-12-26 10:33:29 +010082 ]
83 setupVirtualenv(path, 'python2', requirements)
84}
85
86
Sergey Kolekonovba203982016-12-21 18:32:17 +040087@NonCPS
88def loadJson(rawData) {
89 return new groovy.json.JsonSlurperClassic().parseText(rawData)
90}
91
92/**
93 * Parse content from markup-text tables to variables
94 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +000095 * @param tableStr String representing the table
96 * @param mode Either list (1st row are keys) or item (key, value rows)
97 * @param format Format of the table
Sergey Kolekonovba203982016-12-21 18:32:17 +040098 */
Ales Komarekd874d482016-12-26 10:33:29 +010099def parseTextTable(tableStr, type = 'item', format = 'rest', path = none) {
Ales Komarek0e558ee2016-12-23 13:02:55 +0100100 parserFile = "${env.WORKSPACE}/textTableParser.py"
101 parserScript = """import json
102import argparse
103from docutils.parsers.rst import tableparser
104from docutils import statemachine
105
106def parse_item_table(raw_data):
107 i = 1
108 pretty_raw_data = []
109 for datum in raw_data:
110 if datum != "":
111 if datum[3] != ' ' and i > 4:
112 pretty_raw_data.append(raw_data[0])
113 if i == 3:
114 pretty_raw_data.append(datum.replace('-', '='))
115 else:
116 pretty_raw_data.append(datum)
117 i += 1
118 parser = tableparser.GridTableParser()
119 block = statemachine.StringList(pretty_raw_data)
120 docutils_data = parser.parse(block)
121 final_data = {}
122 for line in docutils_data[2]:
123 key = ' '.join(line[0][3]).strip()
124 value = ' '.join(line[1][3]).strip()
125 if key != "":
126 try:
127 value = json.loads(value)
128 except:
129 pass
130 final_data[key] = value
131 i+=1
132 return final_data
133
134def parse_list_table(raw_data):
135 i = 1
136 pretty_raw_data = []
137 for datum in raw_data:
138 if datum != "":
139 if datum[3] != ' ' and i > 4:
140 pretty_raw_data.append(raw_data[0])
141 if i == 3:
142 pretty_raw_data.append(datum.replace('-', '='))
143 else:
144 pretty_raw_data.append(datum)
145 i += 1
146 parser = tableparser.GridTableParser()
147 block = statemachine.StringList(pretty_raw_data)
148 docutils_data = parser.parse(block)
149 final_data = []
150 keys = []
151 for line in docutils_data[1]:
152 for item in line:
153 keys.append(' '.join(item[3]).strip())
154 for line in docutils_data[2]:
155 final_line = {}
156 key = ' '.join(line[0][3]).strip()
157 value = ' '.join(line[1][3]).strip()
158 if key != "":
159 try:
160 value = json.loads(value)
161 except:
162 pass
163 final_data[key] = value
164 i+=1
165 return final_data
166
167def parse_list_table(raw_data):
168 i = 1
169 pretty_raw_data = []
170 for datum in raw_data:
171 if datum != "":
172 if datum[3] != ' ' and i > 4:
173 pretty_raw_data.append(raw_data[0])
174 if i == 3:
175 pretty_raw_data.append(datum.replace('-', '='))
176 else:
177 pretty_raw_data.append(datum)
178 i += 1
179 parser = tableparser.GridTableParser()
180 block = statemachine.StringList(pretty_raw_data)
181 docutils_data = parser.parse(block)
182 final_data = []
183 keys = []
184 for line in docutils_data[1]:
185 for item in line:
186 keys.append(' '.join(item[3]).strip())
187 for line in docutils_data[2]:
188 final_line = {}
189 i = 0
190 for item in line:
191 value = ' '.join(item[3]).strip()
192 try:
193 value = json.loads(value)
194 except:
195 pass
196 final_line[keys[i]] = value
197 i += 1
198 final_data.append(final_line)
199 return final_data
200
201def read_table_file(file):
202 table_file = open(file, 'r')
Ales Komarekc000c152016-12-23 15:32:54 +0100203 raw_data = table_file.read().split('\\n')
Ales Komarek0e558ee2016-12-23 13:02:55 +0100204 table_file.close()
205 return raw_data
206
207parser = argparse.ArgumentParser()
208parser.add_argument('-f','--file', help='File with table data', required=True)
209parser.add_argument('-t','--type', help='Type of table (list/item)', required=True)
210args = vars(parser.parse_args())
211
212raw_data = read_table_file(args['file'])
213
214if args['type'] == 'list':
215 final_data = parse_list_table(raw_data)
216else:
217 final_data = parse_item_table(raw_data)
218
219print json.dumps(final_data)
220"""
221 writeFile file: parserFile, text: parserScript
Sergey Kolekonovba203982016-12-21 18:32:17 +0400222 tableFile = "${env.WORKSPACE}/prettytable.txt"
223 writeFile file: tableFile, text: tableStr
Ales Komarekd874d482016-12-26 10:33:29 +0100224
225 cmd = "python ${parserFile} --file '${tableFile}' --type ${type}"
226 if (path) {
Ales Komarekc6d28dd2016-12-28 12:59:38 +0100227 rawData = runVirtualenvCommand(path, cmd)
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000228 } else {
229 rawData = sh(
Ales Komarekd874d482016-12-26 10:33:29 +0100230 script: cmd,
231 returnStdout: true
232 ).trim()
233 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400234 data = loadJson(rawData)
235 echo("[Parsed table] ${data}")
236 return data
237}
238
239/**
240 * Install cookiecutter in isolated environment
241 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000242 * @param path Path where virtualenv is created
Sergey Kolekonovba203982016-12-21 18:32:17 +0400243 */
244def setupCookiecutterVirtualenv(path) {
245 requirements = [
246 'cookiecutter',
Jakub Josef4df78272017-04-26 14:36:36 +0200247 'jinja2==2.8.1',
Dmitry Pyzhovb883a2d2018-12-14 16:42:52 +0300248 'PyYAML==3.12',
249 'python-gnupg==0.4.3'
Sergey Kolekonovba203982016-12-21 18:32:17 +0400250 ]
251 setupVirtualenv(path, 'python2', requirements)
252}
253
254/**
255 * Generate the cookiecutter templates with given context
256 *
Jakub Josef4e10c372017-04-26 14:13:50 +0200257 * @param template template
258 * @param context template context
259 * @param path Path where virtualenv is created (optional)
260 * @param templatePath path to cookiecutter template repo (optional)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400261 */
Jakub Josef4e10c372017-04-26 14:13:50 +0200262def buildCookiecutterTemplate(template, context, outputDir = '.', path = null, templatePath = ".") {
azvyagintsev7a123aa2019-01-09 21:38:56 +0200263 def common = new com.mirantis.mk.Common()
Tomáš Kukráldad7b462017-03-27 13:53:05 +0200264 configFile = "default_config.yaml"
Tomáš Kukrál6de85042017-04-12 17:49:05 +0200265 writeFile file: configFile, text: context
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000266 common.warningMsg('Old Cookiecutter env detected!')
267 command = ". ${path}/bin/activate; if [ -f ${templatePath}/generate.py ]; then python ${templatePath}/generate.py --config-file ${configFile} --template ${template} --output-dir ${outputDir}; else cookiecutter --config-file ${configFile} --output-dir ${outputDir} --overwrite-if-exists --verbose --no-input ${template}; fi"
268 output = sh(returnStdout: true, script: command)
269 common.infoMsg('[Cookiecutter build] Result:' + output)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400270}
271
272/**
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400273 *
274 * @param context - context template
275 * @param contextName - context template name
276 * @param saltMasterName - hostname of Salt Master node
277 * @param virtualenv - pyvenv with CC and dep's
278 * @param templateEnvDir - root of CookieCutter
279 * @return
280 */
281def generateModel(context, contextName, saltMasterName, virtualenv, modelEnv, templateEnvDir, multiModels = true) {
Denis Egorenkofa2c6752018-10-18 15:51:45 +0400282 def common = new com.mirantis.mk.Common()
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400283 def generatedModel = multiModels ? "${modelEnv}/${contextName}" : modelEnv
284 def templateContext = readYaml text: context
285 def clusterDomain = templateContext.default_context.cluster_domain
286 def clusterName = templateContext.default_context.cluster_name
287 def outputDestination = "${generatedModel}/classes/cluster/${clusterName}"
288 def templateBaseDir = templateEnvDir
289 def templateDir = "${templateEnvDir}/dir"
290 def templateOutputDir = templateBaseDir
291 dir(templateEnvDir) {
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000292 if (fileExists(new File(templateEnvDir, 'tox.ini').toString())) {
293 tempContextFile = new File(templateEnvDir, 'tempContext.yaml').toString()
294 writeFile file: tempContextFile, text: context
295 common.warningMsg('Generating models using context:\n')
296 print(context)
297 withEnv(["CONFIG_FILE=$tempContextFile",
298 "OUTPUT_DIR=${generatedModel}/classes/cluster/",
299 ]) {
300 print('[Cookiecutter build] Result:\n' +
301 sh(returnStdout: true, script: 'tox -ve generate_auto'))
Aleksey Zvyagintsev4c745e52019-02-21 10:30:02 +0000302 }
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000303 // dropme after impelementation new format
304 sh "mkdir -p ${generatedModel}/nodes/"
305 def nodeFile = "${generatedModel}/nodes/${saltMasterName}.${clusterDomain}.yml"
306 def nodeString = """classes:
azvyagintsev451aed22019-02-20 14:22:13 +0200307- cluster.${clusterName}.infra.config
308parameters:
309 _param:
310 linux_system_codename: xenial
311 reclass_data_revision: master
312 linux:
313 system:
314 name: ${saltMasterName}
315 domain: ${clusterDomain}
316 """
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000317 writeFile(file: nodeFile, text: nodeString)
318 //
319 } else {
320 common.warningMsg("Old format: Generating model from context ${contextName}")
321 def productList = ["infra", "cicd", "kdt", "opencontrail", "kubernetes", "openstack", "oss", "stacklight", "ceph"]
322 for (product in productList) {
323 // get templateOutputDir and productDir
324 templateOutputDir = "${templateEnvDir}/output/${product}"
325 productDir = product
326 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
327 // Bw for 2018.8.1 and older releases
328 if (product.startsWith("stacklight") && (!fileExists(templateDir))) {
329 common.warningMsg("Old release detected! productDir => 'stacklight2' ")
330 productDir = "stacklight2"
331 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
332 }
333 // generate infra unless its explicitly disabled
334 if ((product == "infra" && templateContext.default_context.get("infra_enabled", "True").toBoolean())
335 || (templateContext.default_context.get(product + "_enabled", "False").toBoolean())) {
336
337 common.infoMsg("Generating product " + product + " from " + templateDir + " to " + templateOutputDir)
338
339 sh "rm -rf ${templateOutputDir} || true"
340 sh "mkdir -p ${templateOutputDir}"
341 sh "mkdir -p ${outputDestination}"
342
343 buildCookiecutterTemplate(templateDir, context, templateOutputDir, virtualenv, templateBaseDir)
344 sh "mv -v ${templateOutputDir}/${clusterName}/* ${outputDestination}"
345 } else {
346 common.warningMsg("Product " + product + " is disabled")
347 }
348 }
349
350 def localRepositories = templateContext.default_context.local_repositories
351 localRepositories = localRepositories ? localRepositories.toBoolean() : false
352 def offlineDeployment = templateContext.default_context.offline_deployment
353 offlineDeployment = offlineDeployment ? offlineDeployment.toBoolean() : false
354 if (localRepositories && !offlineDeployment) {
355 def mcpVersion = templateContext.default_context.mcp_version
356 def aptlyModelUrl = templateContext.default_context.local_model_url
357 def ssh = new com.mirantis.mk.Ssh()
358 dir(path: modelEnv) {
359 ssh.agentSh "git submodule add \"${aptlyModelUrl}\" \"classes/cluster/${clusterName}/cicd/aptly\""
360 if (!(mcpVersion in ["nightly", "testing", "stable"])) {
361 ssh.agentSh "cd \"classes/cluster/${clusterName}/cicd/aptly\";git fetch --tags;git checkout ${mcpVersion}"
362 }
363 }
364 }
365
366 def nodeFile = "${generatedModel}/nodes/${saltMasterName}.${clusterDomain}.yml"
367 def nodeString = """classes:
368- cluster.${clusterName}.infra.config
369parameters:
370 _param:
371 linux_system_codename: xenial
372 reclass_data_revision: master
373 linux:
374 system:
375 name: ${saltMasterName}
376 domain: ${clusterDomain}
377 """
378 sh "mkdir -p ${generatedModel}/nodes/"
379 writeFile(file: nodeFile, text: nodeString)
380 }
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400381 }
382}
383
384/**
Sergey Kolekonovba203982016-12-21 18:32:17 +0400385 * Install jinja rendering in isolated environment
386 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000387 * @param path Path where virtualenv is created
Sergey Kolekonovba203982016-12-21 18:32:17 +0400388 */
389def setupJinjaVirtualenv(path) {
390 requirements = [
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000391 'jinja2-cli',
392 'pyyaml',
Sergey Kolekonovba203982016-12-21 18:32:17 +0400393 ]
394 setupVirtualenv(path, 'python2', requirements)
395}
396
397/**
398 * Generate the Jinja templates with given context
399 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000400 * @param path Path where virtualenv is created
Sergey Kolekonovba203982016-12-21 18:32:17 +0400401 */
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000402def jinjaBuildTemplate(template, context, path = none) {
Sergey Kolekonovba203982016-12-21 18:32:17 +0400403 contextFile = "jinja_context.yml"
404 contextString = ""
405 for (parameter in context) {
406 contextString = "${contextString}${parameter.key}: ${parameter.value}\n"
407 }
408 writeFile file: contextFile, text: contextString
409 cmd = "jinja2 ${template} ${contextFile} --format=yaml"
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000410 data = sh(returnStdout: true, script: cmd)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400411 echo(data)
412 return data
413}
Oleg Grigorovbec45582017-09-12 20:29:24 +0300414
415/**
416 * Install salt-pepper in isolated environment
417 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000418 * @param path Path where virtualenv is created
419 * @param url SALT_MASTER_URL
420 * @param credentialsId Credentials to salt api
Oleg Grigorovbec45582017-09-12 20:29:24 +0300421 */
Jakub Josefc9b6d662018-02-21 16:21:03 +0100422def setupPepperVirtualenv(path, url, credentialsId) {
chnydaa0dbb252017-10-05 10:46:09 +0200423 def common = new com.mirantis.mk.Common()
424
425 // virtualenv setup
Mykyta Karpin81756c92018-03-02 13:03:26 +0200426 // pin pepper till https://mirantis.jira.com/browse/PROD-18188 is fixed
427 requirements = ['salt-pepper>=0.5.2,<0.5.4']
Jakub Josefc9b6d662018-02-21 16:21:03 +0100428 setupVirtualenv(path, 'python2', requirements, null, true, true)
chnydabcfff182017-11-29 10:24:36 +0100429
chnydaa0dbb252017-10-05 10:46:09 +0200430 // pepperrc creation
431 rcFile = "${path}/pepperrc"
432 creds = common.getPasswordCredentials(credentialsId)
433 rc = """\
434[main]
435SALTAPI_EAUTH=pam
436SALTAPI_URL=${url}
437SALTAPI_USER=${creds.username}
438SALTAPI_PASS=${creds.password.toString()}
439"""
440 writeFile file: rcFile, text: rc
441 return rcFile
Jakub Josefd067f612017-09-26 13:42:56 +0200442}
Oleh Hryhorov44569fb2017-10-26 17:04:55 +0300443
444/**
445 * Install devops in isolated environment
446 *
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000447 * @param path Path where virtualenv is created
448 * @param clean Define to true is the venv have to cleaned up before install a new one
Oleh Hryhorov44569fb2017-10-26 17:04:55 +0300449 */
Aleksey Zvyagintsevc4f66f62019-02-21 10:39:34 +0000450def setupDevOpsVenv(venv, clean = false) {
Oleh Hryhorov44569fb2017-10-26 17:04:55 +0300451 requirements = ['git+https://github.com/openstack/fuel-devops.git']
452 setupVirtualenv(venv, 'python2', requirements, null, false, clean)
453}