blob: 82e9ee350deff4ef1e8a023fccca91a68402a715 [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)
azvyagintsevb8b7f922019-06-13 13:39:04 +030059 * @param flexAnswer return answer like a dict, with format ['status' : int, 'stderr' : str, 'stdout' : str ]
Sergey Kolekonovba203982016-12-21 18:32:17 +040060 */
azvyagintsevb8b7f922019-06-13 13:39:04 +030061def runVirtualenvCommand(path, cmd, silent = false, flexAnswer = false) {
Tomáš Kukrál69c25452017-07-27 14:59:40 +020062 def common = new com.mirantis.mk.Common()
azvyagintsevb8b7f922019-06-13 13:39:04 +030063 def res
64 def virtualenv_cmd = "set +x; . ${path}/bin/activate; ${cmd}"
65 if (!silent) {
Jakub Josefe2f4ebb2018-01-15 16:11:51 +010066 common.infoMsg("[Python ${path}] Run command ${cmd}")
67 }
azvyagintsevb8b7f922019-06-13 13:39:04 +030068 if (flexAnswer) {
69 res = common.shCmdStatus(virtualenv_cmd)
70 } else {
71 res = sh(
72 returnStdout: true,
73 script: virtualenv_cmd
74 ).trim()
75 }
76 return res
Sergey Kolekonovba203982016-12-21 18:32:17 +040077}
78
Ales Komarekd874d482016-12-26 10:33:29 +010079
80/**
81 * Install docutils in isolated environment
82 *
83 * @param path Path where virtualenv is created
84 */
85def setupDocutilsVirtualenv(path) {
86 requirements = [
87 'docutils',
88 ]
89 setupVirtualenv(path, 'python2', requirements)
90}
91
92
Sergey Kolekonovba203982016-12-21 18:32:17 +040093@NonCPS
94def loadJson(rawData) {
95 return new groovy.json.JsonSlurperClassic().parseText(rawData)
96}
97
98/**
99 * Parse content from markup-text tables to variables
100 *
101 * @param tableStr String representing the table
102 * @param mode Either list (1st row are keys) or item (key, value rows)
103 * @param format Format of the table
104 */
Ales Komarekd874d482016-12-26 10:33:29 +0100105def parseTextTable(tableStr, type = 'item', format = 'rest', path = none) {
Ales Komarek0e558ee2016-12-23 13:02:55 +0100106 parserFile = "${env.WORKSPACE}/textTableParser.py"
107 parserScript = """import json
108import argparse
109from docutils.parsers.rst import tableparser
110from docutils import statemachine
111
112def parse_item_table(raw_data):
113 i = 1
114 pretty_raw_data = []
115 for datum in raw_data:
116 if datum != "":
117 if datum[3] != ' ' and i > 4:
118 pretty_raw_data.append(raw_data[0])
119 if i == 3:
120 pretty_raw_data.append(datum.replace('-', '='))
121 else:
122 pretty_raw_data.append(datum)
123 i += 1
124 parser = tableparser.GridTableParser()
125 block = statemachine.StringList(pretty_raw_data)
126 docutils_data = parser.parse(block)
127 final_data = {}
128 for line in docutils_data[2]:
129 key = ' '.join(line[0][3]).strip()
130 value = ' '.join(line[1][3]).strip()
131 if key != "":
132 try:
133 value = json.loads(value)
134 except:
135 pass
136 final_data[key] = value
137 i+=1
138 return final_data
139
140def parse_list_table(raw_data):
141 i = 1
142 pretty_raw_data = []
143 for datum in raw_data:
144 if datum != "":
145 if datum[3] != ' ' and i > 4:
146 pretty_raw_data.append(raw_data[0])
147 if i == 3:
148 pretty_raw_data.append(datum.replace('-', '='))
149 else:
150 pretty_raw_data.append(datum)
151 i += 1
152 parser = tableparser.GridTableParser()
153 block = statemachine.StringList(pretty_raw_data)
154 docutils_data = parser.parse(block)
155 final_data = []
156 keys = []
157 for line in docutils_data[1]:
158 for item in line:
159 keys.append(' '.join(item[3]).strip())
160 for line in docutils_data[2]:
161 final_line = {}
162 key = ' '.join(line[0][3]).strip()
163 value = ' '.join(line[1][3]).strip()
164 if key != "":
165 try:
166 value = json.loads(value)
167 except:
168 pass
169 final_data[key] = value
170 i+=1
171 return final_data
172
173def parse_list_table(raw_data):
174 i = 1
175 pretty_raw_data = []
176 for datum in raw_data:
177 if datum != "":
178 if datum[3] != ' ' and i > 4:
179 pretty_raw_data.append(raw_data[0])
180 if i == 3:
181 pretty_raw_data.append(datum.replace('-', '='))
182 else:
183 pretty_raw_data.append(datum)
184 i += 1
185 parser = tableparser.GridTableParser()
186 block = statemachine.StringList(pretty_raw_data)
187 docutils_data = parser.parse(block)
188 final_data = []
189 keys = []
190 for line in docutils_data[1]:
191 for item in line:
192 keys.append(' '.join(item[3]).strip())
193 for line in docutils_data[2]:
194 final_line = {}
195 i = 0
196 for item in line:
197 value = ' '.join(item[3]).strip()
198 try:
199 value = json.loads(value)
200 except:
201 pass
202 final_line[keys[i]] = value
203 i += 1
204 final_data.append(final_line)
205 return final_data
206
207def read_table_file(file):
208 table_file = open(file, 'r')
Ales Komarekc000c152016-12-23 15:32:54 +0100209 raw_data = table_file.read().split('\\n')
Ales Komarek0e558ee2016-12-23 13:02:55 +0100210 table_file.close()
211 return raw_data
212
213parser = argparse.ArgumentParser()
214parser.add_argument('-f','--file', help='File with table data', required=True)
215parser.add_argument('-t','--type', help='Type of table (list/item)', required=True)
216args = vars(parser.parse_args())
217
218raw_data = read_table_file(args['file'])
219
220if args['type'] == 'list':
221 final_data = parse_list_table(raw_data)
222else:
223 final_data = parse_item_table(raw_data)
224
225print json.dumps(final_data)
226"""
227 writeFile file: parserFile, text: parserScript
Sergey Kolekonovba203982016-12-21 18:32:17 +0400228 tableFile = "${env.WORKSPACE}/prettytable.txt"
229 writeFile file: tableFile, text: tableStr
Ales Komarekd874d482016-12-26 10:33:29 +0100230
231 cmd = "python ${parserFile} --file '${tableFile}' --type ${type}"
232 if (path) {
Ales Komarekc6d28dd2016-12-28 12:59:38 +0100233 rawData = runVirtualenvCommand(path, cmd)
Ales Komarekd874d482016-12-26 10:33:29 +0100234 }
235 else {
236 rawData = sh (
237 script: cmd,
238 returnStdout: true
239 ).trim()
240 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400241 data = loadJson(rawData)
242 echo("[Parsed table] ${data}")
243 return data
244}
245
246/**
247 * Install cookiecutter in isolated environment
248 *
249 * @param path Path where virtualenv is created
250 */
251def setupCookiecutterVirtualenv(path) {
252 requirements = [
253 'cookiecutter',
Jakub Josef4df78272017-04-26 14:36:36 +0200254 'jinja2==2.8.1',
Dmitry Pyzhovb883a2d2018-12-14 16:42:52 +0300255 'PyYAML==3.12',
256 'python-gnupg==0.4.3'
Sergey Kolekonovba203982016-12-21 18:32:17 +0400257 ]
258 setupVirtualenv(path, 'python2', requirements)
259}
260
261/**
262 * Generate the cookiecutter templates with given context
263 *
Jakub Josef4e10c372017-04-26 14:13:50 +0200264 * @param template template
265 * @param context template context
266 * @param path Path where virtualenv is created (optional)
267 * @param templatePath path to cookiecutter template repo (optional)
Sergey Kolekonovba203982016-12-21 18:32:17 +0400268 */
Jakub Josef4e10c372017-04-26 14:13:50 +0200269def buildCookiecutterTemplate(template, context, outputDir = '.', path = null, templatePath = ".") {
Tomáš Kukráldad7b462017-03-27 13:53:05 +0200270 configFile = "default_config.yaml"
271 configString = "default_context:\n"
Tomáš Kukrál6de85042017-04-12 17:49:05 +0200272 writeFile file: configFile, text: context
Jakub Josef4e61cc02017-04-26 14:29:09 +0200273 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 +0400274 output = sh (returnStdout: true, script: command)
275 echo("[Cookiecutter build] Output: ${output}")
276}
277
278/**
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400279 *
280 * @param context - context template
281 * @param contextName - context template name
282 * @param saltMasterName - hostname of Salt Master node
283 * @param virtualenv - pyvenv with CC and dep's
284 * @param templateEnvDir - root of CookieCutter
285 * @return
286 */
287def generateModel(context, contextName, saltMasterName, virtualenv, modelEnv, templateEnvDir, multiModels = true) {
Denis Egorenkofa2c6752018-10-18 15:51:45 +0400288 def common = new com.mirantis.mk.Common()
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400289 def generatedModel = multiModels ? "${modelEnv}/${contextName}" : modelEnv
290 def templateContext = readYaml text: context
291 def clusterDomain = templateContext.default_context.cluster_domain
292 def clusterName = templateContext.default_context.cluster_name
293 def outputDestination = "${generatedModel}/classes/cluster/${clusterName}"
294 def templateBaseDir = templateEnvDir
295 def templateDir = "${templateEnvDir}/dir"
296 def templateOutputDir = templateBaseDir
297 dir(templateEnvDir) {
298 common.infoMsg("Generating model from context ${contextName}")
299 def productList = ["infra", "cicd", "opencontrail", "kubernetes", "openstack", "oss", "stacklight", "ceph"]
300 for (product in productList) {
301 // get templateOutputDir and productDir
302 templateOutputDir = "${templateEnvDir}/output/${product}"
303 productDir = product
304 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
305 // Bw for 2018.8.1 and older releases
306 if (product.startsWith("stacklight") && (!fileExists(templateDir))) {
307 common.warningMsg("Old release detected! productDir => 'stacklight2' ")
308 productDir = "stacklight2"
309 templateDir = "${templateEnvDir}/cluster_product/${productDir}"
310 }
Adam Tenglera2373132018-10-18 15:40:16 +0200311 // generate infra unless its explicitly disabled
312 if ((product == "infra" && templateContext.default_context.get("infra_enabled", "True").toBoolean())
313 || (templateContext.default_context.get(product + "_enabled", "False").toBoolean())) {
Denis Egorenko6c2e3ae2018-10-17 16:45:56 +0400314
315 common.infoMsg("Generating product " + product + " from " + templateDir + " to " + templateOutputDir)
316
317 sh "rm -rf ${templateOutputDir} || true"
318 sh "mkdir -p ${templateOutputDir}"
319 sh "mkdir -p ${outputDestination}"
320
321 buildCookiecutterTemplate(templateDir, context, templateOutputDir, virtualenv, templateBaseDir)
322 sh "mv -v ${templateOutputDir}/${clusterName}/* ${outputDestination}"
323 } else {
324 common.warningMsg("Product " + product + " is disabled")
325 }
326 }
327
328 def localRepositories = templateContext.default_context.local_repositories
329 localRepositories = localRepositories ? localRepositories.toBoolean() : false
330 def offlineDeployment = templateContext.default_context.offline_deployment
331 offlineDeployment = offlineDeployment ? offlineDeployment.toBoolean() : false
332 if (localRepositories && !offlineDeployment) {
333 def mcpVersion = templateContext.default_context.mcp_version
334 def aptlyModelUrl = templateContext.default_context.local_model_url
335 def ssh = new com.mirantis.mk.Ssh()
336 dir(path: modelEnv) {
337 ssh.agentSh "git submodule add \"${aptlyModelUrl}\" \"classes/cluster/${clusterName}/cicd/aptly\""
338 if (!(mcpVersion in ["nightly", "testing", "stable"])) {
339 ssh.agentSh "cd \"classes/cluster/${clusterName}/cicd/aptly\";git fetch --tags;git checkout ${mcpVersion}"
340 }
341 }
342 }
343
344 def nodeFile = "${generatedModel}/nodes/${saltMasterName}.${clusterDomain}.yml"
345 def nodeString = """classes:
346- cluster.${clusterName}.infra.config
347parameters:
348 _param:
349 linux_system_codename: xenial
350 reclass_data_revision: master
351 linux:
352 system:
353 name: ${saltMasterName}
354 domain: ${clusterDomain}
355 """
356 sh "mkdir -p ${generatedModel}/nodes/"
357 writeFile(file: nodeFile, text: nodeString)
358 }
359}
360
361/**
Sergey Kolekonovba203982016-12-21 18:32:17 +0400362 * Install jinja rendering in isolated environment
363 *
364 * @param path Path where virtualenv is created
365 */
366def setupJinjaVirtualenv(path) {
367 requirements = [
368 'jinja2-cli',
369 'pyyaml',
370 ]
371 setupVirtualenv(path, 'python2', requirements)
372}
373
374/**
375 * Generate the Jinja templates with given context
376 *
377 * @param path Path where virtualenv is created
378 */
379def jinjaBuildTemplate (template, context, path = none) {
380 contextFile = "jinja_context.yml"
381 contextString = ""
382 for (parameter in context) {
383 contextString = "${contextString}${parameter.key}: ${parameter.value}\n"
384 }
385 writeFile file: contextFile, text: contextString
386 cmd = "jinja2 ${template} ${contextFile} --format=yaml"
387 data = sh (returnStdout: true, script: cmd)
388 echo(data)
389 return data
390}
Oleg Grigorovbec45582017-09-12 20:29:24 +0300391
392/**
393 * Install salt-pepper in isolated environment
394 *
395 * @param path Path where virtualenv is created
chnydaa0dbb252017-10-05 10:46:09 +0200396 * @param url SALT_MASTER_URL
397 * @param credentialsId Credentials to salt api
Oleg Grigorovbec45582017-09-12 20:29:24 +0300398 */
Jakub Josefc9b6d662018-02-21 16:21:03 +0100399def setupPepperVirtualenv(path, url, credentialsId) {
chnydaa0dbb252017-10-05 10:46:09 +0200400 def common = new com.mirantis.mk.Common()
401
402 // virtualenv setup
Mykyta Karpin81756c92018-03-02 13:03:26 +0200403 // pin pepper till https://mirantis.jira.com/browse/PROD-18188 is fixed
404 requirements = ['salt-pepper>=0.5.2,<0.5.4']
Jakub Josefc9b6d662018-02-21 16:21:03 +0100405 setupVirtualenv(path, 'python2', requirements, null, true, true)
chnydabcfff182017-11-29 10:24:36 +0100406
chnydaa0dbb252017-10-05 10:46:09 +0200407 // pepperrc creation
408 rcFile = "${path}/pepperrc"
409 creds = common.getPasswordCredentials(credentialsId)
410 rc = """\
411[main]
412SALTAPI_EAUTH=pam
413SALTAPI_URL=${url}
414SALTAPI_USER=${creds.username}
415SALTAPI_PASS=${creds.password.toString()}
416"""
417 writeFile file: rcFile, text: rc
418 return rcFile
Jakub Josefd067f612017-09-26 13:42:56 +0200419}
Oleh Hryhorov44569fb2017-10-26 17:04:55 +0300420
421/**
422 * Install devops in isolated environment
423 *
424 * @param path Path where virtualenv is created
425 * @param clean Define to true is the venv have to cleaned up before install a new one
426 */
427def setupDevOpsVenv(venv, clean=false) {
428 requirements = ['git+https://github.com/openstack/fuel-devops.git']
429 setupVirtualenv(venv, 'python2', requirements, null, false, clean)
430}