def Update(root = None, conf = None, handler = None): """ Perform an update. Calls CheckForUpdates() first, to see if there are any. If there are, then magic happens. """ deleted_packages = [] other_packages = [] def UpdateHandler(op, pkg, old): if op == "delete": deleted_packages.append(pkg) else: other_packages.append((pkg, old)) if handler is not None: handler(op, pkg, old) new_man = CheckForUpdates(root, UpdateHandler) if new_man is None: return # Now we have a list of deleted packages, and a list # of update/install packages. # The task is to delete the first set of packages, # and then run through the others to install/update. # First, however, we need to get the files. # We want to use the system configuration, unless one has been # specified -- we don't want to use the target root's. if conf is None: conf = Configuration.Configuration(root = root) for pkg in deleted_packages: print >> sys.stderr, "Want to delete package %s" % pkg.Name() # if conf.PackageDB().RemovePackageContents(pkg) == False: # print >> sys.stderr, "Unable to remove contents package %s" % pkg.Name() # sys.exit(1) # conf.PackageDB().RemovePackage(pkg.Name()) process_packages = [] for (pkg, old) in other_packages: process_packages.append(pkg) installer = Installer.Installer(manifest = new_man, root = root, config = conf) installer.GetPackages(process_packages) print "Packages = %s" % installer._packages return
def ApplyUpdate(directory, install_handler=None): """ Apply the update in <directory>. As with PendingUpdates(), it will have to verify the contents before it actually installs them, so it has the same behaviour with incomplete or invalid content. """ rv = False conf = Configuration.Configuration() changes = PendingUpdates(directory) if changes is None: # That could have happened for multiple reasons. # PendingUpdates should probably throw an exception # on error return False # Do I have to worry about a race condition here? new_manifest = Manifest.Manifest() new_manifest.LoadPath(directory + "/MANIFEST") conf.SetPackageDir(directory) deleted_packages = [] updated_packages = [] for (pkg, op, old) in changes: if op == "delete": log.debug("Delete package %s" % pkg.Name()) deleted_packages.append(pkg) elif op == "install": log.debug("Install package %s" % pkg.Name()) else: log.debug("Upgrade package %s-%s to %s-%s" % (old.Name(), old.Version(), pkg.Name(), pkg.Version())) updated_packages.append(pkg) if len(deleted_packages) == 0 and len(updated_packages) == 0: # The manifest may have other differences, so we should # probably do something. log.debug("New manifest has no package changes, what should we do?") RemoveUpdate(directory) return True # Now we start doing the update! clone_name = None mount_point = None try: clone_name = "%s-%s" % (Avatar(), new_manifest.Sequence()) if CreateClone(clone_name) is False: s = "Unable to create boot-environment %s" % clone_name log.error(s) raise Exception(s) mount_point = MountClone(clone_name) if mount_point is None: s = "Unable to mount boot-environment %s" % clone_name log.error(s) DeleteClone(clone_name) raise Exception(s) # Remove any deleted packages for pkg in deleted_packages: log.debug("About to delete package %s" % pkg.Name()) if conf.PackageDB(mount_point).RemovePackageContents(pkg) == False: s = "Unable to remove contents for packate %s" % pkg.Name() if mount_point: UnmountClone(clone_name, mount_point) mount_point = None DeleteClone(clone_name) raise Exception(s) conf.PackageDB(mount_point).RemovePackage(pkg.Name()) installer = Installer.Installer(manifest=new_manifest, root=mount_point, config=conf) installer.GetPackages(pkgList=updated_packages) log.debug("Installer got packages %s" % installer._packages) # Now to start installing them rv = False if installer.InstallPackages(handler=install_handler) is False: log.error("Unable to install packages") raise Exception("Unable to install packages") else: new_manifest.Save(mount_point) if mount_point: if UnmountClone(clone_name, mount_point) is False: s = "Unable to unmount clone environment %s from mount point %s" % ( clone_name, mount_point) log.error(s) raise Exception(s) mount_point = None if ActivateClone(clone_name) is False: s = "Unable to activate clone environment %s" % clone_name log.error(s) raise Exception(s) RemoveUpdate(directory) rv = True RunCommand("/sbin/zpool", ["scrub", "freenas-boot"]) except BaseException as e: log.error("Update got exception during update: %s" % str(e)) if mount_point: UnmountClone(clone_name, mount_point) if clone_name: DeleteClone(clone_name) raise e return rv
def Update(root=None, conf=None, train=None, check_handler=None, get_handler=None, install_handler=None, cache_dir=None): """ Perform an update. If cache_dir is set, and contains a valid set of files (MANIFEST exists, and is for the current train, and the sequence is not the same as the system sequence), it will use that, rather than downloading. It calls CheckForUpdates() otherwise, to see if there are any. If there are, then magic happens. """ log.debug("Update(root = %s, conf = %s, train = %s, cache_dir = %s)" % (root, conf, train, cache_dir)) if conf is None: conf = Configuration.Configuration(root) deleted_packages = [] process_packages = [] def UpdateHandler(op, pkg, old): if op == "delete": deleted_packages.append(pkg) else: process_packages.append(pkg) if check_handler is not None: check_handler(op, pkg, old) new_man = None if cache_dir: if os.path.exists(cache_dir + "/MANIFEST"): try: cur = conf.SystemManifest() new_man = Manifest.Manifest() new_man.LoadPath(cache_dir + "/MANIFEST") if new_man.Train() != cur.Train(): new_man = None cache_dir = None else: diffs = Manifest.CompareManifests(cur, new_man) for (pkg, op, old) in diffs: UpdateHandler(op, pkg, old) conf.SetPackageDir(cache_dir) except Exception as e: log.debug("Caught exception %s" % str(e)) new_man = None cache_dir = None if new_man is None: new_man = CheckForUpdates(root, handler=UpdateHandler, train=train) if new_man is None: return if len(deleted_packages) == 0 and len(process_packages) == 0: # We have a case where a manifest was updated, but we # don't actually have any changes to the packages. We # should install the new manifest, and be done -- it # may have new release notes, or other issues. # Right now, I'm not quite sure how to do this. log.debug("Updated manifest but no package differences") return # Now we have a list of deleted packages, and a list # of update/install packages. # The task is to delete the first set of packages, # and then run through the others to install/update. # First, however, we need to get the files. # We want to use the system configuration, unless one has been # specified -- we don't want to use the target root's. if conf is None: conf = Configuration.Configuration() # If root is None, then we will try to create a clone # environment. (If the caller wants to install into the # current boot environment, set root = "" or "/".) clone_name = None mount_point = None try: if root is None: # We clone the existing boot environment to # "Avatar()-<sequence>" clone_name = "%s-%s" % (Avatar(), new_man.Sequence()) if CreateClone(clone_name) is False: log.error("Unable to create boot-environment %s" % clone_name) raise Exception("Unable to create new boot-environment %s" % clone_name) mount_point = MountClone(clone_name) if mount_point is None: log.error("Unable to mount boot-environment %s" % clone_name) DeleteClone(clone_name) raise Exception("Unable to mount boot-environment %s" % clone_name) else: root = mount_point else: mount_point = None for pkg in deleted_packages: log.debug("Want to delete package %s" % pkg.Name()) if conf.PackageDB(root).RemovePackageContents(pkg) == False: log.error("Unable to remove contents package %s" % pkg.Name()) if mount_point: UnmountClone(clone_name, mount_point) mount_point = None DeleteClone(clone_name) raise Exception("Unable to remove contents for package %s" % pkg.Name()) conf.PackageDB(root).RemovePackage(pkg.Name()) installer = Installer.Installer(manifest=new_man, root=root, config=conf) installer.GetPackages(process_packages, handler=get_handler) log.debug("Packages = %s" % installer._packages) # Now let's actually install them. # Only change on success rv = False if installer.InstallPackages(handler=install_handler) is False: log.error("Unable to install packages") else: new_man.Save(root) if mount_point is not None: if UnmountClone(clone_name, mount_point) is False: log.error("Unable to mount clone enivironment %s" % clone_name) else: mount_point = None if ActivateClone(clone_name) is False: log.error("Could not activate clone environment %s" % clone_name) else: # Downloaded package files are open-unlinked, so don't # have to be cleaned up. If there's a cache directory, # however, we need to get rid of it. if cache_dir: import shutil if os.path.exists(cache_dir): try: shutil.rmtree(cache_dir) except Exception as e: # If that doesn't work, for now at least we'll # simply ignore the error. log.debug( "Tried to remove cache directory %s, got exception %s" % (cache_dir, str(e))) rv = True # Start a scrub. We ignore the return value. RunCommand("/sbin/zpool", ["scrub", "freenas-boot"]) except BaseException as e: log.error("Update got exception during update: %s" % str(e)) if clone_name: if mount_point: # Ignore the error here UnmountClone(clone_name, mount_point) DeleteClone(clone_name) raise e # Clean up # That just leaves the clone, which # we should unmount, and destroy if necessary. # Unmounting attempts to delete the mount point that was created. if rv is False: if clone_name and DeleteClone(clone_name) is False: log.error("Unable to delete boot environment %s in failure case" % clone_name) return rv
root = args[0] config = Configuration.SystemConfiguration() if package_dir is not None: config.SetPackageDir(package_dir) if mani_file is None: manifest = config.SystemManifest() else: # We ignore the signature because freenas-install is # called from the ISO install, and the GUI install, which # have their own checksums elsewhere. manifest = Manifest.Manifest(config, require_signature=False) manifest.LoadPath(mani_file) installer = Installer.Installer(manifest=manifest, root=root, config=config) if installer.GetPackages() is not True: print("Huh, could not install and yet it returned", file=sys.stderr) with Installer.ProgressHandler() as pf: # For installation, we assume that we're running the same kernel as the new system. installer.trampoline = False installer.InstallPackages(progressFunc=pf.update, handler=install_handler) manifest.Save(root) sys.exit(0)
def ApplyUpdate(directory, install_handler=None, force_reboot=False): """ Apply the update in <directory>. As with PendingUpdates(), it will have to verify the contents before it actually installs them, so it has the same behaviour with incomplete or invalid content. """ rv = False conf = Configuration.Configuration() # Note that PendingUpdates may raise an exception changes = PendingUpdatesChanges(directory) if changes is None: # This means no updates to apply, and so nothing to do. return None # Do I have to worry about a race condition here? new_manifest = Manifest.Manifest(require_signature=True) try: new_manifest.LoadPath(directory + "/MANIFEST") except ManifestInvalidSignature as e: log.error("Cached manifest has invalid signature: %s" % str(e)) raise e conf.SetPackageDir(directory) # If we're here, then we have some change to make. # PendingUpdatesChanges always sets this, unless it returns None reboot = changes["Reboot"] if force_reboot: # Just in case reboot = True changes.pop("Reboot") if len(changes) == 0: # This shouldn't happen log.debug("ApplyUupdate: changes only has Reboot key") return None deleted_packages = [] updated_packages = [] if "Packages" in changes: for (pkg, op, old) in changes["Packages"]: if op == "delete": log.debug("Delete package %s" % pkg.Name()) deleted_packages.append(pkg) continue elif op == "install": log.debug("Install package %s" % pkg.Name()) updated_packages.append(pkg) elif op == "upgrade": log.debug( "Upgrade package %s-%s to %s-%s" % (old.Name(), old.Version(), pkg.Name(), pkg.Version())) updated_packages.append(pkg) else: log.error("Unknown package operation %s for %s" % (op, pkg.Name())) if new_manifest.Sequence().startswith(Avatar() + "-"): new_boot_name = new_manifest.Sequence() else: new_boot_name = "%s-%s" % (Avatar(), new_manifest.Sequence()) log.debug("new_boot_name = %s, reboot = %s" % (new_boot_name, reboot)) mount_point = None if reboot: # Need to create a new boot environment try: if CreateClone(new_boot_name) is False: log.debug("Failed to create BE %s" % new_boot_name) # It's possible the boot environment already exists. s = None clones = ListClones() if clones: found = False for c in clones: if c["name"] == new_boot_name: found = True if c["mountpoint"] != "/": if c["mountpoint"] != "-": mount_point = c["mountpoint"] else: s = "Cannot create boot-environment with same name as current boot-environment (%s)" % new_boot_name break if found is False: s = "Unable to create boot-environment %s" % new_boot_name else: log.debug("Unable to list clones after creation failure") s = "Unable to create boot-environment %s" % new_boot_name if s: log.error(s) raise UpdateBootEnvironmentException(s) if mount_point is None: mount_point = MountClone(new_boot_name) except: mount_point = None s = sys.exc_info()[0] if mount_point is None: s = "Unable to mount boot-environment %s" % new_boot_name log.error(s) DeleteClone(new_boot_name) raise UpdateBootEnvironmentException(s) else: # Need to do magic to move the current boot environment aside, # and assign the newname to the current boot environment. # Also need to make a snapshot of the current root so we can # clean up on error mount_point = None log.debug("We should try to do a non-rebooty update") root_dataset = GetRootDataset() if root_dataset is None: log.error("Unable to determine root environment name") raise UpdateBootEnvironmentException( "Unable to determine root environment name") # We also want the root name root_env = None clones = ListClones() if clones is None: log.error("Unable to determine root BE") raise UpdateBootEnvironmentException("Unable to determine root BE") for clone in clones: if clone["mountpoint"] == "/": root_env = clone break if root_env is None: log.error("Unable to find root BE!") raise UpdateBootEnvironmentException("Unable to find root BE!") # Now we want to snapshot the current boot environment, # so we can rollback as needed. snapshot_name = "%s@Pre-Uprgade-%s" % (root_dataset, new_manifest.Sequence()) cmd = "/sbin/zfs" args = ["snapshot", "-r", snapshot_name] rv = RunCommand(cmd, args) if rv is False: log.error("Unable to create snapshot %s, bailing for now" % snapshot_name) raise UpdateSnapshotException("Unable to create snapshot %s" % snapshot_name) # We need to remove the beadm:nickname property. I hate knowing this much # about the implementation args = ["inherit", "-r", "beadm:nickname", snapshot_name] RunCommand(cmd, args) # At this point, we'd want to rename the boot environment to be the new # name, which would be new_manifest.Sequence() if CreateClone(new_boot_name, rename=root_env["name"]) is False: log.error("Unable to create new boot environment %s" % new_boot_name) # Roll back and destroy the snapshot we took cmd = "/sbin/zfs" args = ["rollback", snapshot_name] RunCommand(cmd, args) args[0] = "destroy" RunCommand(cmd, args) # And set the beadm:nickname property back args = ["set", "beadm:nickname=%s" % root_env["name"]] RunCommand(cmd, args) raise UpdateBootEnvironmentException( "Unable to create new boot environment %s" % new_boot_nam) # Now we start doing the update! # If we have to reboot, then we need to # make a new boot environment, with the appropriate name. # If we are *not* rebooting, then we want to rename the # current one with the appropriate name, while at the same # time cloning the current one and keeping the existing name. # Easy peasy, right? try: # Remove any deleted packages for pkg in deleted_packages: log.debug("About to delete package %s" % pkg.Name()) if conf.PackageDB(mount_point).RemovePackageContents(pkg) == False: s = "Unable to remove contents for packate %s" % pkg.Name() if mount_point: UnmountClone(new_boot_name, mount_point) mount_point = None DeleteClone(new_boot_name) raise UpdatePackageException(s) conf.PackageDB(mount_point).RemovePackage(pkg.Name()) installer = Installer.Installer(manifest=new_manifest, root=mount_point, config=conf) installer.GetPackages(pkgList=updated_packages) log.debug("Installer got packages %s" % installer._packages) # Now to start installing them rv = False if installer.InstallPackages(handler=install_handler) is False: log.error("Unable to install packages") raise UpdatePackageException("Unable to install packages") else: new_manifest.Save(mount_point) if mount_point: if UnmountClone(new_boot_name, mount_point) is False: s = "Unable to unmount clone environment %s from mount point %s" % ( new_boot_name, mount_point) log.error(s) raise UpdateBootEnvironmentException(s) mount_point = None if reboot: if ActivateClone(new_boot_name) is False: s = "Unable to activate clone environment %s" % new_boot_name log.error(s) raise UpdateBootEnvironmentException(s) if not reboot: # Clean up the emergency holographic snapshot cmd = "/sbin/zfs" args = ["destroy", "-r", snapshot_name] rv = RunCommand(cmd, args) if rv is False: log.error("Unable to destroy snapshot %s" % snapshot_name) RemoveUpdate(directory) # RunCommand("/sbin/zpool", ["scrub", "freenas-boot"]) except BaseException as e: # Cleanup code is entirely different for reboot vs non reboot log.error("Update got exception during update: %s" % str(e)) if reboot: if mount_point: UnmountClone(new_boot_name, mount_point) if new_boot_name: DeleteClone(new_boot_name) else: # Need to roll back # We also need to delete the renamed clone of /, # and then rename / to the original name. # First, however, destroy the clone rv = DeleteClone(root_env["name"]) if rv: # Next, rename the clone rv = RenameClone(new_boot_name, root_env["name"]) if rv: # Now roll back the snapshot, and set the beadm:nickname value cmd = "/sbin/zfs" args = ["rollback", "-r", snapshot_name] rv = RunCommand(cmd, args) if rv is False: log.error("Unable to rollback %s" % snapshot_name) # Don't know what to do then args = [ "set", "beadm:nickname=%s" % root_env["name"], "freenas-boot/ROOT/%s" % root_env["name"] ] rv = RunCommand(cmd, args) if rv is False: log.error( "Unable to set nickname, wonder what I did wrong") args = ["destroy", "-r", snapshot_name] rv = RunCommand(cmd, args) if rv is False: log.error("Unable to destroy snapshot %s" % snapshot_name) raise e return reboot
if len(args) != 1: usage() root = args[0] config = Configuration.Configuration() if package_dir is not None: config.SetPackageDir(package_dir) if mani_file is None: manifest = config.SystemManifest() else: # We ignore the signature because freenas-install is # called from the ISO install, and the GUI install, which # have their own checksums elsewhere. manifest = Manifest.Manifest(config, require_signature=False) manifest.LoadPath(mani_file) installer = Installer.Installer(manifest=manifest, root=root, config=config) if installer.GetPackages() is not True: print("Huh, could not install and yet it returned", file=sys.stderr) # For installation, we assume that we're running the same kernel as the new system. installer.trampoline = False installer.InstallPackages(handler=install_handler) manifest.Save(root) sys.exit(0)
def ApplyUpdate(directory, install_handler=None): """ Apply the update in <directory>. As with PendingUpdates(), it will have to verify the contents before it actually installs them, so it has the same behaviour with incomplete or invalid content. """ rv = False conf = Configuration.Configuration() changes = PendingUpdates(directory) if changes is None: # That could have happened for multiple reasons. # PendingUpdates should probably throw an exception # on error return False # Do I have to worry about a race condition here? new_manifest = Manifest.Manifest(require_signature=True) try: new_manifest.LoadPath(directory + "/MANIFEST") except ManifestInvalidSignature as e: log.error("Cached manifest has invalid signature: %s" % str(e)) return False conf.SetPackageDir(directory) deleted_packages = [] updated_packages = [] for (pkg, op, old) in changes: if op == "delete": log.debug("Delete package %s" % pkg.Name()) deleted_packages.append(pkg) elif op == "install": log.debug("Install package %s" % pkg.Name()) else: log.debug("Upgrade package %s-%s to %s-%s" % (old.Name(), old.Version(), pkg.Name(), pkg.Version())) updated_packages.append(pkg) if len(deleted_packages) == 0 and len(updated_packages) == 0: # The manifest may have other differences, so we should # probably do something. log.debug("New manifest has no package changes, what should we do?") RemoveUpdate(directory) return True # Now we start doing the update! clone_name = None mount_point = None try: # We clone the existing boot environment to # "Avatar()-<sequence>", unless it already does. if new_manifest.Sequence().startswith(Avatar() + "-"): clone_name = new_manifest.Sequence() else: clone_name = "%s-%s" % (Avatar(), new_manifest.Sequence()) if CreateClone(clone_name) is False: log.debug("Failed to create BE %s" % clone_name) # It's possible the boot environment already exists. s = None clones = ListClones() if clones: found = False for c in clones: if c["name"] == clone_name: found = True if c["mountpoint"] != "/": if c["mountpoint"] != "-": mount_point = c["mountpoint"] # We also need to see if grub is mounted # Note: if mount_point or mount_point/boot/grub don't exist, # this is going to throw an exception. try: if os.lstat( mount_point).st_dev == os.lstat( mount_point + grub_dir).st_dev: if RunCommand("/sbin/mount", [ "-t", "nullfs", grub_dir, mount_point + grub_dir ]) == False: s = "Unable to mount grub into already-existing boot environment %s" % clone_name except: log.debug("Unable to check %s grub mount" % mount_point) s = "Unable to set up %s as an installable mount point" % mount_point else: s = "Cannot create boot-environment with same name as current boot-environment (%s)" % clone_name break if found is False: s = "Unable to create boot-environment %s" % clone_name else: log.debug("Unable to list clones after creation failure") s = "Unable to create boot-environment %s" % clone_name if s: log.error(s) raise Exception(s) if mount_point is None: mount_point = MountClone(clone_name) if mount_point is None: s = "Unable to mount boot-environment %s" % clone_name log.error(s) DeleteClone(clone_name) raise Exception(s) # Remove any deleted packages for pkg in deleted_packages: log.debug("About to delete package %s" % pkg.Name()) if conf.PackageDB(mount_point).RemovePackageContents(pkg) == False: s = "Unable to remove contents for packate %s" % pkg.Name() if mount_point: UnmountClone(clone_name, mount_point) mount_point = None DeleteClone(clone_name) raise Exception(s) conf.PackageDB(mount_point).RemovePackage(pkg.Name()) installer = Installer.Installer(manifest=new_manifest, root=mount_point, config=conf) installer.GetPackages(pkgList=updated_packages) log.debug("Installer got packages %s" % installer._packages) # Now to start installing them rv = False if installer.InstallPackages(handler=install_handler) is False: log.error("Unable to install packages") raise Exception("Unable to install packages") else: new_manifest.Save(mount_point) if mount_point: if UnmountClone(clone_name, mount_point) is False: s = "Unable to unmount clone environment %s from mount point %s" % ( clone_name, mount_point) log.error(s) raise Exception(s) mount_point = None if ActivateClone(clone_name) is False: s = "Unable to activate clone environment %s" % clone_name log.error(s) raise Exception(s) RemoveUpdate(directory) rv = True # RunCommand("/sbin/zpool", ["scrub", "freenas-boot"]) except BaseException as e: log.error("Update got exception during update: %s" % str(e)) if mount_point: UnmountClone(clone_name, mount_point) if clone_name: DeleteClone(clone_name) raise e return rv
def Update(root=None, conf=None, check_handler=None, get_handler=None, install_handler=None): """ Perform an update. Calls CheckForUpdates() first, to see if there are any. If there are, then magic happens. """ deleted_packages = [] process_packages = [] def UpdateHandler(op, pkg, old): if op == "delete": deleted_packages.append(pkg) else: process_packages.append(pkg) if check_handler is not None: check_handler(op, pkg, old) new_man = CheckForUpdates(root, UpdateHandler) if new_man is None: return if len(deleted_packages) == 0 and len(process_packages) == 0: # We have a case where a manifest was updated, but we # don't actually have any changes to the packages. We # should install the new manifest, and be done -- it # may have new release notes, or other issues. # Right now, I'm not quite sure how to do this. # I should also learn how to log from python. log.debug("Updated manifest but no package differences") return # Now we have a list of deleted packages, and a list # of update/install packages. # The task is to delete the first set of packages, # and then run through the others to install/update. # First, however, we need to get the files. # We want to use the system configuration, unless one has been # specified -- we don't want to use the target root's. if conf is None: conf = Configuration.Configuration() # If root is None, then we will try to create a clone # environment. (If the caller wants to install into the # current boot environment, set root = "" or "/".) if root is None: # We clone the existing boot environment to # "FreeNAS-<sequence>" clone_name = "FreeNAS-%s" % new_man.Sequence() if CreateClone(clone_name) is False: log.error("Unable to create boot-environment %s" % clone_name) raise Exception("Unable to create new boot-environment %s" % clone_name) mount_point = MountClone(clone_name) if mount_point is None: log.error("Unable to mount boot-environment %s" % clone_name) raise Exception("Unable to mount boot-environment %s" % clone_name) else: root = mount_point else: mount_point = None for pkg in deleted_packages: log.debug("Want to delete package %s" % pkg.Name()) if conf.PackageDB(root).RemovePackageContents(pkg) == False: log.error("Unable to remove contents package %s" % pkg.Name()) UnmountClone(clone_name, mount_point) DestroyClone(clone_name) raise Exception("Unable to remove contents for package %s" % pkg.Name()) conf.PackageDB(root).RemovePackage(pkg.Name()) installer = Installer.Installer(manifest = new_man, root = root, config = conf) installer.GetPackages(process_packages, handler=get_handler) log.debug("Packages = %s" % installer._packages) # Now let's actually install them. # Only change on success rv = False if installer.InstallPackages(handler=install_handler) is False: log.error("Unable to install packages") else: new_man.Save(root) if mount_point is not None: if UnmountClone(clone_name, mount_point) is False: log.error("Unable to mount clone enivironment %s" % clone_name) else: if ActivateClone(clone_name) is False: log.error("Could not activate clone environment %s" % clone_name) else: rv = True # Clean up # The package files are open-unlinked, so should be removed # automatically under *nix. That just leaves the clone, which # we should unmount, and destroy if necessary. # Unmounting attempts to delete the mount point that was created. if rv is False: if DeleteClone(clone_name) is False: log.error("Unable to delete boot environment %s in failure case" % clone_name) return rv
def Install(**kwargs): """ This does the grunt-work of actually doing the install. The possible arguments are: - config Object containing configuration. This is where the download URL and package directories will be specified. - interactive Whether or not to be interactive. If true (default), then bsd.Dialog will be used to for status and error messages. - disks An array of Disk objects to install to. If set, then the disks will be partitioned and erased. If NOT set, then the installer will create a new boot environment on the existing freenas-boot pool. - efi Boolean indicating whether or not to use EFI (default is False). - upgrade_from An unimported ZFSPool object to install to. This must be set when upgrading, and when creating a new BE on an existing pool. - upgrade Boolean indicating whether or not to upgrade. Requires upgrade_from to be valid. - data_dir A string indicating the location of the /data. Normally this will just be "/data", but if installing from something other than the ISO, it will be necessary to specify it. - password A string indicating the root password. Ignored for upgrades; may be None (indicating no password, not recommended). - partitions An array of Partition objects (see Utils). Note that the OS partition will always be installed last. - post_install An array of callable objects, which will be called after installation, as func(mount_point=/path, **kwargs). MUST BE AN ARRAY. - package_handler Call-back for the start of each package. Arguments are (index [int], name [string], packages [array of package names]) be installed. - progress_handler Call-back after each file/directory is installed. Arguments are **kwargs, will [currently] be either done=True (indicating the package is installed), or (total=int [number of objects], index=int [current index], name=string [name of object that was just installed]). - manifest A manifest object. Must be set. - package_directory A path where the package files are located. The package files must already be located in this directory. - trampoline A boolean indicating whether the post-install scripts should be run on reboot (True, default) or during the install (False). """ LogIt("Install({})".format(kwargs)) orig_kwargs = kwargs.copy() config = kwargs.get("config", Configuration.SystemConfiguration()) interactive = kwargs.get("interactive", True) disks = kwargs.get("disks", []) efi = kwargs.get("efi", False) upgrade_pool = kwargs.get("upgrade_from", None) upgrade = kwargs.get("upgrade", False) data_dir = kwargs.get("data_dir", "/data") password = kwargs.get("password", None) extra_partitions = kwargs.get("partitions", []) post_install = kwargs.get("post_install", []) package_notifier = kwargs.get("package_handler", None) progress_notifier = kwargs.get("progress_handler", None) manifest = kwargs.get("manifest", None) trampoline = kwargs.get("trampoline", True) # The default is based on ISO layout package_dir = kwargs.get("package_directory", "/.mount/{}/Packages".format(Project())) if type(post_install) != list: post_install = [post_install] if not manifest: if interactive: try: Dialog.MessageBox(Title(), "No manifest specified for the installation", height=7, width=45).run() except: pass raise InstallationError("No manifest specified for the installation") config.SetPackageDir(package_dir) mount_point = tempfile.mkdtemp() # Quick sanity check if upgrade and upgrade_pool is None: if interactive: Dialog.MessageBox(Title(), "\nNo pool to upgrade from", height=7, width=30).run() raise InstallationError("Upgrade selected but not previous boot pool selected") if disks is None and upgrade_pool is None: if interactive: Dialog.MessageBox(Title(), "\nNo disks or previous pool selected", height=10, width=30).run() raise InstallationError("No disks or previous boot pool selected") if IsTruenas(): # We use a 16g swap partition in TrueNAS. # Note that this is only used if the disks are being formatted. extra_partitions.append(Partition(type="swap", index="3", size=16*1024*1024*1024)) def make_tn_swap(mount_point=None, **kwargs): # This uses the previously-defined variables, not kwargs if disks and mount_point: try: RunCommand("/sbin/gmirror", "label", "-b", "prefer", ["{}p3".format(disk.name) for disk in disks]) with open(os.path.join(mount_point, "data/fstab.swap"), "w") as swaptab: print("/dev/mirror/swap.eli\tnone\tswap\tsw\t0\t0", file=swaptab) except RunCommandException as e: LogIt("Could not create mirrored swap: {}".format(str(e))) post_install.append(make_tn_swap) # First step is to see if we're upgrading. # If so, we want to copy files from the active BE to # a location in /tmp, so we can copy them back later. # This will import, and then export, the freenas-boot pool. if upgrade_pool and upgrade: upgrade_dir = SaveConfiguration(interactive=interactive, pool=upgrade_pool) else: upgrade_dir = None # Second step is to see if we're formatting drives. # If so, we first will destroy the freenas-boot pool; # after that, we will partition the drives. How we partition # depends on the boot method -- efi or bios. We set the # BE name to "default" and create the freenas-boot pool, and # then the grub dataset. # # If we're NOT formatting the drive, we set the pool name # to time.strftime("default-%Y%m%d-%H%M%S") LogIt("disks = {}".format(disks)) if disks: # This means we're formatting # We need to know what size and types to make the partitions. # If we're using EFI, then we need a 100mbyte msdosfs partition; # otherwise a 512k bios-boot. If we have any extra partitions, # we'll take those into account as well. For the big freebsd-zfs # partition, we'll take the minimum of the remaining space, # rounded down to the nearest gbyte. gByte = 1024 * 1024 * 1024 if efi: # 100mbytes for efi partition used = 100 * 1024 * 1024 boot_part = Partition(type="efi", index=1, size=used) else: # BIOS partition gets 512kbytes used = 512 * 1024 boot_part = Partition(type="bios-boot", index=1, size=used) partitions = [boot_part] # For now, we always make the freenas-boot partition index 2, and place # it at the end of the disk. next_index = 3 for part in (extra_partitions or []): # We will ignore the index given here. part.index = next_index used += part.size LogIt("Additional partition {}".format(part)) partitions.append(part) next_index += 1 # At this point, used is the sum of the partitions, in bytes. # This isn't really correct - we should be rounding the size up # to the blocksize of the disk. But partitioning behaves strangely # sometimes with flash drives. As a result, when we do the actual # partitioning, we use the smart-size (e.g., 1G), which rounds down. min_size = 0 for disk in disks: # If the remaining space is too small, this installation won't work well. size = disk.size size = size - used if size < gByte: if size < 0: fspace = "no free space after the other partitions" else: fspace = "free space is {}, minimum is 1Gbyte".format(SmartSize(size)) name = disk.name LogIt("Disk {} is too small {}".format(name, fspace)) ssize = SmartSize(disk.size) if interactive: Dialog.MessageBox(Title(), "Disk {} is too small ({})".format(name, ssize), height=10, width=25).run() raise InstallationException("Disk {} is too small ({})".format(name, ssize)) if (size < min_size) or (not min_size): min_size = size if min_size == 0: if interactive: Dialog.MessageBox(Title(), "Unable to find the size of any of the selected disks", height=15, weidth=60).run() raise InstallationError("Unable to find disk size") # Round min_size down to a gbyte part_size = int(min_size / gByte) * gByte os_part = Partition(type="freebsd-zfs", index=2, size=part_size, os=True) LogIt("OS partition {}".format(os_part)) partitions.append(os_part) # We need to destroy any existing freenas-boot pool. # To do that, we may first need to import the pool. if upgrade_pool is None: try: old_pools = list(zfs.find_import(name="freenas-boot")) except libzfs.ZFSException as e: LogIt("Got ZFS error {} while trying to import freenas-boot for destruction".format(str(e))) old_pools = [] else: old_pools = [upgrade_pool] # We'll be destroying it, so.. upgrade_pool = None for pool in old_pools: try: dead_pool = zfs.import_pool(pool, "freenas-boot", {}) if dead_pool is None: dead_pool = zfs.get("freenas-boot") zfs.destroy("freenas-boot") except libzfs.ZFSException as e: LogIt("Trying to destroy a freenas-boot pool got error {}".format(str(e))) try: freenas_boot = FormatDisks(disks, partitions, interactive) except BaseException as e: LogIt("FormatDisks got exception {}".format(str(e))) raise bename = "freenas-boot/ROOT/default" else: # We need to import the pool (we exported it above if upgrade_pool) try: if upgrade_pool: freenas_boot = zfs.import_pool(upgrade_pool, "freenas-boot", {}) else: freenas_boot = None pools = list(zfs.find_import(name="freenas-boot")) if len(pools) > 1: raise InstallationError("There are multiple unimported freenas-boot pools") if len(pools) == 1: freenas_boot = zfs.import_pool(upgrade_pool, "freenas-boot", {}) if freenas_boot is None: freenas_boot = zfs.get("freenas-boot") except libzfs.ZFSException as e: LogIt("Got ZFS error {} while trying to import pool".format(str(e))) if interactive: Dialog.MessageBox("Error importing boot pool", "The {} Installer was unable to import the boot pool:\n\n\t{}".format(Project(), str(e)), height=25, width=60).run() raise InstallationError("Unable to import boot pool") bename = time.strftime("freenas-boot/ROOT/default-%Y%m%d-%H%M%S") # Next, we create the dataset, and mount it, and then mount # the grub dataset. # We also mount a devfs and tmpfs in the new environment. LogIt("BE name is {}".format(bename)) try: freenas_boot.create(bename, fsopts={ "mountpoint" : "legacy", "sync" : "disabled", }) except libzfs.ZFSException as e: LogIt("Could not create BE {}: {}".format(bename, str(e))) if interactive: Dialog.MessageBox(Title(), "An error occurred creatint the installation boot environment\n" + "\n\t{}".format(str(e)), height=25, width=60).run() raise InstallationError("Could not create BE {}: {}".format(bename, str(e))) MountFilesystems(bename, mount_point) # After this, any exceptions need to have the filesystems unmounted try: # If upgrading, copy the stashed files back if upgrade_dir: RestoreConfiguration(save_path=upgrade_dir, interactive=interactive, destination=mount_point) else: if os.path.exists(data_dir): try: copytree(data_dir, "{}/data".format(mount_point), progress_callback=lambda src, dst: LogIt("Copying {} -> {}".format(src, dst))) except: pass # # We should also handle some FN9 stuff # In this case, we want the newer database file, for migration purposes # XXX -- this is a problem when installing from FreeBSD for dbfile in ["freenas-v1.db", "factory-v1.db"]: if os.path.exists("/data/{}".format(dbfile)): copytree("/data/{}".format(dbfile), "{}/data/{}".format(mount_point, dbfile)) # After that, we do the installlation. # This involves mounting the new BE, # and then running the install code on it. installer = Installer.Installer(manifest=manifest, root=mount_point, config=config) if installer.GetPackages() is not True: LogIt("Installer.GetPackages() failed") raise InstallationError("Unable to load packages") # This should only be true for the ISO installer. installer.trampoline = trampoline start_time = time.time() try: installer.InstallPackages(progressFunc=progress_notifier, handler=package_notifier) except BaseException as e: LogIt("InstallPackaages got exception {}".format(str(e))) raise InstallationError("Could not install packages") # Packages installed! if interactive: try: status = Dialog.MessageBox(Title(), "Preparing new boot environment", height=5, width=35, wait=False) status.clear() status.run() except: pass for f in ["{}/conf/default/etc/fstab".format(mount_point), "{}/conf/base/etc/fstab".format(mount_point) ]: try: os.remove(f) except: LogIt("Unable to remove {} -- ignoring".format(f)) try: with open("{}/etc/fstab".format(mount_point), "w") as fstab: print("freenas-boot/grub\t/boot/grub\tzfs\trw,noatime\t1\t0", file=fstab) except OSError as e: LogIt("Unable to create fstab: {}".format(str(e))) raise InstallationError("Unable to create filesystem table") try: os.link("{}/etc/fstab".format(mount_point), "{}/conf/base/etc/fstab".format(mount_point)) except OSError as e: LogIt("Unable to link /etc/fstab to /conf/base/etc/fstab: {}".format(str(e))) # Here, I should change module_path in boot/loader.conf, and get rid of the kernel line try: lines = [] boot_config = "{}/boot/loader.conf".format(mount_point) with open(boot_config, "r") as bootfile: for line in bootfile: line = line.rstrip() if line.startswith("module_path="): lines.append('module_path="/boot/kernel;/boot/modules;/usr/local/modules"') elif line.startswith("kernel="): lines.append('kernel="kernel"') else: lines.append(line) with open(boot_config, "w") as bootfile: for line in lines: print(line, file=bootfile) except BaseException as e: LogIt("While modifying loader.conf, got exception {}".format(str(e))) # Otherwise I'll ignore it, I think # This is to support Xen try: hvm = RunCommand("/usr/local/sbin/dmidecode", "-s", "system-product-name", chroot=mount_point) if hvm == "HVM domU": with open(os.path.join(mount_point, "boot", "loader.conf.local"), "a") as f: print('hint.hpet.0.clock="0"', file=f) except BaseException as e: LogIt("Got an exception trying to set XEN boot loader hint: {}".format(str(e))) # Now I have to mount a tmpfs on var try: LogIt("Mounting tmpfs on var") bsd.nmount(source="tmpfs", fspath=os.path.join(mount_point, "var"), fstype="tmpfs") except BaseException as e: LogIt("Got exception {} while trying to mount {}/var: {}".format(mount_point, str(e))) raise InstallationError("Unable to mount temporary space in newly-created BE") # Now we need to populate a data structure mtree_command = ["/usr/sbin/mtree", "-deUf" ] if os.path.exists("/usr/sbin/mtree"): mtree_command.append("{}/etc/mtree/BSD.var.dist".format(mount_point)) mtree_command.extend(["-p", "{}/var".format(mount_point)]) chroot=None else: mtree_command.extend(["/etc/mtree/BSD.var.dist", "-p", "/var"]) chroot=mount_point try: RunCommand(*mtree_command, chroot=chroot) except RunCommandException as e: LogIt("{} (chroot={}) failed: {}".format(mtree_command, chroot, str(e))) raise InstallationError("Unable to prepare new boot environment") try: # Now we need to install grub # We do this even if we didn't format the disks. # But if we didn't format the disks, we need to use the same type # of boot loader. if interactive: try: status = Dialog.MessageBox(Title(), "Installing boot loader", height=5, width=35, wait=False) status.clear() status.run() except: pass # We've just repartitioned, so rescan geom geom.scan() # Set the boot dataset freenas_boot.properties["bootfs"].value = bename LogIt("Set bootfs to {}".format(bename)) # This is EXTREMELY ANNOYING. # I'd like to use libzfs to set the property here, but # I have to do it in the chrooted environment, because otherwise # zfs returns an error and doesn't set it. #freenas_boot.properties["cachefile"].value = "/boot/zfs/rpool.cache" try: RunCommand("/sbin/zpool", "set", "cachefile=/boot/zfs/rpool.cache", "freenas-boot", chroot=mount_point) except RunCommandException as e: LogIt("Got exception {} while trying to set cachefile".format(str(e))) raise InstallationException("Could not set cachefile on boot pool") LogIt("Set cachefile to /boot/zfs/rpool.cache") # We need to set the serial port stuff in the database before running grub, # because it'll use that in the configuration file it generates. try: SaveSerialSettings(mount_point) except: raise InstallationError("Could not save serial console settings") try: # All boot pool disks are partitioned using the same type. # Or the darkness rises and squit once again rule the earth. # (It's happened.) use_efi = Utils.BootPartitionType(freenas_boot.disks[0]) == "efi" InstallGrub(chroot=mount_point, disks=freenas_boot.disks, bename=bename, efi=use_efi) except RunCommandException as e: LogIt("Command {} failed: {} (code {})".format(e.command, e.message, e.code)) raise InstallationError("Boot loader installation failure") except BaseException as e: LogIt("InstallGrub got exception {}".format(str(e))) raise if interactive: try: status = Dialog.MessageBox(Title(), "Finalizing installation", height=5, width=35, wait=False) status.clear() status.run() except BaseException as e: LogIt("Finalizing got exception {}".format(str(e))) # This is FN9 specific with open("{}/data/first-boot".format(mount_point), "wb"): pass if upgrade: for sentinel in ["/data/cd-upgrade", "/data/need-update"]: with open(mount_point + sentinel, "wb") as f: pass elif password is not None: if interactive: try: status = Dialog.MessageBox(Title(), "\nSetting root password", height=7, width=35, wait=False) status.clear() status.run() except: pass try: RunCommand("/etc/netcli", "reset_root_pw", password, chroot=mount_point) except RunCommandException as e: LogIt("Setting root password: {}".format(str(e))) raise InstallationError("Unable to set root password") except BaseException as e: LogIt("Got exception {} during configuration".format(str(e))) if interactive: try: Dialog.MessageBox(Title(), "Error during configuration", height=7, width=35).run() except: pass raise # Let's turn sync back to default for the dataset try: ds = zfs.get_dataset(bename) except libzfs.ZFSException as e: LogIt("Got ZFS error {} while trying to get {} dataset".format(str(e), bename)) raise InstallationError("Could not fid newly-created BE {}".format(bename)) try: ds.properties["sync"].inherit() except BaseException as e: LogIt("Unable to set sync on {} to inherit: {}".format(bename, str(e))) # That's all I'm going to do for now # We save the manifest manifest.Save(mount_point) # Okay! Now if there are any post-install functions, we call them for fp in post_install: fp(mount_point=mount_point, **kwargs) # And we're done! end_time = time.time() except InstallationError as e: # This is the outer try block -- it needs to ensure mountpoints are # cleaned up LogIt("Outer block got error {}".format(str(e))) if interactive: try: Dialog.MessageBox("{} Installation Error".format(Project()), e.message, height=25, width=50).run() except: pass raise except BaseException as e: LogIt("Outer block got base exception {}".format(str(e))) raise finally: if package_dir is None: LogIt("Removing downloaded packages directory {}".format(cache_dir)) shutil.rmtree(cache_dir, ignore_errors=True) UnmountFilesystems(mount_point) LogIt("Exporting freenas-boot at end of installation") try: zfs.export_pool(freenas_boot) except libzfs.ZFSException as e: LogIt("Could not export freenas boot: {}".format(str(e))) raise if interactive: total_time = int(end_time - start_time) Dialog.MessageBox(Title(), "The {} installer has finished the installation in {} seconds".format(Project(), total_time), height=8, width=40).run()