Package ganeti :: Module ovf
[hide private]
[frames] | no frames]

Source Code for Module ganeti.ovf

   1  #!/usr/bin/python 
   2  # 
   3   
   4  # Copyright (C) 2011 Google Inc. 
   5  # 
   6  # This program is free software; you can redistribute it and/or modify 
   7  # it under the terms of the GNU General Public License as published by 
   8  # the Free Software Foundation; either version 2 of the License, or 
   9  # (at your option) any later version. 
  10  # 
  11  # This program is distributed in the hope that it will be useful, but 
  12  # WITHOUT ANY WARRANTY; without even the implied warranty of 
  13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
  14  # General Public License for more details. 
  15  # 
  16  # You should have received a copy of the GNU General Public License 
  17  # along with this program; if not, write to the Free Software 
  18  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 
  19  # 02110-1301, USA. 
  20   
  21   
  22  """Converter tools between ovf and ganeti config file 
  23   
  24  """ 
  25   
  26  # pylint: disable=F0401, E1101 
  27   
  28  # F0401 because ElementTree is not default for python 2.4 
  29  # E1101 makes no sense - pylint assumes that ElementTree object is a tuple 
  30   
  31   
  32  import ConfigParser 
  33  import errno 
  34  import logging 
  35  import os 
  36  import os.path 
  37  import re 
  38  import shutil 
  39  import tarfile 
  40  import tempfile 
  41  import xml.dom.minidom 
  42  import xml.parsers.expat 
  43  try: 
  44    import xml.etree.ElementTree as ET 
  45  except ImportError: 
  46    import elementtree.ElementTree as ET 
  47   
  48  try: 
  49    ParseError = ET.ParseError # pylint: disable=E1103 
  50  except AttributeError: 
  51    ParseError = None 
  52   
  53  from ganeti import constants 
  54  from ganeti import errors 
  55  from ganeti import utils 
  56   
  57   
  58  # Schemas used in OVF format 
  59  GANETI_SCHEMA = "http://ganeti" 
  60  OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1" 
  61  RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/" 
  62                 "CIM_ResourceAllocationSettingData") 
  63  VSSD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/" 
  64                 "CIM_VirtualSystemSettingData") 
  65  XML_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance" 
  66   
  67  # File extensions in OVF package 
  68  OVA_EXT = ".ova" 
  69  OVF_EXT = ".ovf" 
  70  MF_EXT = ".mf" 
  71  CERT_EXT = ".cert" 
  72  COMPRESSION_EXT = ".gz" 
  73  FILE_EXTENSIONS = [ 
  74    OVF_EXT, 
  75    MF_EXT, 
  76    CERT_EXT, 
  77  ] 
  78   
  79  COMPRESSION_TYPE = "gzip" 
  80  NO_COMPRESSION = [None, "identity"] 
  81  COMPRESS = "compression" 
  82  DECOMPRESS = "decompression" 
  83  ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS] 
  84   
  85  VMDK = "vmdk" 
  86  RAW = "raw" 
  87  COW = "cow" 
  88  ALLOWED_FORMATS = [RAW, COW, VMDK] 
  89   
  90  # ResourceType values 
  91  RASD_TYPE = { 
  92    "vcpus": "3", 
  93    "memory": "4", 
  94    "scsi-controller": "6", 
  95    "ethernet-adapter": "10", 
  96    "disk": "17", 
  97  } 
  98   
  99  SCSI_SUBTYPE = "lsilogic" 
 100  VS_TYPE = { 
 101    "ganeti": "ganeti-ovf", 
 102    "external": "vmx-04", 
 103  } 
 104   
 105  # AllocationUnits values and conversion 
 106  ALLOCATION_UNITS = { 
 107    "b": ["bytes", "b"], 
 108    "kb": ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"], 
 109    "mb": ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"], 
 110    "gb": ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"], 
 111  } 
 112  CONVERT_UNITS_TO_MB = { 
 113    "b": lambda x: x / (1024 * 1024), 
 114    "kb": lambda x: x / 1024, 
 115    "mb": lambda x: x, 
 116    "gb": lambda x: x * 1024, 
 117  } 
 118   
 119  # Names of the config fields 
 120  NAME = "name" 
 121  OS = "os" 
 122  HYPERV = "hypervisor" 
 123  VCPUS = "vcpus" 
 124  MEMORY = "memory" 
 125  AUTO_BALANCE = "auto_balance" 
 126  DISK_TEMPLATE = "disk_template" 
 127  TAGS = "tags" 
 128  VERSION = "version" 
 129   
 130  # Instance IDs of System and SCSI controller 
 131  INSTANCE_ID = { 
 132    "system": 0, 
 133    "vcpus": 1, 
 134    "memory": 2, 
 135    "scsi": 3, 
 136  } 
 137   
 138  # Disk format descriptions 
 139  DISK_FORMAT = { 
 140    RAW: "http://en.wikipedia.org/wiki/Byte", 
 141    VMDK: "http://www.vmware.com/interfaces/specifications/vmdk.html" 
 142            "#monolithicSparse", 
 143    COW: "http://www.gnome.org/~markmc/qcow-image-format.html", 
 144  } 
145 146 147 -def CheckQemuImg():
148 """ Make sure that qemu-img is present before performing operations. 149 150 @raise errors.OpPrereqError: when qemu-img was not found in the system 151 152 """ 153 if not constants.QEMUIMG_PATH: 154 raise errors.OpPrereqError("qemu-img not found at build time, unable" 155 " to continue")
156
157 158 -def LinkFile(old_path, prefix=None, suffix=None, directory=None):
159 """Create link with a given prefix and suffix. 160 161 This is a wrapper over os.link. It tries to create a hard link for given file, 162 but instead of rising error when file exists, the function changes the name 163 a little bit. 164 165 @type old_path:string 166 @param old_path: path to the file that is to be linked 167 @type prefix: string 168 @param prefix: prefix of filename for the link 169 @type suffix: string 170 @param suffix: suffix of the filename for the link 171 @type directory: string 172 @param directory: directory of the link 173 174 @raise errors.OpPrereqError: when error on linking is different than 175 "File exists" 176 177 """ 178 assert(prefix is not None or suffix is not None) 179 if directory is None: 180 directory = os.getcwd() 181 new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix)) 182 counter = 1 183 while True: 184 try: 185 os.link(old_path, new_path) 186 break 187 except OSError, err: 188 if err.errno == errno.EEXIST: 189 new_path = utils.PathJoin(directory, 190 "%s_%s%s" % (prefix, counter, suffix)) 191 counter += 1 192 else: 193 raise errors.OpPrereqError("Error moving the file %s to %s location:" 194 " %s" % (old_path, new_path, err)) 195 return new_path
196
197 198 -class OVFReader(object):
199 """Reader class for OVF files. 200 201 @type files_list: list 202 @ivar files_list: list of files in the OVF package 203 @type tree: ET.ElementTree 204 @ivar tree: XML tree of the .ovf file 205 @type schema_name: string 206 @ivar schema_name: name of the .ovf file 207 @type input_dir: string 208 @ivar input_dir: directory in which the .ovf file resides 209 210 """
211 - def __init__(self, input_path):
212 """Initialiaze the reader - load the .ovf file to XML parser. 213 214 It is assumed that names of manifesto (.mf), certificate (.cert) and ovf 215 files are the same. In order to account any other files as part of the ovf 216 package, they have to be explicitly mentioned in the Resources section 217 of the .ovf file. 218 219 @type input_path: string 220 @param input_path: absolute path to the .ovf file 221 222 @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some 223 of the files mentioned in Resources section do not exist 224 225 """ 226 self.tree = ET.ElementTree() 227 try: 228 self.tree.parse(input_path) 229 except (ParseError, xml.parsers.expat.ExpatError), err: 230 raise errors.OpPrereqError("Error while reading %s file: %s" % 231 (OVF_EXT, err)) 232 233 # Create a list of all files in the OVF package 234 (input_dir, input_file) = os.path.split(input_path) 235 (input_name, _) = os.path.splitext(input_file) 236 files_directory = utils.ListVisibleFiles(input_dir) 237 files_list = [] 238 for file_name in files_directory: 239 (name, extension) = os.path.splitext(file_name) 240 if extension in FILE_EXTENSIONS and name == input_name: 241 files_list.append(file_name) 242 files_list += self._GetAttributes("{%s}References/{%s}File" % 243 (OVF_SCHEMA, OVF_SCHEMA), 244 "{%s}href" % OVF_SCHEMA) 245 for file_name in files_list: 246 file_path = utils.PathJoin(input_dir, file_name) 247 if not os.path.exists(file_path): 248 raise errors.OpPrereqError("File does not exist: %s" % file_path) 249 logging.info("Files in the OVF package: %s", " ".join(files_list)) 250 self.files_list = files_list 251 self.input_dir = input_dir 252 self.schema_name = input_name
253
254 - def _GetAttributes(self, path, attribute):
255 """Get specified attribute from all nodes accessible using given path. 256 257 Function follows the path from root node to the desired tags using path, 258 then reads the apropriate attribute values. 259 260 @type path: string 261 @param path: path of nodes to visit 262 @type attribute: string 263 @param attribute: attribute for which we gather the information 264 @rtype: list 265 @return: for each accessible tag with the attribute value set, value of the 266 attribute 267 268 """ 269 current_list = self.tree.findall(path) 270 results = [x.get(attribute) for x in current_list] 271 return filter(None, results)
272
273 - def _GetElementMatchingAttr(self, path, match_attr):
274 """Searches for element on a path that matches certain attribute value. 275 276 Function follows the path from root node to the desired tags using path, 277 then searches for the first one matching the attribute value. 278 279 @type path: string 280 @param path: path of nodes to visit 281 @type match_attr: tuple 282 @param match_attr: pair (attribute, value) for which we search 283 @rtype: ET.ElementTree or None 284 @return: first element matching match_attr or None if nothing matches 285 286 """ 287 potential_elements = self.tree.findall(path) 288 (attr, val) = match_attr 289 for elem in potential_elements: 290 if elem.get(attr) == val: 291 return elem 292 return None
293
294 - def _GetElementMatchingText(self, path, match_text):
295 """Searches for element on a path that matches certain text value. 296 297 Function follows the path from root node to the desired tags using path, 298 then searches for the first one matching the text value. 299 300 @type path: string 301 @param path: path of nodes to visit 302 @type match_text: tuple 303 @param match_text: pair (node, text) for which we search 304 @rtype: ET.ElementTree or None 305 @return: first element matching match_text or None if nothing matches 306 307 """ 308 potential_elements = self.tree.findall(path) 309 (node, text) = match_text 310 for elem in potential_elements: 311 if elem.findtext(node) == text: 312 return elem 313 return None
314 315 @staticmethod
316 - def _GetDictParameters(root, schema):
317 """Reads text in all children and creates the dictionary from the contents. 318 319 @type root: ET.ElementTree or None 320 @param root: father of the nodes we want to collect data about 321 @type schema: string 322 @param schema: schema name to be removed from the tag 323 @rtype: dict 324 @return: dictionary containing tags and their text contents, tags have their 325 schema fragment removed or empty dictionary, when root is None 326 327 """ 328 if not root: 329 return {} 330 results = {} 331 for element in list(root): 332 pref_len = len("{%s}" % schema) 333 assert(schema in element.tag) 334 tag = element.tag[pref_len:] 335 results[tag] = element.text 336 return results
337
338 - def VerifyManifest(self):
339 """Verifies manifest for the OVF package, if one is given. 340 341 @raise errors.OpPrereqError: if SHA1 checksums do not match 342 343 """ 344 if "%s%s" % (self.schema_name, MF_EXT) in self.files_list: 345 logging.warning("Verifying SHA1 checksums, this may take a while") 346 manifest_filename = "%s%s" % (self.schema_name, MF_EXT) 347 manifest_path = utils.PathJoin(self.input_dir, manifest_filename) 348 manifest_content = utils.ReadFile(manifest_path).splitlines() 349 manifest_files = {} 350 regexp = r"SHA1\((\S+)\)= (\S+)" 351 for line in manifest_content: 352 match = re.match(regexp, line) 353 if match: 354 file_name = match.group(1) 355 sha1_sum = match.group(2) 356 manifest_files[file_name] = sha1_sum 357 files_with_paths = [utils.PathJoin(self.input_dir, file_name) 358 for file_name in self.files_list] 359 sha1_sums = utils.FingerprintFiles(files_with_paths) 360 for file_name, value in manifest_files.iteritems(): 361 if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value: 362 raise errors.OpPrereqError("SHA1 checksum of %s does not match the" 363 " value in manifest file" % file_name) 364 logging.info("SHA1 checksums verified")
365
366 - def GetInstanceName(self):
367 """Provides information about instance name. 368 369 @rtype: string 370 @return: instance name string 371 372 """ 373 find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA) 374 return self.tree.findtext(find_name)
375
376 - def GetDiskTemplate(self):
377 """Returns disk template from .ovf file 378 379 @rtype: string or None 380 @return: name of the template 381 """ 382 find_template = ("{%s}GanetiSection/{%s}DiskTemplate" % 383 (GANETI_SCHEMA, GANETI_SCHEMA)) 384 return self.tree.findtext(find_template)
385
386 - def GetHypervisorData(self):
387 """Provides hypervisor information - hypervisor name and options. 388 389 @rtype: dict 390 @return: dictionary containing name of the used hypervisor and all the 391 specified options 392 393 """ 394 hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" % 395 (GANETI_SCHEMA, GANETI_SCHEMA)) 396 hypervisor_data = self.tree.find(hypervisor_search) 397 if not hypervisor_data: 398 return {"hypervisor_name": constants.VALUE_AUTO} 399 results = { 400 "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA, 401 default=constants.VALUE_AUTO), 402 } 403 parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA) 404 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA)) 405 return results
406
407 - def GetOSData(self):
408 """ Provides operating system information - os name and options. 409 410 @rtype: dict 411 @return: dictionary containing name and options for the chosen OS 412 413 """ 414 results = {} 415 os_search = ("{%s}GanetiSection/{%s}OperatingSystem" % 416 (GANETI_SCHEMA, GANETI_SCHEMA)) 417 os_data = self.tree.find(os_search) 418 if os_data: 419 results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA) 420 parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA) 421 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA)) 422 return results
423
424 - def GetBackendData(self):
425 """ Provides backend information - vcpus, memory, auto balancing options. 426 427 @rtype: dict 428 @return: dictionary containing options for vcpus, memory and auto balance 429 settings 430 431 """ 432 results = {} 433 434 find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" % 435 (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA)) 436 match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"]) 437 vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus) 438 if vcpus: 439 vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA, 440 default=constants.VALUE_AUTO) 441 else: 442 vcpus_count = constants.VALUE_AUTO 443 results["vcpus"] = str(vcpus_count) 444 445 find_memory = find_vcpus 446 match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"]) 447 memory = self._GetElementMatchingText(find_memory, match_memory) 448 memory_raw = None 449 if memory: 450 alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA) 451 matching_units = [units for units, variants in 452 ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants] 453 if matching_units == []: 454 raise errors.OpPrereqError("Unit %s for RAM memory unknown", 455 alloc_units) 456 units = matching_units[0] 457 memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA, 458 default=constants.VALUE_AUTO)) 459 memory_count = CONVERT_UNITS_TO_MB[units](memory_raw) 460 else: 461 memory_count = constants.VALUE_AUTO 462 results["memory"] = str(memory_count) 463 464 find_balance = ("{%s}GanetiSection/{%s}AutoBalance" % 465 (GANETI_SCHEMA, GANETI_SCHEMA)) 466 balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO) 467 results["auto_balance"] = balance 468 469 return results
470
471 - def GetTagsData(self):
472 """Provides tags information for instance. 473 474 @rtype: string or None 475 @return: string of comma-separated tags for the instance 476 477 """ 478 find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA) 479 results = self.tree.findtext(find_tags) 480 if results: 481 return results 482 else: 483 return None
484
485 - def GetVersionData(self):
486 """Provides version number read from .ovf file 487 488 @rtype: string 489 @return: string containing the version number 490 491 """ 492 find_version = ("{%s}GanetiSection/{%s}Version" % 493 (GANETI_SCHEMA, GANETI_SCHEMA)) 494 return self.tree.findtext(find_version)
495
496 - def GetNetworkData(self):
497 """Provides data about the network in the OVF instance. 498 499 The method gathers the data about networks used by OVF instance. It assumes 500 that 'name' tag means something - in essence, if it contains one of the 501 words 'bridged' or 'routed' then that will be the mode of this network in 502 Ganeti. The information about the network can be either in GanetiSection or 503 VirtualHardwareSection. 504 505 @rtype: dict 506 @return: dictionary containing all the network information 507 508 """ 509 results = {} 510 networks_search = ("{%s}NetworkSection/{%s}Network" % 511 (OVF_SCHEMA, OVF_SCHEMA)) 512 network_names = self._GetAttributes(networks_search, 513 "{%s}name" % OVF_SCHEMA) 514 required = ["ip", "mac", "link", "mode"] 515 for (counter, network_name) in enumerate(network_names): 516 network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" 517 % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA)) 518 ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" % 519 (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA)) 520 network_match = ("{%s}Connection" % RASD_SCHEMA, network_name) 521 ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name) 522 network_data = self._GetElementMatchingText(network_search, network_match) 523 network_ganeti_data = self._GetElementMatchingAttr(ganeti_search, 524 ganeti_match) 525 526 ganeti_data = {} 527 if network_ganeti_data: 528 ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" % 529 GANETI_SCHEMA) 530 ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" % 531 GANETI_SCHEMA) 532 ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" % 533 GANETI_SCHEMA) 534 ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" % 535 GANETI_SCHEMA) 536 mac_data = None 537 if network_data: 538 mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA) 539 540 network_name = network_name.lower() 541 542 # First, some not Ganeti-specific information is collected 543 if constants.NIC_MODE_BRIDGED in network_name: 544 results["nic%s_mode" % counter] = "bridged" 545 elif constants.NIC_MODE_ROUTED in network_name: 546 results["nic%s_mode" % counter] = "routed" 547 results["nic%s_mac" % counter] = mac_data 548 549 # GanetiSection data overrides 'manually' collected data 550 for name, value in ganeti_data.iteritems(): 551 results["nic%s_%s" % (counter, name)] = value 552 553 # Bridged network has no IP - unless specifically stated otherwise 554 if (results.get("nic%s_mode" % counter) == "bridged" and 555 not results.get("nic%s_ip" % counter)): 556 results["nic%s_ip" % counter] = constants.VALUE_NONE 557 558 for option in required: 559 if not results.get("nic%s_%s" % (counter, option)): 560 results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO 561 562 if network_names: 563 results["nic_count"] = str(len(network_names)) 564 return results
565
566 - def GetDisksNames(self):
567 """Provides list of file names for the disks used by the instance. 568 569 @rtype: list 570 @return: list of file names, as referenced in .ovf file 571 572 """ 573 results = [] 574 disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA) 575 disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA) 576 for disk in disk_ids: 577 disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA) 578 disk_match = ("{%s}id" % OVF_SCHEMA, disk) 579 disk_elem = self._GetElementMatchingAttr(disk_search, disk_match) 580 if disk_elem is None: 581 raise errors.OpPrereqError("%s file corrupted - disk %s not found in" 582 " references" % (OVF_EXT, disk)) 583 disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA) 584 disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA) 585 results.append((disk_name, disk_compression)) 586 return results
587
588 589 -def SubElementText(parent, tag, text, attrib={}, **extra):
590 # pylint: disable=W0102 591 """This is just a wrapper on ET.SubElement that always has text content. 592 593 """ 594 if text is None: 595 return None 596 elem = ET.SubElement(parent, tag, attrib=attrib, **extra) 597 elem.text = str(text) 598 return elem
599
600 601 -class OVFWriter(object):
602 """Writer class for OVF files. 603 604 @type tree: ET.ElementTree 605 @ivar tree: XML tree that we are constructing 606 @type virtual_system_type: string 607 @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage 608 in VMWare this requires to be vmx 609 @type hardware_list: list 610 @ivar hardware_list: list of items prepared for VirtualHardwareSection 611 @type next_instance_id: int 612 @ivar next_instance_id: next instance id to be used when creating elements on 613 hardware_list 614 615 """
616 - def __init__(self, has_gnt_section):
617 """Initialize the writer - set the top element. 618 619 @type has_gnt_section: bool 620 @param has_gnt_section: if the Ganeti schema should be added - i.e. this 621 means that Ganeti section will be present 622 623 """ 624 env_attribs = { 625 "xmlns:xsi": XML_SCHEMA, 626 "xmlns:vssd": VSSD_SCHEMA, 627 "xmlns:rasd": RASD_SCHEMA, 628 "xmlns:ovf": OVF_SCHEMA, 629 "xmlns": OVF_SCHEMA, 630 "xml:lang": "en-US", 631 } 632 if has_gnt_section: 633 env_attribs["xmlns:gnt"] = GANETI_SCHEMA 634 self.virtual_system_type = VS_TYPE["ganeti"] 635 else: 636 self.virtual_system_type = VS_TYPE["external"] 637 self.tree = ET.Element("Envelope", attrib=env_attribs) 638 self.hardware_list = [] 639 # INSTANCE_ID contains statically assigned IDs, starting from 0 640 self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
641
642 - def SaveDisksData(self, disks):
643 """Convert disk information to certain OVF sections. 644 645 @type disks: list 646 @param disks: list of dictionaries of disk options from config.ini 647 648 """ 649 references = ET.SubElement(self.tree, "References") 650 disk_section = ET.SubElement(self.tree, "DiskSection") 651 SubElementText(disk_section, "Info", "Virtual disk information") 652 for counter, disk in enumerate(disks): 653 file_id = "file%s" % counter 654 disk_id = "disk%s" % counter 655 file_attribs = { 656 "ovf:href": disk["path"], 657 "ovf:size": str(disk["real-size"]), 658 "ovf:id": file_id, 659 } 660 disk_attribs = { 661 "ovf:capacity": str(disk["virt-size"]), 662 "ovf:diskId": disk_id, 663 "ovf:fileRef": file_id, 664 "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]), 665 } 666 if "compression" in disk: 667 file_attribs["ovf:compression"] = disk["compression"] 668 ET.SubElement(references, "File", attrib=file_attribs) 669 ET.SubElement(disk_section, "Disk", attrib=disk_attribs) 670 671 # Item in VirtualHardwareSection creation 672 disk_item = ET.Element("Item") 673 SubElementText(disk_item, "rasd:ElementName", disk_id) 674 SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id) 675 SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id) 676 SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"]) 677 SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"]) 678 self.hardware_list.append(disk_item) 679 self.next_instance_id += 1
680
681 - def SaveNetworksData(self, networks):
682 """Convert network information to NetworkSection. 683 684 @type networks: list 685 @param networks: list of dictionaries of network options form config.ini 686 687 """ 688 network_section = ET.SubElement(self.tree, "NetworkSection") 689 SubElementText(network_section, "Info", "List of logical networks") 690 for counter, network in enumerate(networks): 691 network_name = "%s%s" % (network["mode"], counter) 692 network_attrib = {"ovf:name": network_name} 693 ET.SubElement(network_section, "Network", attrib=network_attrib) 694 695 # Item in VirtualHardwareSection creation 696 network_item = ET.Element("Item") 697 SubElementText(network_item, "rasd:Address", network["mac"]) 698 SubElementText(network_item, "rasd:Connection", network_name) 699 SubElementText(network_item, "rasd:ElementName", network_name) 700 SubElementText(network_item, "rasd:InstanceID", self.next_instance_id) 701 SubElementText(network_item, "rasd:ResourceType", 702 RASD_TYPE["ethernet-adapter"]) 703 self.hardware_list.append(network_item) 704 self.next_instance_id += 1
705 706 @staticmethod
707 - def _SaveNameAndParams(root, data):
708 """Save name and parameters information under root using data. 709 710 @type root: ET.Element 711 @param root: root element for the Name and Parameters 712 @type data: dict 713 @param data: data from which we gather the values 714 715 """ 716 assert(data.get("name")) 717 name = SubElementText(root, "gnt:Name", data["name"]) 718 params = ET.SubElement(root, "gnt:Parameters") 719 for name, value in data.iteritems(): 720 if name != "name": 721 SubElementText(params, "gnt:%s" % name, value)
722
723 - def SaveGanetiData(self, ganeti, networks):
724 """Convert Ganeti-specific information to GanetiSection. 725 726 @type ganeti: dict 727 @param ganeti: dictionary of Ganeti-specific options from config.ini 728 @type networks: list 729 @param networks: list of dictionaries of network options form config.ini 730 731 """ 732 ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection") 733 734 SubElementText(ganeti_section, "gnt:Version", ganeti.get("version")) 735 SubElementText(ganeti_section, "gnt:DiskTemplate", 736 ganeti.get("disk_template")) 737 SubElementText(ganeti_section, "gnt:AutoBalance", 738 ganeti.get("auto_balance")) 739 SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags")) 740 741 osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem") 742 self._SaveNameAndParams(osys, ganeti["os"]) 743 744 hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor") 745 self._SaveNameAndParams(hypervisor, ganeti["hypervisor"]) 746 747 network_section = ET.SubElement(ganeti_section, "gnt:Network") 748 for counter, network in enumerate(networks): 749 network_name = "%s%s" % (network["mode"], counter) 750 nic_attrib = {"ovf:name": network_name} 751 nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib) 752 SubElementText(nic, "gnt:Mode", network["mode"]) 753 SubElementText(nic, "gnt:MACAddress", network["mac"]) 754 SubElementText(nic, "gnt:IPAddress", network["ip"]) 755 SubElementText(nic, "gnt:Link", network["link"])
756
757 - def SaveVirtualSystemData(self, name, vcpus, memory):
758 """Convert virtual system information to OVF sections. 759 760 @type name: string 761 @param name: name of the instance 762 @type vcpus: int 763 @param vcpus: number of VCPUs 764 @type memory: int 765 @param memory: RAM memory in MB 766 767 """ 768 assert(vcpus > 0) 769 assert(memory > 0) 770 vs_attrib = {"ovf:id": name} 771 virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib) 772 SubElementText(virtual_system, "Info", "A virtual machine") 773 774 name_section = ET.SubElement(virtual_system, "Name") 775 name_section.text = name 776 os_attrib = {"ovf:id": "0"} 777 os_section = ET.SubElement(virtual_system, "OperatingSystemSection", 778 attrib=os_attrib) 779 SubElementText(os_section, "Info", "Installed guest operating system") 780 hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection") 781 SubElementText(hardware_section, "Info", "Virtual hardware requirements") 782 783 # System description 784 system = ET.SubElement(hardware_section, "System") 785 SubElementText(system, "vssd:ElementName", "Virtual Hardware Family") 786 SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"]) 787 SubElementText(system, "vssd:VirtualSystemIdentifier", name) 788 SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type) 789 790 # Item for vcpus 791 vcpus_item = ET.SubElement(hardware_section, "Item") 792 SubElementText(vcpus_item, "rasd:ElementName", 793 "%s virtual CPU(s)" % vcpus) 794 SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"]) 795 SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"]) 796 SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus) 797 798 # Item for memory 799 memory_item = ET.SubElement(hardware_section, "Item") 800 SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20") 801 SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory) 802 SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"]) 803 SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"]) 804 SubElementText(memory_item, "rasd:VirtualQuantity", memory) 805 806 # Item for scsi controller 807 scsi_item = ET.SubElement(hardware_section, "Item") 808 SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"]) 809 SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0") 810 SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"]) 811 SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE) 812 SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"]) 813 814 # Other items - from self.hardware_list 815 for item in self.hardware_list: 816 hardware_section.append(item)
817
818 - def PrettyXmlDump(self):
819 """Formatter of the XML file. 820 821 @rtype: string 822 @return: XML tree in the form of nicely-formatted string 823 824 """ 825 raw_string = ET.tostring(self.tree) 826 parsed_xml = xml.dom.minidom.parseString(raw_string) 827 xml_string = parsed_xml.toprettyxml(indent=" ") 828 text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL) 829 return text_re.sub(">\g<1></", xml_string)
830
831 832 -class Converter(object):
833 """Converter class for OVF packages. 834 835 Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is 836 to provide a common interface for the two. 837 838 @type options: optparse.Values 839 @ivar options: options parsed from the command line 840 @type output_dir: string 841 @ivar output_dir: directory to which the results of conversion shall be 842 written 843 @type temp_file_manager: L{utils.TemporaryFileManager} 844 @ivar temp_file_manager: container for temporary files created during 845 conversion 846 @type temp_dir: string 847 @ivar temp_dir: temporary directory created then we deal with OVA 848 849 """
850 - def __init__(self, input_path, options):
851 """Initialize the converter. 852 853 @type input_path: string 854 @param input_path: path to the Converter input file 855 @type options: optparse.Values 856 @param options: command line options 857 858 @raise errors.OpPrereqError: if file does not exist 859 860 """ 861 input_path = os.path.abspath(input_path) 862 if not os.path.isfile(input_path): 863 raise errors.OpPrereqError("File does not exist: %s" % input_path) 864 self.options = options 865 self.temp_file_manager = utils.TemporaryFileManager() 866 self.temp_dir = None 867 self.output_dir = None 868 self._ReadInputData(input_path)
869
870 - def _ReadInputData(self, input_path):
871 """Reads the data on which the conversion will take place. 872 873 @type input_path: string 874 @param input_path: absolute path to the Converter input file 875 876 """ 877 raise NotImplementedError()
878
879 - def _CompressDisk(self, disk_path, compression, action):
880 """Performs (de)compression on the disk and returns the new path 881 882 @type disk_path: string 883 @param disk_path: path to the disk 884 @type compression: string 885 @param compression: compression type 886 @type action: string 887 @param action: whether the action is compression or decompression 888 @rtype: string 889 @return: new disk path after (de)compression 890 891 @raise errors.OpPrereqError: disk (de)compression failed or "compression" 892 is not supported 893 894 """ 895 assert(action in ALLOWED_ACTIONS) 896 # For now we only support gzip, as it is used in ovftool 897 if compression != COMPRESSION_TYPE: 898 raise errors.OpPrereqError("Unsupported compression type: %s" 899 % compression) 900 disk_file = os.path.basename(disk_path) 901 if action == DECOMPRESS: 902 (disk_name, _) = os.path.splitext(disk_file) 903 prefix = disk_name 904 elif action == COMPRESS: 905 prefix = disk_file 906 new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix, 907 dir=self.output_dir) 908 self.temp_file_manager.Add(new_path) 909 args = ["gzip", "-c", disk_path] 910 run_result = utils.RunCmd(args, output=new_path) 911 if run_result.failed: 912 raise errors.OpPrereqError("Disk %s failed with output: %s" 913 % (action, run_result.stderr)) 914 logging.info("The %s of the disk is completed", action) 915 return (COMPRESSION_EXT, new_path)
916
917 - def _ConvertDisk(self, disk_format, disk_path):
918 """Performes conversion to specified format. 919 920 @type disk_format: string 921 @param disk_format: format to which the disk should be converted 922 @type disk_path: string 923 @param disk_path: path to the disk that should be converted 924 @rtype: string 925 @return path to the output disk 926 927 @raise errors.OpPrereqError: convertion of the disk failed 928 929 """ 930 CheckQemuImg() 931 disk_file = os.path.basename(disk_path) 932 (disk_name, disk_extension) = os.path.splitext(disk_file) 933 if disk_extension != disk_format: 934 logging.warning("Conversion of disk image to %s format, this may take" 935 " a while", disk_format) 936 937 new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format, 938 prefix=disk_name, dir=self.output_dir) 939 self.temp_file_manager.Add(new_disk_path) 940 args = [ 941 constants.QEMUIMG_PATH, 942 "convert", 943 "-O", 944 disk_format, 945 disk_path, 946 new_disk_path, 947 ] 948 run_result = utils.RunCmd(args, cwd=os.getcwd()) 949 if run_result.failed: 950 raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was" 951 ": %s" % (disk_format, run_result.stderr)) 952 return (".%s" % disk_format, new_disk_path)
953 954 @staticmethod
955 - def _GetDiskQemuInfo(disk_path, regexp):
956 """Figures out some information of the disk using qemu-img. 957 958 @type disk_path: string 959 @param disk_path: path to the disk we want to know the format of 960 @type regexp: string 961 @param regexp: string that has to be matched, it has to contain one group 962 @rtype: string 963 @return: disk format 964 965 @raise errors.OpPrereqError: format information cannot be retrieved 966 967 """ 968 CheckQemuImg() 969 args = [constants.QEMUIMG_PATH, "info", disk_path] 970 run_result = utils.RunCmd(args, cwd=os.getcwd()) 971 if run_result.failed: 972 raise errors.OpPrereqError("Gathering info about the disk using qemu-img" 973 " failed, output was: %s" % run_result.stderr) 974 result = run_result.output 975 regexp = r"%s" % regexp 976 match = re.search(regexp, result) 977 if match: 978 disk_format = match.group(1) 979 else: 980 raise errors.OpPrereqError("No file information matching %s found in:" 981 " %s" % (regexp, result)) 982 return disk_format
983
984 - def Parse(self):
985 """Parses the data and creates a structure containing all required info. 986 987 """ 988 raise NotImplementedError()
989
990 - def Save(self):
991 """Saves the gathered configuration in an apropriate format. 992 993 """ 994 raise NotImplementedError()
995
996 - def Cleanup(self):
997 """Cleans the temporary directory, if one was created. 998 999 """ 1000 self.temp_file_manager.Cleanup() 1001 if self.temp_dir: 1002 shutil.rmtree(self.temp_dir) 1003 self.temp_dir = None
1004
1005 1006 -class OVFImporter(Converter):
1007 """Converter from OVF to Ganeti config file. 1008 1009 @type input_dir: string 1010 @ivar input_dir: directory in which the .ovf file resides 1011 @type output_dir: string 1012 @ivar output_dir: directory to which the results of conversion shall be 1013 written 1014 @type input_path: string 1015 @ivar input_path: complete path to the .ovf file 1016 @type ovf_reader: L{OVFReader} 1017 @ivar ovf_reader: OVF reader instance collects data from .ovf file 1018 @type results_name: string 1019 @ivar results_name: name of imported instance 1020 @type results_template: string 1021 @ivar results_template: disk template read from .ovf file or command line 1022 arguments 1023 @type results_hypervisor: dict 1024 @ivar results_hypervisor: hypervisor information gathered from .ovf file or 1025 command line arguments 1026 @type results_os: dict 1027 @ivar results_os: operating system information gathered from .ovf file or 1028 command line arguments 1029 @type results_backend: dict 1030 @ivar results_backend: backend information gathered from .ovf file or 1031 command line arguments 1032 @type results_tags: string 1033 @ivar results_tags: string containing instance-specific tags 1034 @type results_version: string 1035 @ivar results_version: version as required by Ganeti import 1036 @type results_network: dict 1037 @ivar results_network: network information gathered from .ovf file or command 1038 line arguments 1039 @type results_disk: dict 1040 @ivar results_disk: disk information gathered from .ovf file or command line 1041 arguments 1042 1043 """
1044 - def _ReadInputData(self, input_path):
1045 """Reads the data on which the conversion will take place. 1046 1047 @type input_path: string 1048 @param input_path: absolute path to the .ovf or .ova input file 1049 1050 @raise errors.OpPrereqError: if input file is neither .ovf nor .ova 1051 1052 """ 1053 (input_dir, input_file) = os.path.split(input_path) 1054 (_, input_extension) = os.path.splitext(input_file) 1055 1056 if input_extension == OVF_EXT: 1057 logging.info("%s file extension found, no unpacking necessary", OVF_EXT) 1058 self.input_dir = input_dir 1059 self.input_path = input_path 1060 self.temp_dir = None 1061 elif input_extension == OVA_EXT: 1062 logging.info("%s file extension found, proceeding to unpacking", OVA_EXT) 1063 self._UnpackOVA(input_path) 1064 else: 1065 raise errors.OpPrereqError("Unknown file extension; expected %s or %s" 1066 " file" % (OVA_EXT, OVF_EXT)) 1067 assert ((input_extension == OVA_EXT and self.temp_dir) or 1068 (input_extension == OVF_EXT and not self.temp_dir)) 1069 assert self.input_dir in self.input_path 1070 1071 if self.options.output_dir: 1072 self.output_dir = os.path.abspath(self.options.output_dir) 1073 if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) != 1074 constants.EXPORT_DIR): 1075 logging.warning("Export path is not under %s directory, import to" 1076 " Ganeti using gnt-backup may fail", 1077 constants.EXPORT_DIR) 1078 else: 1079 self.output_dir = constants.EXPORT_DIR 1080 1081 self.ovf_reader = OVFReader(self.input_path) 1082 self.ovf_reader.VerifyManifest()
1083
1084 - def _UnpackOVA(self, input_path):
1085 """Unpacks the .ova package into temporary directory. 1086 1087 @type input_path: string 1088 @param input_path: path to the .ova package file 1089 1090 @raise errors.OpPrereqError: if file is not a proper tarball, one of the 1091 files in the archive seem malicious (e.g. path starts with '../') or 1092 .ova package does not contain .ovf file 1093 1094 """ 1095 input_name = None 1096 if not tarfile.is_tarfile(input_path): 1097 raise errors.OpPrereqError("The provided %s file is not a proper tar" 1098 " archive", OVA_EXT) 1099 ova_content = tarfile.open(input_path) 1100 temp_dir = tempfile.mkdtemp() 1101 self.temp_dir = temp_dir 1102 for file_name in ova_content.getnames(): 1103 file_normname = os.path.normpath(file_name) 1104 try: 1105 utils.PathJoin(temp_dir, file_normname) 1106 except ValueError, err: 1107 raise errors.OpPrereqError("File %s inside %s package is not safe" % 1108 (file_name, OVA_EXT)) 1109 if file_name.endswith(OVF_EXT): 1110 input_name = file_name 1111 if not input_name: 1112 raise errors.OpPrereqError("No %s file in %s package found" % 1113 (OVF_EXT, OVA_EXT)) 1114 logging.warning("Unpacking the %s archive, this may take a while", 1115 input_path) 1116 self.input_dir = temp_dir 1117 self.input_path = utils.PathJoin(self.temp_dir, input_name) 1118 try: 1119 try: 1120 extract = ova_content.extractall 1121 except AttributeError: 1122 # This is a prehistorical case of using python < 2.5 1123 for member in ova_content.getmembers(): 1124 ova_content.extract(member, path=self.temp_dir) 1125 else: 1126 extract(self.temp_dir) 1127 except tarfile.TarError, err: 1128 raise errors.OpPrereqError("Error while extracting %s archive: %s" % 1129 (OVA_EXT, err)) 1130 logging.info("OVA package extracted to %s directory", self.temp_dir)
1131
1132 - def Parse(self):
1133 """Parses the data and creates a structure containing all required info. 1134 1135 The method reads the information given either as a command line option or as 1136 a part of the OVF description. 1137 1138 @raise errors.OpPrereqError: if some required part of the description of 1139 virtual instance is missing or unable to create output directory 1140 1141 """ 1142 self.results_name = self._GetInfo("instance name", self.options.name, 1143 self._ParseNameOptions, self.ovf_reader.GetInstanceName) 1144 if not self.results_name: 1145 raise errors.OpPrereqError("Name of instance not provided") 1146 1147 self.output_dir = utils.PathJoin(self.output_dir, self.results_name) 1148 try: 1149 utils.Makedirs(self.output_dir) 1150 except OSError, err: 1151 raise errors.OpPrereqError("Failed to create directory %s: %s" % 1152 (self.output_dir, err)) 1153 1154 self.results_template = self._GetInfo("disk template", 1155 self.options.disk_template, self._ParseTemplateOptions, 1156 self.ovf_reader.GetDiskTemplate) 1157 if not self.results_template: 1158 logging.info("Disk template not given") 1159 1160 self.results_hypervisor = self._GetInfo("hypervisor", 1161 self.options.hypervisor, self._ParseHypervisorOptions, 1162 self.ovf_reader.GetHypervisorData) 1163 assert self.results_hypervisor["hypervisor_name"] 1164 if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO: 1165 logging.debug("Default hypervisor settings from the cluster will be used") 1166 1167 self.results_os = self._GetInfo("OS", self.options.os, 1168 self._ParseOSOptions, self.ovf_reader.GetOSData) 1169 if not self.results_os.get("os_name"): 1170 raise errors.OpPrereqError("OS name must be provided") 1171 1172 self.results_backend = self._GetInfo("backend", self.options.beparams, 1173 self._ParseBackendOptions, self.ovf_reader.GetBackendData) 1174 assert self.results_backend.get("vcpus") 1175 assert self.results_backend.get("memory") 1176 assert self.results_backend.get("auto_balance") is not None 1177 1178 self.results_tags = self._GetInfo("tags", self.options.tags, 1179 self._ParseTags, self.ovf_reader.GetTagsData) 1180 1181 ovf_version = self.ovf_reader.GetVersionData() 1182 if ovf_version: 1183 self.results_version = ovf_version 1184 else: 1185 self.results_version = constants.EXPORT_VERSION 1186 1187 self.results_network = self._GetInfo("network", self.options.nics, 1188 self._ParseNicOptions, self.ovf_reader.GetNetworkData, 1189 ignore_test=self.options.no_nics) 1190 1191 self.results_disk = self._GetInfo("disk", self.options.disks, 1192 self._ParseDiskOptions, self._GetDiskInfo, 1193 ignore_test=self.results_template == constants.DT_DISKLESS) 1194 1195 if not self.results_disk and not self.results_network: 1196 raise errors.OpPrereqError("Either disk specification or network" 1197 " description must be present")
1198 1199 @staticmethod
1200 - def _GetInfo(name, cmd_arg, cmd_function, nocmd_function, 1201 ignore_test=False):
1202 """Get information about some section - e.g. disk, network, hypervisor. 1203 1204 @type name: string 1205 @param name: name of the section 1206 @type cmd_arg: dict 1207 @param cmd_arg: command line argument specific for section 'name' 1208 @type cmd_function: callable 1209 @param cmd_function: function to call if 'cmd_args' exists 1210 @type nocmd_function: callable 1211 @param nocmd_function: function to call if 'cmd_args' is not there 1212 1213 """ 1214 if ignore_test: 1215 logging.info("Information for %s will be ignored", name) 1216 return {} 1217 if cmd_arg: 1218 logging.info("Information for %s will be parsed from command line", name) 1219 results = cmd_function() 1220 else: 1221 logging.info("Information for %s will be parsed from %s file", 1222 name, OVF_EXT) 1223 results = nocmd_function() 1224 logging.info("Options for %s were succesfully read", name) 1225 return results
1226
1227 - def _ParseNameOptions(self):
1228 """Returns name if one was given in command line. 1229 1230 @rtype: string 1231 @return: name of an instance 1232 1233 """ 1234 return self.options.name
1235
1236 - def _ParseTemplateOptions(self):
1237 """Returns disk template if one was given in command line. 1238 1239 @rtype: string 1240 @return: disk template name 1241 1242 """ 1243 return self.options.disk_template
1244
1245 - def _ParseHypervisorOptions(self):
1246 """Parses hypervisor options given in a command line. 1247 1248 @rtype: dict 1249 @return: dictionary containing name of the chosen hypervisor and all the 1250 options 1251 1252 """ 1253 assert type(self.options.hypervisor) is tuple 1254 assert len(self.options.hypervisor) == 2 1255 results = {} 1256 if self.options.hypervisor[0]: 1257 results["hypervisor_name"] = self.options.hypervisor[0] 1258 else: 1259 results["hypervisor_name"] = constants.VALUE_AUTO 1260 results.update(self.options.hypervisor[1]) 1261 return results
1262
1263 - def _ParseOSOptions(self):
1264 """Parses OS options given in command line. 1265 1266 @rtype: dict 1267 @return: dictionary containing name of chosen OS and all its options 1268 1269 """ 1270 assert self.options.os 1271 results = {} 1272 results["os_name"] = self.options.os 1273 results.update(self.options.osparams) 1274 return results
1275
1276 - def _ParseBackendOptions(self):
1277 """Parses backend options given in command line. 1278 1279 @rtype: dict 1280 @return: dictionary containing vcpus, memory and auto-balance options 1281 1282 """ 1283 assert self.options.beparams 1284 backend = {} 1285 backend.update(self.options.beparams) 1286 must_contain = ["vcpus", "memory", "auto_balance"] 1287 for element in must_contain: 1288 if backend.get(element) is None: 1289 backend[element] = constants.VALUE_AUTO 1290 return backend
1291
1292 - def _ParseTags(self):
1293 """Returns tags list given in command line. 1294 1295 @rtype: string 1296 @return: string containing comma-separated tags 1297 1298 """ 1299 return self.options.tags
1300
1301 - def _ParseNicOptions(self):
1302 """Parses network options given in a command line or as a dictionary. 1303 1304 @rtype: dict 1305 @return: dictionary of network-related options 1306 1307 """ 1308 assert self.options.nics 1309 results = {} 1310 for (nic_id, nic_desc) in self.options.nics: 1311 results["nic%s_mode" % nic_id] = \ 1312 nic_desc.get("mode", constants.VALUE_AUTO) 1313 results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO) 1314 results["nic%s_link" % nic_id] = \ 1315 nic_desc.get("link", constants.VALUE_AUTO) 1316 if nic_desc.get("mode") == "bridged": 1317 results["nic%s_ip" % nic_id] = constants.VALUE_NONE 1318 else: 1319 results["nic%s_ip" % nic_id] = constants.VALUE_AUTO 1320 results["nic_count"] = str(len(self.options.nics)) 1321 return results
1322
1323 - def _ParseDiskOptions(self):
1324 """Parses disk options given in a command line. 1325 1326 @rtype: dict 1327 @return: dictionary of disk-related options 1328 1329 @raise errors.OpPrereqError: disk description does not contain size 1330 information or size information is invalid or creation failed 1331 1332 """ 1333 CheckQemuImg() 1334 assert self.options.disks 1335 results = {} 1336 for (disk_id, disk_desc) in self.options.disks: 1337 results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id 1338 if disk_desc.get("size"): 1339 try: 1340 disk_size = utils.ParseUnit(disk_desc["size"]) 1341 except ValueError: 1342 raise errors.OpPrereqError("Invalid disk size for disk %s: %s" % 1343 (disk_id, disk_desc["size"])) 1344 new_path = utils.PathJoin(self.output_dir, str(disk_id)) 1345 args = [ 1346 constants.QEMUIMG_PATH, 1347 "create", 1348 "-f", 1349 "raw", 1350 new_path, 1351 disk_size, 1352 ] 1353 run_result = utils.RunCmd(args) 1354 if run_result.failed: 1355 raise errors.OpPrereqError("Creation of disk %s failed, output was:" 1356 " %s" % (new_path, run_result.stderr)) 1357 results["disk%s_size" % disk_id] = str(disk_size) 1358 results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id 1359 else: 1360 raise errors.OpPrereqError("Disks created for import must have their" 1361 " size specified") 1362 results["disk_count"] = str(len(self.options.disks)) 1363 return results
1364
1365 - def _GetDiskInfo(self):
1366 """Gathers information about disks used by instance, perfomes conversion. 1367 1368 @rtype: dict 1369 @return: dictionary of disk-related options 1370 1371 @raise errors.OpPrereqError: disk is not in the same directory as .ovf file 1372 1373 """ 1374 results = {} 1375 disks_list = self.ovf_reader.GetDisksNames() 1376 for (counter, (disk_name, disk_compression)) in enumerate(disks_list): 1377 if os.path.dirname(disk_name): 1378 raise errors.OpPrereqError("Disks are not allowed to have absolute" 1379 " paths or paths outside main OVF directory") 1380 disk, _ = os.path.splitext(disk_name) 1381 disk_path = utils.PathJoin(self.input_dir, disk_name) 1382 if disk_compression not in NO_COMPRESSION: 1383 _, disk_path = self._CompressDisk(disk_path, disk_compression, 1384 DECOMPRESS) 1385 disk, _ = os.path.splitext(disk) 1386 if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw": 1387 logging.info("Conversion to raw format is required") 1388 ext, new_disk_path = self._ConvertDisk("raw", disk_path) 1389 1390 final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext, 1391 directory=self.output_dir) 1392 final_name = os.path.basename(final_disk_path) 1393 disk_size = os.path.getsize(final_disk_path) / (1024 * 1024) 1394 results["disk%s_dump" % counter] = final_name 1395 results["disk%s_size" % counter] = str(disk_size) 1396 results["disk%s_ivname" % counter] = "disk/%s" % str(counter) 1397 if disks_list: 1398 results["disk_count"] = str(len(disks_list)) 1399 return results
1400
1401 - def Save(self):
1402 """Saves all the gathered information in a constant.EXPORT_CONF_FILE file. 1403 1404 @raise errors.OpPrereqError: when saving to config file failed 1405 1406 """ 1407 logging.info("Conversion was succesfull, saving %s in %s directory", 1408 constants.EXPORT_CONF_FILE, self.output_dir) 1409 results = { 1410 constants.INISECT_INS: {}, 1411 constants.INISECT_BEP: {}, 1412 constants.INISECT_EXP: {}, 1413 constants.INISECT_OSP: {}, 1414 constants.INISECT_HYP: {}, 1415 } 1416 1417 results[constants.INISECT_INS].update(self.results_disk) 1418 results[constants.INISECT_INS].update(self.results_network) 1419 results[constants.INISECT_INS]["hypervisor"] = \ 1420 self.results_hypervisor["hypervisor_name"] 1421 results[constants.INISECT_INS]["name"] = self.results_name 1422 if self.results_template: 1423 results[constants.INISECT_INS]["disk_template"] = self.results_template 1424 if self.results_tags: 1425 results[constants.INISECT_INS]["tags"] = self.results_tags 1426 1427 results[constants.INISECT_BEP].update(self.results_backend) 1428 1429 results[constants.INISECT_EXP]["os"] = self.results_os["os_name"] 1430 results[constants.INISECT_EXP]["version"] = self.results_version 1431 1432 del self.results_os["os_name"] 1433 results[constants.INISECT_OSP].update(self.results_os) 1434 1435 del self.results_hypervisor["hypervisor_name"] 1436 results[constants.INISECT_HYP].update(self.results_hypervisor) 1437 1438 output_file_name = utils.PathJoin(self.output_dir, 1439 constants.EXPORT_CONF_FILE) 1440 1441 output = [] 1442 for section, options in results.iteritems(): 1443 output.append("[%s]" % section) 1444 for name, value in options.iteritems(): 1445 if value is None: 1446 value = "" 1447 output.append("%s = %s" % (name, value)) 1448 output.append("") 1449 output_contents = "\n".join(output) 1450 1451 try: 1452 utils.WriteFile(output_file_name, data=output_contents) 1453 except errors.ProgrammerError, err: 1454 raise errors.OpPrereqError("Saving the config file failed: %s" % err) 1455 1456 self.Cleanup()
1457
1458 1459 -class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1460 """This is just a wrapper on SafeConfigParser, that uses default values 1461 1462 """
1463 - def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1464 try: 1465 result = ConfigParser.SafeConfigParser.get(self, section, options, \ 1466 raw=raw, vars=vars) 1467 except ConfigParser.NoOptionError: 1468 result = None 1469 return result
1470
1471 - def getint(self, section, options):
1472 try: 1473 result = ConfigParser.SafeConfigParser.get(self, section, options) 1474 except ConfigParser.NoOptionError: 1475 result = 0 1476 return int(result)
1477
1478 1479 -class OVFExporter(Converter):
1480 """Converter from Ganeti config file to OVF 1481 1482 @type input_dir: string 1483 @ivar input_dir: directory in which the config.ini file resides 1484 @type output_dir: string 1485 @ivar output_dir: directory to which the results of conversion shall be 1486 written 1487 @type packed_dir: string 1488 @ivar packed_dir: if we want OVA package, this points to the real (i.e. not 1489 temp) output directory 1490 @type input_path: string 1491 @ivar input_path: complete path to the config.ini file 1492 @type output_path: string 1493 @ivar output_path: complete path to .ovf file 1494 @type config_parser: L{ConfigParserWithDefaults} 1495 @ivar config_parser: parser for the config.ini file 1496 @type reference_files: list 1497 @ivar reference_files: files referenced in the ovf file 1498 @type results_disk: list 1499 @ivar results_disk: list of dictionaries of disk options from config.ini 1500 @type results_network: list 1501 @ivar results_network: list of dictionaries of network options form config.ini 1502 @type results_name: string 1503 @ivar results_name: name of the instance 1504 @type results_vcpus: string 1505 @ivar results_vcpus: number of VCPUs 1506 @type results_memory: string 1507 @ivar results_memory: RAM memory in MB 1508 @type results_ganeti: dict 1509 @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini 1510 1511 """
1512 - def _ReadInputData(self, input_path):
1513 """Reads the data on which the conversion will take place. 1514 1515 @type input_path: string 1516 @param input_path: absolute path to the config.ini input file 1517 1518 @raise errors.OpPrereqError: error when reading the config file 1519 1520 """ 1521 input_dir = os.path.dirname(input_path) 1522 self.input_path = input_path 1523 self.input_dir = input_dir 1524 if self.options.output_dir: 1525 self.output_dir = os.path.abspath(self.options.output_dir) 1526 else: 1527 self.output_dir = input_dir 1528 self.config_parser = ConfigParserWithDefaults() 1529 logging.info("Reading configuration from %s file", input_path) 1530 try: 1531 self.config_parser.read(input_path) 1532 except ConfigParser.MissingSectionHeaderError, err: 1533 raise errors.OpPrereqError("Error when trying to read %s: %s" % 1534 (input_path, err)) 1535 if self.options.ova_package: 1536 self.temp_dir = tempfile.mkdtemp() 1537 self.packed_dir = self.output_dir 1538 self.output_dir = self.temp_dir 1539 1540 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1541
1542 - def _ParseName(self):
1543 """Parses name from command line options or config file. 1544 1545 @rtype: string 1546 @return: name of Ganeti instance 1547 1548 @raise errors.OpPrereqError: if name of the instance is not provided 1549 1550 """ 1551 if self.options.name: 1552 name = self.options.name 1553 else: 1554 name = self.config_parser.get(constants.INISECT_INS, NAME) 1555 if name is None: 1556 raise errors.OpPrereqError("No instance name found") 1557 return name
1558
1559 - def _ParseVCPUs(self):
1560 """Parses vcpus number from config file. 1561 1562 @rtype: int 1563 @return: number of virtual CPUs 1564 1565 @raise errors.OpPrereqError: if number of VCPUs equals 0 1566 1567 """ 1568 vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS) 1569 if vcpus == 0: 1570 raise errors.OpPrereqError("No CPU information found") 1571 return vcpus
1572
1573 - def _ParseMemory(self):
1574 """Parses vcpus number from config file. 1575 1576 @rtype: int 1577 @return: amount of memory in MB 1578 1579 @raise errors.OpPrereqError: if amount of memory equals 0 1580 1581 """ 1582 memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY) 1583 if memory == 0: 1584 raise errors.OpPrereqError("No memory information found") 1585 return memory
1586
1587 - def _ParseGaneti(self):
1588 """Parses Ganeti data from config file. 1589 1590 @rtype: dictionary 1591 @return: dictionary of Ganeti-specific options 1592 1593 """ 1594 results = {} 1595 # hypervisor 1596 results["hypervisor"] = {} 1597 hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV) 1598 if hyp_name is None: 1599 raise errors.OpPrereqError("No hypervisor information found") 1600 results["hypervisor"]["name"] = hyp_name 1601 pairs = self.config_parser.items(constants.INISECT_HYP) 1602 for (name, value) in pairs: 1603 results["hypervisor"][name] = value 1604 # os 1605 results["os"] = {} 1606 os_name = self.config_parser.get(constants.INISECT_EXP, OS) 1607 if os_name is None: 1608 raise errors.OpPrereqError("No operating system information found") 1609 results["os"]["name"] = os_name 1610 pairs = self.config_parser.items(constants.INISECT_OSP) 1611 for (name, value) in pairs: 1612 results["os"][name] = value 1613 # other 1614 others = [ 1615 (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"), 1616 (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"), 1617 (constants.INISECT_INS, TAGS, "tags"), 1618 (constants.INISECT_EXP, VERSION, "version"), 1619 ] 1620 for (section, element, name) in others: 1621 results[name] = self.config_parser.get(section, element) 1622 return results
1623
1624 - def _ParseNetworks(self):
1625 """Parses network data from config file. 1626 1627 @rtype: list 1628 @return: list of dictionaries of network options 1629 1630 @raise errors.OpPrereqError: then network mode is not recognized 1631 1632 """ 1633 results = [] 1634 counter = 0 1635 while True: 1636 data_link = \ 1637 self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter) 1638 if data_link is None: 1639 break 1640 results.append({ 1641 "mode": self.config_parser.get(constants.INISECT_INS, 1642 "nic%s_mode" % counter), 1643 "mac": self.config_parser.get(constants.INISECT_INS, 1644 "nic%s_mac" % counter), 1645 "ip": self.config_parser.get(constants.INISECT_INS, 1646 "nic%s_ip" % counter), 1647 "link": data_link, 1648 }) 1649 if results[counter]["mode"] not in constants.NIC_VALID_MODES: 1650 raise errors.OpPrereqError("Network mode %s not recognized" 1651 % results[counter]["mode"]) 1652 counter += 1 1653 return results
1654
1655 - def _GetDiskOptions(self, disk_file, compression):
1656 """Convert the disk and gather disk info for .ovf file. 1657 1658 @type disk_file: string 1659 @param disk_file: name of the disk (without the full path) 1660 @type compression: bool 1661 @param compression: whether the disk should be compressed or not 1662 1663 @raise errors.OpPrereqError: when disk image does not exist 1664 1665 """ 1666 disk_path = utils.PathJoin(self.input_dir, disk_file) 1667 results = {} 1668 if not os.path.isfile(disk_path): 1669 raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path) 1670 if os.path.dirname(disk_file): 1671 raise errors.OpPrereqError("Path for the disk: %s contains a directory" 1672 " name" % disk_path) 1673 disk_name, _ = os.path.splitext(disk_file) 1674 ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path) 1675 results["format"] = self.options.disk_format 1676 results["virt-size"] = self._GetDiskQemuInfo(new_disk_path, 1677 "virtual size: \S+ \((\d+) bytes\)") 1678 if compression: 1679 ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip", 1680 COMPRESS) 1681 disk_name, _ = os.path.splitext(disk_name) 1682 results["compression"] = "gzip" 1683 ext += ext2 1684 final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext, 1685 directory=self.output_dir) 1686 final_disk_name = os.path.basename(final_disk_path) 1687 results["real-size"] = os.path.getsize(final_disk_path) 1688 results["path"] = final_disk_name 1689 self.references_files.append(final_disk_path) 1690 return results
1691
1692 - def _ParseDisks(self):
1693 """Parses disk data from config file. 1694 1695 @rtype: list 1696 @return: list of dictionaries of disk options 1697 1698 """ 1699 results = [] 1700 counter = 0 1701 while True: 1702 disk_file = \ 1703 self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter) 1704 if disk_file is None: 1705 break 1706 results.append(self._GetDiskOptions(disk_file, self.options.compression)) 1707 counter += 1 1708 return results
1709
1710 - def Parse(self):
1711 """Parses the data and creates a structure containing all required info. 1712 1713 """ 1714 try: 1715 utils.Makedirs(self.output_dir) 1716 except OSError, err: 1717 raise errors.OpPrereqError("Failed to create directory %s: %s" % 1718 (self.output_dir, err)) 1719 1720 self.references_files = [] 1721 self.results_name = self._ParseName() 1722 self.results_vcpus = self._ParseVCPUs() 1723 self.results_memory = self._ParseMemory() 1724 if not self.options.ext_usage: 1725 self.results_ganeti = self._ParseGaneti() 1726 self.results_network = self._ParseNetworks() 1727 self.results_disk = self._ParseDisks()
1728
1729 - def _PrepareManifest(self, path):
1730 """Creates manifest for all the files in OVF package. 1731 1732 @type path: string 1733 @param path: path to manifesto file 1734 1735 @raise errors.OpPrereqError: if error occurs when writing file 1736 1737 """ 1738 logging.info("Preparing manifest for the OVF package") 1739 lines = [] 1740 files_list = [self.output_path] 1741 files_list.extend(self.references_files) 1742 logging.warning("Calculating SHA1 checksums, this may take a while") 1743 sha1_sums = utils.FingerprintFiles(files_list) 1744 for file_path, value in sha1_sums.iteritems(): 1745 file_name = os.path.basename(file_path) 1746 lines.append("SHA1(%s)= %s" % (file_name, value)) 1747 lines.append("") 1748 data = "\n".join(lines) 1749 try: 1750 utils.WriteFile(path, data=data) 1751 except errors.ProgrammerError, err: 1752 raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1753 1754 @staticmethod
1755 - def _PrepareTarFile(tar_path, files_list):
1756 """Creates tarfile from the files in OVF package. 1757 1758 @type tar_path: string 1759 @param tar_path: path to the resulting file 1760 @type files_list: list 1761 @param files_list: list of files in the OVF package 1762 1763 """ 1764 logging.info("Preparing tarball for the OVF package") 1765 open(tar_path, mode="w").close() 1766 ova_package = tarfile.open(name=tar_path, mode="w") 1767 for file_path in files_list: 1768 file_name = os.path.basename(file_path) 1769 ova_package.add(file_path, arcname=file_name) 1770 ova_package.close()
1771
1772 - def Save(self):
1773 """Saves the gathered configuration in an apropriate format. 1774 1775 @raise errors.OpPrereqError: if unable to create output directory 1776 1777 """ 1778 output_file = "%s%s" % (self.results_name, OVF_EXT) 1779 output_path = utils.PathJoin(self.output_dir, output_file) 1780 self.ovf_writer = OVFWriter(not self.options.ext_usage) 1781 logging.info("Saving read data to %s", output_path) 1782 1783 self.output_path = utils.PathJoin(self.output_dir, output_file) 1784 files_list = [self.output_path] 1785 1786 self.ovf_writer.SaveDisksData(self.results_disk) 1787 self.ovf_writer.SaveNetworksData(self.results_network) 1788 if not self.options.ext_usage: 1789 self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network) 1790 1791 self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus, 1792 self.results_memory) 1793 1794 data = self.ovf_writer.PrettyXmlDump() 1795 utils.WriteFile(self.output_path, data=data) 1796 1797 manifest_file = "%s%s" % (self.results_name, MF_EXT) 1798 manifest_path = utils.PathJoin(self.output_dir, manifest_file) 1799 self._PrepareManifest(manifest_path) 1800 files_list.append(manifest_path) 1801 1802 files_list.extend(self.references_files) 1803 1804 if self.options.ova_package: 1805 ova_file = "%s%s" % (self.results_name, OVA_EXT) 1806 packed_path = utils.PathJoin(self.packed_dir, ova_file) 1807 try: 1808 utils.Makedirs(self.packed_dir) 1809 except OSError, err: 1810 raise errors.OpPrereqError("Failed to create directory %s: %s" % 1811 (self.packed_dir, err)) 1812 self._PrepareTarFile(packed_path, files_list) 1813 logging.info("Creation of the OVF package was successfull") 1814 self.Cleanup()
1815