Advertisement
opexxx

convert.py

Jun 3rd, 2014
322
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 30.69 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. import getopt
  4. import logging
  5. import os
  6. import struct
  7. import subprocess
  8. import sys
  9. import tempfile
  10.  
  11. import glanceclient
  12. import guestfs
  13. import hivex
  14. from keystoneclient.v2_0 import client as kc
  15.  
  16.  
  17. def glance_upload(image_path, name):
  18.     """
  19.    there should probably be some more glance utility functions,
  20.    or output and input paths should probably be expressible as
  21.    glance urls or something like that.  meh.
  22.  
  23.    in truth, this is likely better done in bash using the glance
  24.    client cli, but for some reason people aren't happy with
  25.    chaining together multiple tools -- they want a single
  26.    monolithic tool that works poorly.  So here you go -- this is
  27.    that.  You're welcome.
  28.    """
  29.  
  30.     # we'll assume that credentials and whatnot are expressed
  31.     # in the environment already.
  32.     username = os.environ['OS_USERNAME']
  33.     password = os.environ['OS_PASSWORD']
  34.     tenant = os.environ['OS_TENANT_NAME']
  35.     auth_url = os.environ['OS_AUTH_URL']
  36.  
  37.     # we'll just plain flat ignore the case where you
  38.     # have an environment token and not user/pass
  39.     kcli = kc.Client(username=username,
  40.                      password=password,
  41.                      tenant_name=tenant,
  42.                      auth_url=auth_url)
  43.  
  44.     auth_token = kcli.auth_token
  45.     glance_url = kcli.service_catalog.url_for(service_type='image')
  46.  
  47.     # this is sort of stupid
  48.     decomposed_url = glance_url.split('/')
  49.     if decomposed_url[-1] == 'v1':
  50.         decomposed_url.pop(-1)
  51.     glance_url = '/'.join(decomposed_url)
  52.  
  53.     # push the image
  54.     gcli = glanceclient.Client('1', endpoint=glance_url,
  55.                                token=auth_token)
  56.  
  57.     gimage = gcli.images.create(name=name, disk_format='qcow2',
  58.                                 container_format='bare',
  59.                                 is_public=False)
  60.  
  61.     gimage.update(data=open(image_path, 'rb'))
  62.  
  63.  
  64.  
  65. class SimpleHivex(object):
  66.     """
  67.    Simple hivex class to make it easier to jank around hives
  68.    """
  69.     # Just a key without a value
  70.     REG_NONE = 0
  71.     # A Windows string (encoding is unknown, but often UTF16-LE)
  72.     REG_SZ = 1
  73.     # A Windows string that contains %env% (environment variable expansion)
  74.     REG_EXPAND_SZ = 2
  75.     # A blob of binary
  76.     REG_BINARY = 3
  77.     # DWORD (32 bit integer), little endian
  78.     REG_DWORD = 4
  79.     # DWORD (32 bit integer), big endian
  80.     REG_DWORD_BIG_ENDIAN = 5
  81.     # Symbolic link to another part of the registry tree
  82.     REG_LINK = 6
  83.     # Multiple Windows strings.  See http://blogs.msdn.com/oldnewthing/archive/2009/10/08/9904646.aspx
  84.     REG_MULTI_SZ = 7
  85.     # Resource list
  86.     REG_RESOURCE_LIST = 8
  87.     # Resource descriptor
  88.     REG_FULL_RESOURCE_DESCRIPTOR = 9
  89.     # Resouce requirements list
  90.     REG_RESOURCE_REQUIREMENTS_LIST = 10
  91.     # QWORD (64 bit integer), unspecified endianness but usually little endian
  92.     REG_QWORD = 11
  93.  
  94.     def __init__(self, hive_path):
  95.         self.h = hivex.Hivex(hive_path, write=True)
  96.         self.at_root = True
  97.         self.current_node = self.h.root()
  98.         self.current_path = '/'
  99.  
  100.         classname = self.__class__.__name__.lower()
  101.         if __name__ != '__main__':
  102.             self.logger = logging.getLogger('%s.%s' % (__name__, classname))
  103.         else:
  104.             self.logger = logging.getLogger(classname)
  105.  
  106.         select = self.h.node_get_child(self.current_node, 'Select')
  107.         if select is None:
  108.             self.ccs = 'CurrentControlSet'
  109.             self.logger.debug('Not a system hive')
  110.         else:
  111.             ccs = self.h.node_get_value(select, 'Current')
  112.             self.ccs = 'ControlSet%03d' % (self.h.value_dword(ccs))
  113.             self.logger.debug('System hive: CCS: %s' % self.ccs)
  114.  
  115.     def navigate_to(self, key_path, create=False):
  116.         keys = key_path.split('/')
  117.         if keys[0] == '':
  118.             keys.pop(0)
  119.             self.at_root = True
  120.             self.current_node = self.h.root()
  121.             self.current_path = '/'
  122.  
  123.         if self.at_root is True:
  124.             # transparently replace ccs with the actual
  125.             # control set
  126.             if keys[0].lower() == 'currentcontrolset':
  127.                 keys[0] = self.ccs
  128.  
  129.         for key in keys:
  130.             next_node = self.h.node_get_child(self.current_node, key)
  131.             if next_node is None:
  132.                 if create is True:
  133.                     self.add_subkey(key)
  134.                 else:
  135.                     raise ValueError('No key %s' % key)
  136.             else:
  137.                 self.current_node = next_node
  138.                 self.at_root = False
  139.  
  140.             self.current_path += key + '/'
  141.  
  142.     def has_subkey(self, name):
  143.         sk = self.h.node_get_child(self.current_node, name)
  144.         if sk is None:
  145.             return False
  146.         return True
  147.  
  148.     def delete_subkey(self, name):
  149.         if not self.has_subkey(name):
  150.             return
  151.  
  152.         self.logger.debug('deleting subkey %s%s' % (self.current_path, name))
  153.  
  154.         gone = self.h.node_get_child(self.current_node, name)
  155.         self.h.node_delete_child(gone)
  156.  
  157.     def add_subkey(self, name):
  158.         if self.has_subkey(name):
  159.             self.navigate_to(name)
  160.         else:
  161.             self.logger.debug('adding subkey %s%s' % (self.current_path, name))
  162.             sk = self.h.node_add_child(self.current_node, name)
  163.             if sk is None:
  164.                 raise RuntimeError('Cannot add subkey: %s' % (name, ))
  165.  
  166.             self.current_node = sk
  167.             self.at_root = False
  168.  
  169.     def _add_value(self, value_type, key, value):
  170.         val = None
  171.  
  172.         if value_type == SimpleHivex.REG_SZ:
  173.             val = value.encode('utf-16le') + '\0\0'
  174.         elif value_type == SimpleHivex.REG_EXPAND_SZ:
  175.             val = value.encode('utf-16le') + '\0\0'
  176.         elif value_type == SimpleHivex.REG_DWORD:
  177.             val = struct.pack('I', value)
  178.         else:
  179.             raise ValueError('Unknown value type: %s' % (value_type, ))
  180.  
  181.         new_value = {'key': key,
  182.                      't': value_type,
  183.                      'value': val}
  184.  
  185.         self.logger.debug('setting %s%s to %s' %
  186.                           (self.current_path, key, str(value)))
  187.  
  188.         self.h.node_set_value(self.current_node, new_value)
  189.  
  190.     def has_value(self, key):
  191.         if self.h.node_get_value(self.current_node, key) is None:
  192.             return False
  193.         return True
  194.  
  195.     def _get_val(self, what, key):
  196.         val = self.h.node_get_value(self.current_node, key)
  197.         if val is None:
  198.             return None
  199.  
  200.         if what == SimpleHivex.REG_SZ:
  201.             return self.h.value_string(val)
  202.         elif what == SimpleHivex.REG_DWORD:
  203.             return self.h.value_dword(val)
  204.         else:
  205.             raise ValueError('Unknown type: %d' % what)
  206.  
  207.     def get_string(self, key):
  208.         return self._get_val(SimpleHivex.REG_SZ, key)
  209.  
  210.     def get_dword(self, key):
  211.         return self._get_val(SimpleHivex.REG_DWORD, key)
  212.  
  213.     def add_reg_sz(self, key, value):
  214.         return self._add_value(SimpleHivex.REG_SZ, key, value)
  215.  
  216.     def add_reg_expand_sz(self, key, value):
  217.         return self._add_value(SimpleHivex.REG_EXPAND_SZ, key, value)
  218.  
  219.     def add_reg_dword(self, key, value):
  220.         return self._add_value(SimpleHivex.REG_DWORD, key, value)
  221.  
  222.     def commit(self):
  223.         self.h.commit(None)
  224.  
  225.  
  226. def active_guestfs(func):
  227.     def f(self, *args, **kwargs):
  228.         if self.gfs is None:
  229.             self.gfs = guestfs.GuestFS()
  230.             readonly = 0
  231.             if self.readonly:
  232.                 readonly = 1
  233.  
  234.             self.gfs.add_drive_opts(self.image_path, readonly=readonly)
  235.             self.logger.debug('Launching guestfs')
  236.             self.gfs.launch()
  237.  
  238.         result = func(self, *args, **kwargs)
  239.         return result
  240.  
  241.     return f
  242.  
  243.  
  244. def registered_info(func):
  245.     def f(self, *args, **kwargs):
  246.         if getattr(self, 'disk_format', None) is None:
  247.             info = self.info()
  248.             self.logger.debug('Volume info: %s' % (info, ))
  249.  
  250.         result = func(self, *args, **kwargs)
  251.         return result
  252.  
  253.     return f
  254.  
  255.  
  256. def mounted_devices(func):
  257.     def f(self, *args, **kwargs):
  258.         current_mounts = self.gfs.mounts()
  259.  
  260.         def compare(a, b):
  261.             return len(a) - len(b)
  262.  
  263.         for device in sorted(self.mountpoints.keys(), compare):
  264.             if self.mountpoints[device] in current_mounts:
  265.                 continue
  266.  
  267.             self.logger.debug('Mounting device %s' % device)
  268.  
  269.             try:
  270.                 if self.readonly == 1:
  271.                     self.gfs.mount_ro(self.mountpoints[device], device)
  272.                 else:
  273.                     self.gfs.mount(self.mountpoints[device], device)
  274.  
  275.             except RuntimeError as msg:
  276.                 print '%s (ignored)' % msg
  277.  
  278.         self.logger.debug('Current mounts: %s' % (self.gfs.mounts(), ))
  279.         result = func(self, *args, **kwargs)
  280.         return result
  281.  
  282.     return f
  283.  
  284.  
  285. class ConversionDriver(object):
  286.     def __init__(self, gfs):
  287.         self.gfs = gfs
  288.  
  289.         classname = self.__class__.__name__.lower()
  290.         if __name__ != '__main__':
  291.             self.logger = logging.getLogger('%s.%s' % (__name__, classname))
  292.         else:
  293.             self.logger = logging.getLogger(classname)
  294.  
  295.         roots = self.gfs.inspect_get_roots()
  296.         self.root = roots[0]
  297.         self.ostype = self.gfs.inspect_get_type(self.root)
  298.         self.arch = self.gfs.inspect_get_arch(self.root)
  299.  
  300.         self.tmpdir = tempfile.mkdtemp()
  301.  
  302.     def cleanup(self):
  303.         shutil.rmtree(self.tmpdir)
  304.  
  305.     def convert(self):
  306.         raise NotImplementedError('Base class!')
  307.  
  308.     def _upload_directory(self, src_dir, dst_dir, recursive=True):
  309.         """
  310.        given a directory of files, upload them to the guest at the
  311.        destination directory.
  312.  
  313.        If the directory does not exist, it will be created
  314.        """
  315.  
  316.         self.logger.debug('Uploading "%s" to "%s"' %
  317.                           (src_dir, dst_dir))
  318.  
  319.         if not self.gfs.is_dir(dst_dir):
  320.             self.gfs.mkdir_p(dst_dir)
  321.  
  322.         for f in os.listdir(src_dir):
  323.             src_file = os.path.join(src_dir, f)
  324.             dst_file = self.gfs.case_sensitive_path(dst_dir)
  325.             dst_file = os.path.join(dst_file, f)
  326.  
  327.             if os.path.isfile(src_file):
  328.                 # upload it!
  329.                 self.logger.debug(" %s => %s" % (src_file, dst_file))
  330.                 self.gfs.upload(src_file, dst_file)
  331.  
  332.             elif os.path.isdir(dst_file):
  333.                 if recursive is True:
  334.                     self._upload_directory(src_file, dst_file)
  335.             # otherwise skip it!
  336.  
  337.  
  338. class WindowsConversionDriver(ConversionDriver):
  339.     """
  340.    shared utility functions for windows converters
  341.    """
  342.     def __init__(self, gfs):
  343.         super(WindowsConversionDriver, self).__init__(gfs)
  344.  
  345.         self.logger.debug('Current mounts: %s' % (self.gfs.mounts(), ))
  346.  
  347.         # # !??!
  348.         # self.gfs.mount('/dev/sda1', '/')
  349.  
  350.         self.systemroot = self.gfs.inspect_get_windows_systemroot(self.root)
  351.  
  352.         self.system_hive = self._download_hive('system', self.tmpdir)
  353.         self.software_hive = self._download_hive('software', self.tmpdir)
  354.  
  355.         self.logger.debug('System hive in %s' % (self.system_hive, ))
  356.         self.logger.debug('Software hive in %s' % (self.software_hive, ))
  357.  
  358.         self.major = self.gfs.inspect_get_major_version(self.root)
  359.         self.minor = self.gfs.inspect_get_minor_version(self.root)
  360.         self.product = self.gfs.inspect_get_product_name(self.root)
  361.         self.variant = self.gfs.inspect_get_product_variant(self.root)
  362.  
  363.  
  364.     def _download_hive(self, hive, download_dir):
  365.         remote_path = os.path.join(self.systemroot,
  366.                                    'system32/config',
  367.                                    hive)
  368.  
  369.         self.logger.debug('remote_path: %s' % remote_path)
  370.         remote_path = self.gfs.case_sensitive_path(remote_path)
  371.         local_path = os.path.join(download_dir, hive)
  372.  
  373.         self.logger.debug('Downloading hive "%s"' % (remote_path, ))
  374.         self.gfs.download(remote_path, local_path)
  375.         return local_path
  376.  
  377.     def _upload_hive(self, hive_path):
  378.         what_hive = os.path.basename(hive_path)
  379.         remote_path = os.path.join(self.systemroot,
  380.                                    'system32/config',
  381.                                    what_hive)
  382.         remote_path = self.gfs.case_sensitive_path(remote_path)
  383.  
  384.         self.logger.debug('Uploading %s => %s' % (hive_path, remote_path))
  385.         self.gfs.upload(hive_path, remote_path)
  386.  
  387.     def _windows_common_fixups(self, virtio_base='virtio'):
  388.         """
  389.        here, we'll fix up the boot stuff and any other thing that is
  390.        hypervisor agnostic.
  391.        """
  392.         self._disable_processor_drivers()
  393.         self._set_auto_reboot(0)
  394.  
  395.     def _set_auto_reboot(self, value):
  396.         h = SimpleHivex(self.system_hive)
  397.  
  398.         h.navigate_to('/CurrentControlSet/Control/CrashControl', True)
  399.         h.add_reg_dword('AutoReboot', value)
  400.  
  401.         h.commit()
  402.  
  403.     def _disable_processor_drivers(self):
  404.         """
  405.        Not strictly necessary, perhaps, but worth doing anyway, I think.
  406.  
  407.        http://blogs.msdn.com/b/virtual_pc_guy/archive/2005/10/24/484461.aspx
  408.        """
  409.  
  410.         h = SimpleHivex(self.system_hive)
  411.  
  412.         h.navigate_to('/CurrentControlSet/Services')
  413.         h.delete_subkey('Processor')
  414.         h.delete_subkey('Intelppm')
  415.  
  416.         h.commit()
  417.  
  418.     def _install_service(self, service_path, display_name):
  419.         """
  420.        Install a service on a dead windows disk
  421.        """
  422.  
  423.         # http://support.microsoft.com/kb/103000
  424.         #
  425.         # [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\<name>]
  426.         # "Type"=dword:00000010 (service controlled service)
  427.         # "Start"=dword:00000002 (scm autoload)
  428.         # "ErrorControl"=dword:00000001
  429.         # "ImagePath"="..."
  430.         # "DisplayName"="..."
  431.         # "ObjectName"="LocalSystem"
  432.  
  433.         h = SimpleHivex(self.system_hive)
  434.  
  435.         service_name = display_name.replace(' ', '_').lower()
  436.  
  437.         h.navigate_to('/CurrentControlSet/services/%s' % service_name, True)
  438.         h.add_reg_dword('Type', 0x10)
  439.         h.add_reg_dword('Start', 0x02)
  440.         h.add_reg_dword('ErrorControl', 0x01)
  441.         h.add_reg_sz('ImagePath', service_path)
  442.         h.add_reg_sz('DisplayName', display_name)
  443.         h.add_reg_sz('ObjectName', 'LocalSystem')
  444.  
  445.         h.commit()
  446.  
  447. class KvmWindowsConversion(WindowsConversionDriver):
  448.     def __init__(self, gfs):
  449.         super(KvmWindowsConversion, self).__init__(gfs)
  450.  
  451.     def _stub_viostor(self):
  452.         """
  453.        Jank in the settings to force the system to PnP the virtio
  454.        storage driver.  This is basically cribbed from
  455.        http://support.microsoft.com/kb/314082
  456.  
  457.        viostor is a scsi class adaptor, so GUID is
  458.        4D36E97B-E325-11CE-BFC1-08002BE10318, unlike the kb article,
  459.        which specifies atapi
  460.        """
  461.  
  462.         h = SimpleHivex(self.system_hive)
  463.  
  464.         # First, pump in the pci vid/did stuff
  465.  
  466.         # [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\CriticalDeviceDatabase\pci#ven_1af4&dev_1001]
  467.         # "ClassGUID"="{4D36E97B-E325-11CE-BFC1-08002BE10318}"
  468.         # "Service"="viostor"
  469.  
  470.         h.navigate_to('/CurrentControlSet/Control/CriticalDeviceDatabase')
  471.         h.add_subkey('pci#ven_1af4&dev_1001&subsys_00021af4&rev_00')
  472.         h.add_reg_sz('ClassGUID', '{4d36e97b-e325-11ce-bfc1-08002be10318}')
  473.         h.add_reg_sz('Service', 'viostor')
  474.  
  475.         # Next, let's do the driver thing.  Should look like this
  476.         # [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\viostor]
  477.         # "ErrorControl"=dword:00000001
  478.         # "Group"="SCSI miniport"
  479.         # "Start"=dword:00000000
  480.         # "Tag"=dword:00000021
  481.         # "Type"=dword:00000001
  482.         # "ImagePath"= (REG_EXPAND_SZ) "...path..."
  483.  
  484.         h.navigate_to('/CurrentControlSet/services/viostor', True)
  485.         h.add_reg_dword('Type', 1)
  486.         h.add_reg_dword('Start', 0)
  487.         h.add_reg_dword('ErrorControl', 1)
  488.         h.add_reg_dword('Tag', 64)
  489.         h.add_reg_expand_sz('ImagePath', 'system32\\DRIVERS\\viostor.sys')
  490.         h.add_reg_sz('Group', 'SCSI miniport')
  491.  
  492.         # Set up default parameters
  493.         #
  494.         # Probably don't need this if we are just going to turn around do the
  495.         # real installation of the official drivers.  Oh well.
  496.         #
  497.         h.navigate_to('/CurrentControlSet/services/viostor/Parameters', True)
  498.         h.add_reg_dword('BusType', 1)
  499.  
  500.         h.add_subkey('PnpInterface')
  501.         h.add_reg_dword('5', 1)
  502.  
  503.         h.navigate_to('/CurrentControlSet/Services/viostor/Enum', True)
  504.         h.add_reg_sz('0', 'PCI\\VEN_1AF4&DEV_1001&SUBSYS_00021AF4&REV_00\\3&13c0b0c5&0&28')
  505.         h.add_reg_dword('Count', 1)
  506.         h.add_reg_dword('NextInstance', 1)
  507.         h.commit()
  508.  
  509.     def _upload_virtio(self, virtio_base):
  510.         """
  511.        determine what version of the virtio drivers the
  512.        destination machine requires, and upload them onto
  513.        the system.  Presumably to be installed at some later
  514.        date.  (firstboot script, maybe?)
  515.        """
  516.  
  517.         dest_path = '/v2v-virtio'
  518.  
  519.         version_map = {'6.2': 'WIN8',   # server 2k12
  520.                        '6.1': 'WIN7',   # server 2k8r2
  521.                        '6.0': 'WLH',    # server 2k8
  522.                        '5.2': 'WNET'}   # server 2k3/2k3r2
  523.  
  524.         version = '%d.%d' % (self.major, self.minor)
  525.         win_arch = {'x86_64': 'AMD64',
  526.                     'i386': 'X86'}[self.arch]
  527.  
  528.         if not version in version_map:
  529.             raise ValueError('No virtio drivers for version "%s"' % version)
  530.  
  531.         source_path = os.path.join(virtio_base,
  532.                                    version_map[version],
  533.                                    win_arch)
  534.  
  535.         self._upload_directory(source_path, dest_path)
  536.  
  537.         # we also need to handle viostor.sys specially --
  538.         if self.gfs.is_file(self.gfs.case_sensitive_path(
  539.                 os.path.join(dest_path, 'viostor.sys'))):
  540.             # must copy this to system32/drivers
  541.             src_file = os.path.join(dest_path, 'viostor.sys')
  542.             src_file = self.gfs.case_sensitive_path(src_file)
  543.  
  544.             dst_file = os.path.join(self.systemroot,
  545.                                     'system32/drivers')
  546.             dst_file = self.gfs.case_sensitive_path(dst_file)
  547.             dst_file = os.path.join(dst_file, 'viostor.sys')
  548.  
  549.             self.logger.debug('Copying %s => %s' % (src_file, dst_file))
  550.             self.gfs.cp(src_file, dst_file)
  551.  
  552.         # now, add dest_path to the drier search path
  553.         h = SimpleHivex(self.software_hive)
  554.         h.navigate_to('/Microsoft/Windows/CurrentVersion')
  555.  
  556.         append_data = 'c:\\v2v-virtio'
  557.  
  558.         old_path = h.get_string('DevicePath')
  559.         new_path = None
  560.  
  561.         if old_path is None:
  562.             new_path = append_data
  563.         elif append_data not in old_path:
  564.             new_path = '%s;%s' % (old_path, append_data)
  565.  
  566.         if new_path is not None:
  567.             h.add_reg_expand_sz('DevicePath', new_path)
  568.             h.commit()
  569.  
  570.         h = None
  571.  
  572.         return True
  573.  
  574.     def convert(self):
  575.         """
  576.        This is the actual conversion to kvm for windows
  577.        """
  578.         self._windows_common_fixups()
  579.  
  580.         if self._upload_virtio('virtio') is False:
  581.             raise ValueError('No virtio drivers for this version')
  582.  
  583.         self._stub_viostor()
  584.  
  585.         self._upload_hive(self.system_hive)
  586.         self._upload_hive(self.software_hive)
  587.  
  588.  
  589. class XenWindowsConversion(WindowsConversionDriver):
  590.     def convert(self):
  591.         raise NotImplementedError('Xen?')
  592.  
  593.  
  594. class LinuxConversionDriver(ConversionDriver):
  595.     """
  596.    shared utility functions for linux converters
  597.    """
  598.     def __init__(self, gfs):
  599.         super(LinuxConversionDriver, self).__init__(gfs)
  600.  
  601.         self.distro = self.gfs.inspect_get_distro(self.root)
  602.         self.mountpoints = dict(self.gfs.inspect_get_mountpoint(self.root))
  603.  
  604.  
  605. class KvmLinuxConversion(LinuxConversionDriver):
  606.     def init(self, gfs):
  607.         super(KvmLinuxConversionDriver, self).__init__(gfs)
  608.  
  609.     def convert(self):
  610.         """
  611.        actual conversion of linux images to kvm.  This could verify
  612.        proper kernel and whatnot, but I expect there aren't many
  613.        (any?) distros running non-virtio enabled kernels.  Largely,
  614.        the only thing to be done here is fix up fstab if it's got
  615.        xenish looking mounts in it.
  616.        """
  617.         pass
  618.  
  619.  
  620. class XenLinxuConversion(LinuxConversionDriver):
  621.     def convert(self, gfs):
  622.         raise NotImplementedError
  623.  
  624.  
  625. class Image(object):
  626.     """
  627.    Simple image class to simplify the tasks of image conversion
  628.    and windows driver injection.
  629.    """
  630.     def __init__(self, image_path, readonly=True):
  631.         self.image_path = image_path
  632.         self.readonly = readonly
  633.         self.gfs = None
  634.  
  635.         classname = self.__class__.__name__.lower()
  636.         if __name__ != '__main__':
  637.             self.logger = logging.getLogger('%s.%s' % (__name__, classname))
  638.         else:
  639.             self.logger = logging.getLogger(classname)
  640.  
  641.     @active_guestfs
  642.     def info(self):
  643.         roots = self.gfs.inspect_os()
  644.  
  645.         if len(roots) != 1:
  646.             raise ValueError('Bad disk image: roots = %s' % len(roots))
  647.  
  648.         self.root = roots[0]
  649.  
  650.         self.disk_format = 'unknown'
  651.         if self.image_path.endswith('.qcow2'):
  652.             self.disk_format = 'qcow2'
  653.         elif self.image_path.endswith('.vmdk'):
  654.             self.disk_format = 'vmdk'
  655.  
  656.         self.distro = self.gfs.inspect_get_distro(self.root)
  657.         self.arch = self.gfs.inspect_get_arch(self.root)
  658.         self.fs = self.gfs.inspect_get_filesystems(self.root)
  659.         self.format = self.gfs.inspect_get_format(self.root)
  660.         self.hostname = self.gfs.inspect_get_hostname(self.root)
  661.         self.major = self.gfs.inspect_get_major_version(self.root)
  662.         self.minor = self.gfs.inspect_get_minor_version(self.root)
  663.         self.ostype = self.gfs.inspect_get_type(self.root)
  664.         self.product = self.gfs.inspect_get_product_name(self.root)
  665.         self.variant = self.gfs.inspect_get_product_variant(self.root)
  666.         self.mountpoints = dict(self.gfs.inspect_get_mountpoints(self.root))
  667.  
  668.         return dict([x, getattr(self, x)] for x in ['arch', 'distro', 'fs',
  669.                                                     'format', 'hostname',
  670.                                                     'major', 'minor', 'ostype',
  671.                                                     'product', 'variant',
  672.                                                     'disk_format',
  673.                                                     'mountpoints'])
  674.  
  675.     def _dev_from_root(self):
  676.         """
  677.        this is only true for devs like 'sdX'.  Other device
  678.        types (nbdXpY) behave differently.  We might want to
  679.        special-case in here by device name
  680.        """
  681.  
  682.         root_dev = self.root
  683.         while len(root_dev) > 0 and root_dev[-1] >= '0' and \
  684.                 root_dev[-1] <= '9':
  685.             root_dev = root_dev[:-1]
  686.  
  687.         return root_dev
  688.  
  689.     @active_guestfs
  690.     @registered_info
  691.     @mounted_devices
  692.     def convert(self, destination_hypervisor='kvm'):
  693.         """
  694.        Convert from whatever on disk format to the destination
  695.        disk format.  This is not the format (vmdk vs. qcow2 or
  696.        whatever), but rather the on-disk data.  Like, if you are
  697.        trying to make a vmdk boot on Xen, you'll have to change
  698.        root device and all that.
  699.  
  700.        I don't really care about Xen, because it's a fail, but
  701.        someone with a less discerning taste than I could add it.
  702.  
  703.        Valid destination hypervisors:
  704.  
  705.          - kvm
  706.          - xen
  707.  
  708.        """
  709.  
  710.         conversion_class = "%s%sConversion" % (
  711.             destination_hypervisor.lower().title(),
  712.             self.ostype.lower().title())
  713.  
  714.         cc = globals().get(conversion_class, None)
  715.         if cc is None:
  716.             raise ValueError('No converter to "%s" for platform "%s"' %
  717.                              (destination_hypervisor, self.ostype))
  718.  
  719.         self.logger.debug('Initializing converter')
  720.         cvrt = cc(self.gfs)
  721.         self.logger.debug('Starting conversion')
  722.         result = cvrt.convert()
  723.         self.logger.debug('Conversion complete')
  724.         return result
  725.  
  726.  
  727.     @active_guestfs
  728.     def to_qcow2(self, destination_path=None):
  729.         """
  730.        Convert an image from whatever native format it is in
  731.        to qcow2 format.  Sparseness is in question.  Stoopid
  732.        python-guestfs doesn't pass through sparseness options
  733.        and whatnot, apparently.  The qemu2 format might be smart
  734.        enough to sparse it appropriately.  We'll see.
  735.        """
  736.  
  737.         if getattr(self, 'disk_format', None) is None:
  738.             self.info()
  739.  
  740.         if self.disk_format == "qcow2":
  741.             return None
  742.  
  743.         # we need to add a new disk image and dd it
  744.         # over to the new disk.
  745.         #
  746.         # Sadly, python-guestfs does not expose the
  747.         # sparse argument, so we can't well use this.
  748.         # Back to the drawing board.
  749.  
  750.         # if destination_path is None:
  751.         #     if '.' in self.image_path:
  752.         #         base = self.image_path.rsplit('.')
  753.         #         destination_path = '%s.qcow2' % (base, )
  754.         #     else:
  755.         #         destination_path = '%s.qcow2' % self.image_path
  756.         #
  757.         # self.logger.debug('Converting disk of type "%s" to qcow2' %
  758.         #                   (self.disk_format, ))
  759.         #
  760.         # # generate an empty qcow2 with preallocated meta
  761.         # self.logger.debug('Creating empty sparse qcow2 at %s' %
  762.         #                   (destination_path, ))
  763.         #
  764.         #
  765.         # root_device = self._dev_from_root()
  766.         #
  767.         # src_size = self.gfs.blockdev_getsize64(root_device)
  768.         # self.logger.debug('Source device size: %d bytes' % (src_size, ))
  769.         #
  770.         # # qemu-img create -f qcow2 -o preallocation-metadata "path" "size"
  771.         # result = subprocess.call(['qemu-img', 'create',
  772.         #                           '-f', 'qcow2', '-o',
  773.         #                           'preallocation=metadata',
  774.         #                           destination_path,
  775.         #                           str(src_size)])
  776.         #
  777.         #
  778.         # # okay...we have a qcow.  Spin up a new instance with both
  779.         # # drives mounted and copy the stuffs
  780.         # gfs_new = guestfs.GuestFS()
  781.         #
  782.         # # guestfs guarantees this will be /dev/sda
  783.         # gfs_new.add_drive_opts(self.image_path, readonly=1)
  784.         # gfs_new.add_drive_opts(destination_path, readonly=0)
  785.         #
  786.         # self.logger.debug('Launching conversion guest')
  787.         # gfs_new.launch()
  788.         #
  789.         # self.logger.debug('Converting image')
  790.         # gfs_new.copy_device_to_device('/dev/sda', '/dev/sdb')
  791.         #
  792.         # gfs_new.close()
  793.  
  794.         # note, can't preallocate metadata and compress
  795.         # at the same time (despite the fact it would
  796.         # be useful)
  797.         result = subprocess.call(['qemu-img', 'convert',
  798.                                   '-c', '-O', 'qcow2',
  799.                                   self.image_path, destination_path])
  800.  
  801.         return destination_path
  802.  
  803.  
  804. if __name__ == "__main__":
  805.     def usagequit(program):
  806.         print >>sys.stderr, 'Usage: %s [options]\n' % program
  807.         print >>sys.stderr, 'Options:'
  808.         print >>sys.stderr, '-i, --input <path>     image to convert'
  809.         print >>sys.stderr, '-o, --output <path>    output file name (qcow2)'
  810.         print >>sys.stderr, '-n, --name <name>      glance name (if uploading)'
  811.         print >>sys.stderr, '-u, --upload           enable glance upload'
  812.         print >>sys.stderr, '-s, --sysprep          sysprep windows image'
  813.         print >>sys.stderr, '-d, --debug <1-5>      debuglevel (5 is verbose)'
  814.         sys.exit(1)
  815.  
  816.     image = None
  817.     output = None
  818.     name = None
  819.     upload = False
  820.     sysprep = False
  821.     debuglevel = 2
  822.  
  823.     try:
  824.         opts, args = getopt.getopt(
  825.             sys.argv[1:], 'i:o:n:usd:', ['input=', 'output=', 'name=',
  826.                                          'upload', 'sysprep',
  827.                                          'debug='])
  828.     except getopt.GetoptError as err:
  829.         print str(err)
  830.         usagequit(sys.argv[0])
  831.  
  832.     for o, a in opts:
  833.         if o in ['-i', '--input']:
  834.             image = a
  835.         elif o in ['-o', '--output']:
  836.             output = a
  837.         elif o in ['-n', '--name']:
  838.             name = a
  839.         elif o in ['-u', '--upload']:
  840.             upload = True
  841.         elif o in ['-s', '--sysprep']:
  842.             sysprep = True
  843.         elif o in ['-d', '--debug']:
  844.             a = int(a)
  845.             a = a if a < 5 else 4
  846.             a = a if a > 0 else 1
  847.             debuglevel = a
  848.         else:
  849.             print >>sys.stderr, 'Unhandled option: %s' % o
  850.  
  851.     logging.basicConfig(level=[None,
  852.                                logging.ERROR,
  853.                                logging.WARNING,
  854.                                logging.INFO,
  855.                                logging.DEBUG][debuglevel])
  856.  
  857.     if image is None:
  858.         print >>sys.stderr, 'input image (-i, --image) required'
  859.         sys.exit(1)
  860.  
  861.     if output is None:
  862.         if '.' in image:
  863.             base = image.rsplit('.', 1)[0]
  864.             output = '%s.qcow2' % base
  865.         else:
  866.             output = '%s.qcow2' % image
  867.  
  868.     if name is None:
  869.         name = os.path.basename(image).rsplit('.',1)[0]
  870.  
  871.     if upload:
  872.         for key in ['OS_USERNAME', 'OS_PASSWORD', 'OS_TENANT_NAME',
  873.                     'OS_AUTH_URL']:
  874.             fail = False
  875.             if key not in os.environ:
  876.                 fail = True
  877.                 print >>sys.stderr, 'missing nova environment (%s)' % key
  878.  
  879.         if fail is True:
  880.             sys.exit(1)
  881.  
  882.     # let's do this thing.
  883.     working_image_path = image
  884.  
  885.     # the function to inspect image format is curiously
  886.     # absent from python-guestfs, so we'll use the poor-man's
  887.     # image detection method...
  888.     if not image.lower().endswith('.qcow2'):
  889.         logging.getLogger().info('Converting to qcow2 format')
  890.         i = Image(image, readonly=True)
  891.         working_image_path = i.to_qcow2(destination_path=output)
  892.         i = None
  893.  
  894.     # image is in qcow2 now, let's do the v2v migration
  895.     logging.getLogger().info('Performing v2v actions')
  896.     conv = Image(working_image_path, readonly=False)
  897.     conv.convert()
  898.     conf = None
  899.  
  900.     # now we need to upload the thing.
  901.     if upload:
  902.         logging.getLogger().info('Performing image upload')
  903.         glance_upload(working_image_path, name=name)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement