# # Copyright 2006-2009 Red Hat, Inc. # Daniel P. Berrange # # 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; either version 2 of the License, or # (at your option) any later version. # # 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. import logging import os import shutil import subprocess import tempfile import urlgrabber import Storage import support import _util import Installer from VirtualDisk import VirtualDisk from User import User import OSDistro def _is_url(url, is_local): """ Check if passed string is a (pseudo) valid http, ftp, or nfs url. """ if is_local and os.path.exists(url): if os.path.isdir(url): return True else: return False return (url.startswith("http://") or url.startswith("ftp://") or url.startswith("nfs:")) def _sanitize_url(url): """ Do nothing for http or ftp, but make sure nfs is in the expected format """ if url.startswith("nfs://"): # Convert RFC compliant NFS nfs://server/path/to/distro # to what mount/anaconda expect nfs:server:/path/to/distro # and carry the latter form around internally url = "nfs:" + url[6:] # If we need to add the : after the server index = url.find("/", 4) if index == -1: raise ValueError(_("Invalid NFS format: No path specified.")) if url[index - 1] != ":": url = url[:index] + ":" + url[index:] return url def _build_pool(conn, meter, path): pool = _util.lookup_pool_by_path(conn, path) if pool: logging.debug("Existing pool '%s' found for %s", pool.name(), path) pool.refresh(0) return pool name = _util.generate_name("boot-scratch", conn.storagePoolLookupByName) logging.debug("Building storage pool: path=%s name=%s", path, name) poolbuild = Storage.DirectoryPool(conn=conn, name=name, target_path=path) # Explicitly don't build? since if we are creating this directory # we probably don't have correct perms return poolbuild.install(meter=meter, create=True, build=False, autostart=True) def _upload_file(conn, meter, destpool, src): # Build stream object stream = conn.newStream(0) def safe_send(data): while True: ret = stream.send(data) if ret == 0 or ret == len(data): break data = data[ret:] if meter is None: meter = urlgrabber.progress.BaseMeter() # Build placeholder volume size = os.path.getsize(src) basename = os.path.basename(src) poolpath = _util.get_xml_path(destpool.XMLDesc(0), "/pool/target/path") name = Storage.StorageVolume.find_free_name(basename, pool_object=destpool) if name != basename: logging.debug("Generated non-colliding volume name %s", name) disk = VirtualDisk(conn=conn, path=os.path.join(poolpath, name), sizebytes=size, sparse=True) disk.setup_dev(meter=meter) vol = disk.vol_object if not vol: raise RuntimeError(_("Failed to lookup scratch media volume")) try: # Register upload offset = 0 length = size flags = 0 stream.upload(vol, offset, length, flags) # Open source file fileobj = file(src, "r") # Start transfer total = 0 meter.start(size=size, text=_("Transferring %s") % os.path.basename(src)) while True: #blocksize = (1024 ** 2) blocksize = 1024 data = fileobj.read(blocksize) if not data: break safe_send(data) total += len(data) meter.update(total) # Cleanup stream.finish() meter.end(size) except: if vol: vol.delete(0) raise return vol class DistroInstaller(Installer.Installer): def __init__(self, type="xen", location=None, boot=None, extraargs=None, os_type=None, conn=None, caps=None): Installer.Installer.__init__(self, type, location, boot, extraargs, os_type, conn=conn, caps=caps) self._livecd = False # True == location is a filesystem path # False == location is a url self._location_is_path = True # DistroInstaller specific methods/overwrites def _get_livecd(self): return self._livecd def _set_livecd(self, val): self._livecd = bool(val) livecd = property(_get_livecd, _set_livecd) def get_location(self): return self._location def set_location(self, val): """ Valid values for location: 1) it can be a local file (ex. boot.iso), directory (ex. distro tree) or physical device (ex. cdrom media) 2) tuple of the form (poolname, volname) pointing to a file or device which will set location as that path 3) http, ftp, or nfs path for an install tree """ is_tuple = False validated = True self._location_is_path = True is_local = (not self.conn or not self.is_remote()) # Basic validation if type(val) is not str and (type(val) is not tuple and len(val) != 2): raise ValueError(_("Invalid 'location' type %s." % type(val))) if type(val) is tuple and len(val) == 2: logging.debug("DistroInstaller location is a (poolname, volname)" " tuple") if not self.conn: raise ValueError(_("'conn' must be specified if 'location' is" " a storage tuple.")) is_tuple = True elif _is_url(val, is_local): val = _sanitize_url(val) self._location_is_path = False logging.debug("DistroInstaller location is a network source.") elif os.path.exists(os.path.abspath(val)) and is_local: val = os.path.abspath(val) logging.debug("DistroInstaller location is a local " "file/path: %s", val) else: # Didn't determine anything about the location validated = False if self._location_is_path or (validated == False and self.conn and _util.is_storage_capable(self.conn)): # If user passed a storage tuple, OR # We couldn't determine the location type and a storage capable # connection was passed: # Pass the parameters off to VirtualDisk to validate, and pull # out the path stuple = (is_tuple and val) or None path = (not is_tuple and val) or None try: d = VirtualDisk(path=path, device=VirtualDisk.DEVICE_CDROM, transient=True, readOnly=True, conn=self.conn, volName=stuple) val = d.path except: logging.debug("Error validating install location", exc_info=True) raise ValueError(_("Checking installer location failed: " "Could not find media '%s'." % str(val))) elif not validated: raise ValueError(_("Install media location must be an NFS, HTTP " "or FTP network install source, or an existing " "file/device")) if (not self._location_is_path and val.startswith("nfs:") and not User.current().has_priv(User.PRIV_NFS_MOUNT, (self.conn and self.get_uri()))): raise ValueError(_('Privilege is required for NFS installations')) self._location = val location = property(get_location, set_location) # Private helper methods def _prepare_cdrom(self, guest, meter): transient = not self.livecd if not self._location_is_path: # Xen needs a boot.iso if its a http://, ftp://, or nfs: url (store_ignore, os_type_ignore, os_variant_ignore, media) = \ OSDistro.acquireBootDisk(guest, self.location, meter, self.scratchdir) cdrom = media self._tmpfiles.append(cdrom) transient = True else: cdrom = self.location disk = VirtualDisk(path=cdrom, conn=guest.conn, device=VirtualDisk.DEVICE_CDROM, readOnly=True, transient=transient) self.install_devices.append(disk) def _perform_initrd_injections(self, initrd): """ Insert files into the root directory of the initial ram disk """ tempdir = tempfile.mkdtemp(dir=self.scratchdir) os.chmod(tempdir, 0775) for filename in self._initrd_injections: logging.debug("Copying %s to the initrd.", filename) shutil.copy(filename, tempdir) logging.debug("Appending to the initrd.") find_proc = subprocess.Popen(['find', '.', '-print0'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=tempdir) cpio_proc = subprocess.Popen(['cpio', '-o', '--null', '-Hnewc', '--quiet'], stdin=find_proc.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=tempdir) f = open(initrd, 'ab') gzip_proc = subprocess.Popen(['gzip'], stdin=cpio_proc.stdout, stdout=f, stderr=subprocess.PIPE) cpio_proc.wait() find_proc.wait() gzip_proc.wait() f.close() shutil.rmtree(tempdir) finderr = find_proc.stderr.read() cpioerr = cpio_proc.stderr.read() gziperr = gzip_proc.stderr.read() if finderr: logging.debug("find stderr=%s", finderr) if cpioerr: logging.debug("cpio stderr=%s", cpioerr) if gziperr: logging.debug("gzip stderr=%s", gziperr) def support_remote_url_install(self): if not self.conn: return False if hasattr(self.conn, "_virtinst__fake_conn"): return False return support.check_stream_support(self.conn, support.SUPPORT_STREAM_UPLOAD) def _upload_media(self, guest, meter, kernel, initrd): conn = guest.conn system_scratchdir = self._get_system_scratchdir() if (not guest.is_remote() and (self.is_session_uri() or self.scratchdir == system_scratchdir)): # We have access to system scratchdir, don't jump through hoops logging.debug("Have access to preferred scratchdir so" " nothing to upload") return kernel, initrd if not self.support_remote_url_install(): logging.debug("Media upload not supported") return kernel, initrd # Build pool logging.debug("Uploading kernel/initrd media") pool = _build_pool(conn, meter, system_scratchdir) kvol = _upload_file(conn, meter, pool, kernel) newkernel = kvol.path() self._tmpvols.append(kvol) ivol = _upload_file(conn, meter, pool, initrd) newinitrd = ivol.path() self._tmpvols.append(ivol) return newkernel, newinitrd def _prepare_kernel_and_initrd(self, guest, meter): disk = None # If installing off a local path, map it through to a virtual CD/disk if (self.location is not None and self._location_is_path and not os.path.isdir(self.location)): device = VirtualDisk.DEVICE_CDROM if (self.is_xenpv() and not guest._lookup_osdict_key('pv_cdrom_install')): device = VirtualDisk.DEVICE_DISK disk = VirtualDisk(conn=guest.conn, device=device, path=self.location, readOnly=True, transient=True) # Make sure we always fetch kernel here if required if self._install_bootconfig.kernel and not self.scratchdir_required(): return disk # Need to fetch the kernel & initrd from a remote site, or # out of a loopback mounted disk image/device ignore, os_type, os_variant, media = OSDistro.acquireKernel(guest, self.location, meter, self.scratchdir, self.os_type) (kernelfn, initrdfn, args) = media if guest.get_os_autodetect(): if os_type: logging.debug("Auto detected OS type as: %s", os_type) guest.os_type = os_type if (os_variant and guest.os_type == os_type): logging.debug("Auto detected OS variant as: %s", os_variant) guest.os_variant = os_variant self._tmpfiles.append(kernelfn) if initrdfn: self._tmpfiles.append(initrdfn) if self._initrd_injections: self._perform_initrd_injections(initrdfn) # If required, upload media to an accessible guest location kernelfn, initrdfn = self._upload_media(guest, meter, kernelfn, initrdfn) self._install_bootconfig.kernel = kernelfn self._install_bootconfig.initrd = initrdfn self._install_bootconfig.kernel_args = args return disk def _persistent_cd(self): return (self._location_is_path and self.cdrom and self.livecd) def _get_bootdev(self, isinstall, guest): if isinstall or self._persistent_cd(): bootdev = self.bootconfig.BOOT_DEVICE_CDROM else: bootdev = self.bootconfig.BOOT_DEVICE_HARDDISK return bootdev # General Installer methods def scratchdir_required(self): if not self.location: return False is_url = not self._location_is_path mount_dvd = self._location_is_path and not self.cdrom return bool(is_url or mount_dvd) def prepare(self, guest, meter): self.cleanup() dev = None if self.cdrom: if self.location: dev = self._prepare_cdrom(guest, meter) else: # Booting from a cdrom directly allocated to the guest pass else: dev = self._prepare_kernel_and_initrd(guest, meter) if dev: self.install_devices.append(dev) def check_location(self): if self._location_is_path: # We already mostly validated this return True else: # This will throw an error for us OSDistro.detectMediaDistro(location=self.location, arch=self.arch) return True def detect_distro(self): try: dist_info = OSDistro.detectMediaDistro(location=self.location, arch=self.arch) except: logging.exception("Error attempting to detect distro.") return (None, None) # detectMediaDistro should only return valid values dtype, dvariant = dist_info return (dtype, dvariant)