blob: 29288f9971909c36e1d8c134be22ca3c8d3d1af5 [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 }
Jakub Josefa2491ad2018-01-15 16:26:27 +010056 runVirtualenvCommand(path, "pip install -r ${reqs_path}", true)
Sergey Kolekonovba203982016-12-21 18:32:17 +040057}
58
59/**
60 * Run command in specific python virtualenv
61 *
62 * @param path Path to virtualenv
63 * @param cmd Command to be executed
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010064 * @param silent dont print any messages (optional, default false)
azvyagintsevb8b7f922019-06-13 13:39:04 +030065 * @param flexAnswer return answer like a dict, with format ['status' : int, 'stderr' : str, 'stdout' : str ]
Sergey Kolekonovba203982016-12-21 18:32:17 +040066 */
azvyagintsevb8b7f922019-06-13 13:39:04 +030067def runVirtualenvCommand(path, cmd, silent = false, flexAnswer = false) {
Tomáš Kukrál69c25452017-07-27 14:59:40 +020068 def common = new com.mirantis.mk.Common()
azvyagintsevb8b7f922019-06-13 13:39:04 +030069 def res
70 def virtualenv_cmd = "set +x; . ${path}/bin/activate; ${cmd}"
71 if (!silent) {
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010072 common.infoMsg("[Python ${path}] Run command ${cmd}")
73 }
azvyagintsevb8b7f922019-06-13 13:39:04 +030074 if (flexAnswer) {
75 res = common.shCmdStatus(virtualenv_cmd)
76 } else {
77 res = sh(
78 returnStdout: true,
79 script: virtualenv_cmd
80 ).trim()
81 }
82 return res
Sergey Kolekonovba203982016-12-21 18:32:17 +040083}
84
Ales Komarekd874d482016-12-26 10:33:29 +010085
86/**
87 * Install docutils in isolated environment
88 *
89 * @param path Path where virtualenv is created
90 */
Vasyl Saienkoa56c1ab2020-01-13 10:00:04 +020091def setupDocutilsVirtualenv(path, python="python2") {
Ales Komarekd874d482016-12-26 10:33:29 +010092 requirements = [
Denis Egorenko71754dc2020-01-13 14:22:49 +040093 'docutils==0.16',
Ales Komarekd874d482016-12-26 10:33:29 +010094 ]
Vasyl Saienkoa56c1ab2020-01-13 10:00:04 +020095 setupVirtualenv(path, python, requirements)
Ales Komarekd874d482016-12-26 10:33:29 +010096}
97
98
Sergey Kolekonovba203982016-12-21 18:32:17 +040099@NonCPS
100def loadJson(rawData) {
101 return new groovy.json.JsonSlurperClassic().parseText(rawData)
102}
103
104/**
105 * Parse content from markup-text tables to variables
106 *
107 * @param tableStr String representing the table
108 * @param mode Either list (1st row are keys) or item (key, value rows)
109 * @param format Format of the table
110 */
Ales Komarekd874d482016-12-26 10:33:29 +0100111def parseTextTable(tableStr, type = 'item', format = 'rest', path = none) {
Ales Komarek0e558ee2016-12-23 13:02:55 +0100112 parserFile = "${env.WORKSPACE}/textTableParser.py"
113 parserScript = """import json
114import argparse
115from docutils.parsers.rst import tableparser
116from docutils import statemachine
117
118def parse_item_table(raw_data):
119 i = 1
120 pretty_raw_data = []
121 for datum in raw_data:
122 if datum != "":
123 if datum[3] != ' ' and i > 4:
124 pretty_raw_data.append(raw_data[0])
125 if i == 3:
126 pretty_raw_data.append(datum.replace('-', '='))
127 else:
128 pretty_raw_data.append(datum)
129 i += 1
130 parser = tableparser.GridTableParser()
131 block = statemachine.StringList(pretty_raw_data)
132 docutils_data = parser.parse(block)
133 final_data = {}
134 for line in docutils_data[2]:
135 key = ' '.join(line[0][3]).strip()
136 value = ' '.join(line[1][3]).strip()
137 if key != "":
138 try:
139 value = json.loads(value)
140 except:
141 pass
142 final_data[key] = value
143 i+=1
144 return final_data
145
146def parse_list_table(raw_data):
147 i = 1
148 pretty_raw_data = []
149 for datum in raw_data:
150 if datum != "":
151 if datum[3] != ' ' and i > 4:
152 pretty_raw_data.append(raw_data[0])
153 if i == 3:
154 pretty_raw_data.append(datum.replace('-', '='))
155 else:
156 pretty_raw_data.append(datum)
157 i += 1
158 parser = tableparser.GridTableParser()
159 block = statemachine.StringList(pretty_raw_data)
160 docutils_data = parser.parse(block)
161 final_data = []
162 keys = []
163 for line in docutils_data[1]:
164 for item in line:
165 keys.append(' '.join(item[3]).strip())
166 for line in docutils_data[2]:
167 final_line = {}
168 key = ' '.join(line[0][3]).strip()
169 value = ' '.join(line[1][3]).strip()
170 if key != "":
171 try:
172 value = json.loads(value)
173 except:
174 pass
175 final_data[key] = value
176 i+=1
177 return final_data
178
179def parse_list_table(raw_data):
180 i = 1
181 pretty_raw_data = []
182 for datum in raw_data:
183 if datum != "":
184 if datum[3] != ' ' and i > 4:
185 pretty_raw_data.append(raw_data[0])
186 if i == 3:
187 pretty_raw_data.append(datum.replace('-', '='))
188 else:
189 pretty_raw_data.append(datum)
190 i += 1
191 parser = tableparser.GridTableParser()
192 block = statemachine.StringList(pretty_raw_data)
193 docutils_data = parser.parse(block)
194 final_data = []
195 keys = []
196 for line in docutils_data[1]:
197 for item in line:
198 keys.append(' '.join(item[3]).strip())
199 for line in docutils_data[2]:
200 final_line = {}
201 i = 0
202 for item in line:
203 value = ' '.join(item[3]).strip()
204 try:
205 value = json.loads(value)
206 except:
207 pass
208 final_line[keys[i]] = value
209 i += 1
210 final_data.append(final_line)
211 return final_data
212
213def read_table_file(file):
214 table_file = open(file, 'r')
Ales Komarekc000c152016-12-23 15:32:54 +0100215 raw_data = table_file.read().split('\\n')
Ales Komarek0e558ee2016-12-23 13:02:55 +0100216 table_file.close()
217 return raw_data
218
219parser = argparse.ArgumentParser()
220parser.add_argument('-f','--file', help='File with table data', required=True)
221parser.add_argument('-t','--type', help='Type of table (list/item)', required=True)
222args = vars(parser.parse_args())
223
224raw_data = read_table_file(args['file'])
225
226if args['type'] == 'list':
227 final_data = parse_list_table(raw_data)
228else:
229 final_data = parse_item_table(raw_data)
230
231print json.dumps(final_data)
232"""
233 writeFile file: parserFile, text: parserScript
Sergey Kolekonovba203982016-12-21 18:32:17 +0400234 tableFile = "${env.WORKSPACE}/prettytable.txt"
235 writeFile file: tableFile, text: tableStr
Ales Komarekd874d482016-12-26 10:33:29 +0100236
237 cmd = "python ${parserFile} --file '${tableFile}' --type ${type}"
238 if (path) {
Ales Komarekc6d28dd2016-12-28 12:59:38 +0100239 rawData = runVirtualenvCommand(path, cmd)
Ales Komarekd874d482016-12-26 10:33:29 +0100240 }
241 else {
242 rawData = sh (
243 script: cmd,
244 returnStdout: true
245 ).trim()
246 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400247 data = loadJson(rawData)
248 echo("[Parsed table] ${data}")
249 return data
250}
251
252/**
253 * Install cookiecutter in isolated environment
254 *
255 * @param path Path where virtualenv is created
256 */
257def setupCookiecutterVirtualenv(path) {
258 requirements = [
259 'cookiecutter',
Jakub Josef4df78272017-04-26 14:36:36 +0200260 'jinja2==2.8.1',
Dmitry Pyzhovb883a2d2018-12-14 16:42:52 +0300261 'PyYAML==3.12',
262 'python-gnupg==0.4.3'
Sergey Kolekonovba203982016-12-21 18:32:17 +0400263 ]
264 setupVirtualenv(path, 'python2', requirements)
265}
266
267/**
268 * Generate the cookiecutter templates with given context
269 *
Jakub Josef4e10c372017-04-26 14:13:50 +0200270 * @param template template
271 * @param context template context
272 * @param path Path where virtualenv is created (optional)
273 * @param templatePath path to cookiecutter template repo (optional)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400274 */
Jakub Josef4e10c372017-04-26 14:13:50 +0200275def buildCookiecutterTemplate(template, context, outputDir = '.', path = null, templatePath = ".") {
Tomáš Kukráldad7b462017-03-27 13:53:05 +0200276 configFile = "default_config.yaml"
277 configString = "default_context:\n"
Tomáš Kukrál6de85042017-04-12 17:49:05 +0200278 writeFile file: configFile, text: context
Jakub Josef4e61cc02017-04-26 14:29:09 +0200279 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 +0400280 output = sh (returnStdout: true, script: command)
281 echo("[Cookiecutter build] Output: ${output}")
282}
283
284/**
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400285 *
286 * @param context - context template
287 * @param contextName - context template name
288 * @param saltMasterName - hostname of Salt Master node
289 * @param virtualenv - pyvenv with CC and dep's
290 * @param templateEnvDir - root of CookieCutter
291 * @return
292 */
293def generateModel(context, contextName, saltMasterName, virtualenv, modelEnv, templateEnvDir, multiModels = true) {
Denis Egorenkofa2c6752018-10-18 15:51:45 +0400294 def common = new com.mirantis.mk.Common()
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400295 def generatedModel = multiModels ? "${modelEnv}/${contextName}" : modelEnv
296 def templateContext = readYaml text: context
297 def clusterDomain = templateContext.default_context.cluster_domain
298 def clusterName = templateContext.default_context.cluster_name
299 def outputDestination = "${generatedModel}/classes/cluster/${clusterName}"
300 def templateBaseDir = templateEnvDir
301 def templateDir = "${templateEnvDir}/dir"
302 def templateOutputDir = templateBaseDir
303 dir(templateEnvDir) {
304 common.infoMsg("Generating model from context ${contextName}")
305 def productList = ["infra", "cicd", "opencontrail", "kubernetes", "openstack", "oss", "stacklight", "ceph"]
306 for (product in productList) {
307 // get templateOutputDir and productDir
308 templateOutputDir = "${templateEnvDir}/output/${product}"
309 productDir = product
310 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
311 // Bw for 2018.8.1 and older releases
312 if (product.startsWith("stacklight") && (!fileExists(templateDir))) {
313 common.warningMsg("Old release detected! productDir => 'stacklight2' ")
314 productDir = "stacklight2"
315 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
316 }
Adam Tenglera2373132018-10-18 15:40:16 +0200317 // generate infra unless its explicitly disabled
318 if ((product == "infra" && templateContext.default_context.get("infra_enabled", "True").toBoolean())
319 || (templateContext.default_context.get(product + "_enabled", "False").toBoolean())) {
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400320
321 common.infoMsg("Generating product " + product + " from " + templateDir + " to " + templateOutputDir)
322
323 sh "rm -rf ${templateOutputDir} || true"
324 sh "mkdir -p ${templateOutputDir}"
325 sh "mkdir -p ${outputDestination}"
326
327 buildCookiecutterTemplate(templateDir, context, templateOutputDir, virtualenv, templateBaseDir)
328 sh "mv -v ${templateOutputDir}/${clusterName}/* ${outputDestination}"
329 } else {
330 common.warningMsg("Product " + product + " is disabled")
331 }
332 }
333
334 def localRepositories = templateContext.default_context.local_repositories
335 localRepositories = localRepositories ? localRepositories.toBoolean() : false
336 def offlineDeployment = templateContext.default_context.offline_deployment
337 offlineDeployment = offlineDeployment ? offlineDeployment.toBoolean() : false
338 if (localRepositories && !offlineDeployment) {
339 def mcpVersion = templateContext.default_context.mcp_version
340 def aptlyModelUrl = templateContext.default_context.local_model_url
341 def ssh = new com.mirantis.mk.Ssh()
342 dir(path: modelEnv) {
343 ssh.agentSh "git submodule add \"${aptlyModelUrl}\" \"classes/cluster/${clusterName}/cicd/aptly\""
344 if (!(mcpVersion in ["nightly", "testing", "stable"])) {
345 ssh.agentSh "cd \"classes/cluster/${clusterName}/cicd/aptly\";git fetch --tags;git checkout ${mcpVersion}"
346 }
347 }
348 }
349
350 def nodeFile = "${generatedModel}/nodes/${saltMasterName}.${clusterDomain}.yml"
351 def nodeString = """classes:
352- cluster.${clusterName}.infra.config
353parameters:
354 _param:
355 linux_system_codename: xenial
356 reclass_data_revision: master
357 linux:
358 system:
359 name: ${saltMasterName}
360 domain: ${clusterDomain}
361 """
362 sh "mkdir -p ${generatedModel}/nodes/"
363 writeFile(file: nodeFile, text: nodeString)
364 }
365}
366
367/**
Sergey Kolekonovba203982016-12-21 18:32:17 +0400368 * Install jinja rendering in isolated environment
369 *
370 * @param path Path where virtualenv is created
371 */
372def setupJinjaVirtualenv(path) {
373 requirements = [
Denis Egorenko71754dc2020-01-13 14:22:49 +0400374 'jinja2-cli==0.7.0',
375 'pyyaml==5.3',
Sergey Kolekonovba203982016-12-21 18:32:17 +0400376 ]
377 setupVirtualenv(path, 'python2', requirements)
378}
379
380/**
381 * Generate the Jinja templates with given context
382 *
383 * @param path Path where virtualenv is created
384 */
385def jinjaBuildTemplate (template, context, path = none) {
386 contextFile = "jinja_context.yml"
387 contextString = ""
388 for (parameter in context) {
389 contextString = "${contextString}${parameter.key}: ${parameter.value}\n"
390 }
391 writeFile file: contextFile, text: contextString
392 cmd = "jinja2 ${template} ${contextFile} --format=yaml"
393 data = sh (returnStdout: true, script: cmd)
394 echo(data)
395 return data
396}
Oleg Grigorovbec45582017-09-12 20:29:24 +0300397
398/**
399 * Install salt-pepper in isolated environment
400 *
401 * @param path Path where virtualenv is created
chnydaa0dbb252017-10-05 10:46:09 +0200402 * @param url SALT_MASTER_URL
403 * @param credentialsId Credentials to salt api
Oleg Grigorovbec45582017-09-12 20:29:24 +0300404 */
Jakub Josefc9b6d662018-02-21 16:21:03 +0100405def setupPepperVirtualenv(path, url, credentialsId) {
chnydaa0dbb252017-10-05 10:46:09 +0200406 def common = new com.mirantis.mk.Common()
407
408 // virtualenv setup
Mykyta Karpin81756c92018-03-02 13:03:26 +0200409 // pin pepper till https://mirantis.jira.com/browse/PROD-18188 is fixed
410 requirements = ['salt-pepper>=0.5.2,<0.5.4']
Jakub Josefc9b6d662018-02-21 16:21:03 +0100411 setupVirtualenv(path, 'python2', requirements, null, true, true)
chnydabcfff182017-11-29 10:24:36 +0100412
chnydaa0dbb252017-10-05 10:46:09 +0200413 // pepperrc creation
414 rcFile = "${path}/pepperrc"
415 creds = common.getPasswordCredentials(credentialsId)
416 rc = """\
417[main]
418SALTAPI_EAUTH=pam
419SALTAPI_URL=${url}
420SALTAPI_USER=${creds.username}
421SALTAPI_PASS=${creds.password.toString()}
422"""
423 writeFile file: rcFile, text: rc
424 return rcFile
Jakub Josefd067f612017-09-26 13:42:56 +0200425}
Oleh Hryhorov44569fb2017-10-26 17:04:55 +0300426
427/**
428 * Install devops in isolated environment
429 *
430 * @param path Path where virtualenv is created
431 * @param clean Define to true is the venv have to cleaned up before install a new one
432 */
433def setupDevOpsVenv(venv, clean=false) {
434 requirements = ['git+https://github.com/openstack/fuel-devops.git']
435 setupVirtualenv(venv, 'python2', requirements, null, false, clean)
436}