#!/usr/bin/python # -*- coding: utf-8 -*- # # guest-image-ovf-creator.py - Copyright (C) 2013 Red Hat, Inc. # Written by Joey Boggs # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. A copy of the GNU General Public License is # also available at http://www.gnu.org/copyleft/gpl.html. import logging import optparse import os import shutil import struct import sys import tarfile import tempfile import time import uuid LOG_PREFIX = "guest-image-ovf-creator" OVF_VERSION = "3.3.0.0" XML_TEMPLATE = """ %(NVR)s x86_64 1 524288 """ OVF_TEMPLATE = """
List of Networks
%(product_name)s %(product_name)s %(product_name)s %(timestamp)s false 1 1 1
Guest OS rhel_7x64
1 CPU, 512 Memory RHEVM 4.6.0.163 1 virtual CPU Number of virtual CPU 1 3 1 1 512 MB of memory Memory Size 2 4 MegaBytes 512 Drive 1 %(disk_file_name)s 17 %(disk_path)s 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000000 %(storage_pool_id)s %(timestamp)s %(timestamp)s Ethernet 0 rhevm 3 10 3 rhevm eth0 1000 Graphics 5 20 1
""" DISK_META_TEMPLATE = """DOMAIN=%(domain_uuid)s VOLTYPE=LEAF CTIME=%(create_time)s FORMAT=COW IMAGE=%(image_uuid)s DISKTYPE=1 PUUID=00000000-0000-0000-0000-000000000000 LEGALITY=LEGAL MTIME=%(create_time)s POOL_UUID=%(pool_id)s SIZE=%(disk_size)s TYPE=SPARSE DESCRIPTION=%(description)s EOF""" def initLogger(): logger = logging.getLogger(LOG_PREFIX) log_file = "/tmp/ovf-creator.log" formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)-8s - ' '%(message)s') conformatter = logging.Formatter('%(levelname)-8s' ' %(message)s') handler = logging.FileHandler(log_file) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.DEBUG) console = logging.StreamHandler() console.setLevel(logging.INFO) console.setFormatter(conformatter) logger.addHandler(console) class Base(object): def __init__(self): self._logger = logging.getLogger( '%s.%s' % (LOG_PREFIX, self.__class__.__name__)) class OVFCreator(Base): def __init__(self): super(OVFCreator, self).__init__() self._parse_options() if self._options.output: if not os.path.isdir(self._options.output): raise RuntimeError("Output path is not a directory") self._tmp_dir = self._options.output else: self._tmp_dir = tempfile.mkdtemp() self._image_name = os.path.basename(os.path.splitext( self._options.disk)[0]) self._logger.info("Image Name: %s" % self._image_name) self._raw_create_time = time.time() self._create_time = time.gmtime(self._raw_create_time) self._images_dir = os.path.join(self._tmp_dir, "images") self._master_dir = os.path.join(self._tmp_dir, "master") self._images_vm_dir = os.path.join(self._images_dir, self._image_name) self._master_vms_dir = os.path.join(self._master_dir, "vms") self._master_dest_dir = os.path.join(self._master_vms_dir, self._image_name) self._ovf_template_dest = os.path.join(self._master_dest_dir, self._image_name + ".ovf") self._meta_template_dest = os.path.join(self._images_vm_dir, self._image_name + ".meta") self._xml_template_dest = os.path.join(self._tmp_dir, self._image_name + ".xml") self._disk_dest = os.path.join(self._images_vm_dir, self._image_name) self.rel_disk_path = os.path.join(self._image_name, self._image_name) self._logger.info("OVF Template: %s" % self._ovf_template_dest) self._logger.info("XML Template: %s" % self._xml_template_dest) self._logger.info("Disk Destination: %s" % self._disk_dest) def _parse_options(self): parser = optparse.OptionParser() parser.add_option("--disk", type="string", dest="disk", metavar="FILE", help="Disk image to create ovf" "archive from") parser.add_option("--output", type="string", dest="output", metavar="DIRECTORY", help="Disk image to create ovf" "archive from") parser.add_option("--release", type="string", dest="release", help="Release String") parser.add_option("--symlink", action="store_true", dest="symlink", metavar="FILE", default=False, help="Create a gzip archive") parser.add_option("--gzip", action="store_true", dest="gzip", metavar="FILE", default=False, help="Create a gzip archive") (self._options, args) = parser.parse_args() if not self._options.disk: raise RuntimeError("An input disk image must be defined") elif not os.path.exists(self._options.disk): raise RuntimeError("Image path is invalid") def _create_dir_structure(self): uid = 36 # vdsm user gid = 36 # kvm group for d in [self._images_dir, self._images_vm_dir, self._master_dir, self._master_vms_dir, self._master_dest_dir]: os.mkdir(d) try: os.chown(d, uid, gid) except: self._logger.error("Unable to chown directory: %s" % d) def _get_qcow_size(self): qcow_struct = ">IIQIIQIIQQIIQ" # > means big-endian qcow_magic = 0x514649FB # 'Q' 'F' 'I' 0xFB f = open(self._options.disk, "r") pack = f.read(struct.calcsize(qcow_struct)) f.close() try: unpack = struct.unpack(qcow_struct, pack) if unpack[0] == qcow_magic: size = unpack[5] except: raise RuntimeError("Unable to determine disk size") return size def _write_ovf_template(self): disk_size = self._get_qcow_size() ovf_dict = {"product_name": self._image_name, "ovf_version": OVF_VERSION, "disk_path": self.rel_disk_path, "disk_file_name": os.path.basename(self.rel_disk_path), "snapshot_id": str(uuid.uuid4()), "storage_pool_id": str(uuid.uuid4()), "raw_disk_size": disk_size, "disk_size_gb": disk_size / (1024 * 1024 * 1024), "timestamp": time.strftime("%Y/%m/%d %H:%M:%S", self._create_time) } self._logger.info("Writing OVF Template") try: with open(self._ovf_template_dest, "w") as f: f.write(OVF_TEMPLATE % ovf_dict) except: raise RuntimeError("Unable to write ovf template") def _write_meta_template(self): disk_size = self._get_qcow_size() / 512 meta_dict = {"domain": self._image_name, "create_time": str(int(self._raw_create_time)), "domain_uuid": str(uuid.uuid4()), "image_uuid": str(uuid.uuid4()), "pool_id": str(uuid.uuid4()), "pool_uuid": str(uuid.uuid4()), "description": self._image_name, "disk_size": disk_size } self._logger.debug(DISK_META_TEMPLATE) self._logger.debug(meta_dict) self._logger.info("Writing Metadata Template") try: with open(self._meta_template_dest, "w") as f: f.write(DISK_META_TEMPLATE % meta_dict) except: raise RuntimeError("Unable to write meta template") def _write_xml_template(self): xml_dict = {"NVR": self._options.release, } self._logger.debug(XML_TEMPLATE) self._logger.debug(xml_dict) self._logger.info("Writing xml template") #try: with open(self._xml_template_dest, "w") as f: f.write(XML_TEMPLATE % xml_dict) #except: # raise RuntimeError("Unable to write xml template") def _package(self): try: self._logger.info("Copying %s to %s" % (self._options.disk, self._disk_dest)) if not self._options.symlink: shutil.copy(self._options.disk, self._disk_dest) else: os.symlink(self._options.disk, self._disk_dest) if self._options.gzip: self._logger.info("Creating OVF Archive") archive = tarfile.open(self._image_name + ".ovf", "w|gz") archive.add(self._images_dir, arcname="images") archive.add(self._master_dir, arcname="master") archive.close() self._logger.info("Completed OVF Archive: %s.ovf" % os.path.join(os.getcwd(), self._image_name)) else: self._logger.info("Completed Dir Structure at: %s" % self._tmp_dir) except: raise RuntimeError("Unable to complete packaging") def _cleanup(self): self._logger.info("Performing Cleanup") try: shutil.rmtree(self._tmp_dir) except: raise RuntimeError("Error cleaning up temporary directory") def run(self): try: self._parse_options() self._create_dir_structure() self._write_meta_template() self._write_ovf_template() if self._options.release: self._write_xml_template() self._package() except Exception as e: self._logger.exception('Error: OVF Archive Creation Failed: %s', e) finally: if self._options.gzip: self._cleanup() sys.exit(0) if __name__ == "__main__": initLogger() creator = OVFCreator() creator.run()