Re: [virt-install PATCH 3/3] virt-install: Add --unattended support

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On 2/6/19 8:21 AM, Fabiano Fidêncio wrote:
> This patch introduces the preliminary support for unattended
> installations using virt-install.
> 
> The command-line added is something like:
> --unattended
> profile="jeos|desktop",user-password="...",admin-password="...",product-key="..."
> 
> In case no profile is passed, "jeos" is used as the default one.
> In case no user or admin password is passed, those are going to be
> automatically generated and printed to the user.
> The "product-key" argument is needed for Windows(es) installations.
> 
> The --unattended argument works with:
> - --cdrom, thus the user can pass a media to be used;
> - --location, thus the user can pass a tree/media to be used;
> - with no arguments, then the tree is taken from osinfo-db for that
> distro;
> 
> Using --os-variant is required for when using --unattended.
> 
> There are still a few things missing in this path that will require some
> more work to be done, as:
> - Windowses:
>   - Handle more than one script (usually needed for installing drivers);
>   - Support a second disk containing drivers;
> - General:
>   - Have a proper check and help about which OSes support which kind of
>   unattended installations;
>   - Have a proper way to detect the keyboard layout being used by the
>   user (currently we just guess it by the user's system language);
> 

Thanks for working on this! Will definitely be nice to have this easily
accessible with virt-install.

Does it work with Fedora? I tried --os-variant fedora28 --unattended but
it isn't working... osinfo-db doesn't list 'initrd' as an injection
method. Are there missing osinfo-db patches?

Reviewing this gave me some ideas for cleanups and improvements that
will smooth out this process a bit. I pushed some commits to git and
rebased your patch on top, see the attachment. So in my points below
I'll call out some stuff I added/changed


There's a lot of different usages here and they should probably all
be their own patches. Here's how I think the series should go in broad
changes:

1) grabbing URL from libosinfo and doing a network install

This should be decoupled from the unattended stuff. The cli invocation
is the open question. In .git now, --os-variant is a VirtCLIParser, just
taking name= (the default) and full_id= values. This plumbing can be
used for something like:

  virt-install --os-variant fedora28,install=tree|media

But with just the tree part implemented. Maybe we want to use
install=location|cdrom to match virt-install names too, I'm not quite
sure, pick one and we can change or extend it later if we want. The
other option is a --location suboption but that doesn't seem as nice.

(Later we can discuss whether we want plain 'virt-install --os-variant
fedora28' to do that by default, but that will be part of a bigger
discussion to use libosinfo defaults much more for default naming etc.)

When this is split out I will help test and write tests for it. At least
a case with --os-variant $windows that should fail of course because no
URL is available, and maybe some invocation in test_urls to test an
actual working case, I'll need to play with it.

2) Some of the plumbing for actually generating the script content. Most
if not all should live in osdict. We could add a couple basic unit tests
in tests/osdict.py to generate a couple different scripts and do just
basic string searches over them to verify the output is sane.

2) --unattended install cli plumbing: just a no-op for starters. Add an
_add_argcomplete_cmd test in clitest.py. This can be the virt-install
option collision checks and everything up until actually altering any
data passed to the installer. Kind of an artificial split but we could
commit this early and it will reduce the diff

4) --unattended linux network install. should handle implicit location
and explicit location. ignore cdrom handling for now. testing is an open
question, we will probably need to add some env variables or hacks in
the code to let us mock test this from clitest.py, but I will help with that

5) --unattended linux network install but with passed in --cdrom media.
I see this below, where we turn options.cdrom into options.location.
That might take some testing, maybe improve the error message in cases
this won't work, like if a Fedora livecd is passed or something. I'm
just calling this case out explicitly because I need to consider it more

6) windows --cdrom unattended support. Now if this comes last it will be
easier to see all the ways this alters the previous code, right now it's
mixed in and a bit difficult to parse where the differentiation is at.
Also since this requires invoking external tools we need to consider
things like RPM and testing deps, and will probably need some hacks to
get unittest coverage here, so it will take more consideration.

Okay hopefully that doesn't sound too daunting. Some specific comments
below.

> Signed-off-by: Fabiano Fidêncio <fidencio@xxxxxxxxxx>
> ---
>  virt-install                   |  46 +++++++++++--
>  virtinst/cli.py                |  31 ++++++++-
>  virtinst/installer.py          | 116 +++++++++++++++++++++++++++++++++
>  virtinst/installertreemedia.py |  17 +++++
>  virtinst/osdict.py             | 106 ++++++++++++++++++++++++++++++
>  5 files changed, 311 insertions(+), 5 deletions(-)
> 
> diff --git a/virt-install b/virt-install
> index a7cf0c2d..af465df3 100755
> --- a/virt-install
> +++ b/virt-install
> @@ -355,6 +355,11 @@ def validate_required_options(options, guest, installer):
>              _("An install method must be specified\n(%(methods)s)") %
>              {"methods": install_methods})
>  
> +    if options.unattended:
> +        if not options.distro_variant:
> +            msg += "\n" + (
> +                _("An OS variant must be specified\n"))
> +

In the rebased patch, I tweaked this a bit to specifically mention
--unattended

>      if msg:
>          fail(msg)
>  
> @@ -398,6 +403,10 @@ def check_option_collisions(options, guest, installer):
>          fail(_("--initrd-inject only works if specified with --location.") +
>               cdrom_err)
>  
> +    if options.unattended:
> +        if options.initrd_inject or options.extra_args:
> +            fail(_("--unattended does not support --initrd-inject nor --extra-args."))
> +
> 
>  def _show_nographics_warnings(options, guest, installer):
>      if guest.devices.graphics:
> @@ -472,8 +481,21 @@ def build_installer(options, guest):
>      location_kernel = None
>      location_initrd = None
>      install_bootdev = None
> +    network_install = False
>  
>      has_installer = True
> +    if options.unattended:
> +        if not guest.osinfo.is_windows():
> +            if options.cdrom:
> +                options.location = options.cdrom
> +                options.cdrom = None
> +            elif options.location:
> +                if not options.location.endswith(".iso"):
> +                    network_install = True
> +            else:
> +                options.location = guest.osinfo.get_url(guest.os.arch)
> +                network_install = True
> +
>      if options.location:
>          (location,
>           location_kernel,
> @@ -502,10 +524,21 @@ def build_installer(options, guest):
>              install_bootdev=install_bootdev)
>      if cdrom and options.livecd:
>          installer.livecd = True
> -    if options.extra_args:
> -        installer.extra_args = options.extra_args
> -    if options.initrd_inject:
> -        installer.set_initrd_injections(options.initrd_inject)
> +
> +    if network_install:
> +        installer.networkinstall = True
> +
> +    if options.unattended:
> +        unattended_data = cli.parse_unattended(options.unattended)
> +        options.unattended = None
> +
> +        installer.set_unattended(guest, options.distro_variant, options.name, unattended_data)
> +    else:
> +        if options.extra_args:
> +            installer.extra_args = options.extra_args
> +        if options.initrd_inject:
> +            installer.set_initrd_injections(options.initrd_inject)
> +
>      if options.autostart:
>          installer.autostart = True
>  
> @@ -531,6 +564,9 @@ def build_guest_instance(conn, options):
>          guest.os.machine = options.machine
>  
>      guest.set_capabilities_defaults()
> +    if options.unattended:
> +        guest.set_os_name(options.distro_variant)
> +

My patches took care of the movement here. It did away with
set_distro_variant, since it was conflating two cases: when an explicit
--os-variant was specified, and when we are detecting os-variant from
explicit media specified. With more stuff being determined from the
os-variant, the first case needed to be handled later.

>      installer = build_installer(options, guest)
>      if installer:
>          set_distro_variant(options, guest, installer)
> @@ -787,6 +823,8 @@ def parse_args():
>                             "booted from --location"))
>      insg.add_argument("--initrd-inject", action="append",
>                      help=_("Add given file to root of initrd from --location"))
> +    insg.add_argument("--unattended", const="profile=jeos", default="", nargs='?',
> +                    help=_("Unattended installation"))
>  

I don't think the const= is needed, and the default= can probably be
dropped too.

>      # Takes a URL and just prints to stdout the detected distro name
>      insg.add_argument("--test-media-detection", help=argparse.SUPPRESS)
> diff --git a/virtinst/cli.py b/virtinst/cli.py
> index 52a876e1..ea36b015 100644
> --- a/virtinst/cli.py
> +++ b/virtinst/cli.py
> @@ -462,7 +462,7 @@ def get_meter():
>  ###########################
>  
>  def _get_completer_parsers():
> -    return VIRT_PARSERS + [ParseCLICheck, ParserLocation]
> +    return VIRT_PARSERS + [ParseCLICheck, ParserLocation, ParseCLIUnattended]
>  
>  
>  def _virtparser_completer(prefix, **kwargs):
> @@ -1417,6 +1417,35 @@ class VirtCLIParser(metaclass=InitClass):
>          """Do nothing callback"""
>  
>  
> +########################
> +# --unattended parsing #
> +########################
> +
> +class ParseCLIUnattended(VirtCLIParser):
> +    cli_arg_name = "unattended"
> +
> +    @classmethod
> +    def __init_class__(cls, **kwargs):
> +        VirtCLIParser.__init_class__(**kwargs)
> +        cls.add_arg("profile", "profile")
> +        cls.add_arg("admin_password", "admin-password")
> +        cls.add_arg("user_password", "user-password")
> +        cls.add_arg("product_key", "product-key")
> +
> +def parse_unattended(unattended):
> +    class UnattendedData:
> +        def __init__(self):
> +            self.profile = "jeos"
> +            self.admin_password = None
> +            self.user_password = None
> +            self.product_key = None
> +
> +    ret = UnattendedData()
> +    parser = ParseCLIUnattended(None, unattended)
> +    parser.parse(ret)
> +    return ret
> +
> +
>  ###################
>  # --check parsing #
>  ###################
> diff --git a/virtinst/installer.py b/virtinst/installer.py
> index e3ccfa42..31772527 100644
> --- a/virtinst/installer.py
> +++ b/virtinst/installer.py
> @@ -8,6 +8,7 @@
>  
>  import os
>  import logging
> +import subprocess
>  

I'd like all the floppy subprocess stuff to go into its own file, along
the lines of initrdinject.py

>  import libvirt
>  
> @@ -17,6 +18,11 @@ from .osdict import OSDB
>  from .installertreemedia import InstallerTreeMedia
>  from . import util
>  
> +import gi
> +gi.require_version('Libosinfo', '1.0')
> +from gi.repository import Libosinfo as libosinfo
> +from gi.repository import Gio as gio
> +
> 

It would be nice if all the libosinfo stuff could live in osdict.py. I
didn't look too deeply at what's happening in this file though so let me
know if you disagree.

>  class Installer(object):
>      """
> @@ -39,6 +45,7 @@ class Installer(object):
>              location_kernel=None, location_initrd=None):
>          self.conn = conn
>  
> +        self.networkinstall = False
>          self.livecd = False
>          self.extra_args = []
>  
> @@ -49,6 +56,8 @@ class Installer(object):
>          self._install_kernel = None
>          self._install_initrd = None
>          self._install_cdrom_device = None
> +        self._unattended_floppy_device = None
> +        self._unattended_files = []
>          self._defaults_are_set = False
>  
>          if location_kernel or location_initrd:
> @@ -111,6 +120,28 @@ class Installer(object):
>          self._install_cdrom_device.path = None
>          self._install_cdrom_device.sync_path_props()
>  
> +    def _add_unattended_floppy_device(self, guest, location):
> +        if self._unattended_floppy_device:
> +            return
> +        dev = DeviceDisk(self.conn)
> +        dev.device = dev.DEVICE_FLOPPY
> +        dev.path = location
> +        dev.sync_path_props()
> +        dev.validate()
> +        self._unattended_floppy_device = dev
> +        guest.add_device(self._unattended_floppy_device)
> +
> +    def _remove_unattended_floppy_device(self, guest):
> +        if not self._unattended_floppy_device:
> +            return
> +
> +        self._unattended_floppy_device.path = None
> +        self._unattended_floppy_device.sync_path_props()
> +
> +    def _cleanup_unattended_files(self):
> +        for f in self._unattended_files:
> +            os.unlink(f)
> +
>      def _build_boot_order(self, guest, bootdev):
>          bootorder = [bootdev]
>  
> @@ -168,6 +199,8 @@ class Installer(object):
>      def _cleanup(self, guest):
>          if self._treemedia:
>              self._treemedia.cleanup(guest)
> +        else:
> +            self._cleanup_unattended_files()
>  
>      def _get_postinstall_bootdev(self, guest):
>          if self.cdrom and self.livecd:
> @@ -272,6 +305,88 @@ class Installer(object):
>          logging.debug("installer.detect_distro returned=%s", ret)
>          return ret
>  
> +    def set_unattended(self, guest, variant, name, unattended_data):
> +        """
> +        Set up the unattended installation based on the OS variant of the
> +        guest. In case some error happens, a warning will be printed and
> +        no unattended installation will happen.
> +        """
> +        # Those bits about installation source and injection method are
> +        # mostly needed for Linuxes and they become a no-op on Windows(es)
> +        # Guests.
> +        installation_source = libosinfo.InstallScriptInstallationSource.MEDIA \
> +            if not self.networkinstall \
> +            else libosinfo.InstallScriptInstallationSource.NETWORK
> +
> +        injection_method = libosinfo.InstallScriptInjectionMethod.FLOPPY \
> +            if guest.osinfo.is_windows() \
> +            else libosinfo.InstallScriptInjectionMethod.INITRD
> +
> +        script = guest.osinfo.get_install_script(unattended_data.profile)
> +
> +        supported_injection_methods = script.get_injection_methods()
> +
> +        if (injection_method & supported_injection_methods == 0):
> +             logging.warning(
> +                 _("%s does not support unattended installation for the %s profile "
> +                   "when using initrd as injection method."), name, profile)
> +             return
> +

I turned this into an explicit failure. We can loosen it later if
desired, but I'd rather be strict now, will make testing easier.

> +        script.set_preferred_injection_method(injection_method)
> +        script.set_installation_source(installation_source)
> +
> +        config = guest.osinfo.get_install_script_config(script, guest.os.arch, name)
> +
> +        if unattended_data.admin_password and not unattended_data.user_password:
> +            unattended_data.user_password = unattended_data.admin_password
> +
> +        if unattended_data.user_password and not unattended_data.admin_password:
> +            unattended_data.admin_password = unattended_data.user_password
> +
> +        if unattended_data.user_password or unattended_data.admin_password:
> +            config.set_admin_password(unattended_data.admin_password)
> +            config.set_user_password(unattended_data.user_password)
> +        else:
> +            # "desktop" profiles will always have a user set up. The same
> +            # happen for Windows(es) installations, even the "jeos" ones.
> +            if unattended_data.profile == "desktop" or guest.osinfo.is_windows():
> +                print(_("User password: %s" % config.get_user_password()))
> +            print(_("Admin password: %s" % config.get_admin_password()))
> +
> +        if self._treemedia:
> +            cmdline = self._treemedia.generate_install_script(guest, script, config)
> +            self.extra_args = [cmdline]
> +        else:
> +            # This branch targets Windows(es) unattended installations, where
> +            # we have to:
> +            # - set the target disk accordingly;
> +            # - set the product key
> +            # - create a floppy drive with a MS-DOS filesystem
> +            # - copy the installation files to the drive
> +            config.set_target_disk("C")
> +            config.set_reg_product_key(unattended_data.product_key)
> +
> +            scratch = os.path.join(util.get_cache_dir(), "unattended")
> +            if not os.path.exists(scratch):
> +                os.makedirs(scratch, 0o751)
> +
> +            img = os.path.join(scratch, name + "-unattended.img")
> +            self._unattended_files.append(img)
> +
> +            guest.osinfo.generate_install_script_output(script, config, gio.File.new_for_path(scratch))
> +            path = os.path.join(scratch, script.get_expected_filename())
> +            self._unattended_files.append(path)
> +
> +            cmd = ["mkfs.msdos", "-C", img, "1440"]
> +            logging.debug("Running mkisofs: %s", cmd)
> +            output = subprocess.check_output(cmd)
> +
> +            cmd = ["mcopy", "-i", img, path, "::"]
> +            logging.debug("Running mcopy: %s", cmd)
> +            output = subprocess.check_output(cmd)
> +
> +            self._add_unattended_floppy_device(guest, img)
> +
> 

This actual floppy creation can't happen here, it needs to happen via
the _prepare step

Actually since I see the UnattendedData class from cli.py is passed all
the way down to here, it should be public. Maybe the floppy and
UnattendedData stuff could go into its own file like installunattended.
Something to consider

I didn't wrap my head around the details too much, I'll look more
closely on a v2

Thanks,
Cole
>From a80dc084ca486abeb510e849896b0aa4c378c5fe Mon Sep 17 00:00:00 2001
Message-Id: <a80dc084ca486abeb510e849896b0aa4c378c5fe.1549576762.git.crobinso@xxxxxxxxxx>
From: =?UTF-8?q?Fabiano=20Fid=C3=AAncio?= <fidencio@xxxxxxxxxx>
Date: Wed, 6 Feb 2019 14:21:31 +0100
Subject: [PATCH virt-manager] virt-install: Add --unattended support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces the preliminary support for unattended
installations using virt-install.

The command-line added is something like:
--unattended
profile="jeos|desktop",user-password="...",admin-password="...",product-key="..."

In case no profile is passed, "jeos" is used as the default one.
In case no user or admin password is passed, those are going to be
automatically generated and printed to the user.
The "product-key" argument is needed for Windows(es) installations.

The --unattended argument works with:
- --cdrom, thus the user can pass a media to be used;
- --location, thus the user can pass a tree/media to be used;
- with no arguments, then the tree is taken from osinfo-db for that
distro;

Using --os-variant is required for when using --unattended.

There are still a few things missing in this path that will require some
more work to be done, as:
- Windowses:
  - Handle more than one script (usually needed for installing drivers);
  - Support a second disk containing drivers;
- General:
  - Have a proper check and help about which OSes support which kind of
  unattended installations;
  - Have a proper way to detect the keyboard layout being used by the
  user (currently we just guess it by the user's system language);

Signed-off-by: Fabiano Fidêncio <fidencio@xxxxxxxxxx>
Signed-off-by: Cole Robinson <crobinso@xxxxxxxxxx>
---
 virt-install                   |  42 ++++++++++--
 virtinst/cli.py                |  33 +++++++++-
 virtinst/installer.py          | 117 +++++++++++++++++++++++++++++++++
 virtinst/installertreemedia.py |  17 +++++
 virtinst/osdict.py             | 106 +++++++++++++++++++++++++++++
 5 files changed, 310 insertions(+), 5 deletions(-)

diff --git a/virt-install b/virt-install
index 715039fe..9c57fae9 100755
--- a/virt-install
+++ b/virt-install
@@ -333,6 +333,10 @@ def validate_required_options(options, guest, installer):
             _("An install method must be specified\n(%(methods)s)") %
             {"methods": install_methods})
 
+    if options.unattended:
+        if options.os_variant.is_none or options.os_variant.is_auto:
+            msg += "\n" + _("--unattended requires an explicit --os-variant")
+
     if msg:
         fail(msg)
 
@@ -364,6 +368,10 @@ def check_option_collisions(options, guest, installer):
         fail(_("--initrd-inject only works if specified with --location.") +
              cdrom_err)
 
+    if options.unattended:
+        if options.initrd_inject or options.extra_args:
+            fail(_("--unattended does not support --initrd-inject nor --extra-args."))
+
 
 def _show_nographics_warnings(options, guest, installer):
     if guest.devices.graphics:
@@ -439,8 +447,21 @@ def build_installer(options, guest):
     location_kernel = None
     location_initrd = None
     install_bootdev = None
+    network_install = False
 
     has_installer = True
+    if options.unattended:
+        if not guest.osinfo.is_windows():
+            if options.cdrom:
+                options.location = options.cdrom
+                options.cdrom = None
+            elif options.location:
+                if not options.location.endswith(".iso"):
+                    network_install = True
+            else:
+                options.location = guest.osinfo.get_url(guest.os.arch)
+                network_install = True
+
     if options.location:
         (location,
          location_kernel,
@@ -469,10 +490,21 @@ def build_installer(options, guest):
             install_bootdev=install_bootdev)
     if cdrom and options.livecd:
         installer.livecd = True
-    if options.extra_args:
-        installer.extra_args = options.extra_args
-    if options.initrd_inject:
-        installer.set_initrd_injections(options.initrd_inject)
+
+    if network_install:
+        installer.networkinstall = True
+
+    if options.unattended:
+        unattended_data = cli.parse_unattended(options.unattended)
+        options.unattended = None
+
+        installer.set_unattended(guest, unattended_data)
+    else:
+        if options.extra_args:
+            installer.extra_args = options.extra_args
+        if options.initrd_inject:
+            installer.set_initrd_injections(options.initrd_inject)
+
     if options.autostart:
         installer.autostart = True
 
@@ -780,6 +812,8 @@ def parse_args():
                            "booted from --location"))
     insg.add_argument("--initrd-inject", action="append",
                     help=_("Add given file to root of initrd from --location"))
+    insg.add_argument("--unattended", const="profile=jeos", default="", nargs='?',
+                    help=_("Unattended installation"))
 
     # Takes a URL and just prints to stdout the detected distro name
     insg.add_argument("--test-media-detection", help=argparse.SUPPRESS)
diff --git a/virtinst/cli.py b/virtinst/cli.py
index fad0e6a7..3c150728 100644
--- a/virtinst/cli.py
+++ b/virtinst/cli.py
@@ -462,7 +462,8 @@ def get_meter():
 ###########################
 
 def _get_completer_parsers():
-    return VIRT_PARSERS + [ParseCLICheck, ParserLocation, ParserOSVariant]
+    return VIRT_PARSERS + [ParseCLICheck, ParserLocation, ParserOSVariant,
+            ParseCLIUnattended]
 
 
 def _virtparser_completer(prefix, **kwargs):
@@ -1417,6 +1418,36 @@ class VirtCLIParser(metaclass=InitClass):
         """Do nothing callback"""
 
 
+########################
+# --unattended parsing #
+########################
+
+class ParseCLIUnattended(VirtCLIParser):
+    cli_arg_name = "unattended"
+
+    @classmethod
+    def __init_class__(cls, **kwargs):
+        VirtCLIParser.__init_class__(**kwargs)
+        cls.add_arg("profile", "profile")
+        cls.add_arg("admin_password", "admin-password")
+        cls.add_arg("user_password", "user-password")
+        cls.add_arg("product_key", "product-key")
+
+
+def parse_unattended(unattended):
+    class UnattendedData:
+        def __init__(self):
+            self.profile = "jeos"
+            self.admin_password = None
+            self.user_password = None
+            self.product_key = None
+
+    ret = UnattendedData()
+    parser = ParseCLIUnattended(None, unattended)
+    parser.parse(ret)
+    return ret
+
+
 ###################
 # --check parsing #
 ###################
diff --git a/virtinst/installer.py b/virtinst/installer.py
index fe840abf..b3b2cdeb 100644
--- a/virtinst/installer.py
+++ b/virtinst/installer.py
@@ -8,6 +8,7 @@
 
 import os
 import logging
+import subprocess
 
 import libvirt
 
@@ -17,6 +18,11 @@ from .osdict import OSDB
 from .installertreemedia import InstallerTreeMedia
 from . import util
 
+import gi
+gi.require_version('Libosinfo', '1.0')
+from gi.repository import Libosinfo as libosinfo
+from gi.repository import Gio as gio
+
 
 class Installer(object):
     """
@@ -39,6 +45,7 @@ class Installer(object):
             location_kernel=None, location_initrd=None):
         self.conn = conn
 
+        self.networkinstall = False
         self.livecd = False
         self.extra_args = []
 
@@ -49,6 +56,8 @@ class Installer(object):
         self._install_kernel = None
         self._install_initrd = None
         self._install_cdrom_device = None
+        self._unattended_floppy_device = None
+        self._unattended_files = []
         self._defaults_are_set = False
 
         if location_kernel or location_initrd:
@@ -111,6 +120,28 @@ class Installer(object):
         self._install_cdrom_device.path = None
         self._install_cdrom_device.sync_path_props()
 
+    def _add_unattended_floppy_device(self, guest, location):
+        if self._unattended_floppy_device:
+            return
+        dev = DeviceDisk(self.conn)
+        dev.device = dev.DEVICE_FLOPPY
+        dev.path = location
+        dev.sync_path_props()
+        dev.validate()
+        self._unattended_floppy_device = dev
+        guest.add_device(self._unattended_floppy_device)
+
+    def _remove_unattended_floppy_device(self, guest):
+        if not self._unattended_floppy_device:
+            return
+
+        self._unattended_floppy_device.path = None
+        self._unattended_floppy_device.sync_path_props()
+
+    def _cleanup_unattended_files(self):
+        for f in self._unattended_files:
+            os.unlink(f)
+
     def _build_boot_order(self, guest, bootdev):
         bootorder = [bootdev]
 
@@ -168,6 +199,8 @@ class Installer(object):
     def _cleanup(self, guest):
         if self._treemedia:
             self._treemedia.cleanup(guest)
+        else:
+            self._cleanup_unattended_files()
 
     def _get_postinstall_bootdev(self, guest):
         if self.cdrom and self.livecd:
@@ -276,6 +309,89 @@ class Installer(object):
         logging.debug("installer.detect_distro returned=%s", ret)
         return ret
 
+    def set_unattended(self, guest, unattended_data):
+        """
+        Set up the unattended installation based on the OS variant of the
+        guest. In case some error happens, a warning will be printed and
+        no unattended installation will happen.
+        """
+        # Those bits about installation source and injection method are
+        # mostly needed for Linuxes and they become a no-op on Windows(es)
+        # Guests.
+        installation_source = libosinfo.InstallScriptInstallationSource.MEDIA \
+            if not self.networkinstall \
+            else libosinfo.InstallScriptInstallationSource.NETWORK
+
+        injection_method = libosinfo.InstallScriptInjectionMethod.FLOPPY \
+            if guest.osinfo.is_windows() \
+            else libosinfo.InstallScriptInjectionMethod.INITRD
+
+        script = guest.osinfo.get_install_script(unattended_data.profile)
+
+        supported_injection_methods = script.get_injection_methods()
+
+        if (injection_method & supported_injection_methods == 0):
+            raise RuntimeError(
+                _("OS '%s' profile '%s' unattended install is not supported") %
+                (guest.osinfo.name, unattended_data.profile))
+
+        script.set_preferred_injection_method(injection_method)
+        script.set_installation_source(installation_source)
+
+        config = guest.osinfo.get_install_script_config(script,
+                guest.os.arch, guest.name)
+
+        if unattended_data.admin_password and not unattended_data.user_password:
+            unattended_data.user_password = unattended_data.admin_password
+
+        if unattended_data.user_password and not unattended_data.admin_password:
+            unattended_data.admin_password = unattended_data.user_password
+
+        if unattended_data.user_password or unattended_data.admin_password:
+            config.set_admin_password(unattended_data.admin_password)
+            config.set_user_password(unattended_data.user_password)
+        else:
+            # "desktop" profiles will always have a user set up. The same
+            # happen for Windows(es) installations, even the "jeos" ones.
+            if unattended_data.profile == "desktop" or guest.osinfo.is_windows():
+                print(_("User password: %s" % config.get_user_password()))
+            print(_("Admin password: %s" % config.get_admin_password()))
+
+        if self._treemedia:
+            cmdline = self._treemedia.generate_install_script(guest, script, config)
+            self.extra_args = [cmdline]
+        else:
+            # This branch targets Windows(es) unattended installations, where
+            # we have to:
+            # - set the target disk accordingly;
+            # - set the product key
+            # - create a floppy drive with a MS-DOS filesystem
+            # - copy the installation files to the drive
+            config.set_target_disk("C")
+            config.set_reg_product_key(unattended_data.product_key)
+
+            scratch = os.path.join(util.get_cache_dir(), "unattended")
+            if not os.path.exists(scratch):
+                os.makedirs(scratch, 0o751)
+
+            img = os.path.join(scratch, guest.name + "-unattended.img")
+            self._unattended_files.append(img)
+
+            guest.osinfo.generate_install_script_output(script,
+                    config, gio.File.new_for_path(scratch))
+            path = os.path.join(scratch, script.get_expected_filename())
+            self._unattended_files.append(path)
+
+            cmd = ["mkfs.msdos", "-C", img, "1440"]
+            logging.debug("Running mkisofs: %s", cmd)
+            output = subprocess.check_output(cmd)
+
+            cmd = ["mcopy", "-i", img, path, "::"]
+            logging.debug("Running mcopy: %s", cmd)
+            output = subprocess.check_output(cmd)
+
+            self._add_unattended_floppy_device(guest, img)
+
 
     ##########################
     # guest install handling #
@@ -301,6 +417,7 @@ class Installer(object):
             return ret
         finally:
             self._remove_install_cdrom_media(guest)
+            self._remove_unattended_floppy_device(guest)
             self._finish_get_install_xml(guest, data)
 
     def _build_xml(self, guest):
diff --git a/virtinst/installertreemedia.py b/virtinst/installertreemedia.py
index 3162e717..5264b85c 100644
--- a/virtinst/installertreemedia.py
+++ b/virtinst/installertreemedia.py
@@ -7,6 +7,9 @@
 import logging
 import os
 
+import gi
+from gi.repository import Gio as gio
+
 from . import urldetect
 from . import urlfetcher
 from . import util
@@ -166,6 +169,20 @@ class InstallerTreeMedia(object):
     # Public API #
     ##############
 
+    def generate_install_script(self, guest, script, config):
+        scratch = os.path.join(util.get_cache_dir(), "unattended")
+        if not os.path.exists(scratch):
+            os.makedirs(scratch, 0o751)
+
+        guest.osinfo.generate_install_script_output(script, config, gio.File.new_for_path(scratch))
+
+        path = os.path.join(scratch, script.get_expected_filename())
+        self.initrd_injections = [path]
+
+        self._tmpfiles.append(path)
+
+        return guest.osinfo.generate_install_script_cmdline(script, config)
+
     def prepare(self, guest, meter):
         fetcher = self._get_fetcher(guest, meter)
         return self._prepare_kernel_url(guest, fetcher)
diff --git a/virtinst/osdict.py b/virtinst/osdict.py
index 72edc4a6..755bbae7 100644
--- a/virtinst/osdict.py
+++ b/virtinst/osdict.py
@@ -13,6 +13,7 @@ import re
 import gi
 gi.require_version('Libosinfo', '1.0')
 from gi.repository import Libosinfo as libosinfo
+from gi.repository import GLib as glib, Gio as gio
 
 
 ###################
@@ -477,5 +478,110 @@ class _OsVariant(object):
 
         return None
 
+    def get_url(self, arch):
+        if not self._os:
+            return None
+
+        tree_filter = libosinfo.Filter()
+        tree_filter.add_constraint(libosinfo.TREE_PROP_ARCHITECTURE, arch)
+        tree_list = self._os.get_tree_list()
+        if tree_list.get_length() < 1:
+            logging.warning(
+                _("%s does not support unattended tree installation"),
+                self.name)
+            return None
+        filtered_tree_list = tree_list.new_filtered(tree_filter)
+        if filtered_tree_list.get_length() < 1:
+            logging.warning(
+                _("%s does not support unattened tree installation for the "
+                  "%s architecture", self.name, arch))
+            return None
+        return filtered_tree_list.get_nth(0).get_url()
+
+    def get_install_script(self, profile):
+        if not self._os:
+            return None
+
+        script_list = self._os.get_install_script_list()
+        if script_list.get_length == 0:
+            logging.warning(
+                _("%s does not support unattended installation."), self.name)
+
+        profile_filter = libosinfo.Filter()
+        profile_filter.add_constraint(libosinfo.INSTALL_SCRIPT_PROP_PROFILE, profile)
+
+        filtered_script_list = script_list.new_filtered(profile_filter)
+        if filtered_script_list.get_length() == 0:
+            logging.warning(
+                _("%s does not support unattended installation for the %s profile."),
+                self.name, profile)
+            return None
+
+        return filtered_script_list.get_nth(0)
+
+    def get_install_script_config(self, script, arch, hostname):
+
+        def get_timezone():
+            TZ_FILE = "/etc/localtime"
+            localtime = gio.File.new_for_path(TZ_FILE)
+            if not localtime.query_exists():
+                return None
+            info = localtime.query_info(gio.FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS)
+            if not info:
+                return None
+            target = info.get_symlink_target()
+            if not target:
+                return None
+            tokens = target.split("zoneinfo/")
+            if not tokens or len(tokens) < 2:
+                return None
+            return tokens[1]
+
+        def get_language():
+            names = glib.get_language_names()
+            if not names or len(names) < 2:
+                return None
+
+            return names[1]
+
+        config = libosinfo.InstallConfig()
+
+        config.set_user_login(glib.get_user_name())
+        config.set_user_realname(glib.get_user_name())
+        config.set_user_password(config.get_admin_password())
+
+        config.set_target_disk("/dev/vda" if self.supports_virtiodisk() else "/dev/sda")
+        config.set_hardware_arch(arch)
+        config.set_hostname(hostname)
+
+        timezone = get_timezone()
+        if timezone:
+            config.set_l10n_timezone(timezone)
+        else:
+            logging.warning(
+                _("us timezone will be used for the unattended installation"))
+
+        language = get_language()
+        if language:
+            config.set_l10n_language(language)
+            config.set_l10n_keyboard(language)
+        else:
+            logging.warning(
+                _("us language and keyboard layout will be used for the "
+                  "unattended installation"))
+
+        return config
+
+    def generate_install_script_output(self, script, config, output_dir):
+        if not self._os:
+            return None
+
+        script.generate_output(self._os, config, output_dir)
+
+    def generate_install_script_cmdline(self, script, config):
+        if not self._os:
+            return None
+
+        return script.generate_command_line(self._os, config)
 
 OSDB = _OSDB()
-- 
2.20.1

_______________________________________________
virt-tools-list mailing list
virt-tools-list@xxxxxxxxxx
https://www.redhat.com/mailman/listinfo/virt-tools-list

[Index of Archives]     [Linux Virtualization]     [KVM Development]     [CentOS Virtualization]     [Netdev]     [Ethernet Bridging]     [Linux Wireless]     [Kernel Newbies]     [Security]     [Linux for Hams]     [Netfilter]     [Bugtraq]     [Yosemite Forum]     [MIPS Linux]     [ARM Linux]     [Linux RAID]     [Linux Admin]     [Samba]     [Video 4 Linux]

  Powered by Linux