blob: 08da637c6ddfc8b951f2b8e3550f7791d6be9d05 [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 {
Jakub Josefa2491ad2018-01-15 16:26:27 +010037 runVirtualenvCommand(path, "pip install -U setuptools pip")
Jakub Josef5f97d532017-11-29 18:51:41 +010038 } catch(Exception e) {
Jakub Josefa2491ad2018-01-15 16:26:27 +010039 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 +010040 }
Yuriy Taraday67352e92017-10-12 10:54:23 +000041 }
Vladislav Naumov11103862017-07-19 17:02:39 +030042 if (reqs_path==null) {
43 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 *
56 * @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 */
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010060def 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}"
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010064 if(!silent){
65 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/**
76 * Install docutils in isolated environment
77 *
78 * @param path Path where virtualenv is created
79 */
80def setupDocutilsVirtualenv(path) {
81 requirements = [
82 'docutils',
83 ]
84 setupVirtualenv(path, 'python2', requirements)
85}
86
87
Sergey Kolekonovba203982016-12-21 18:32:17 +040088@NonCPS
89def loadJson(rawData) {
90 return new groovy.json.JsonSlurperClassic().parseText(rawData)
91}
92
93/**
94 * Parse content from markup-text tables to variables
95 *
96 * @param tableStr String representing the table
97 * @param mode Either list (1st row are keys) or item (key, value rows)
98 * @param format Format of the table
99 */
Ales Komarekd874d482016-12-26 10:33:29 +0100100def parseTextTable(tableStr, type = 'item', format = 'rest', path = none) {
Ales Komarek0e558ee2016-12-23 13:02:55 +0100101 parserFile = "${env.WORKSPACE}/textTableParser.py"
102 parserScript = """import json
103import argparse
104from docutils.parsers.rst import tableparser
105from docutils import statemachine
106
107def parse_item_table(raw_data):
108 i = 1
109 pretty_raw_data = []
110 for datum in raw_data:
111 if datum != "":
112 if datum[3] != ' ' and i > 4:
113 pretty_raw_data.append(raw_data[0])
114 if i == 3:
115 pretty_raw_data.append(datum.replace('-', '='))
116 else:
117 pretty_raw_data.append(datum)
118 i += 1
119 parser = tableparser.GridTableParser()
120 block = statemachine.StringList(pretty_raw_data)
121 docutils_data = parser.parse(block)
122 final_data = {}
123 for line in docutils_data[2]:
124 key = ' '.join(line[0][3]).strip()
125 value = ' '.join(line[1][3]).strip()
126 if key != "":
127 try:
128 value = json.loads(value)
129 except:
130 pass
131 final_data[key] = value
132 i+=1
133 return final_data
134
135def parse_list_table(raw_data):
136 i = 1
137 pretty_raw_data = []
138 for datum in raw_data:
139 if datum != "":
140 if datum[3] != ' ' and i > 4:
141 pretty_raw_data.append(raw_data[0])
142 if i == 3:
143 pretty_raw_data.append(datum.replace('-', '='))
144 else:
145 pretty_raw_data.append(datum)
146 i += 1
147 parser = tableparser.GridTableParser()
148 block = statemachine.StringList(pretty_raw_data)
149 docutils_data = parser.parse(block)
150 final_data = []
151 keys = []
152 for line in docutils_data[1]:
153 for item in line:
154 keys.append(' '.join(item[3]).strip())
155 for line in docutils_data[2]:
156 final_line = {}
157 key = ' '.join(line[0][3]).strip()
158 value = ' '.join(line[1][3]).strip()
159 if key != "":
160 try:
161 value = json.loads(value)
162 except:
163 pass
164 final_data[key] = value
165 i+=1
166 return final_data
167
168def parse_list_table(raw_data):
169 i = 1
170 pretty_raw_data = []
171 for datum in raw_data:
172 if datum != "":
173 if datum[3] != ' ' and i > 4:
174 pretty_raw_data.append(raw_data[0])
175 if i == 3:
176 pretty_raw_data.append(datum.replace('-', '='))
177 else:
178 pretty_raw_data.append(datum)
179 i += 1
180 parser = tableparser.GridTableParser()
181 block = statemachine.StringList(pretty_raw_data)
182 docutils_data = parser.parse(block)
183 final_data = []
184 keys = []
185 for line in docutils_data[1]:
186 for item in line:
187 keys.append(' '.join(item[3]).strip())
188 for line in docutils_data[2]:
189 final_line = {}
190 i = 0
191 for item in line:
192 value = ' '.join(item[3]).strip()
193 try:
194 value = json.loads(value)
195 except:
196 pass
197 final_line[keys[i]] = value
198 i += 1
199 final_data.append(final_line)
200 return final_data
201
202def read_table_file(file):
203 table_file = open(file, 'r')
Ales Komarekc000c152016-12-23 15:32:54 +0100204 raw_data = table_file.read().split('\\n')
Ales Komarek0e558ee2016-12-23 13:02:55 +0100205 table_file.close()
206 return raw_data
207
208parser = argparse.ArgumentParser()
209parser.add_argument('-f','--file', help='File with table data', required=True)
210parser.add_argument('-t','--type', help='Type of table (list/item)', required=True)
211args = vars(parser.parse_args())
212
213raw_data = read_table_file(args['file'])
214
215if args['type'] == 'list':
216 final_data = parse_list_table(raw_data)
217else:
218 final_data = parse_item_table(raw_data)
219
220print json.dumps(final_data)
221"""
222 writeFile file: parserFile, text: parserScript
Sergey Kolekonovba203982016-12-21 18:32:17 +0400223 tableFile = "${env.WORKSPACE}/prettytable.txt"
224 writeFile file: tableFile, text: tableStr
Ales Komarekd874d482016-12-26 10:33:29 +0100225
226 cmd = "python ${parserFile} --file '${tableFile}' --type ${type}"
227 if (path) {
Ales Komarekc6d28dd2016-12-28 12:59:38 +0100228 rawData = runVirtualenvCommand(path, cmd)
Ales Komarekd874d482016-12-26 10:33:29 +0100229 }
230 else {
231 rawData = sh (
232 script: cmd,
233 returnStdout: true
234 ).trim()
235 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400236 data = loadJson(rawData)
237 echo("[Parsed table] ${data}")
238 return data
239}
240
241/**
242 * Install cookiecutter in isolated environment
243 *
244 * @param path Path where virtualenv is created
245 */
246def setupCookiecutterVirtualenv(path) {
247 requirements = [
248 'cookiecutter',
Jakub Josef4df78272017-04-26 14:36:36 +0200249 'jinja2==2.8.1',
Dmitry Pyzhovb883a2d2018-12-14 16:42:52 +0300250 'PyYAML==3.12',
251 'python-gnupg==0.4.3'
Sergey Kolekonovba203982016-12-21 18:32:17 +0400252 ]
253 setupVirtualenv(path, 'python2', requirements)
254}
255
256/**
257 * Generate the cookiecutter templates with given context
258 *
Jakub Josef4e10c372017-04-26 14:13:50 +0200259 * @param template template
260 * @param context template context
261 * @param path Path where virtualenv is created (optional)
262 * @param templatePath path to cookiecutter template repo (optional)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400263 */
Jakub Josef4e10c372017-04-26 14:13:50 +0200264def buildCookiecutterTemplate(template, context, outputDir = '.', path = null, templatePath = ".") {
azvyagintsev7a123aa2019-01-09 21:38:56 +0200265 def common = new com.mirantis.mk.Common()
Tomáš Kukráldad7b462017-03-27 13:53:05 +0200266 configFile = "default_config.yaml"
Tomáš Kukrál6de85042017-04-12 17:49:05 +0200267 writeFile file: configFile, text: context
azvyagintsev95d43e02019-01-10 12:08:59 +0200268 if (fileExists(new File(templatePath, 'tox.ini').toString())) {
azvyagintsev7a123aa2019-01-09 21:38:56 +0200269 withEnv(["CONFIG_FILE=$configFile",
270 "OUTPUT_DIR=$outputDir",
271 "TEMPLATE=$template"
272 ]) {
273 output = sh(returnStdout: true, script: "tox -ve generate")
274 }
275 } else {
276 common.warningMsg('Old Cookiecutter env detected!')
277 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"
278 output = sh(returnStdout: true, script: command)
279 }
280 common.infoMsg("[Cookiecutter build] Result: ${output}")
Sergey Kolekonovba203982016-12-21 18:32:17 +0400281}
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}