Michael Lando | f5f13c4 | 2017-02-19 12:35:04 +0200 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | ############################################################################## |
| 4 | ### |
| 5 | ### generate-manifest.py |
| 6 | ### |
| 7 | ### A Vendor utility to generate a valid heat zip manifest file for the AT&T onboarding. |
| 8 | ### |
| 9 | ### Usage: |
| 10 | ### |
| 11 | ### generate-manifest.py [-f|--folder] vendor-heat-directory [-n|--name] manifest-name [-d|--description] manifet-description |
| 12 | ### |
| 13 | ### For example: |
| 14 | ### |
| 15 | ### ./generate-manifest.py --folder ./vota --name vOTA --description "HOT template to create vOTA server" |
| 16 | ### |
| 17 | ### Help: |
| 18 | ### The script is doing the following: |
| 19 | ### 1) Split the files into different types |
| 20 | ### a. .env files |
| 21 | ### b. Network files (anything containing the string network) |
| 22 | ### c. Volume files (anything containing the string volume) |
| 23 | ### d. Top level Heat files |
| 24 | ### e. Other types |
| 25 | ### 2) Match env files to heat files – looking for same name ignoring suffix and extension |
| 26 | ### 3) Match Network childs |
| 27 | ### a. Look for Top level heats which name is a substring of the name of the Network heat name. |
| 28 | ### 4) Match Volume childs |
| 29 | ### a. Look for Top level heats which name is a substring of the name of the Volume heat name. |
| 30 | ### 5) Generate the JSON file from the above |
| 31 | ### |
| 32 | ### |
| 33 | ### Author: Avi Ziv |
AviZi | 280f801 | 2017-06-09 02:39:56 +0300 | [diff] [blame] | 34 | ### Version 1.4 for ASDC/AT&T 1.0 |
| 35 | ### Date: 13 July 2016 |
Michael Lando | f5f13c4 | 2017-02-19 12:35:04 +0200 | [diff] [blame] | 36 | ### |
| 37 | ############################################################################## |
| 38 | |
| 39 | # import os,sys,getopt,json,re |
| 40 | import os, sys, getopt, re |
| 41 | from collections import OrderedDict |
| 42 | from json import JSONEncoder |
| 43 | import json |
| 44 | |
| 45 | VERSION = "1.4" |
| 46 | ENV_EXT = ".env" |
| 47 | SHELL_EXT = ".sh" |
| 48 | YAML_EXT = [".yaml", ".yml"] |
| 49 | # VERSION_DELIMITER_PATTERN='_v\d{*}.\d{*}' |
| 50 | # VERSION_DELIMITER_PATTERN='_v*.*' |
| 51 | #v1.0 |
| 52 | VERSION_DELIMITER_PATTERN = '_v\d+.\d+' |
| 53 | #07_12_2016 |
| 54 | VERSION_DELIMITER_PATTERN2 = '_\d+-\d+-\d+' |
| 55 | |
| 56 | # types |
| 57 | HEAT = "HEAT" |
| 58 | HEAT_BASE = "HEAT_BASE" |
| 59 | HEAT_NET = "HEAT_NET" |
| 60 | HEAT_VOL = "HEAT_VOL" |
| 61 | HEAT_ENV = "HEAT_ENV" |
| 62 | SHELL = "SHELL" |
| 63 | OTHER = "OTHER" |
| 64 | |
| 65 | globalVolumeVal = "VOLUME" |
| 66 | globalNetworkVal = "NETWORK" |
| 67 | globalBaseVal = "BASE" |
| 68 | |
| 69 | |
| 70 | def version(): |
| 71 | return VERSION |
| 72 | |
| 73 | |
| 74 | def usage(): |
| 75 | print ('usage: ' + sys.argv[0] + ' [-f|--folder] vendor-heat-directory [-n|--name] manifest-name [-d|--description] manifet-description' ) |
| 76 | |
| 77 | |
| 78 | def header(): |
| 79 | print ("\nASDC Vendor manifest file generator, version " + version() + "\n") |
| 80 | |
| 81 | |
| 82 | def getEnvVariables(value, defaultValue): |
| 83 | try: |
| 84 | eVal = os.environ[value] |
| 85 | return eVal |
| 86 | except KeyError: |
| 87 | print ("Missing ${" + value + "} envirunment variable. Using default value: " + defaultValue) |
| 88 | return defaultValue |
| 89 | |
| 90 | |
| 91 | def getF(listFiles): |
| 92 | print ("Analyzing files ...") |
| 93 | foundABase = False |
| 94 | files = listFiles |
| 95 | jsons = {} |
| 96 | lOfEnvs = {} |
| 97 | lOfVolumes = {} |
| 98 | lOfNetworks = {} |
| 99 | lOfHeats = {} |
| 100 | lOfShels = {} |
| 101 | lOfArtifacts = {} |
| 102 | |
| 103 | for f in files: |
| 104 | fullFilename = f[1] |
| 105 | fObj = ManifestFileInfo(fullFilename) |
| 106 | if fObj.isEnv(): |
| 107 | lOfEnvs[fObj.file_name] = fObj |
| 108 | elif fObj.isShell(): |
| 109 | lOfShels[fObj.file_name] = fObj |
| 110 | elif fObj.isVolume(): |
| 111 | lOfVolumes[fObj.file_name] = fObj |
| 112 | elif fObj.isNetwork(): |
| 113 | lOfNetworks[fObj.file_name] = fObj |
| 114 | elif (fObj.isYaml() and not fObj.isBase()): |
| 115 | lOfHeats[fObj.file_name] = fObj |
| 116 | elif fObj.isArtifact(): |
| 117 | lOfArtifacts[fObj.file_name] = fObj |
| 118 | elif (fObj.isBase() and fObj.isYaml()): |
| 119 | foundABase = True |
| 120 | lOfHeats[fObj.file_name] = fObj |
| 121 | |
| 122 | jsons['heats'] = lOfHeats |
| 123 | jsons['envs'] = lOfEnvs |
| 124 | jsons['shells'] = lOfShels |
| 125 | jsons['volumes'] = lOfVolumes |
| 126 | jsons['networks'] = lOfNetworks |
| 127 | jsons['artifacts'] = lOfArtifacts |
| 128 | |
| 129 | if not foundABase: |
| 130 | print (">>> Warning: No Base was found") |
| 131 | return jsons |
| 132 | |
| 133 | def loadFilesToList(folder): |
| 134 | print ("Analyzing files in folder: << " + folder + " >>") |
| 135 | files = os.listdir(folder) |
| 136 | listOfFiles = [] |
| 137 | for f in files: |
| 138 | if os.path.isdir(os.path.join(folder, f)): |
| 139 | ConsoleLogger.warning("Sub folders are ignored by this script, you may want to remove it before archiving") |
| 140 | continue |
| 141 | |
| 142 | filename, file_extension = os.path.splitext(f) |
| 143 | if filename == 'MANIFEST': |
| 144 | ConsoleLogger.warning("Your folder already contains a manifest file that will be overridden") |
| 145 | continue |
| 146 | listOfFiles.append([filename, f]) |
| 147 | return listOfFiles |
| 148 | |
| 149 | |
| 150 | def make(files): |
| 151 | flist = [] |
| 152 | dEnvs = {} |
| 153 | dEnvs = files['envs'] |
| 154 | dHeats = files['heats'] |
| 155 | dNetworks = files['networks'] |
| 156 | dVolumes = files['volumes'] |
| 157 | dArtifacts = files['artifacts'] |
| 158 | dShells = files['shells'] |
| 159 | |
| 160 | env_items = dEnvs.items() |
| 161 | for fileItem in env_items: |
| 162 | env_name = fileItem[1].file_name |
| 163 | env_base = fileItem[1].base_file_name |
| 164 | if env_name in dHeats: |
| 165 | dHeats[env_name].add(fileItem[1]) |
| 166 | continue |
| 167 | |
| 168 | if env_name in dNetworks.items(): |
| 169 | dNetworks[env_name].add(fileItem[1]) |
| 170 | continue |
| 171 | |
| 172 | if env_name in dVolumes.items(): |
| 173 | dVolumes[env_name[0]].add(env_name[1]) |
| 174 | continue |
| 175 | |
| 176 | for fName in dHeats: |
| 177 | heat_base = dHeats[fName].base_file_name |
| 178 | if env_base in heat_base: |
| 179 | dHeats[fName].add(dEnvs[env_name]) |
| 180 | break |
| 181 | else: |
| 182 | for fName in dNetworks: |
| 183 | net_base = dNetworks[fName].base_file_name |
| 184 | if env_base in net_base: |
| 185 | dNetworks[fName].add(dEnvs[env_name]) |
| 186 | break |
| 187 | else: |
| 188 | for fName in dVolumes: |
| 189 | vol_base = dVolumes[fName].base_file_name |
| 190 | if env_base in vol_base: |
| 191 | dVolumes[fName].add(dEnvs[env_name]) |
| 192 | break |
| 193 | |
| 194 | else: |
| 195 | flist.append(dEnvs[env_name]) |
| 196 | |
| 197 | for fName in dVolumes: |
| 198 | vol_base = dVolumes[fName].base_file_name |
| 199 | for hfName in dHeats: |
| 200 | heat_base = dHeats[hfName].base_file_name |
| 201 | if heat_base in vol_base: |
| 202 | dHeats[hfName].add(dVolumes[fName]) |
| 203 | break |
| 204 | else: |
| 205 | flist.append(dVolumes[fName]) |
| 206 | |
| 207 | for fName in dNetworks: |
| 208 | net_base = dNetworks[fName].base_file_name |
| 209 | for hfName in dHeats: |
| 210 | heat_base = dHeats[hfName].base_file_name |
| 211 | if heat_base in net_base: |
| 212 | dHeats[hfName].add(dNetworks[fName]) |
| 213 | break |
| 214 | else: |
| 215 | flist.append(dNetworks[fName]) |
| 216 | |
| 217 | for fName in dHeats: |
| 218 | flist.append(dHeats[fName]) |
| 219 | for fName in dShells: |
| 220 | flist.append(dShells[fName]) |
| 221 | for fName in dArtifacts: |
| 222 | flist.append(dArtifacts[fName]) |
| 223 | |
| 224 | print ("\n------------------------------------------------------------\n") |
| 225 | return flist |
| 226 | |
| 227 | |
| 228 | def generate(folder, name, description): |
| 229 | print ("Checking envirunment variables ...") |
| 230 | global globalVolumeVal |
| 231 | globalVolumeVal = getEnvVariables("VOLUME", globalVolumeVal) |
| 232 | |
| 233 | global globalNetworkVal |
| 234 | globalNetworkVal = getEnvVariables("NETWORK", globalNetworkVal) |
| 235 | |
| 236 | global globalBaseVal |
| 237 | globalBaseVal = getEnvVariables("BASE", globalBaseVal) |
| 238 | |
| 239 | YamlTabCleaner(folder).cleanYamlTabs() |
| 240 | |
| 241 | print ("Generating manifest file ...") |
| 242 | jsons = getF(loadFilesToList(folder)) |
| 243 | |
| 244 | lFiles = make(jsons) |
| 245 | manifest = Manifest(name, description, '1.0', lFiles) |
| 246 | output_json = json.dumps(manifest, default=jdefault, indent=4, sort_keys=False) |
| 247 | |
| 248 | f = open(os.path.join(folder, 'MANIFEST.json'), 'w') |
| 249 | f.write(output_json) |
| 250 | print("MANIFEST file created") |
| 251 | |
| 252 | |
| 253 | ################ |
| 254 | |
| 255 | def jdefault(obj): |
| 256 | if hasattr(obj, '__json__'): |
| 257 | return obj.__json__() |
| 258 | else: |
| 259 | return obj.__dict__ |
| 260 | |
| 261 | |
| 262 | class ManifestFileInfo(object): |
| 263 | def __init__(self, filename): |
| 264 | self.name = filename |
| 265 | self.base = 'false' |
| 266 | self.data = [] |
| 267 | self.file_name, self.file_extension = os.path.splitext(filename) |
| 268 | self.base_file_name = re.sub(VERSION_DELIMITER_PATTERN, '', self.file_name) |
| 269 | self.base_file_name = re.sub(VERSION_DELIMITER_PATTERN2, '', self.base_file_name) |
| 270 | |
| 271 | if self.isEnv(): |
| 272 | self.heat_type = Types.ENV |
| 273 | elif self.isShell(): |
| 274 | self.heat_type = Types.SHELL |
| 275 | elif self.isVolume(): |
| 276 | self.heat_type = Types.VOL |
| 277 | elif self.isNetwork(): |
| 278 | self.heat_type = Types.NET |
| 279 | elif self.isYaml() and not self.isBase(): |
| 280 | self.heat_type = Types.HEAT |
| 281 | elif self.isArtifact(): |
| 282 | self.heat_type = Types.OTHER |
| 283 | elif (self.isBase() and self.isYaml()): |
| 284 | self.heat_type = Types.HEAT |
| 285 | self.base = 'true' |
| 286 | |
| 287 | def set(self, data): |
| 288 | self.data = data |
| 289 | |
| 290 | def add(self, item): |
| 291 | self.data.append(item) |
| 292 | |
| 293 | def isYaml(self): |
| 294 | return any(val in self.file_extension.lower() for val in YAML_EXT) |
| 295 | |
| 296 | def isEnv(self): |
| 297 | return self.file_extension.lower() == ENV_EXT.lower() |
| 298 | |
| 299 | def isShell(self): |
| 300 | return self.file_extension.lower() == SHELL_EXT.lower() |
| 301 | |
| 302 | def isVolume(self): |
| 303 | res = globalVolumeVal.lower() in self.file_name.lower() |
| 304 | return res |
| 305 | |
| 306 | def isNetwork(self): |
| 307 | res = globalNetworkVal.lower() in self.file_name.lower() |
| 308 | return res |
| 309 | |
| 310 | def isBase(self): |
| 311 | res = globalBaseVal.lower() in self.file_name.lower() |
| 312 | return res |
| 313 | |
| 314 | def isArtifact(self): |
| 315 | return (not self.isBase() and not self.isVolume() and not self.isNetwork() and not self.isEnv()) |
| 316 | |
| 317 | def isHEAT(self): |
| 318 | return ((self.heat_type == Types.HEAT) | (self.heat_type == Types.BASE) | (self.heat_type == Types.NET) | ( |
| 319 | self.heat_type == Types.VOL)) |
| 320 | |
| 321 | def __json__(self): |
| 322 | dict = OrderedDict( |
| 323 | [('file', self.name), ('type', self.heat_type)]) |
| 324 | if self.isHEAT(): |
| 325 | dict['isBase'] = self.base |
| 326 | if self.data != []: |
| 327 | dict['data'] = self.data |
| 328 | |
| 329 | return dict |
| 330 | |
| 331 | |
| 332 | class Manifest(object): |
| 333 | def __init__(self, name, description, version, data): |
| 334 | self.name = name |
| 335 | self.description = description |
| 336 | self.version = version |
| 337 | self.data = data |
| 338 | |
| 339 | def add(self, data): |
| 340 | self.data.append(data) |
| 341 | |
| 342 | def __json__(self): |
| 343 | return OrderedDict([('name', self.name), ('description', self.description), ('data', self.data)]) |
| 344 | |
| 345 | |
| 346 | class YamlTabCleaner(object): |
| 347 | def __init__(self, folder): |
| 348 | self.folder = folder |
| 349 | |
| 350 | def replaceTabs(self, sourceFile, targetFile): |
| 351 | with open(sourceFile, "rt") as fin: |
| 352 | if '\t' in fin.read(): |
| 353 | print("\'tab\' character was found in the file: " + sourceFile + "\na clean version of the file can be found under \'clean\' folder") |
| 354 | target = os.path.dirname(targetFile) |
| 355 | if not os.path.exists(target): |
| 356 | os.makedirs(target) |
| 357 | fin.seek(0) |
| 358 | with open(targetFile, "wt") as fout: |
| 359 | for line in fin: |
| 360 | fout.write(line.replace('\t', ' ')) |
| 361 | |
| 362 | def cleanYamlTabs(self): |
| 363 | included_extenstions = ['yml', 'yaml'] |
| 364 | files = [fn for fn in os.listdir(self.folder) |
| 365 | if any(fn.endswith(ext) for ext in included_extenstions)] |
| 366 | target = os.path.join(self.folder, "clean") |
| 367 | for file in files: |
| 368 | self.replaceTabs(os.path.join(self.folder, file), os.path.join(target, file)) |
| 369 | |
| 370 | class ConsoleLogger(object): |
| 371 | @classmethod |
| 372 | def error(cls, message): |
| 373 | print(">>> Error: " + message) |
| 374 | |
| 375 | @classmethod |
| 376 | def warning(cls, message): |
| 377 | print(">>> Warning: " + message) |
| 378 | |
| 379 | |
| 380 | @classmethod |
| 381 | def info(cls, message): |
| 382 | print(">>> Info: " + message) |
| 383 | |
| 384 | |
| 385 | def enum(**named_values): |
| 386 | return type('Enum', (), named_values) |
| 387 | |
| 388 | |
| 389 | ################ |
| 390 | |
| 391 | def main(argv): |
| 392 | action = '' |
| 393 | folderName = '.' |
| 394 | name = '' |
| 395 | description = '' |
| 396 | version = '' |
| 397 | |
| 398 | try: |
| 399 | opts, args = getopt.getopt(argv, "h:f:n:d", ["folder=", "name=", "description=", ]) |
| 400 | except getopt.GetoptError as err: |
| 401 | # print help information and exit: |
| 402 | print ('>>>>' + str(err)) |
| 403 | usage() |
| 404 | sys.exit(2) |
| 405 | for opt, arg in opts: |
| 406 | if opt == '-h': |
| 407 | usage() |
| 408 | sys.exit() |
| 409 | elif opt in ('-f', '--folder'): |
| 410 | action = 'generate' |
| 411 | if not arg: |
| 412 | print ("Error: missing heat files directory") |
| 413 | usage() |
| 414 | sys.exit(2) |
| 415 | else: |
| 416 | folderName = arg |
| 417 | elif opt in ('-n', '--name'): |
| 418 | name = arg |
| 419 | elif opt in ('-d', '--description'): |
| 420 | description = arg |
| 421 | else: |
| 422 | usage() |
| 423 | |
| 424 | if action == 'generate': |
| 425 | generate(folderName, name, description) |
| 426 | sys.exit() |
| 427 | else: |
| 428 | usage() |
| 429 | |
| 430 | |
| 431 | if __name__ == "__main__": |
| 432 | header() |
| 433 | Types = enum(HEAT='HEAT', BASE='HEAT_BASE', NET='HEAT_NET', VOL='HEAT_VOL', ENV='HEAT_ENV', SHELL='SHELL', |
| 434 | OTHER='OTHER') |
| 435 | main(sys.argv[1:]) |