1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Converter tools between ovf and ganeti config file
23
24 """
25
26
27
28
29
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
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
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
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
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
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
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
131 INSTANCE_ID = {
132 "system": 0,
133 "vcpus": 1,
134 "memory": 2,
135 "scsi": 3,
136 }
137
138
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 }
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
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 """
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
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
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
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
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
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
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
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
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
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
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
484
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
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
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
550 for name, value in ganeti_data.iteritems():
551 results["nic%s_%s" % (counter, name)] = value
552
553
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
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
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
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 """
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
640 self.next_instance_id = len(INSTANCE_ID)
641
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
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
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
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
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
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
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
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
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
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
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
815 for item in self.hardware_list:
816 hardware_section.append(item)
817
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
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
878
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
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
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
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
985 """Parses the data and creates a structure containing all required info.
986
987 """
988 raise NotImplementedError()
989
991 """Saves the gathered configuration in an apropriate format.
992
993 """
994 raise NotImplementedError()
995
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
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 """
1083
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
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
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
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
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
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
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
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
1300
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
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
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
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
1460 """This is just a wrapper on SafeConfigParser, that uses default values
1461
1462 """
1463 - def get(self, section, options, raw=None, vars=None):
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
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 """
1541
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
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
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
1588 """Parses Ganeti data from config file.
1589
1590 @rtype: dictionary
1591 @return: dictionary of Ganeti-specific options
1592
1593 """
1594 results = {}
1595
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
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
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
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
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
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
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
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
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
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