Ejemplo n.º 1
0
    def run(self, path, reboot_post_install=False):
        self.set_progress(0, 'Extracting update from tarfile...')

        cache_dir = self.dispatcher.call_sync('update.update_cache_getter',
                                              'cache_dir')
        if cache_dir is None:
            try:
                cache_dir = self.dispatcher.call_sync(
                    'system_dataset.request_directory', 'update')
            except RpcException:
                cache_dir = '/var/tmp/update'
        # Frozen tarball.  We'll extract it into the cache directory, and
        # then add a couple of things to make it pass sanity, and then apply it.
        # For now we just copy the code above.
        # First, remove the cache directory
        # Hrm, could overstep a locked file.
        shutil.rmtree(cache_dir, ignore_errors=True)
        try:
            os.makedirs(cache_dir)
        except BaseException as e:
            raise TaskException(
                errno.EPERM,
                "Unable to create cache directory {0}: {1}".format(
                    cache_dir, str(e)))

        try:
            with tarfile.open(path) as tf:
                files = tf.getmembers()
                for f in files:
                    if f.name in ("./", ".", "./."):
                        continue
                    if not f.name.startswith("./"):
                        continue
                    if len(f.name.split("/")) != 2:
                        continue
                    tf.extract(f.name, path=cache_dir)
        except BaseException as e:
            raise TaskException(
                errno.EIO, "Unable to extract frozen update {0}: {1}".format(
                    path, str(e)))

        config = Configuration.SystemConfiguration()
        # Exciting!  Now we need to have a SEQUENCE file, or it will fail verification.
        with open(os.path.join(cache_dir, "SEQUENCE"), "w") as s:
            s.write(config.SystemManifest().Sequence())
        # And now the SERVER file
        with open(os.path.join(cache_dir, "SERVER"), "w") as s:
            s.write(config.UpdateServerName())

        # Now we can go for the update apply task
        self.run_subtask_sync('update.apply',
                              reboot_post_install,
                              progress_callback=lambda p, m=
                              'Installing updates from extracted tarfile', e=
                              None: self.chunk_progress(0, 100, '', p, m, e))
Ejemplo n.º 2
0
    def run(self, props):
        if 'train' in props:
            train_to_set = props.get('train')
            conf = Configuration.Configuration()
            conf.LoadTrainsConfig()
            trains = conf.AvailableTrains() or []
            if trains:
                trains = list(trains.keys())
            if train_to_set not in trains:
                raise TaskException(
                    errno.ENOENT,
                    '{0} is not a valid train'.format(train_to_set))
            self.configstore.set('update.train', train_to_set)

        if 'check_auto' in props:
            self.configstore.set('update.check_auto', props['check_auto'])

        if 'internal' in props:
            conf = Configuration.SystemConfiguration()
            if 'internal' not in conf.ListUpdateServers():
                conf.AddUpdateServer(
                    Configuration.UpdateServer(
                        'internal',
                        'http://update-int.ixsystems.com/FreeNAS/',
                        signing=False))

            conf.SetUpdateServer(
                'internal' if props['internal'] else 'default')

        cache_dir = self.dispatcher.call_sync('update.update_cache_getter',
                                              'cache_dir')
        check_updates(self.dispatcher,
                      self.configstore,
                      cache_dir=cache_dir,
                      check_now=True)

        self.dispatcher.dispatch_event('update.changed',
                                       {'operation': 'update'})
Ejemplo n.º 3
0
def do_install():
    """
    Do the UI for the install.
    This will either return, or raise an exception.  DialogEscape means that
    the installer should start over.
    
    If we have any command-line arguments, let's handle them now
    """
    arg_parser = argparse.ArgumentParser(description=Title(), prog="Installer")
    arg_parser.register('type', 'bool',
                        lambda x: x.lower() in ["yes", "y", "true", "t"])
    arg_parser.add_argument('-p',
                            '--project',
                            dest='project',
                            default=Project(),
                            help="Specify project (default {})".format(
                                Project()))
    arg_parser.add_argument('-T',
                            '--train',
                            dest="train",
                            help="Specify train name")
    arg_parser.add_argument('-U',
                            '--url',
                            dest="url",
                            help="URL to use when fetching files")
    arg_parser.add_argument('-D',
                            "--data",
                            dest='data_dir',
                            help='Path to /data prototype directory')
    arg_parser.add_argument('-M',
                            '--manifest',
                            dest='manifest',
                            help="Path to manifest file")
    arg_parser.add_argument('-P',
                            '--packages',
                            dest='package_dir',
                            help='Path to package files')
    arg_parser.add_argument(
        "-B",
        "--trampoline",
        dest='trampoline',
        default=True,
        type='bool',
        help="Run post-install scripts on reboot (default)")
    args = arg_parser.parse_args()
    if args:
        LogIt("Command line args: {}".format(args))

    SetProject(args.project)

    try:
        validate_system()
    except ValidationError as e:
        LogIt("Could not validate system: {}".format(e.message))
        Dialog.MessageBox(
            Title(),
            "\nSystem memory is too small.  Minimum memory size is 8Gbytes",
            height=10,
            width=45).run()
        return

    if args.manifest:
        if os.path.exists(args.manifest):
            manifest_path = args.manifest
        else:
            Dialog.MessageBox(
                Title(),
                "A manifest file was specified on the command line, but does not exist.  The manifest file specified was\n\n\t{}"
                .format(args.manifest),
                height=15,
                width=45).run()
            raise InstallationError(
                "Command-line manifest file {} does not exist".foramt(
                    args.manifet))
    else:
        manifest_path = "/.mount/{}-MANIFEST".format(Project())
        if not os.path.exists(manifest_path):
            manifest_path = None

    package_dir = args.package_dir or "/.mount/{}/Packages".format(Project())
    if not os.path.exists(package_dir):
        # This will be used later to see if we should try downloading
        package_dir = None
    # If we aren't given a URL, we can try to use the default url.
    # If we have a manifest file, we can figure out train;
    # if we don't have a train or manifest file, we're not okay.
    if (not manifest_path and not args.train):
        LogIt("Command-line URL {}, train {}, manifest {}".format(
            args.url, args.train, manifest_path))
        box = Dialog.MessageBox(Title(), "", height=15, width=45)
        box.text = "Neither a manifest file nor train were specified"
        box.run()
        raise InstallationError("Incorrect command-line arguments given")

    conf = Configuration.SystemConfiguration()
    if conf is None:
        raise RuntimeError("No configuration?!")

    # Okay, if we're going to have to download, let's make an update server object
    if args.url:
        temp_update_server = Configuration.UpdateServer(
            name="Installer Server", url=args.url, signing=False)
        # This is SO cheating
        # It can't write to the file, but it does that after setting it.
        try:
            conf.AddUpdateServer(temp_update_server)
        except:
            pass
        conf.SetUpdateServer("Installer Server", save=False)

    # If we have a train, and no manifest file, let's grab one

    if manifest_path:
        manifest = Manifest.Manifest()
        try:
            manifest.LoadPath(manifest_path)
        except:
            manifest = None
    else:
        manifest = None

    if args.train and not manifest:
        try:
            status = Dialog.MessageBox(
                Title(),
                "Attempting to download manifest for train {}".format(
                    args.train),
                height=15,
                width=30,
                wait=False)
            status.clear()
            status.run()
        except:
            pass
        try:
            manifest = conf.FindLatestManifest(train=args.train,
                                               require_signature=False)
        except:
            manifest = None

    # At this point, if we don't have a manifest, we can't do anything
    if manifest is None:
        LogIt("Could not load a manifest")
        text = "Despite valiant efforts, no manifest file could be located."

        Dialog.MessageBox(Title(), text, height=15, width=30).run()
        raise InstallationError("Unable to locate a manifest file")

    LogIt("Manifest:  Version {}, Train {}, Sequence {}".format(
        manifest.Version(), manifest.Train(), manifest.Sequence()))
    do_upgrade = False
    boot_method = None
    disks = SelectDisks()
    if not disks:
        try:
            Dialog.MessageBox(Title(),
                              "No suitable disks were found for installation",
                              height=15,
                              width=60).run()
        except:
            pass
        raise InstallationError("No disks selected for installation")

    if found_bootpool:
        if UpgradePossible():
            text = """The {} installer can upgrade the existing {} installation.
Do you want to upgrade?""".format(Project(), Project())
            yesno = Dialog.YesNo("Perform Upgrade",
                                 text,
                                 height=12,
                                 width=60,
                                 yes_label="Upgrade",
                                 no_label="Do not Upgrade",
                                 default=True)
            do_upgrade = yesno.result
        else:
            Dialog.MessageBox("No upgrade possible", "").run()

    format_disks = True
    if found_bootpool:
        # If the selected disks are not the same as the existing boot-pool
        # disks, then we _will_ be formatting, and do not ask this question.
        disk_set = set([x.name for x in disks])
        pool_set = set([Utils.Disk(x).name for x in found_bootpool.disks])
        LogIt("disk_set = {}, pool_set = {}".format(disk_set, pool_set))
        if pool_set == disk_set:
            yesno = Dialog.YesNo(
                Title(),
                "The {} installer can reformat the disk{}, or create a new boot environment.\nReformatting will erase all of your data"
                .format(Project(), "s" if len(disks) > 1 else ""),
                height=10,
                width=60,
                yes_label="Re-format",
                no_label="Create New BE",
                default=True)
            format_disks = yesno.result
            yesno.clear()

    if format_disks:
        # If there is already a freenas-boot, and we're not using all of
        # the disks in it, then this will cause problems.
        # If we made it this far, there is only one freenas-boot pool.
        if found_bootpool:
            pool_disks = [Utils.Disk(x) for x in found_bootpool.disks]

            disk_set = set([x.name for x in disks])
            pool_set = set([x.name for x in pool_disks])
            LogIt("disk_set = {}, pool_set = {}".format(disk_set, pool_set))
            if not pool_set <= disk_set:
                # This means there would be two freenas-boot pools, which
                # is too much of a problem.
                yesno = Dialog.YesNo(
                    Title(),
                    "The existing boot pool contains disks that are not in the selected set of disks, which would result in errors.  Select Start Over, or press Escape, otherwise the {} installer will destroy the existing pool"
                    .format(Project()),
                    width=60,
                    yes_label="Destroy Pool",
                    no_label="Start over",
                    default=False)
                yesno.prompt += "\nSelected Disks: " + " ,".join(
                    sorted([x.name for x in disks]))
                yesno.prompt += "\nPool Disks:     " + " ,".join(
                    sorted([x.name for x in pool_disks]))
                if yesno.result is False:
                    raise Dialog.DialogEscape

        current_method = BootMethod()
        yesno = Dialog.YesNo(
            "Boot Method",
            "{} can boot via BIOS or (U)EFI.  Selecting the wrong method can result in a non-bootable system"
            .format(Project()),
            height=10,
            width=60,
            yes_label="BIOS",
            no_label="(U)EFI",
            default=False if current_method == "efi" else True)
        if yesno.result is True:
            boot_method = "bios"
        else:
            boot_method = "efi"

    if not do_upgrade:
        # Ask for root password
        while True:
            password_fields = [
                Dialog.FormItem(
                    Dialog.FormLabel("Password:"******"",
                                     width=20,
                                     maximum_input=50,
                                     hidden=True)),
                Dialog.FormItem(
                    Dialog.FormLabel("Confirm Password:"******"",
                                     width=20,
                                     maximum_input=50,
                                     hidden=True)),
            ]
            try:
                password_input = Dialog.Form(
                    "Root Password",
                    "Enter the root password.  (Escape to quit, or select No Password)",
                    width=60,
                    height=15,
                    cancel_label="No Password",
                    form_height=10,
                    form_items=password_fields)
                results = password_input.result
                if results and results[0].value.value != results[1].value.value:
                    Dialog.MessageBox("Password Error",
                                      "Passwords did not match",
                                      width=35,
                                      ok_label="Try again").run()
                    continue
                else:
                    new_password = results[0].value.value if results else None
                    break
            except Dialog.DialogEscape:
                try:
                    Diallog.MessageBox("No Password Selected",
                                       "You have selected an empty password",
                                       height=7,
                                       width=35).run()
                except:
                    pass
                new_password = None
                break

    # I'm not sure if this should be done here, or in Install()

    if package_dir is None:
        cache_dir = tempfile.mkdtemp()
    else:
        cache_dir = package_dir

    try:
        Utils.GetPackages(manifest, conf, cache_dir, interactive=True)
    except BaseException as e:
        LogIt("GetPackages raised an exception {}".format(str(e)))
        if package_dir is None:
            shutil.rmtree(cache_dir, ignore_errors=True)
        raise
    LogIt("Done getting packages?")
    # Let's confirm everything
    text = "The {} Installer will perform the following actions:\n\n".format(
        Project())
    height = 10
    if format_disks:
        text += "* The following disks will be reformatted, and all data lost:\n"
        height += 1
        for disk in disks:
            text += "\t* {} {} ({}bytes)\n".format(disk.name,
                                                   disk.description[:25],
                                                   SmartSize(disk.size))
            height += 1
        if found_bootpool:
            text += "* The existing boot pool will be destroyed\n"
            height += 1
        text += "* {} Booting\n".format(
            "BIOS" if boot_method is "bios" else "(U)EFI")
    else:
        text += "* A new Boot Environment will be created\n"
        height += 1

    if do_upgrade:
        text += "* {} will be upgraded\n".format(Project())
    else:
        text += "* {} will be freshly installed\n".format(Project())
    height += 1

    yesno.prompt = text
    yesno.default = False
    yesno = Dialog.YesNo("{} Installation Confirmation".format(Project()),
                         text,
                         height=min(15, height),
                         width=60,
                         default=False)
    if yesno.result == False:
        LogIt("Installation aborted at first confirmation")
        raise Dialog.DialogEscape

    if format_disks:
        yesno = Dialog.YesNo(
            "LAST CHANCE",
            "The {} installer will format the selected disks and all data on them will be erased.\n\nSelect Start Over or hit Escape to start over!"
            .format(Project()),
            height=10,
            width=50,
            yes_label="Continue",
            no_label="Start Over",
            default=False)
        if yesno.result is False:
            LogIt("Installation aborted at second confirmation")
            raise Dialog.DialogEscape

    # This may take a while, it turns out
    try:
        status = Dialog.MessageBox(Title(),
                                   "\nBeginning installation",
                                   height=7,
                                   width=25,
                                   wait=False)
        status.clear()
        status.run()
    except:
        pass
    # Okay, time to do the install
    with InstallationHandler() as handler:
        try:
            Install.Install(
                interactive=True,
                manifest=manifest,
                config=conf,
                package_directory=cache_dir,
                disks=disks if format_disks else None,
                efi=True if boot_method is "efi" else False,
                upgrade_from=found_bootpool if found_bootpool else None,
                upgrade=do_upgrade,
                data_dir=args.data_dir if args.data_dir else "/data",
                package_handler=handler.start_package,
                progress_handler=handler.package_update,
                password=None if do_upgrade else new_password,
                trampoline=args.trampoline)
        except BaseException as e:
            LogIt("Install got exception {}".format(str(e)))
            raise
    return
Ejemplo n.º 4
0
def main():
    global log

    def usage():
        print(
            """Usage: {0} [-C cache_dir] [-d] [-T train] [--no-delta] [--reboot|-R] [--server|-S server][-B|--trampline yes|no] [--force|-F] [-v] <cmd>
or	{0} <update_tar_file>
where cmd is one of:
        check\tCheck for updates
        update\tDo an update""".format(sys.argv[0]),
            file=sys.stderr)
        sys.exit(1)

    try:
        short_opts = "B:C:dFRS:T:v"
        long_opts = [
            "cache=", "debug", "reboot", "train=", "verbose", "no-delta",
            "force", "server=", "trampoline=", "snl"
        ]
        opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts)
    except getopt.GetoptError as err:
        print(str(err), file=sys.stderr)
        usage()

    do_reboot = False
    verbose = False
    debug = 0
    config = None
    # Should I get this from a configuration file somewhere?
    cache_dir = "/var/db/system/update"
    train = None
    pkg_type = None
    snl = False
    force = False
    server = None
    force_trampoline = None

    for o, a in opts:
        if o in ("-v", "--verbose"):
            verbose = True
        elif o in ("-d", "--debug"):
            debug += 1
        elif o in ('-C', "--cache"):
            cache_dir = a
        elif o in ("-R", "--reboot"):
            do_reboot = True
        elif o in ("-S", "--server"):
            server = a
        elif o in ("-T", "--train"):
            train = a
        elif o in ("--no-delta"):
            pkg_type = Update.PkgFileFullOnly
        elif o in ("-B", "--trampoline"):
            if a in ("true", "True", "yes", "YES", "Yes"):
                force_trampoline = True
            elif a in ("false", "False", "no", "NO", "No"):
                force_trampoline = False
            else:
                print("Trampoline option must be boolean [yes/no]",
                      file=sys.stderr)
                usage()
        elif o in ("--snl"):
            snl = True
        elif o in ("-F", "--force"):
            force = True
        else:
            assert False, "unhandled option {0}".format(o)

    if verbose:
        log_to_handler('stderr')
    log = logging.getLogger('freenas-update')

    config = Configuration.SystemConfiguration()
    if server:
        assert server in config.ListUpdateServers(
        ), "Unknown update server {}".format(server)
        config.SetUpdateServer(server, save=False)

    if train is None:
        train = config.SystemManifest().Train()

    if len(args) != 1:
        usage()

    if args[0] == "check":
        # To see if we have an update available, we
        # call Update.DownloadUpdate.  If we have been
        # given a cache directory, we pass that in; otherwise,
        # we make a temporary directory and use that.  We
        # have to clean up afterwards in that case.

        rv = DoDownload(train,
                        cache_dir,
                        pkg_type,
                        verbose,
                        ignore_space=force)
        if rv is False:
            if verbose:
                print("No updates available")
            Update.RemoveUpdate(cache_dir)
            sys.exit(1)
        else:
            diffs = Update.PendingUpdatesChanges(cache_dir)
            if diffs is None or len(diffs) == 0:
                print(
                    "Strangely, DownloadUpdate says there updates, but PendingUpdates says otherwise",
                    file=sys.stderr)
                sys.exit(1)
            PrintDifferences(diffs)
            if snl:
                print(
                    "I've got a fever, and the only prescription is applying the pending update."
                )
            sys.exit(0)

    elif args[0] == "update":
        # This will attempt to apply an update.
        # If cache_dir is given, then we will only check that directory,
        # not force a download if it is already there.  If cache_dir is not
        # given, however, then it downloads.  (The reason is that you would
        # want to run "freenas-update -c /foo check" to look for an update,
        # and it will download the latest one as necessary, and then run
        # "freenas-update -c /foo update" if it said there was an update.

        # See if the cache directory has an update downloaded already
        do_download = True
        try:
            f = Update.VerifyUpdate(cache_dir)
            if f:
                f.close()
                do_download = False
        except Exceptions.UpdateBusyCacheException:
            print("Cache directory busy, cannot update", file=sys.stderr)
            sys.exit(0)
        except (Exceptions.UpdateInvalidCacheException,
                Exceptions.UpdateIncompleteCacheException):
            pass
        except:
            raise

        if do_download:
            rv = DoDownload(train,
                            cache_dir,
                            pkg_type,
                            verbose,
                            ignore_space=force)
            if rv is False:
                if verbose:
                    print("No updates available")
                Update.RemoveUpdate(cache_dir)
                sys.exit(1)

        try:
            rv = DoUpdate(cache_dir,
                          verbose,
                          ignore_space=force,
                          force_trampoline=force_trampoline)
        except:
            sys.exit(1)
        else:
            if rv:
                if do_reboot:
                    os.system("/sbin/shutdown -r now")
                sys.exit(0)
            else:
                sys.exit(1)
    else:
        # If it's not a tarfile (possibly because it doesn't exist),
        # print usage and exit.
        try:
            if len(args) > 1:
                usage()
            if not tarfile.is_tarfile(args[0]):
                usage()
        except:
            usage()

        # Frozen tarball.  We'll extract it into the cache directory, and
        # then add a couple of things to make it pass sanity, and then apply it.
        # For now we just copy the code above.
        # First, remove the cache directory
        # Hrm, could overstep a locked file.
        shutil.rmtree(cache_dir, ignore_errors=True)
        try:
            os.makedirs(cache_dir)
        except BaseException as e:
            print("Unable to create cache directory {0}: {1}".format(
                cache_dir, str(e)))
            sys.exit(1)

        try:
            Update.ExtractFrozenUpdate(args[0], cache_dir, verbose=verbose)
        except BaseException as e:
            print("Unable to extract frozen update {0}: {1}".format(
                args[0], str(e)))
            sys.exit(1)
        try:
            rv = DoUpdate(cache_dir,
                          verbose,
                          ignore_space=force,
                          force_trampoline=force_trampoline)
        except:
            sys.exit(1)
        else:
            if rv:
                if do_reboot:
                    os.system("/sbin/shutdown -r now")
                sys.exit(0)
            else:
                sys.exit(1)
Ejemplo n.º 5
0
        usage()

    for (o, a) in opts:
        if o == "-M":
            mani_file = a
        elif o == "-P":
            package_dir = a
        else:
            usage()

    if len(args) != 1:
        usage()

    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)
Ejemplo n.º 6
0
    LogIt("Currently cannot handle an upgrade, sorry")
    raise InstallationError("Cannot currently handle an upgrade")

install_disks = []
for disk in install_config["disks"]:
    install_disks.append(Utils.Disk(disk))

# Okay, at this point we are nearly all set.  Let's set up some variables
manifest_path = os.path.join(args.buildroot[0],
                             "release/LATEST/{}-MANIFEST".format(Project()))
manifest = Manifest.Manifest()
manifest.LoadPath(manifest_path)

package_dir = os.path.join(args.buildroot[0], "release/LATEST/Packages")

fn_conf = Configuration.SystemConfiguration()

template_dir = os.path.join(args.buildroot[0], "objs/instufs/data")

try:
    Install(interactive=False,
            manifest=manifest,
            config=fn_conf,
            package_directory=package_dir,
            disks=install_disks,
            efi=install_config.get("format", "bios") == "efi",
            upgrade=False,
            data_dir=template_dir,
            password=install_config.get("password", ""),
            trampoline=True)
except BaseException as e:
Ejemplo n.º 7
0
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()