blob: 9839f4a00587f9eb7338331b9265a8f55be95d8c [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 *
Vladislav Naumov11103862017-07-19 17:02:39 +030012 * @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 */
Jakub Josef996f4ef2017-10-24 13:20:43 +020017def 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}"
Jakub Josef996f4ef2017-10-24 13:20:43 +020022 if (useSystemPackages){
23 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
Jakub Josef87a8a3c2018-01-26 12:11:11 +010030 if(offlineDeployment){
31 virtualenv_cmd+=" --no-download"
32 }
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)
Vasyl Saienkoa56c1ab2020-01-13 10:00:04 +020035 if (!offlineDeployment) {
36 try {
37 def pipPackage = 'pip'
38 if (python == 'python2') {
39 pipPackage = "\"pip<=19.3.1\""
40 common.infoMsg("Pinning pip package due to end of life of Python2 to ${pipPackage} version.")
41 }
42 // NOTE(vsaienko): pin setuptools explicitly for latest version that works with python2
43 runVirtualenvCommand(path, "pip install -U \"setuptools<45.0.0\" ${pipPackage}")
44 } catch (Exception e) {
45 common.warningMsg("Setuptools and pip cannot be updated, you might be offline but OFFLINE_DEPLOYMENT global property not initialized!")
46 }
Yuriy Taraday67352e92017-10-12 10:54:23 +000047 }
Vladislav Naumov11103862017-07-19 17:02:39 +030048 if (reqs_path==null) {
49 def args = ""
50 for (req in reqs) {
51 args = args + "${req}\n"
52 }
53 writeFile file: "${path}/requirements.txt", text: args
54 reqs_path = "${path}/requirements.txt"
Sergey Kolekonovba203982016-12-21 18:32:17 +040055 }
Vladimir Khlyunev6fb7a502022-02-24 14:14:44 +040056
57 def install_cmd = 'pip install'
58 if (offlineDeployment) {
59 install_cmd += " --find-links=/opt/pip-mirror "
60 }
61 runVirtualenvCommand(path, "${install_cmd} -r ${reqs_path}", true)
Sergey Kolekonovba203982016-12-21 18:32:17 +040062}
63
64/**
65 * Run command in specific python virtualenv
66 *
67 * @param path Path to virtualenv
68 * @param cmd Command to be executed
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010069 * @param silent dont print any messages (optional, default false)
azvyagintsevb8b7f922019-06-13 13:39:04 +030070 * @param flexAnswer return answer like a dict, with format ['status' : int, 'stderr' : str, 'stdout' : str ]
Sergey Kolekonovba203982016-12-21 18:32:17 +040071 */
azvyagintsevb8b7f922019-06-13 13:39:04 +030072def runVirtualenvCommand(path, cmd, silent = false, flexAnswer = false) {
Tomáš Kukrál69c25452017-07-27 14:59:40 +020073 def common = new com.mirantis.mk.Common()
azvyagintsevb8b7f922019-06-13 13:39:04 +030074 def res
75 def virtualenv_cmd = "set +x; . ${path}/bin/activate; ${cmd}"
76 if (!silent) {
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010077 common.infoMsg("[Python ${path}] Run command ${cmd}")
78 }
azvyagintsevb8b7f922019-06-13 13:39:04 +030079 if (flexAnswer) {
80 res = common.shCmdStatus(virtualenv_cmd)
81 } else {
82 res = sh(
83 returnStdout: true,
84 script: virtualenv_cmd
85 ).trim()
86 }
87 return res
Sergey Kolekonovba203982016-12-21 18:32:17 +040088}
89
Ales Komarekd874d482016-12-26 10:33:29 +010090
91/**
92 * Install docutils in isolated environment
93 *
94 * @param path Path where virtualenv is created
95 */
Vasyl Saienkoa56c1ab2020-01-13 10:00:04 +020096def setupDocutilsVirtualenv(path, python="python2") {
Ales Komarekd874d482016-12-26 10:33:29 +010097 requirements = [
Denis Egorenko71754dc2020-01-13 14:22:49 +040098 'docutils==0.16',
Ales Komarekd874d482016-12-26 10:33:29 +010099 ]
Vasyl Saienkoa56c1ab2020-01-13 10:00:04 +0200100 setupVirtualenv(path, python, requirements)
Ales Komarekd874d482016-12-26 10:33:29 +0100101}
102
103
Sergey Kolekonovba203982016-12-21 18:32:17 +0400104@NonCPS
105def loadJson(rawData) {
106 return new groovy.json.JsonSlurperClassic().parseText(rawData)
107}
108
109/**
110 * Parse content from markup-text tables to variables
111 *
112 * @param tableStr String representing the table
113 * @param mode Either list (1st row are keys) or item (key, value rows)
114 * @param format Format of the table
115 */
Ales Komarekd874d482016-12-26 10:33:29 +0100116def parseTextTable(tableStr, type = 'item', format = 'rest', path = none) {
Ales Komarek0e558ee2016-12-23 13:02:55 +0100117 parserFile = "${env.WORKSPACE}/textTableParser.py"
118 parserScript = """import json
119import argparse
120from docutils.parsers.rst import tableparser
121from docutils import statemachine
122
123def parse_item_table(raw_data):
124 i = 1
125 pretty_raw_data = []
126 for datum in raw_data:
127 if datum != "":
128 if datum[3] != ' ' and i > 4:
129 pretty_raw_data.append(raw_data[0])
130 if i == 3:
131 pretty_raw_data.append(datum.replace('-', '='))
132 else:
133 pretty_raw_data.append(datum)
134 i += 1
135 parser = tableparser.GridTableParser()
136 block = statemachine.StringList(pretty_raw_data)
137 docutils_data = parser.parse(block)
138 final_data = {}
139 for line in docutils_data[2]:
140 key = ' '.join(line[0][3]).strip()
141 value = ' '.join(line[1][3]).strip()
142 if key != "":
143 try:
144 value = json.loads(value)
145 except:
146 pass
147 final_data[key] = value
148 i+=1
149 return final_data
150
151def parse_list_table(raw_data):
152 i = 1
153 pretty_raw_data = []
154 for datum in raw_data:
155 if datum != "":
156 if datum[3] != ' ' and i > 4:
157 pretty_raw_data.append(raw_data[0])
158 if i == 3:
159 pretty_raw_data.append(datum.replace('-', '='))
160 else:
161 pretty_raw_data.append(datum)
162 i += 1
163 parser = tableparser.GridTableParser()
164 block = statemachine.StringList(pretty_raw_data)
165 docutils_data = parser.parse(block)
166 final_data = []
167 keys = []
168 for line in docutils_data[1]:
169 for item in line:
170 keys.append(' '.join(item[3]).strip())
171 for line in docutils_data[2]:
172 final_line = {}
173 key = ' '.join(line[0][3]).strip()
174 value = ' '.join(line[1][3]).strip()
175 if key != "":
176 try:
177 value = json.loads(value)
178 except:
179 pass
180 final_data[key] = value
181 i+=1
182 return final_data
183
184def parse_list_table(raw_data):
185 i = 1
186 pretty_raw_data = []
187 for datum in raw_data:
188 if datum != "":
189 if datum[3] != ' ' and i > 4:
190 pretty_raw_data.append(raw_data[0])
191 if i == 3:
192 pretty_raw_data.append(datum.replace('-', '='))
193 else:
194 pretty_raw_data.append(datum)
195 i += 1
196 parser = tableparser.GridTableParser()
197 block = statemachine.StringList(pretty_raw_data)
198 docutils_data = parser.parse(block)
199 final_data = []
200 keys = []
201 for line in docutils_data[1]:
202 for item in line:
203 keys.append(' '.join(item[3]).strip())
204 for line in docutils_data[2]:
205 final_line = {}
206 i = 0
207 for item in line:
208 value = ' '.join(item[3]).strip()
209 try:
210 value = json.loads(value)
211 except:
212 pass
213 final_line[keys[i]] = value
214 i += 1
215 final_data.append(final_line)
216 return final_data
217
218def read_table_file(file):
219 table_file = open(file, 'r')
Ales Komarekc000c152016-12-23 15:32:54 +0100220 raw_data = table_file.read().split('\\n')
Ales Komarek0e558ee2016-12-23 13:02:55 +0100221 table_file.close()
222 return raw_data
223
224parser = argparse.ArgumentParser()
225parser.add_argument('-f','--file', help='File with table data', required=True)
226parser.add_argument('-t','--type', help='Type of table (list/item)', required=True)
227args = vars(parser.parse_args())
228
229raw_data = read_table_file(args['file'])
230
231if args['type'] == 'list':
232 final_data = parse_list_table(raw_data)
233else:
234 final_data = parse_item_table(raw_data)
235
236print json.dumps(final_data)
237"""
238 writeFile file: parserFile, text: parserScript
Sergey Kolekonovba203982016-12-21 18:32:17 +0400239 tableFile = "${env.WORKSPACE}/prettytable.txt"
240 writeFile file: tableFile, text: tableStr
Ales Komarekd874d482016-12-26 10:33:29 +0100241
242 cmd = "python ${parserFile} --file '${tableFile}' --type ${type}"
243 if (path) {
Ales Komarekc6d28dd2016-12-28 12:59:38 +0100244 rawData = runVirtualenvCommand(path, cmd)
Ales Komarekd874d482016-12-26 10:33:29 +0100245 }
246 else {
247 rawData = sh (
248 script: cmd,
249 returnStdout: true
250 ).trim()
251 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400252 data = loadJson(rawData)
253 echo("[Parsed table] ${data}")
254 return data
255}
256
257/**
258 * Install cookiecutter in isolated environment
259 *
260 * @param path Path where virtualenv is created
261 */
262def setupCookiecutterVirtualenv(path) {
263 requirements = [
264 'cookiecutter',
Jakub Josef4df78272017-04-26 14:36:36 +0200265 'jinja2==2.8.1',
Dmitry Pyzhovb883a2d2018-12-14 16:42:52 +0300266 'PyYAML==3.12',
267 'python-gnupg==0.4.3'
Sergey Kolekonovba203982016-12-21 18:32:17 +0400268 ]
269 setupVirtualenv(path, 'python2', requirements)
270}
271
272/**
273 * Generate the cookiecutter templates with given context
274 *
Jakub Josef4e10c372017-04-26 14:13:50 +0200275 * @param template template
276 * @param context template context
277 * @param path Path where virtualenv is created (optional)
278 * @param templatePath path to cookiecutter template repo (optional)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400279 */
Jakub Josef4e10c372017-04-26 14:13:50 +0200280def buildCookiecutterTemplate(template, context, outputDir = '.', path = null, templatePath = ".") {
Tomáš Kukráldad7b462017-03-27 13:53:05 +0200281 configFile = "default_config.yaml"
282 configString = "default_context:\n"
Tomáš Kukrál6de85042017-04-12 17:49:05 +0200283 writeFile file: configFile, text: context
Jakub Josef4e61cc02017-04-26 14:29:09 +0200284 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"
Sergey Kolekonovba203982016-12-21 18:32:17 +0400285 output = sh (returnStdout: true, script: command)
286 echo("[Cookiecutter build] Output: ${output}")
287}
288
289/**
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400290 *
291 * @param context - context template
292 * @param contextName - context template name
293 * @param saltMasterName - hostname of Salt Master node
294 * @param virtualenv - pyvenv with CC and dep's
295 * @param templateEnvDir - root of CookieCutter
296 * @return
297 */
298def generateModel(context, contextName, saltMasterName, virtualenv, modelEnv, templateEnvDir, multiModels = true) {
Denis Egorenkofa2c6752018-10-18 15:51:45 +0400299 def common = new com.mirantis.mk.Common()
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400300 def generatedModel = multiModels ? "${modelEnv}/${contextName}" : modelEnv
301 def templateContext = readYaml text: context
302 def clusterDomain = templateContext.default_context.cluster_domain
303 def clusterName = templateContext.default_context.cluster_name
304 def outputDestination = "${generatedModel}/classes/cluster/${clusterName}"
305 def templateBaseDir = templateEnvDir
306 def templateDir = "${templateEnvDir}/dir"
307 def templateOutputDir = templateBaseDir
308 dir(templateEnvDir) {
309 common.infoMsg("Generating model from context ${contextName}")
310 def productList = ["infra", "cicd", "opencontrail", "kubernetes", "openstack", "oss", "stacklight", "ceph"]
311 for (product in productList) {
312 // get templateOutputDir and productDir
313 templateOutputDir = "${templateEnvDir}/output/${product}"
314 productDir = product
315 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
316 // Bw for 2018.8.1 and older releases
317 if (product.startsWith("stacklight") && (!fileExists(templateDir))) {
318 common.warningMsg("Old release detected! productDir => 'stacklight2' ")
319 productDir = "stacklight2"
320 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
321 }
Adam Tenglera2373132018-10-18 15:40:16 +0200322 // generate infra unless its explicitly disabled
323 if ((product == "infra" && templateContext.default_context.get("infra_enabled", "True").toBoolean())
324 || (templateContext.default_context.get(product + "_enabled", "False").toBoolean())) {
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400325
326 common.infoMsg("Generating product " + product + " from " + templateDir + " to " + templateOutputDir)
327
328 sh "rm -rf ${templateOutputDir} || true"
329 sh "mkdir -p ${templateOutputDir}"
330 sh "mkdir -p ${outputDestination}"
331
332 buildCookiecutterTemplate(templateDir, context, templateOutputDir, virtualenv, templateBaseDir)
333 sh "mv -v ${templateOutputDir}/${clusterName}/* ${outputDestination}"
334 } else {
335 common.warningMsg("Product " + product + " is disabled")
336 }
337 }
338
339 def localRepositories = templateContext.default_context.local_repositories
340 localRepositories = localRepositories ? localRepositories.toBoolean() : false
341 def offlineDeployment = templateContext.default_context.offline_deployment
342 offlineDeployment = offlineDeployment ? offlineDeployment.toBoolean() : false
343 if (localRepositories && !offlineDeployment) {
344 def mcpVersion = templateContext.default_context.mcp_version
345 def aptlyModelUrl = templateContext.default_context.local_model_url
346 def ssh = new com.mirantis.mk.Ssh()
347 dir(path: modelEnv) {
348 ssh.agentSh "git submodule add \"${aptlyModelUrl}\" \"classes/cluster/${clusterName}/cicd/aptly\""
349 if (!(mcpVersion in ["nightly", "testing", "stable"])) {
350 ssh.agentSh "cd \"classes/cluster/${clusterName}/cicd/aptly\";git fetch --tags;git checkout ${mcpVersion}"
351 }
352 }
353 }
354
355 def nodeFile = "${generatedModel}/nodes/${saltMasterName}.${clusterDomain}.yml"
356 def nodeString = """classes:
357- cluster.${clusterName}.infra.config
358parameters:
359 _param:
360 linux_system_codename: xenial
361 reclass_data_revision: master
362 linux:
363 system:
364 name: ${saltMasterName}
365 domain: ${clusterDomain}
366 """
367 sh "mkdir -p ${generatedModel}/nodes/"
368 writeFile(file: nodeFile, text: nodeString)
369 }
370}
371
372/**
Sergey Kolekonovba203982016-12-21 18:32:17 +0400373 * Install jinja rendering in isolated environment
374 *
375 * @param path Path where virtualenv is created
376 */
377def setupJinjaVirtualenv(path) {
378 requirements = [
Denis Egorenko71754dc2020-01-13 14:22:49 +0400379 'jinja2-cli==0.7.0',
380 'pyyaml==5.3',
Sergey Kolekonovba203982016-12-21 18:32:17 +0400381 ]
382 setupVirtualenv(path, 'python2', requirements)
383}
384
385/**
386 * Generate the Jinja templates with given context
387 *
388 * @param path Path where virtualenv is created
389 */
390def jinjaBuildTemplate (template, context, path = none) {
391 contextFile = "jinja_context.yml"
392 contextString = ""
393 for (parameter in context) {
394 contextString = "${contextString}${parameter.key}: ${parameter.value}\n"
395 }
396 writeFile file: contextFile, text: contextString
397 cmd = "jinja2 ${template} ${contextFile} --format=yaml"
398 data = sh (returnStdout: true, script: cmd)
399 echo(data)
400 return data
401}
Oleg Grigorovbec45582017-09-12 20:29:24 +0300402
403/**
404 * Install salt-pepper in isolated environment
405 *
406 * @param path Path where virtualenv is created
chnydaa0dbb252017-10-05 10:46:09 +0200407 * @param url SALT_MASTER_URL
408 * @param credentialsId Credentials to salt api
Oleg Grigorovbec45582017-09-12 20:29:24 +0300409 */
Jakub Josefc9b6d662018-02-21 16:21:03 +0100410def setupPepperVirtualenv(path, url, credentialsId) {
chnydaa0dbb252017-10-05 10:46:09 +0200411 def common = new com.mirantis.mk.Common()
412
413 // virtualenv setup
Mykyta Karpin81756c92018-03-02 13:03:26 +0200414 // pin pepper till https://mirantis.jira.com/browse/PROD-18188 is fixed
415 requirements = ['salt-pepper>=0.5.2,<0.5.4']
Jakub Josefc9b6d662018-02-21 16:21:03 +0100416 setupVirtualenv(path, 'python2', requirements, null, true, true)
chnydabcfff182017-11-29 10:24:36 +0100417
chnydaa0dbb252017-10-05 10:46:09 +0200418 // pepperrc creation
419 rcFile = "${path}/pepperrc"
420 creds = common.getPasswordCredentials(credentialsId)
421 rc = """\
422[main]
423SALTAPI_EAUTH=pam
424SALTAPI_URL=${url}
425SALTAPI_USER=${creds.username}
426SALTAPI_PASS=${creds.password.toString()}
427"""
428 writeFile file: rcFile, text: rc
429 return rcFile
Jakub Josefd067f612017-09-26 13:42:56 +0200430}
Oleh Hryhorov44569fb2017-10-26 17:04:55 +0300431
432/**
433 * Install devops in isolated environment
434 *
435 * @param path Path where virtualenv is created
436 * @param clean Define to true is the venv have to cleaned up before install a new one
437 */
438def setupDevOpsVenv(venv, clean=false) {
439 requirements = ['git+https://github.com/openstack/fuel-devops.git']
440 setupVirtualenv(venv, 'python2', requirements, null, false, clean)
441}