def _check_connectivity(self): """Check if winexe works on the Windows VM""" retries = self.sysprep_params['connection_retries'].value timeout = [5] for i in xrange(1, retries - 1): timeout.insert(0, timeout[0] * 2) # If the connection_retries parameter is set to 0 disable the # connectivity check if retries == 0: return True for i in xrange(retries): (stdout, stderr, rc) = self.vm.rexec('cmd /C', fatal=False, debug=True) if rc == 0: return True log = tempfile.NamedTemporaryFile(delete=False) try: log.file.write("STDOUT:\n%s\n" % stdout) log.file.write("STDERR:\n%s\n" % stderr) finally: log.close() self.out.info("failed! See: `%s' for the full output" % log.name) if i < retries - 1: wait = timeout.pop() self.out.info("retrying in %d seconds ..." % wait, False) time.sleep(wait) raise FatalError( "Connection to the Windows VM failed after %d retries" % retries)
def umount(self, lazy=False): """umount the previously mounted image file system""" assert self.mount_local_support, \ "MOUNT LOCAL not supported for this build of libguestfs" assert self._mount_thread is not None, "Image is not mounted" try: # Maybe the image was umounted externally if not self._mount_thread.is_alive(): self._mount_thread = None return True try: args = (['-l'] if lazy else []) + [self._mount_thread.mpoint] get_command('umount')(*args) except: return False # Wait for a little while. If the image is umounted, # mount_local_run should have terminated self._mount_thread.join(5) if self._mount_thread.is_alive(): raise FatalError('Unable to join the mount thread') self._mount_thread = None return True finally: self.os.umount()
def _update_dword(self, keyname, valuename, data, default, valid): """Updates a registry key dword value to data. default: The default value is the key does not exist valid: a range of valid values Returns: The old value of the field """ if data not in valid: raise ValueError("Valid values for parameter are %r" % (valid, )) hivename, keyname = keyname.split('/', 1) with self.open_hive(hivename, write=True) as hive: key = traverse(hive, keyname) value = None for v in hive.node_values(key): if hive.value_key(v) == valuename: value = v break old = default if value is not None: old = hive.value_dword(value) elif old is None: raise FatalError("Value: `%s' of key: `%s/%s' is missing." % (valuename, hivename, keyname)) if old != data: hive.node_set_value(key, reg_dword(valuename, data)) hive.commit(None) return old
def get_setup_state(self): """Returns the stage of Windows Setup the image is in. The method will return an int with one of the following values: 0 => IMAGE_STATE_COMPLETE 1 => IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE 2 => IMAGE_STATE_GENERALIZE_RESEAL_TO_AUDIT 3 => IMAGE_STATE_SPECIALIZE_RESEAL_TO_OOBE 4 => IMAGE_STATE_SPECIALIZE_RESEAL_TO_AUDIT For more information check here: http://technet.microsoft.com/en-us/library/hh824815.aspx """ with self.open_hive('SOFTWARE') as hive: # Navigate to: # SOFTWARE/Microsoft/Windows/CurrentVersion/Setup/State state = hive.root() for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Setup', 'State'): state = hive.node_get_child(state, child) image_state = hive.node_get_value(state, 'ImageState') vtype, value = hive.value_value(image_state) assert vtype == 1L, \ "ImageState field type (=%d) is not REG_SZ" % vtype value = value.decode('utf-16le') ret = 0 for known_state in WINDOWS_SETUP_STATES: if value == known_state + '\x00': # REG_SZ is null-terminated return ret ret += 1 raise FatalError("Unknown Windows Setup State: %s" % value)
def _shrink(self): """Shrink the last file system. Please make sure the file system is defragged. """ # Shrink the volume as much as possible and then give 100MB back. # From ntfsresize: # Practically the smallest shrunken size generally is at around # "used space" + (20-200 MB). Please also take into account that # Windows might need about 50-100 MB free space left to boot safely. cmd = (r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' + r'ECHO SELECT DISK 0 > %SCRIPT% & ' + 'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num + r'ECHO SHRINK NOERR >> %SCRIPT% & ' + r'ECHO EXTEND SIZE=100 NOERR >> %SCRIPT% & ' + r'ECHO EXIT >> %SCRIPT% & ' + r'DISKPART /S %SCRIPT% & ' + r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' + r'DEL /Q %SCRIPT%"') stdout, _, rc = self.vm.rexec(cmd, fatal=False) if rc != 0: raise FatalError( "Shrinking failed. Please make sure the media is defragged.") for line in stdout.splitlines(): line = line.strip() if not len(line): continue self.out.info(" %s" % line) self.shrinked = True
def __init__(self, device, output, **kwargs): """Create a new Image instance""" self.device = device self.out = output self.format = kwargs['format'] if 'format' in kwargs else 'raw' self.meta = kwargs['meta'] if 'meta' in kwargs else {} self.sysprep_params = \ kwargs['sysprep_params'] if 'sysprep_params' in kwargs else {} self.progress_bar = None self.guestfs_device = None self.size = 0 self.g = guestfs.GuestFS() self.guestfs_enabled = False self.guestfs_version = self.g.version() # This is needed if the image format is not raw self.nbd = QemuNBD(device) if self.nbd.qemu_nbd is None and self.format != 'raw': raise FatalError("qemu-nbd command is missing, only raw input " "media are supported") # Check If MOUNT LOCAL is supported for this guestfs build self.mount_local_support = hasattr(self.g, "mount_local") if self.mount_local_support: self._mount_thread = None
def _last_partition(self): """Return the last partition of the image disk""" def is_extended(partition): """Returns True if the partition is an extended partition""" return self.g.part_get_mbr_id(self.guestfs_device, partition['part_num']) in (0x5, 0xf) def is_logical(partition): """Returns True if the partition is a logical partition""" return self.meta['PARTITION_TABLE'] == 'msdos' and \ partition['part_num'] > 4 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt': msg = "Unsupported partition table: %s. Only msdos and gpt " \ "partition tables are supported" % self.meta['PARTITION_TABLE'] raise FatalError(msg) partitions = self.g.part_list(self.guestfs_device) last_partition = partitions[-1] if is_logical(last_partition): # The disk contains extended and logical partitions.... extended = [p for p in partitions if is_extended(p)][0] last_primary = [p for p in partitions if p['part_num'] <= 4][-1] # check if extended is the last primary partition if last_primary['part_num'] > extended['part_num']: last_partition = last_primary return last_partition
def handler(signum, frame): """Signal handler""" self.process.terminate() time.sleep(1) if self.isalive(): self.process.kill() self.process.wait() if fatal: raise FatalError("Stopping the VM timed-out")
def rexec(self, command, **kwargs): """Remote execute a command on the windows VM The following optional flags are allowed: * fatal: If True, a FatalError is thrown if the command fails * debug: If True, WinEXE is executed in the highest debug level * uninstall: If True, the winexesvc.exe service will be uninstalled after the execution of the command. """ fatal = kwargs['fatal'] if 'fatal' in kwargs else True debug = kwargs['debug'] if 'debug' in kwargs else False uninstall = kwargs['uninstall'] if 'uninstall' in kwargs else False winexe = WinEXE(self.admin.name, 'localhost', password=self.password) winexe.no_pass() if debug: winexe.debug(9) if uninstall: winexe.uninstall() try: (stdout, stderr, rc) = winexe.run(command) except WinexeTimeout: raise FatalError("Command: `%s' timeout out." % command) if rc != 0 and fatal: log = tempfile.NamedTemporaryFile(delete=False) try: log.file.write("STDOUT:\n%s\n" % stdout) log.file.write("STDERR:\n%s\n" % stderr) finally: fname = log.name log.close() raise FatalError("Command: `%s' failed (rc=%d). See: %s" % (command, rc, fname)) return (stdout, stderr, rc)
def _get_syslinux_base_dir(self): """Find the installation directory we need to use to when updating syslinux """ cfg = None for path in self.syslinux.search_paths: if self.image.g.is_file(path): cfg = path break if not cfg: # Maybe we should fail here self.out.warn("Unable to find syslinux configuration file!") return "/boot" kernel_regexp = re.compile(r'\s*kernel\s+(.+)', re.IGNORECASE) initrd_regexp = re.compile(r'\s*initrd\s+(.+)', re.IGNORECASE) append_regexp = re.compile( r'\s*[Aa][Pp][Pp][Ee][Nn][Dd]\s+.*\binitrd=([^\s]+)') kernel = None initrd = None for line in self.image.g.cat(cfg).splitlines(): kernel_match = kernel_regexp.match(line) if kernel_match: kernel = kernel_match.group(1).strip() continue initrd_match = initrd_regexp.match(line) if initrd_match: initrd = initrd_match.group(1).strip() continue append_match = append_regexp.match(line) if append_match: initrd = append_match.group(1) if kernel and kernel[0] != '/': relative_path = kernel elif initrd and initrd[0] != '/': relative_path = initrd else: # The config does not contain relative paths. Use the directory of # the config. return os.path.dirname(cfg) for d in self.syslinux.search_dirs: if self.image.g.is_file(d + relative_path): return d raise FatalError("Unable to find the working directory of extlinux")
def enable_guestfs(self): """Enable the guestfs handler""" if self.guestfs_enabled: self.out.warn("Guestfs is already enabled") return # Before version 1.18.4 the behavior of kill_subprocess was different # and you need to reset the guestfs handler to relaunch a previously # shut down QEMU backend if self.check_guestfs_version(1, 18, 4) < 0: self.g = guestfs.GuestFS() self.g.add_drive_opts(self.device, readonly=0) # Before version 1.17.14 the recovery process, which is a fork of the # original process that called libguestfs, did not close its inherited # file descriptors. This can cause problems especially if the parent # process has opened pipes. Since the recovery process is an optional # feature of libguestfs, it's better to disable it. if self.check_guestfs_version(1, 17, 14) >= 0: self.out.info("Enabling recovery process ...", False) self.g.set_recovery_proc(1) self.out.success('done') else: self.g.set_recovery_proc(0) # self.g.set_trace(1) # self.g.set_verbose(1) self.out.info('Launching helper VM (may take a while) ...', False) # self.progressbar = self.out.Progress(100, "Launching helper VM", # "percent") # eh = self.g.set_event_callback(self.progress_callback, # guestfs.EVENT_PROGRESS) try: self.g.launch() except RuntimeError as e: raise FatalError( "Launching libguestfs's helper VM failed!\nReason: %s.\n\n" "Please run `libguestfs-test-tool' for more info." % str(e)) self.guestfs_enabled = True # self.g.delete_event_callback(eh) # self.progressbar.success('done') # self.progressbar = None if self.check_guestfs_version(1, 18, 4) < 0: self.g.inspect_os() # some calls need this self.out.success('done')
def _install_viostor_driver(self, dirname): """Quick and dirty installation of the VirtIO SCSI controller driver. It is done to make the image boot from the VirtIO disk. http://rwmj.wordpress.com/2010/04/30/ tip-install-a-device-driver-in-a-windows-vm/ """ drivers_path = "%s/system32/drivers" % self.systemroot try: drivers_path = self.image.g.case_sensitive_path(drivers_path) except RuntimeError as err: raise FatalError("Unable to browse to directory: %s. Reason: %s" % (drivers_path, str(err))) viostor = dirname + os.sep + 'viostor.sys' try: self.image.g.upload(viostor, drivers_path + '/viostor.sys') except RuntimeError as err: raise FatalError("Unable to upload file %s to %s. Reason: %s" % (viostor, drivers_path, str(err))) self.registry.add_viostor()
def _dir_to_disk(self): """Create a disk out of a directory.""" if self.source == '/': bundle = BundleVolume(self.out, self.meta) image = '%s/%s.raw' % (self.tmp, uuid.uuid4().hex) def check_unlink(path): """Unlinks file if exists""" if os.path.exists(path): os.unlink(path) self._add_cleanup(check_unlink, image) bundle.create_image(image) return image raise FatalError("Using a directory as media source is supported")
def wait_on_serial(self, timeout): """Wait until the random token appears on the VM's serial port""" self._ntokens += 1 for _ in xrange(timeout): time.sleep(1) with open(self.serial) as f: current = 0 for line in f: if line.startswith(RANDOM_TOKEN): current += 1 if current == self._ntokens: return True if not self.isalive(): (_, stderr, rc) = self.wait() raise FatalError("Windows VM died unexpectedly!\n\n" "(rc=%d)\n%s" % (rc, stderr)) return False
def file(self): """Convert the source media into a file.""" if self._file is not None: return self._file self.out.info("Examining source media `%s' ..." % self.source, False) mode = os.stat(self.source).st_mode if stat.S_ISDIR(mode): self.out.success('looks like a directory') self._file = self._dir_to_disk() elif stat.S_ISREG(mode): self.out.success('looks like an image file') self._file = self.source elif not stat.S_ISBLK(mode): raise FatalError("Invalid media source. Only block devices, " "regular files and directories are supported.") else: self.out.success('looks like a block device') self._file = self.source return self._file
def open_hive(self, hive, write=False): """Returns a context manager for opening a hive file of the image for reading or writing. """ systemroot = self.image.g.inspect_get_windows_systemroot(self.root) path = "%s/system32/config/%s" % (systemroot, hive) try: path = self.image.g.case_sensitive_path(path) except RuntimeError as err: raise FatalError("Unable to retrieve file: %s. Reason: %s" % (hive, str(err))) # OpenHive class needs this since 'self' gets overwritten g = self.image.g class OpenHive(object): """The OpenHive context manager""" def __enter__(self): # pylint: disable=attribute-defined-outside-init localfd, self.localpath = tempfile.mkstemp() try: os.close(localfd) g.download(path, self.localpath) hive = hivex.Hivex(self.localpath, write=write) except: os.unlink(self.localpath) raise return hive def __exit__(self, exc_type, exc_value, traceback): try: if write: g.upload(self.localpath, path) finally: os.unlink(self.localpath) return OpenHive()
def image_creator(options, out): """snf-mkimage main function""" if os.geteuid() != 0: raise FatalError("You must run %s as root" % os.path.basename(sys.argv[0])) # Check if the authentication info is valid. The earlier the better if options.token is not None and options.url is not None: try: account = Kamaki.create_account(options.url, options.token) if account is None: raise FatalError("The authentication token and/or URL you " "provided is not valid!") else: kamaki = Kamaki(account, out) except ClientError as e: raise FatalError("Astakos client: %d %s" % (e.status, e.message)) elif options.cloud: avail_clouds = Kamaki.get_clouds() if options.cloud not in avail_clouds.keys(): raise FatalError( "Cloud: `%s' does not exist.\n\nAvailable clouds:\n\n\t%s\n" % (options.cloud, "\n\t".join(avail_clouds.keys()))) try: account = Kamaki.get_account(options.cloud) if account is None: raise FatalError("Cloud: `%s' exists but is not valid!" % options.cloud) else: kamaki = Kamaki(account, out) except ClientError as e: raise FatalError("Astakos client: %d %s" % (e.status, e.message)) if options.upload and not options.force: if kamaki.object_exists(options.container, options.upload): raise FatalError("Remote storage service object: `%s' exists " "(use --force to overwrite it)." % options.upload) if kamaki.object_exists(options.container, "%s.md5sum" % options.upload): raise FatalError("Remote storage service object: `%s.md5sum' " "exists (use --force to overwrite it)." % options.upload) if options.register and not options.force: if kamaki.object_exists(options.container, "%s.meta" % options.upload): raise FatalError("Remote storage service object `%s.meta' exists " "(use --force to overwrite it)." % options.upload) disk = Disk(options.source, out, options.tmp) # pylint: disable=unused-argument def signal_handler(signum, frame): disk.cleanup() signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) try: # There is no need to snapshot the media if it was created by the Disk # instance as a temporary object. device = disk.file if not options.snapshot else disk.snapshot() image = disk.get_image(device, sysprep_params=options.sysprep_params) if image.is_unsupported() and not options.allow_unsupported: raise FatalError( "The media seems to be unsupported.\n\n" + textwrap.fill("To create an image from an unsupported media, " "you'll need to use the`--allow-unsupported' " "command line option. Using this is highly " "discouraged, since the resulting image will " "not be cleared out of sensitive data and will " "not get customized during the deployment.")) if len(options.host_run) != 0 and not image.mount_local_support: raise FatalError("Running scripts against the guest media is not " "supported for this build of libguestfs.") if len(options.host_run) != 0: for script in options.host_run: if not os.path.isfile(script): raise FatalError("File: `%s' does not exist." % script) if not os.access(script, os.X_OK): raise FatalError("File: `%s' is not executable." % script) for name in options.disabled_syspreps: sysprep = image.os.get_sysprep_by_name(name) if sysprep is not None: image.os.disable_sysprep(sysprep) else: out.warn("Sysprep: `%s' does not exist. Can't disable it." % name) for name in options.enabled_syspreps: sysprep = image.os.get_sysprep_by_name(name) if sysprep is not None: image.os.enable_sysprep(sysprep) else: out.warn("Sysprep: `%s' does not exist. Can't enable it." % name) if image.is_unsupported(): image.meta['EXCLUDE_ALL_TASKS'] = "yes" # Add command line metadata to the collected ones... image.meta.update(options.metadata) if options.print_syspreps: image.os.print_syspreps() out.info() if options.print_sysprep_params: image.os.print_sysprep_params() out.info() if options.print_metadata: image.os.print_metadata() out.info() if options.outfile is None and not options.upload: return 0 if options.virtio is not None and \ hasattr(image.os, 'install_virtio_drivers'): image.os.install_virtio_drivers() if len(options.host_run) != 0: # Export image metadata as environment variables to make them # visible by the scripts for key, value in image.meta.items(): os.environ["SNF_IMAGE_CREATOR_METADATA_%s" % key] = str(value) out.info("Running scripts on the input media:") mpoint = tempfile.mkdtemp() try: image.mount(mpoint) if not image.is_mounted(): raise FatalError("Mounting the media on the host failed.") try: size = len(options.host_run) cnt = 1 for script in options.host_run: script = os.path.abspath(script) out.info(("(%d/%d)" % (cnt, size)).ljust(7), False) out.info("Running `%s'" % script) ret = subprocess.Popen([script], cwd=mpoint).wait() if ret != 0: raise FatalError("Script: `%s' failed (rc=%d)" % (script, ret)) cnt += 1 finally: while not image.umount(): out.warn("Unable to umount the media. Retrying ...") time.sleep(1) out.info() finally: os.rmdir(mpoint) if options.sysprep: image.os.do_sysprep() checksum = image.md5() image_meta = {} for k, v in image.meta.items(): image_meta[str(k)] = str(v) metastring = json.dumps( { 'properties': image_meta, 'disk-format': 'diskdump' }, ensure_ascii=False) img_properties = json.dumps(image_meta, ensure_ascii=False) if options.outfile is not None: if os.path.realpath(options.outfile) == '/dev/null': out.warn('Not dumping file to /dev/null') else: image.dump(options.outfile) out.info('Dumping metadata file ...', False) with open('%s.%s' % (options.outfile, 'meta'), 'w') as f: f.write(metastring) out.success('done') out.info('Dumping md5sum file ...', False) with open('%s.%s' % (options.outfile, 'md5sum'), 'w') as f: f.write('%s %s\n' % (checksum, os.path.basename(options.outfile))) out.success('done') out.info('Dumping variant file ...', False) with open('%s.%s' % (options.outfile, 'variant'), 'w') as f: f.write( to_shell(IMG_ID=options.outfile, IMG_FORMAT="diskdump", IMG_PROPERTIES=img_properties)) out.success('done') out.info() try: if options.upload: out.info("Uploading image to the storage service:") with image.raw_device() as raw: with open(raw, 'rb') as f: remote = kamaki.upload( f, image.size, options.upload, options.container, None, "(1/3) Calculating block hashes", "(2/3) Uploading missing blocks") out.info("(3/3) Uploading md5sum file ...", False) md5sumstr = '%s %s\n' % (checksum, os.path.basename(options.upload)) kamaki.upload(StringIO.StringIO(md5sumstr), size=len(md5sumstr), remote_path="%s.%s" % (options.upload, 'md5sum'), container=options.container, content_type="text/plain") out.success('done') out.info() if options.register: img_type = 'public' if options.public else 'private' out.info( 'Registering %s image with the compute service ...' % img_type, False) result = kamaki.register(options.register, remote, image.meta, options.public) out.success('done') out.info("Uploading metadata file ...", False) metastring = unicode( json.dumps(result, ensure_ascii=False, indent=4)) kamaki.upload(StringIO.StringIO(metastring.encode('utf8')), size=len(metastring), remote_path="%s.%s" % (options.upload, 'meta'), container=options.container, content_type="application/json") out.success('done') if options.public: out.info("Sharing md5sum file ...", False) kamaki.share("%s.md5sum" % options.upload) out.success('done') out.info("Sharing metadata file ...", False) kamaki.share("%s.meta" % options.upload) out.success('done') out.result(json.dumps(result, indent=4, ensure_ascii=False)) out.info() except ClientError as e: raise FatalError("Service client: %d %s" % (e.status, e.message)) finally: out.info('cleaning up ...') disk.cleanup() out.success("snf-image-creator exited without errors") return 0
def __init__(self, disk, params, admin): """Create VM instance""" self.disk = disk self.params = params self.admin = admin self.interface = 'virtio' # expected number of token occurrences in serial port self._ntokens = 0 kvm, needed_args = get_kvm_binary() if kvm is None: raise FatalError("Can't find the kvm binary") self.kvm = [kvm] + list(needed_args) def random_mac(): """creates a random mac address""" mac = [0x00, 0x16, 0x3e, random.randint(0x00, 0x7f), random.randint(0x00, 0xff), random.randint(0x00, 0xff)] return ':'.join(['%02x' % x for x in mac]) self.mac = random_mac() def random_password(): """Creates a random password""" # I borrowed this from Synnefo pool = lowercase + uppercase + digits lowerset = set(lowercase) upperset = set(uppercase) digitset = set(digits) length = 10 password = ''.join(random.choice(pool) for i in range(length - 2)) # Make sure the password is compliant chars = set(password) if not chars & lowerset: password += random.choice(lowercase) if not chars & upperset: password += random.choice(uppercase) if not chars & digitset: password += random.choice(digits) # Pad if necessary to reach required length password += ''.join(random.choice(pool) for i in range(length - len(password))) return password self.password = random_password() # Use Ganeti's VNC port range for a random vnc port self.display = random.randint(11000, 14999) - 5900 self.serial = None self.process = None
def _boot_virtio_vm(self): """Boot the media and install the VirtIO drivers""" old_windows = self.check_version(6, 1) <= 0 self.image.disable_guestfs() try: timeout = self.sysprep_params['boot_timeout'].value shutdown_timeout = self.sysprep_params['shutdown_timeout'].value virtio_timeout = self.sysprep_params['virtio_timeout'].value self.out.info("Starting Windows VM ...", False) booted = False try: if old_windows: self.vm.start() else: self.vm.interface = 'ide' self.vm.start(extra_disk=('/dev/null', 'virtio')) self.vm.interface = 'virtio' self.out.success("started (console on VNC display: %d)" % self.vm.display) self.out.info("Waiting for Windows to boot ...", False) if not self.vm.wait_on_serial(timeout): raise FatalError("Windows VM booting timed out!") self.out.success('done') booted = True self.out.info("Installing new drivers ...", False) if not self.vm.wait_on_serial(virtio_timeout): raise FatalError("Windows VirtIO installation timed out!") self.out.success('done') self.out.info('Shutting down ...', False) (_, stderr, rc) = self.vm.wait(shutdown_timeout) if rc != 0 or "terminating on signal" in stderr: raise FatalError("Windows VM died unexpectedly!\n\n" "(rc=%d)\n%s" % (rc, stderr)) self.out.success('done') finally: self.vm.stop(shutdown_timeout if booted else 1, fatal=False) finally: self.image.enable_guestfs() with self.mount(readonly=True, silent=True): self.virtio_state = self.compute_virtio_state() viostor_service_found = self.registry.check_viostor_service() if not (len(self.virtio_state['viostor']) and viostor_service_found): raise FatalError("viostor was not successfully installed") if self.check_version(6, 1) > 0: # Hopefully restart in safe mode. Newer windows will not boot from # a viostor device unless we initially start them in safe mode try: self.out.info('Rebooting Windows VM in safe mode ...', False) self.vm.start() (_, stderr, rc) = self.vm.wait(timeout + shutdown_timeout) if rc != 0 or "terminating on signal" in stderr: raise FatalError("Windows VM died unexpectedly!\n\n" "(rc=%d)\n%s" % (rc, stderr)) self.out.success('done') finally: self.vm.stop(1, fatal=True)
def handler(signum, frame): """Signal handler""" raise FatalError("VM wait timed-out.")
def __init__(self, image, **kwargs): super(Windows, self).__init__(image, **kwargs) # The commit with the following message was added in # libguestfs 1.17.18 and was backported in version 1.16.11: # # When a Windows guest doesn't have a HKLM\SYSTEM\MountedDevices node, # inspection fails. However inspection should not completely fail just # because we cannot get the drive letter mapping from a guest. # # Since Microsoft Sysprep removes the aforementioned key, image # creation for windows can only be supported if the installed guestfs # version is 1.17.18 or higher if self.image.check_guestfs_version(1, 17, 18) < 0 and \ (self.image.check_guestfs_version(1, 17, 0) >= 0 or self.image.check_guestfs_version(1, 16, 11) < 0): raise FatalError( 'For windows support libguestfs 1.16.11 or above is required') device = self.image.g.part_to_dev(self.root) self.last_part_num = self.image.g.part_list(device)[-1]['part_num'] self.product_name = self.image.g.inspect_get_product_name(self.root) self.systemroot = self.image.g.inspect_get_windows_systemroot( self.root) self.registry = Registry(self.image) with self.mount(readonly=True, silent=True): self.out.info("Checking media state ...", False) # Enumerate the windows users (self.usernames, self.active_users, self.admins) = self.registry.enum_users() assert ADMIN_RID in self.usernames, "Administrator account missing" self.virtio_state = self.compute_virtio_state() self.arch = self.image.g.inspect_get_arch(self.root) if self.arch == 'x86_64': self.arch = 'amd64' elif self.arch == 'i386': self.arch = 'x86' major = int(self.image.g.inspect_get_major_version(self.root)) minor = int(self.image.g.inspect_get_minor_version(self.root)) self.nt_version = (major, minor) # The get_setup_state() command does not work for old windows if self.nt_version[0] >= 6: # If the image is already sysprepped, we cannot further # customize it. self.sysprepped = self.registry.get_setup_state() > 0 else: # Fallback to NO although we done know # TODO: Add support for detecting the setup state on XP self.sysprepped = False self.out.success("done") # If the image is sysprepped no driver mappings will be present. self.systemdrive = None for drive, root in self.image.g.inspect_get_drive_mappings(self.root): if root == self.root: self.systemdrive = drive active_admins = [u for u in self.admins if u in self.active_users] if ADMIN_RID in self.active_users or len(active_admins) == 0: admin = ADMIN_RID else: active_admins.sort() admin = active_admins[0] self.vm = VM( self.image.device, self.sysprep_params, namedtuple('User', 'rid name')(admin, self.usernames[admin]))
def run(self, src, dest, slabel='source', dlabel='destination'): """Run the actual command""" cmd = [] cmd.append('rsync') cmd.extend(self._options) for i in self._exclude: cmd.extend(['--exclude', i]) self._out.info("Calculating total number of %s files ..." % slabel, False) # If you don't specify a destination, rsync will list the source files. dry_run = subprocess.Popen(cmd + [src], shell=False, stdout=subprocess.PIPE, bufsize=0) try: total = 0 for _ in iter(dry_run.stdout.readline, b''): total += 1 finally: dry_run.communicate() if dry_run.returncode != 0: raise FatalError("rsync failed") self._out.success("%d" % total) progress = self._out.Progress(total, "Copying files to %s" % dlabel) run = subprocess.Popen(cmd + [src, dest], shell=False, stdout=subprocess.PIPE, bufsize=0) try: t = time.time() i = 0 for _ in iter(run.stdout.readline, b''): i += 1 current = time.time() if current - t > 0.1: t = current progress.goto(i) progress.success('done') finally: def handler(signum, frame): # pylint: disable=unused-argument """Signal handler""" run.terminate() time.sleep(1) run.poll() if run.returncode is None: run.kill() run.wait() signal.signal(signal.SIGALRM, handler) signal.alarm(2) run.communicate() signal.alarm(0) if run.returncode != 0: raise FatalError("rsync failed")
def do_sysprep(self): """Prepare system for image creation.""" self.out.info('Preparing system for image creation:') # Check if winexe is installed if not WinEXE.is_installed(): raise FatalError( "Winexe not found! In order to be able to customize a Windows " "image you need to have Winexe installed.") if self.sysprepped: raise FatalError( "Microsoft's System Preparation Tool has ran on the media. " "Further image customization is not possible.") if len(self.virtio_state['viostor']) == 0: raise FatalError( "The media has no VirtIO SCSI controller driver installed. " "Further image customization is not possible.") if len(self.virtio_state['netkvm']) == 0: raise FatalError( "The media has no VirtIO Ethernet Adapter driver installed. " "Further image customization is not possible.") timeout = self.sysprep_params['boot_timeout'].value shutdown_timeout = self.sysprep_params['shutdown_timeout'].value self.out.info("Preparing media for boot ...", False) with self.mount(readonly=False, silent=True): if not self.registry.reset_account(self.vm.admin.rid): self._add_cleanup('sysprep', self.registry.reset_account, self.vm.admin.rid, False) old = self.registry.update_uac(0) if old != 0: self._add_cleanup('sysprep', self.registry.update_uac, old) old = self.registry.update_uac_remote_setting(1) if old != 1: self._add_cleanup('sysprep', self.registry.update_uac_remote_setting, old) def if_not_sysprepped(task, *args): """Only perform this if the image is not sysprepped""" if not self.sysprepped: task(*args) # The next 2 registry values get completely removed by Microsoft # Sysprep. They should not be reverted if Sysprep gets executed. old = self.registry.update_noautoupdate(1) if old != 1: self._add_cleanup('sysprep', if_not_sysprepped, self.registry.update_noautoupdate, old) old = self.registry.update_auoptions(1) if old != 1: self._add_cleanup('sysprep', if_not_sysprepped, self.registry.update_auoptions, old) # disable the firewalls self._add_cleanup('sysprep', self.registry.update_firewalls, *self.registry.update_firewalls(0, 0, 0)) v_val = self.registry.reset_passwd(self.vm.admin.rid) self._add_boot_scripts() # Delete the pagefile. It will be recreated when the system boots try: pagefile = "%s/pagefile.sys" % self.systemroot self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile)) except RuntimeError: pass self.out.success('done') self.image.disable_guestfs() booted = False try: self.out.info("Starting windows VM ...", False) self.vm.start() try: self.out.success("started (console on VNC display: %d)" % self.vm.display) self.out.info("Waiting for OS to boot ...", False) if not self.vm.wait_on_serial(timeout): raise FatalError("Windows VM booting timed out!") self.out.success('done') booted = True # Since the password is reset when logging in, sleep a little # bit before checking the connectivity, to avoid race # conditions time.sleep(5) self.out.info("Checking connectivity to the VM ...", False) self._check_connectivity() # self.out.success('done') # self.out.info("Disabling automatic logon ...", False) self._disable_autologon() self.out.success('done') self._exec_sysprep_tasks() self.out.info("Waiting for windows to shut down ...", False) (_, stderr, rc) = self.vm.wait(shutdown_timeout) if rc != 0 or "terminating on signal" in stderr: raise FatalError("Windows VM died unexpectedly!\n\n" "(rc=%d)\n%s" % (rc, stderr)) self.out.success("done") finally: # if the VM is not already dead here, a Fatal Error will have # already been raised. There is no reason to make the command # fatal. self.vm.stop(shutdown_timeout if booted else 1, fatal=False) finally: self.image.enable_guestfs() self.out.info("Reverting media boot preparations ...", False) with self.mount(readonly=False, silent=True, fatal=False): if not self.ismounted: self.out.warn("The boot changes cannot be reverted. " "The snapshot may be in a corrupted state.") else: if not self.sysprepped: # Reset the old password self.registry.reset_passwd(self.vm.admin.rid, v_val) self._cleanup('sysprep') self.out.success("done") self.image.shrink(silent=True)
def install_virtio_drivers(self, upgrade=True): """Install new VirtIO drivers on the input media. If upgrade is True, then the old drivers found in the media will be removed. """ dirname = self.sysprep_params['virtio'].value if not dirname: raise FatalError('No directory hosting the VirtIO drivers defined') self.out.info('Installing VirtIO drivers:') valid_drvs = self._fetch_virtio_drivers(dirname) if not len(valid_drvs): self.out.warn('No suitable driver found to install!') return remove = [] certs = [] install = [] add = [] # Check which drivers we need to install, which to add to the database # and which to remove. for dtype in valid_drvs: versions = [v['DriverVer'] for k, v in valid_drvs[dtype].items()] certs.extend([ v['CatalogFile'] for k, v in valid_drvs[dtype].items() if 'CatalogFile' in v ]) installed = [(k, v['DriverVer']) for k, v in self.virtio_state[dtype].items()] found = [d[0] for d in installed if d[1] in versions] not_found = [d[0] for d in installed if d[1] not in versions] for drvr in found: details = self.virtio_state[dtype][drvr] self.out.warn( '%s driver with version %s is already installed!' % (dtype, details['DriverVer'])) if upgrade: remove.extend(not_found) if dtype == 'viostor': install.extend([d for d in valid_drvs[dtype]]) else: add.extend([d for d in valid_drvs[dtype]]) try: self._update_driver_database('virtio', upload=dirname, certs=certs, add=add, install=install, remove=remove) finally: with self.mount(readonly=False, silent=True, fatal=False): if not self.ismounted: self.out.warn("The boot changes cannot be reverted. " "The image may be in a corrupted state.") else: self._cleanup('virtio') self.out.success("VirtIO drivers were successfully installed") self.out.info()
def create_image(session, answers): """Create an image using the information collected by the wizard""" image = session['image'] with_progress = OutputWthProgress() image.out.append(with_progress) try: image.out.clear() if 'virtio' in answers and image.os.sysprep_params['virtio'].value: image.os.install_virtio_drivers() # Sysprep image.os.do_sysprep() metadata = image.os.meta update_background_title(session) metadata['DESCRIPTION'] = answers['ImageDescription'] # MD5 session['checksum'] = image.md5() image.out.info() try: image.out.info("Uploading image to the cloud:") account = Kamaki.get_account(answers['Cloud']) assert account, "Cloud: %s is not valid" % answers['Cloud'] kamaki = Kamaki(account, image.out) name = "%s-%s.diskdump" % (answers['ImageName'], time.strftime("%Y%m%d%H%M")) with image.raw_device() as raw: with open(raw, 'rb') as device: remote = kamaki.upload(device, image.size, name, CONTAINER, None, "(1/3) Calculating block hashes", "(2/3) Uploading image blocks") image.out.info("(3/3) Uploading md5sum file ...", False) md5sumstr = '%s %s\n' % (session['checksum'], name) kamaki.upload(StringIO.StringIO(md5sumstr), size=len(md5sumstr), remote_path="%s.%s" % (name, 'md5sum'), container=CONTAINER, content_type="text/plain") image.out.success('done') image.out.info() image.out.info( 'Registering %s image with the cloud ...' % answers['RegistrationType'].lower(), False) result = kamaki.register(answers['ImageName'], remote, metadata, answers['RegistrationType'] == "Public") image.out.success('done') image.out.info("Uploading metadata file ...", False) metastring = unicode(json.dumps(result, ensure_ascii=False)).encode('utf8') kamaki.upload(StringIO.StringIO(metastring), size=len(metastring), remote_path="%s.%s" % (name, 'meta'), container=CONTAINER, content_type="application/json") image.out.success('done') if answers['RegistrationType'] == "Public": image.out.info("Sharing md5sum file ...", False) kamaki.share("%s.md5sum" % name) image.out.success('done') image.out.info("Sharing metadata file ...", False) kamaki.share("%s.meta" % name) image.out.success('done') image.out.info() except ClientError as error: raise FatalError("Storage service client: %d %s" % (error.status, error.message)) finally: image.out.remove(with_progress) text = "The %s image was successfully uploaded to the storage service " \ "and registered with the compute service of %s. Would you like " \ "to keep a local copy?" % \ (answers['RegistrationType'].lower(), answers['Cloud']) if session['dialog'].yesno(text, width=PAGE_WIDTH) == session['dialog'].OK: extract_image(session)