blob: f00910759451b613058c16d7a2b064186eba4f5a [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)
Jakub Josef87a8a3c2018-01-26 12:11:11 +010035 if(!offlineDeployment){
Jakub Josef5f97d532017-11-29 18:51:41 +010036 try {
Denis Egorenkoca250832019-11-11 15:47:11 +040037 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 runVirtualenvCommand(path, "pip install -U setuptools ${pipPackage}")
Jakub Josef5f97d532017-11-29 18:51:41 +010043 } catch(Exception e) {
Jakub Josefa2491ad2018-01-15 16:26:27 +010044 common.warningMsg("Setuptools and pip cannot be updated, you might be offline but OFFLINE_DEPLOYMENT global property not initialized!")
Jakub Josef5f97d532017-11-29 18:51:41 +010045 }
Yuriy Taraday67352e92017-10-12 10:54:23 +000046 }
Vladislav Naumov11103862017-07-19 17:02:39 +030047 if (reqs_path==null) {
48 def args = ""
49 for (req in reqs) {
50 args = args + "${req}\n"
51 }
52 writeFile file: "${path}/requirements.txt", text: args
53 reqs_path = "${path}/requirements.txt"
Sergey Kolekonovba203982016-12-21 18:32:17 +040054 }
Jakub Josefa2491ad2018-01-15 16:26:27 +010055 runVirtualenvCommand(path, "pip install -r ${reqs_path}", true)
Sergey Kolekonovba203982016-12-21 18:32:17 +040056}
57
58/**
59 * Run command in specific python virtualenv
60 *
61 * @param path Path to virtualenv
62 * @param cmd Command to be executed
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010063 * @param silent dont print any messages (optional, default false)
azvyagintsevb8b7f922019-06-13 13:39:04 +030064 * @param flexAnswer return answer like a dict, with format ['status' : int, 'stderr' : str, 'stdout' : str ]
Sergey Kolekonovba203982016-12-21 18:32:17 +040065 */
azvyagintsevb8b7f922019-06-13 13:39:04 +030066def runVirtualenvCommand(path, cmd, silent = false, flexAnswer = false) {
Tomáš Kukrál69c25452017-07-27 14:59:40 +020067 def common = new com.mirantis.mk.Common()
azvyagintsevb8b7f922019-06-13 13:39:04 +030068 def res
69 def virtualenv_cmd = "set +x; . ${path}/bin/activate; ${cmd}"
70 if (!silent) {
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010071 common.infoMsg("[Python ${path}] Run command ${cmd}")
72 }
azvyagintsevb8b7f922019-06-13 13:39:04 +030073 if (flexAnswer) {
74 res = common.shCmdStatus(virtualenv_cmd)
75 } else {
76 res = sh(
77 returnStdout: true,
78 script: virtualenv_cmd
79 ).trim()
80 }
81 return res
Sergey Kolekonovba203982016-12-21 18:32:17 +040082}
83
Ales Komarekd874d482016-12-26 10:33:29 +010084
85/**
86 * Install docutils in isolated environment
87 *
88 * @param path Path where virtualenv is created
89 */
90def setupDocutilsVirtualenv(path) {
91 requirements = [
92 'docutils',
93 ]
94 setupVirtualenv(path, 'python2', requirements)
95}
96
97
Sergey Kolekonovba203982016-12-21 18:32:17 +040098@NonCPS
99def loadJson(rawData) {
100 return new groovy.json.JsonSlurperClassic().parseText(rawData)
101}
102
103/**
104 * Parse content from markup-text tables to variables
105 *
106 * @param tableStr String representing the table
107 * @param mode Either list (1st row are keys) or item (key, value rows)
108 * @param format Format of the table
109 */
Ales Komarekd874d482016-12-26 10:33:29 +0100110def parseTextTable(tableStr, type = 'item', format = 'rest', path = none) {
Ales Komarek0e558ee2016-12-23 13:02:55 +0100111 parserFile = "${env.WORKSPACE}/textTableParser.py"
112 parserScript = """import json
113import argparse
114from docutils.parsers.rst import tableparser
115from docutils import statemachine
116
117def parse_item_table(raw_data):
118 i = 1
119 pretty_raw_data = []
120 for datum in raw_data:
121 if datum != "":
122 if datum[3] != ' ' and i > 4:
123 pretty_raw_data.append(raw_data[0])
124 if i == 3:
125 pretty_raw_data.append(datum.replace('-', '='))
126 else:
127 pretty_raw_data.append(datum)
128 i += 1
129 parser = tableparser.GridTableParser()
130 block = statemachine.StringList(pretty_raw_data)
131 docutils_data = parser.parse(block)
132 final_data = {}
133 for line in docutils_data[2]:
134 key = ' '.join(line[0][3]).strip()
135 value = ' '.join(line[1][3]).strip()
136 if key != "":
137 try:
138 value = json.loads(value)
139 except:
140 pass
141 final_data[key] = value
142 i+=1
143 return final_data
144
145def parse_list_table(raw_data):
146 i = 1
147 pretty_raw_data = []
148 for datum in raw_data:
149 if datum != "":
150 if datum[3] != ' ' and i > 4:
151 pretty_raw_data.append(raw_data[0])
152 if i == 3:
153 pretty_raw_data.append(datum.replace('-', '='))
154 else:
155 pretty_raw_data.append(datum)
156 i += 1
157 parser = tableparser.GridTableParser()
158 block = statemachine.StringList(pretty_raw_data)
159 docutils_data = parser.parse(block)
160 final_data = []
161 keys = []
162 for line in docutils_data[1]:
163 for item in line:
164 keys.append(' '.join(item[3]).strip())
165 for line in docutils_data[2]:
166 final_line = {}
167 key = ' '.join(line[0][3]).strip()
168 value = ' '.join(line[1][3]).strip()
169 if key != "":
170 try:
171 value = json.loads(value)
172 except:
173 pass
174 final_data[key] = value
175 i+=1
176 return final_data
177
178def parse_list_table(raw_data):
179 i = 1
180 pretty_raw_data = []
181 for datum in raw_data:
182 if datum != "":
183 if datum[3] != ' ' and i > 4:
184 pretty_raw_data.append(raw_data[0])
185 if i == 3:
186 pretty_raw_data.append(datum.replace('-', '='))
187 else:
188 pretty_raw_data.append(datum)
189 i += 1
190 parser = tableparser.GridTableParser()
191 block = statemachine.StringList(pretty_raw_data)
192 docutils_data = parser.parse(block)
193 final_data = []
194 keys = []
195 for line in docutils_data[1]:
196 for item in line:
197 keys.append(' '.join(item[3]).strip())
198 for line in docutils_data[2]:
199 final_line = {}
200 i = 0
201 for item in line:
202 value = ' '.join(item[3]).strip()
203 try:
204 value = json.loads(value)
205 except:
206 pass
207 final_line[keys[i]] = value
208 i += 1
209 final_data.append(final_line)
210 return final_data
211
212def read_table_file(file):
213 table_file = open(file, 'r')
Ales Komarekc000c152016-12-23 15:32:54 +0100214 raw_data = table_file.read().split('\\n')
Ales Komarek0e558ee2016-12-23 13:02:55 +0100215 table_file.close()
216 return raw_data
217
218parser = argparse.ArgumentParser()
219parser.add_argument('-f','--file', help='File with table data', required=True)
220parser.add_argument('-t','--type', help='Type of table (list/item)', required=True)
221args = vars(parser.parse_args())
222
223raw_data = read_table_file(args['file'])
224
225if args['type'] == 'list':
226 final_data = parse_list_table(raw_data)
227else:
228 final_data = parse_item_table(raw_data)
229
230print json.dumps(final_data)
231"""
232 writeFile file: parserFile, text: parserScript
Sergey Kolekonovba203982016-12-21 18:32:17 +0400233 tableFile = "${env.WORKSPACE}/prettytable.txt"
234 writeFile file: tableFile, text: tableStr
Ales Komarekd874d482016-12-26 10:33:29 +0100235
236 cmd = "python ${parserFile} --file '${tableFile}' --type ${type}"
237 if (path) {
Ales Komarekc6d28dd2016-12-28 12:59:38 +0100238 rawData = runVirtualenvCommand(path, cmd)
Ales Komarekd874d482016-12-26 10:33:29 +0100239 }
240 else {
241 rawData = sh (
242 script: cmd,
243 returnStdout: true
244 ).trim()
245 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400246 data = loadJson(rawData)
247 echo("[Parsed table] ${data}")
248 return data
249}
250
251/**
252 * Install cookiecutter in isolated environment
253 *
254 * @param path Path where virtualenv is created
255 */
256def setupCookiecutterVirtualenv(path) {
257 requirements = [
258 'cookiecutter',
Jakub Josef4df78272017-04-26 14:36:36 +0200259 'jinja2==2.8.1',
Dmitry Pyzhovb883a2d2018-12-14 16:42:52 +0300260 'PyYAML==3.12',
261 'python-gnupg==0.4.3'
Sergey Kolekonovba203982016-12-21 18:32:17 +0400262 ]
263 setupVirtualenv(path, 'python2', requirements)
264}
265
266/**
267 * Generate the cookiecutter templates with given context
268 *
Jakub Josef4e10c372017-04-26 14:13:50 +0200269 * @param template template
270 * @param context template context
271 * @param path Path where virtualenv is created (optional)
272 * @param templatePath path to cookiecutter template repo (optional)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400273 */
Jakub Josef4e10c372017-04-26 14:13:50 +0200274def buildCookiecutterTemplate(template, context, outputDir = '.', path = null, templatePath = ".") {
Tomáš Kukráldad7b462017-03-27 13:53:05 +0200275 configFile = "default_config.yaml"
276 configString = "default_context:\n"
Tomáš Kukrál6de85042017-04-12 17:49:05 +0200277 writeFile file: configFile, text: context
Jakub Josef4e61cc02017-04-26 14:29:09 +0200278 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 +0400279 output = sh (returnStdout: true, script: command)
280 echo("[Cookiecutter build] Output: ${output}")
281}
282
283/**
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400284 *
285 * @param context - context template
286 * @param contextName - context template name
287 * @param saltMasterName - hostname of Salt Master node
288 * @param virtualenv - pyvenv with CC and dep's
289 * @param templateEnvDir - root of CookieCutter
290 * @return
291 */
292def generateModel(context, contextName, saltMasterName, virtualenv, modelEnv, templateEnvDir, multiModels = true) {
Denis Egorenkofa2c6752018-10-18 15:51:45 +0400293 def common = new com.mirantis.mk.Common()
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400294 def generatedModel = multiModels ? "${modelEnv}/${contextName}" : modelEnv
295 def templateContext = readYaml text: context
296 def clusterDomain = templateContext.default_context.cluster_domain
297 def clusterName = templateContext.default_context.cluster_name
298 def outputDestination = "${generatedModel}/classes/cluster/${clusterName}"
299 def templateBaseDir = templateEnvDir
300 def templateDir = "${templateEnvDir}/dir"
301 def templateOutputDir = templateBaseDir
302 dir(templateEnvDir) {
303 common.infoMsg("Generating model from context ${contextName}")
304 def productList = ["infra", "cicd", "opencontrail", "kubernetes", "openstack", "oss", "stacklight", "ceph"]
305 for (product in productList) {
306 // get templateOutputDir and productDir
307 templateOutputDir = "${templateEnvDir}/output/${product}"
308 productDir = product
309 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
310 // Bw for 2018.8.1 and older releases
311 if (product.startsWith("stacklight") && (!fileExists(templateDir))) {
312 common.warningMsg("Old release detected! productDir => 'stacklight2' ")
313 productDir = "stacklight2"
314 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
315 }
Adam Tenglera2373132018-10-18 15:40:16 +0200316 // generate infra unless its explicitly disabled
317 if ((product == "infra" && templateContext.default_context.get("infra_enabled", "True").toBoolean())
318 || (templateContext.default_context.get(product + "_enabled", "False").toBoolean())) {
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400319
320 common.infoMsg("Generating product " + product + " from " + templateDir + " to " + templateOutputDir)
321
322 sh "rm -rf ${templateOutputDir} || true"
323 sh "mkdir -p ${templateOutputDir}"
324 sh "mkdir -p ${outputDestination}"
325
326 buildCookiecutterTemplate(templateDir, context, templateOutputDir, virtualenv, templateBaseDir)
327 sh "mv -v ${templateOutputDir}/${clusterName}/* ${outputDestination}"
328 } else {
329 common.warningMsg("Product " + product + " is disabled")
330 }
331 }
332
333 def localRepositories = templateContext.default_context.local_repositories
334 localRepositories = localRepositories ? localRepositories.toBoolean() : false
335 def offlineDeployment = templateContext.default_context.offline_deployment
336 offlineDeployment = offlineDeployment ? offlineDeployment.toBoolean() : false
337 if (localRepositories && !offlineDeployment) {
338 def mcpVersion = templateContext.default_context.mcp_version
339 def aptlyModelUrl = templateContext.default_context.local_model_url
340 def ssh = new com.mirantis.mk.Ssh()
341 dir(path: modelEnv) {
342 ssh.agentSh "git submodule add \"${aptlyModelUrl}\" \"classes/cluster/${clusterName}/cicd/aptly\""
343 if (!(mcpVersion in ["nightly", "testing", "stable"])) {
344 ssh.agentSh "cd \"classes/cluster/${clusterName}/cicd/aptly\";git fetch --tags;git checkout ${mcpVersion}"
345 }
346 }
347 }
348
349 def nodeFile = "${generatedModel}/nodes/${saltMasterName}.${clusterDomain}.yml"
350 def nodeString = """classes:
351- cluster.${clusterName}.infra.config
352parameters:
353 _param:
354 linux_system_codename: xenial
355 reclass_data_revision: master
356 linux:
357 system:
358 name: ${saltMasterName}
359 domain: ${clusterDomain}
360 """
361 sh "mkdir -p ${generatedModel}/nodes/"
362 writeFile(file: nodeFile, text: nodeString)
363 }
364}
365
366/**
Sergey Kolekonovba203982016-12-21 18:32:17 +0400367 * Install jinja rendering in isolated environment
368 *
369 * @param path Path where virtualenv is created
370 */
371def setupJinjaVirtualenv(path) {
372 requirements = [
373 'jinja2-cli',
374 'pyyaml',
375 ]
376 setupVirtualenv(path, 'python2', requirements)
377}
378
379/**
380 * Generate the Jinja templates with given context
381 *
382 * @param path Path where virtualenv is created
383 */
384def jinjaBuildTemplate (template, context, path = none) {
385 contextFile = "jinja_context.yml"
386 contextString = ""
387 for (parameter in context) {
388 contextString = "${contextString}${parameter.key}: ${parameter.value}\n"
389 }
390 writeFile file: contextFile, text: contextString
391 cmd = "jinja2 ${template} ${contextFile} --format=yaml"
392 data = sh (returnStdout: true, script: cmd)
393 echo(data)
394 return data
395}
Oleg Grigorovbec45582017-09-12 20:29:24 +0300396
397/**
398 * Install salt-pepper in isolated environment
399 *
400 * @param path Path where virtualenv is created
chnydaa0dbb252017-10-05 10:46:09 +0200401 * @param url SALT_MASTER_URL
402 * @param credentialsId Credentials to salt api
Oleg Grigorovbec45582017-09-12 20:29:24 +0300403 */
Jakub Josefc9b6d662018-02-21 16:21:03 +0100404def setupPepperVirtualenv(path, url, credentialsId) {
chnydaa0dbb252017-10-05 10:46:09 +0200405 def common = new com.mirantis.mk.Common()
406
407 // virtualenv setup
Mykyta Karpin81756c92018-03-02 13:03:26 +0200408 // pin pepper till https://mirantis.jira.com/browse/PROD-18188 is fixed
409 requirements = ['salt-pepper>=0.5.2,<0.5.4']
Jakub Josefc9b6d662018-02-21 16:21:03 +0100410 setupVirtualenv(path, 'python2', requirements, null, true, true)
chnydabcfff182017-11-29 10:24:36 +0100411
chnydaa0dbb252017-10-05 10:46:09 +0200412 // pepperrc creation
413 rcFile = "${path}/pepperrc"
414 creds = common.getPasswordCredentials(credentialsId)
415 rc = """\
416[main]
417SALTAPI_EAUTH=pam
418SALTAPI_URL=${url}
419SALTAPI_USER=${creds.username}
420SALTAPI_PASS=${creds.password.toString()}
421"""
422 writeFile file: rcFile, text: rc
423 return rcFile
Jakub Josefd067f612017-09-26 13:42:56 +0200424}
Oleh Hryhorov44569fb2017-10-26 17:04:55 +0300425
426/**
427 * Install devops in isolated environment
428 *
429 * @param path Path where virtualenv is created
430 * @param clean Define to true is the venv have to cleaned up before install a new one
431 */
432def setupDevOpsVenv(venv, clean=false) {
433 requirements = ['git+https://github.com/openstack/fuel-devops.git']
434 setupVirtualenv(venv, 'python2', requirements, null, false, clean)
435}