[gnome-boxes] Merge virt-installer as gnome-boxes-installer



commit 07c183e22680577cefd8a4fa9575c98ead9a5511
Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
Date:   Thu Oct 27 19:28:44 2011 +0300

    Merge virt-installer as gnome-boxes-installer
    
    For now we just merge it as a separate binary only.

 data/Makefile.am              |   13 +++
 data/floppy.img               |  Bin 0 -> 2097152 bytes
 data/ks.cfg                   |   28 ++++++
 data/win2k.sif                |   42 ++++++++
 data/win2k.xml                |   94 ++++++++++++++++++
 data/win2k3.sif               |   48 +++++++++
 data/win7.xml                 |   96 +++++++++++++++++++
 data/winxp.sif                |   48 +++++++++
 src/Makefile.am               |   17 +++-
 src/fedora-installer.vala     |  105 ++++++++++++++++++++
 src/installer-main.vala       |   74 ++++++++++++++
 src/installer-media.vala      |  105 ++++++++++++++++++++
 src/installer-util.vala       |    1 +
 src/os-database.vala          |  120 +++++++++++++++++++++++
 src/unattended-installer.vala |  140 +++++++++++++++++++++++++++
 src/util.vala                 |   16 +++
 src/vm-creator.vala           |  212 +++++++++++++++++++++++++++++++++++++++++
 src/win7-installer.vala       |    9 ++
 src/winxp-installer.vala      |    9 ++
 19 files changed, 1176 insertions(+), 1 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index 131a472..3583447 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -18,6 +18,19 @@ source_DATA       = QEMU_Session
 
 iconsdir = $(pkgdatadir)/icons
 
+unattendeddir = $(datadir)/gnome-boxes/unattended
+unattended_DATA = ks.cfg 			\
+		  win7.xml 			\
+		  win2k.xml 			\
+		  winxp.sif 			\
+		  win2k.sif 			\
+		  win2k3.sif 			\
+		  floppy.img			\
+		  $(NULL)
+
+floppy.img:
+	rm -f floppy.img && mkfs.msdos -C floppy.img 2048
+
 EXTRA_DIST =					\
 	gnome-boxes.desktop.in			\
 	org.gnome.boxes.gschema.xml.in		\
diff --git a/data/floppy.img b/data/floppy.img
new file mode 100644
index 0000000..df58d6d
Binary files /dev/null and b/data/floppy.img differ
diff --git a/data/ks.cfg b/data/ks.cfg
new file mode 100644
index 0000000..5c348ef
--- /dev/null
+++ b/data/ks.cfg
@@ -0,0 +1,28 @@
+install
+text
+keyboard us
+lang en_US.UTF-8
+skipx
+network --device eth0 --bootproto dhcp
+rootpw whatever
+firewall --disabled
+authconfig --enableshadow --enablemd5
+selinux --enforcing
+timezone --utc Europe/Helsinki
+bootloader --location=mbr
+zerombr
+clearpart --all --drives=sda
+
+part /boot --fstype ext4 --recommended --ondisk=sda
+part pv.2 --size=1 --grow --ondisk=sda
+volgroup VolGroup00 --pesize=32768 pv.2
+logvol swap --fstype swap --name=LogVol01 --vgname=VolGroup00 --size=768 --grow --maxsize=1536
+logvol / --fstype ext4 --name=LogVol00 --vgname=VolGroup00 --size=1024 --grow
+reboot
+
+%packages
+ base
+ core
+ hardware-support
+
+%end
diff --git a/data/win2k.sif b/data/win2k.sif
new file mode 100644
index 0000000..d0a0a19
--- /dev/null
+++ b/data/win2k.sif
@@ -0,0 +1,42 @@
+;SetupMgrTag
+[Data]
+    AutoPartition=1
+    MsDosInitiated=No
+    UnattendedInstall=Yes
+
+[Unattended]
+    UnattendMode=FullUnattended
+    OemSkipEula=Yes
+    OemPreinstall=No
+    TargetPath=\WINNT
+    Repartition=Yes
+    WaitForReboot="No"
+    UnattendSwitch="Yes"
+
+[GuiUnattended]
+    AdminPassword=%ROOTPW%
+    AutoLogon=Yes
+    AutoLogonCount=1
+    OEMSkipRegional=1
+    TimeZone=35
+    OemSkipWelcome=1
+
+[UserData]
+    ProductID=
+    FullName="OZ"
+    ComputerName=Whatever
+
+[Display]
+    BitsPerPel=16
+    Xresolution=640
+    YResolution=480
+    Vrefresh=60
+
+[RegionalSettings]
+    LanguageGroup=1
+
+[Identification]
+    JoinWorkgroup=WORKGROUP
+
+[Networking]
+    InstallDefaultComponents=Yes
diff --git a/data/win2k.xml b/data/win2k.xml
new file mode 100644
index 0000000..d51c433
--- /dev/null
+++ b/data/win2k.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<unattend xmlns="urn:schemas-microsoft-com:unattend">
+  <settings pass="windowsPE">
+    <component name="Microsoft-Windows-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+      <DiskConfiguration>
+	<WillShowUI>OnError</WillShowUI>
+	<Disk>
+	  <CreatePartitions>
+	    <CreatePartition>
+	      <Order>1</Order>
+	      <Size>1</Size>
+	      <Type>Primary</Type>
+	    </CreatePartition>
+	  </CreatePartitions>
+	  <DiskID>0</DiskID>
+	  <WillWipeDisk>true</WillWipeDisk>
+	  <ModifyPartitions>
+	    <ModifyPartition>
+	      <Active>true</Active>
+	      <Extend>true</Extend>
+	      <Format>NTFS</Format>
+	      <Label>C drive</Label>
+	      <Letter>C</Letter>
+	      <Order>1</Order>
+	      <PartitionID>1</PartitionID>
+	    </ModifyPartition>
+	  </ModifyPartitions>
+	</Disk>
+      </DiskConfiguration>
+      <ImageInstall>
+	<OSImage>
+	  <InstallFrom>
+	    <MetaData wcm:action="add">
+	      <Key>/IMAGE/INDEX</Key>
+	      <Value>1</Value>
+	    </MetaData>
+	  </InstallFrom>
+	  <InstallTo>
+	    <DiskID>0</DiskID>
+	    <PartitionID>1</PartitionID>
+	  </InstallTo>
+	  <WillShowUI>OnError</WillShowUI>
+	</OSImage>
+      </ImageInstall>
+      <UserData>
+	<AcceptEula>true</AcceptEula>
+      </UserData>
+    </component>
+    <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+      <SetupUILanguage>
+	<UILanguage>en-US</UILanguage>
+      </SetupUILanguage>
+      <SystemLocale>en-US</SystemLocale>
+      <UILanguage>en-US</UILanguage>
+      <UserLocale>en-US</UserLocale>
+    </component>
+  </settings>
+  <settings pass="oobeSystem">
+    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+      <UserAccounts>
+	<AdministratorPassword>
+	  <Value></Value>
+	  <PlainText>true</PlainText>
+	</AdministratorPassword>
+      </UserAccounts>
+      <AutoLogon>
+	<Password>
+	  <Value></Value>
+	  <PlainText>true</PlainText>
+	</Password>
+	<Enabled>true</Enabled>
+	<LogonCount>5</LogonCount>
+	<Username>Administrator</Username>
+      </AutoLogon>
+      <RegisteredOwner/>
+      <OOBE>
+	<HideEULAPage>true</HideEULAPage>
+	<ProtectYourPC>3</ProtectYourPC>
+      </OOBE>
+      <FirstLogonCommands>
+	<SynchronousCommand wcm:action="add">
+	  <Order>1</Order>
+	  <Description>Shutting down Windows</Description>
+	  <CommandLine>cmd /C shutdown /s /t 0</CommandLine>
+	</SynchronousCommand>
+      </FirstLogonCommands>
+    </component>
+  </settings>
+  <settings pass="specialize">
+    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+      <ProductKey></ProductKey>
+    </component>
+  </settings>
+</unattend>
diff --git a/data/win2k3.sif b/data/win2k3.sif
new file mode 100644
index 0000000..e888fd1
--- /dev/null
+++ b/data/win2k3.sif
@@ -0,0 +1,48 @@
+;SetupMgrTag
+[Data]
+    AutoPartition=1
+    MsDosInitiated="0"
+    UnattendedInstall="Yes"
+
+[Unattended]
+    UnattendMode=FullUnattended
+    OemSkipEula=Yes
+    OemPreinstall=No
+    TargetPath=\WINDOWS
+    Repartition=Yes
+    WaitForReboot="No"
+    UnattendSwitch="Yes"
+
+[GuiUnattended]
+    AdminPassword="%ROOTPW%"
+    EncryptedAdminPassword=NO
+    AutoLogon=Yes
+    AutoLogonCount=1
+    OEMSkipRegional=1
+    TimeZone=35
+    OemSkipWelcome=1
+
+[UserData]
+    ProductKey=
+    FullName="OZ"
+    OrgName=""
+    ComputerName=Whatever
+
+[LicenseFilePrintData]
+    AutoMode=PerServer
+    AutoUsers=5
+
+[Identification]
+    JoinWorkgroup=WORKGROUP
+
+[Networking]
+    InstallDefaultComponents=Yes
+
+[SetupParams]
+    UserExecute = "sc config TlntSvr start= auto"
+
+[WindowsFirewall]
+    Profiles = WindowsFirewall.TurnOffFirewall
+
+[WindowsFirewall.TurnOffFirewall]
+    Mode = 0
diff --git a/data/win7.xml b/data/win7.xml
new file mode 100644
index 0000000..c691257
--- /dev/null
+++ b/data/win7.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<unattend xmlns="urn:schemas-microsoft-com:unattend">
+  <settings pass="windowsPE">
+    <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+      <DiskConfiguration>
+	<WillShowUI>OnError</WillShowUI>
+	<Disk>
+	  <CreatePartitions>
+	    <CreatePartition>
+	      <Order>1</Order>
+	      <Size>1</Size>
+	      <Type>Primary</Type>
+	    </CreatePartition>
+	  </CreatePartitions>
+	  <DiskID>0</DiskID>
+	  <WillWipeDisk>true</WillWipeDisk>
+	  <ModifyPartitions>
+	    <ModifyPartition>
+	      <Active>true</Active>
+	      <Extend>true</Extend>
+	      <Format>NTFS</Format>
+	      <Label>C drive</Label>
+	      <Letter>C</Letter>
+	      <Order>1</Order>
+	      <PartitionID>1</PartitionID>
+	    </ModifyPartition>
+	  </ModifyPartitions>
+	</Disk>
+      </DiskConfiguration>
+      <ImageInstall>
+	<OSImage>
+	  <InstallFrom>
+	    <MetaData wcm:action="add">
+	      <Key>/IMAGE/INDEX</Key>
+	      <Value>1</Value>
+	    </MetaData>
+	  </InstallFrom>
+	  <InstallTo>
+	    <DiskID>0</DiskID>
+	    <PartitionID>1</PartitionID>
+	  </InstallTo>
+	  <WillShowUI>OnError</WillShowUI>
+	</OSImage>
+      </ImageInstall>
+      <UserData>
+	<AcceptEula>true</AcceptEula>
+      </UserData>
+    </component>
+    <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+      <SetupUILanguage>
+	<UILanguage>en-US</UILanguage>
+      </SetupUILanguage>
+      <SystemLocale>en-US</SystemLocale>
+      <UILanguage>en-US</UILanguage>
+      <UserLocale>en-US</UserLocale>
+    </component>
+  </settings>
+  <settings pass="oobeSystem">
+    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+      <UserAccounts>
+	<AdministratorPassword>
+	  <Value>whatever</Value>
+	  <PlainText>true</PlainText>
+	</AdministratorPassword>
+      </UserAccounts>
+      <AutoLogon>
+	<Password>
+	  <Value>whatever</Value>
+	  <PlainText>true</PlainText>
+	</Password>
+	<Enabled>true</Enabled>
+	<LogonCount>5</LogonCount>
+	<Username>Administrator</Username>
+      </AutoLogon>
+      <OOBE>
+	<NetworkLocation>Work</NetworkLocation>
+	<HideEULAPage>true</HideEULAPage>
+	<ProtectYourPC>3</ProtectYourPC>
+	<SkipMachineOOBE>true</SkipMachineOOBE>
+	<SkipUserOOBE>true</SkipUserOOBE>
+      </OOBE>
+      <FirstLogonCommands>
+	<SynchronousCommand wcm:action="add">
+	  <Order>1</Order>
+	  <Description>Turn Off Network Selection pop-up</Description>
+	  <CommandLine>cmd /c reg add "HKLM\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff"</CommandLine>
+	</SynchronousCommand>
+	<SynchronousCommand wcm:action="add">
+	  <Order>2</Order>
+	  <Description>Shutting down Windows</Description>
+	  <CommandLine>cmd /C shutdown /s /t 0</CommandLine>
+	</SynchronousCommand>
+      </FirstLogonCommands>
+    </component>
+  </settings>
+</unattend>
diff --git a/data/winxp.sif b/data/winxp.sif
new file mode 100644
index 0000000..c860bff
--- /dev/null
+++ b/data/winxp.sif
@@ -0,0 +1,48 @@
+;SetupMgrTag
+[Data]
+    AutoPartition=1
+    MsDosInitiated="0"
+    UnattendedInstall="Yes"
+
+[Unattended]
+    UnattendMode=FullUnattended
+    OemSkipEula=Yes
+    OemPreinstall=No
+    TargetPath=\WINDOWS
+    Repartition=Yes
+    WaitForReboot="No"
+    UnattendSwitch="Yes"
+
+[GuiUnattended]
+    AdminPassword="%ROOTPW%"
+    EncryptedAdminPassword=NO
+    AutoLogon=Yes
+    AutoLogonCount=1
+    OEMSkipRegional=1
+    TimeZone=35
+    OemSkipWelcome=1
+
+[UserData]
+    ProductKey=
+    FullName="OZ"
+    OrgName=""
+    ComputerName=Nothing
+
+[LicenseFilePrintData]
+    AutoMode=PerServer
+    AutoUsers=5
+
+[Identification]
+    JoinWorkgroup=WORKGROUP
+
+[Networking]
+    InstallDefaultComponents=Yes
+
+[SetupParams]
+    UserExecute = "sc config TlntSvr start= auto"
+
+[WindowsFirewall]
+    Profiles = WindowsFirewall.TurnOffFirewall
+
+[WindowsFirewall.TurnOffFirewall]
+    Mode = 0
diff --git a/src/Makefile.am b/src/Makefile.am
index 79bd5bc..5ecf99f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -26,7 +26,7 @@ AM_VALAFLAGS =						\
 	--pkg libosinfo-1.0				\
 	$(NULL)
 
-bin_PROGRAMS = gnome-boxes
+bin_PROGRAMS = gnome-boxes gnome-boxes-installer
 
 gnome_boxes_SOURCES =				\
 	app.vala				\
@@ -49,9 +49,24 @@ gnome_boxes_SOURCES =				\
 	wizard.vala				\
 	$(NULL)
 
+gnome_boxes_installer_SOURCES = 		\
+	installer-media.vala 			\
+	unattended-installer.vala 		\
+	fedora-installer.vala 			\
+	win7-installer.vala 			\
+	winxp-installer.vala 			\
+	os-database.vala 			\
+	vm-creator.vala 			\
+	installer-util.vala			\
+	installer-main.vala			\
+	$(NULL)
+
 gnome_boxes_LDADD = $(BOXES_LIBS)
 gnome_boxes_CFLAGS = $(BOXES_CFLAGS)
 
+gnome_boxes_installer_LDADD = $(BOXES_LIBS)
+gnome_boxes_installer_CFLAGS = $(BOXES_CFLAGS)
+
 MAINTAINERCLEANFILES =				\
 	$(gnome_boxes_SOURCES:.vala=.c)		\
 	*.stamp					\
diff --git a/src/fedora-installer.vala b/src/fedora-installer.vala
new file mode 100644
index 0000000..3d65bc4
--- /dev/null
+++ b/src/fedora-installer.vala
@@ -0,0 +1,105 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+private class Boxes.FedoraInstaller: UnattendedInstaller {
+    private bool mounted;
+
+    private File source_dir;
+    private File kernel_file;
+    private File initrd_file;
+
+    public FedoraInstaller.copy (InstallerMedia media) throws GLib.Error {
+        var source_path = get_unattended_dir ("ks.cfg");
+
+        base.copy (media, source_path, "ks.cfg");
+    }
+
+    protected override async void prepare_direct_boot (Cancellable? cancellable) throws GLib.Error {
+        var kernel_path = os_media.get_kernel_path ();
+        var initrd_path = os_media.get_initrd_path ();
+
+        if (kernel_path == null || initrd_path == null)
+            return;
+
+        yield mount_media (cancellable);
+
+        yield extract_boot_files (kernel_path, initrd_path, cancellable);
+
+        yield normal_clean_up (cancellable);
+    }
+
+    protected override void clean_up () throws GLib.Error {
+        base.clean_up ();
+
+        if (kernel_file != null) {
+            debug ("Removing '%s'..", kernel_path);
+            kernel_file.delete ();
+            debug ("Removed '%s'.", kernel_path);
+        }
+
+        if (initrd_file != null) {
+            debug ("Removing '%s'..", initrd_path);
+            initrd_file.delete ();
+            debug ("Removed '%s'.", initrd_path);
+        }
+    }
+
+    private async void normal_clean_up (Cancellable? cancellable) throws GLib.Error {
+        if (!mounted)
+            return;
+
+        debug ("Unmounting '%s'..", mount_point);
+        string[] argv = { "fusermount", "-u", mount_point };
+        yield exec (argv, cancellable);
+        debug ("Unmounted '%s'.", mount_point);
+
+        source_dir.delete ();
+        debug ("Removed '%s'.", mount_point);
+    }
+
+    private async void mount_media (Cancellable? cancellable) throws GLib.Error {
+        if (mount_point != null) {
+            source_dir = File.new_for_path (mount_point);
+
+            return;
+        }
+
+        mount_point = get_user_unattended_dir (os.short_id);
+        var dir = File.new_for_path (mount_point);
+        try {
+            dir.make_directory (null);
+        } catch (IOError.EXISTS error) {}
+        source_dir = dir;
+
+        debug ("Mounting '%s' on '%s'..", device_file, mount_point);
+        string[] argv = { "fuseiso", device_file, mount_point };
+        yield exec (argv, cancellable);
+        debug ("'%s' now mounted on '%s'.", device_file, mount_point);
+
+        mounted = true;
+    }
+
+    private async void extract_boot_files (string kernel, string initrd, Cancellable? cancellable) throws GLib.Error {
+        kernel_path = Path.build_filename (mount_point, kernel);
+        kernel_file = File.new_for_path (kernel_path);
+        initrd_path = Path.build_filename (mount_point, initrd);
+        initrd_file = File.new_for_path (initrd_path);
+
+        if (!mounted)
+            return;
+
+        kernel_path = get_user_unattended_dir (os.short_id + "-kernel");
+        kernel_file = yield copy_file (kernel_file, kernel_path, cancellable);
+        initrd_path = get_user_unattended_dir (os.short_id + "-initrd");
+        initrd_file = yield copy_file (initrd_file, initrd_path, cancellable);
+    }
+
+    private async File copy_file (File file, string dest_path, Cancellable? cancellable) throws GLib.Error {
+        var dest_file = File.new_for_path (dest_path);
+
+        try {
+            yield file.copy_async (dest_file, 0, Priority.DEFAULT, cancellable);
+        } catch (IOError.EXISTS error) {}
+
+        return dest_file;
+    }
+}
diff --git a/src/installer-main.vala b/src/installer-main.vala
new file mode 100644
index 0000000..bf63491
--- /dev/null
+++ b/src/installer-main.vala
@@ -0,0 +1,74 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+private class Boxes.Main {
+    private OSDatabase os_db;
+    private VMCreator vm_creator;
+    private GUdev.Client client;
+    private MainLoop main_loop;
+    private string[] paths;
+
+    public Main (string[] paths) throws GLib.Error {
+        os_db = new OSDatabase ();
+        client = new GUdev.Client ({"block"});
+        vm_creator = new VMCreator ("qemu:///session");
+        main_loop = new MainLoop ();
+        this.paths = paths;
+    }
+
+    public async void launch_new_vm_for_path (string path, Cancellable? cancellable) throws GLib.Error {
+        var install_media = yield InstallerMedia.instantiate (path, os_db, client, cancellable);
+        var resources = os_db.get_resources_for_os (install_media.os);
+        var domain = yield vm_creator.create_domain_for_installer (install_media, resources, cancellable);
+
+        domain.start (0);
+
+        // Launch view to newly created VM
+        var commandline = "virt-viewer --connect qemu:///session -w " + domain.get_uuid ();
+        debug ("Launching: %s", commandline);
+        var app = AppInfo.create_from_commandline (commandline, install_media.label, AppInfoCreateFlags.NONE);
+
+        app.launch (null, null);
+        debug ("Launched: %s", commandline);
+    }
+
+    public int run () {
+        int ret = 0;
+        uint8 vms_created = 0;
+
+        foreach (var path in paths) {
+            // FIXME: Work-around for bug#628336
+            var _path = path;
+
+            launch_new_vm_for_path.begin (path, null, (obj, res) => {
+                try {
+                    launch_new_vm_for_path.end (res);
+                } catch (GLib.Error error) {
+                    printerr ("Failed to create VM for '%s': %s", _path, error.message);
+
+                    ret = -error.code;
+                }
+
+                vms_created++;
+
+                if (vms_created == paths.length)
+                    main_loop.quit ();
+            });
+        }
+
+        main_loop.run ();
+
+        return ret;
+    }
+
+    public static int main (string[] args) {
+        try {
+            var main = new Main (args[1:args.length]);
+
+            return main.run ();
+        } catch (GLib.Error error) {
+            printerr ("Failed to initialize: %s", error.message);
+
+            return -error.code;
+        }
+    }
+}
diff --git a/src/installer-media.vala b/src/installer-media.vala
new file mode 100644
index 0000000..3bc55c3
--- /dev/null
+++ b/src/installer-media.vala
@@ -0,0 +1,105 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+using Osinfo;
+using GUdev;
+
+private class Boxes.InstallerMedia : Object {
+    public Os os;
+    public Media os_media;
+    public string label;
+    public string device_file;
+    public string mount_point;
+    public bool from_image;
+
+    public static async InstallerMedia instantiate (string       path,
+                                                    OSDatabase   os_db,
+                                                    Client       client,
+                                                    Cancellable? cancellable) throws GLib.Error {
+        var media = new InstallerMedia ();
+        yield media.setup_for_path (path, os_db, client, cancellable);
+
+        if (media.os == null)
+            return media;
+
+        switch (media.os.short_id) {
+        case "fedora14":
+        case "fedora15":
+        case "fedora16":
+            media = new FedoraInstaller.copy (media);
+
+            break;
+
+        case "win7":
+        case "win2k8":
+            media = new Win7Installer.copy (media);
+
+            break;
+
+        case "winxp":
+        case "win2k":
+        case "win2k3":
+            media = new WinXPInstaller.copy (media);
+
+            break;
+
+        default:
+            return media;
+        }
+
+        return media;
+    }
+
+    private async void setup_for_path (string       path,
+                                       OSDatabase   os_db,
+                                       Client       client,
+                                       Cancellable? cancellable) throws GLib.Error {
+        var device = yield get_device_from_path (path, client, cancellable);
+
+        if (device != null)
+            get_os_and_label_from_device (device, os_db);
+        else {
+            from_image = true;
+            os = yield os_db.guess_os_from_install_media (device_file, cancellable, out os_media);
+        }
+
+        if (os != null)
+            label = os.get_name ();
+
+        if (label == null)
+            label = Path.get_basename (device_file);
+    }
+
+    private async GUdev.Device? get_device_from_path (string path, Client client, Cancellable? cancellable) {
+        try {
+            var mount_dir = File.new_for_commandline_arg (path);
+            var mount = yield mount_dir.find_enclosing_mount_async (Priority.DEFAULT, cancellable);
+            var root_dir = mount.get_root ();
+            if (root_dir.get_path () == mount_dir.get_path ()) {
+                var volume = mount.get_volume ();
+                device_file = volume.get_identifier (VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
+                mount_point = path;
+            } else
+                // Assume direct path to device node/image
+                device_file = path;
+        } catch (GLib.Error error) {
+            // Assume direct path to device node/image
+            device_file = path;
+        }
+
+        return client.query_by_device_file (device_file);
+    }
+
+    private void get_os_and_label_from_device (GUdev.Device device, OSDatabase os_db) throws OSDatabaseError {
+        if (!device.get_property_as_boolean ("OSINFO_BOOTABLE"))
+            throw new OSDatabaseError.NON_BOOTABLE ("Media %s is not bootable.", device_file);
+
+        label = device.get_property ("ID_FS_LABEL");
+
+        var os_id = device.get_property ("OSINFO_INSTALLER");
+        if (os_id != null) {
+            os = os_db.get_os_by_id (os_id);
+            if (os == null)
+                throw new OSDatabaseError.UNKNOWN_OS_ID ("Unknown OS ID '%s'", os_id);
+        }
+    }
+}
diff --git a/src/installer-util.vala b/src/installer-util.vala
new file mode 120000
index 0000000..ec2be93
--- /dev/null
+++ b/src/installer-util.vala
@@ -0,0 +1 @@
+util.vala
\ No newline at end of file
diff --git a/src/os-database.vala b/src/os-database.vala
new file mode 100644
index 0000000..5ddcde6
--- /dev/null
+++ b/src/os-database.vala
@@ -0,0 +1,120 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+using Osinfo;
+
+public errordomain Boxes.OSDatabaseError {
+    NON_BOOTABLE,
+    UNKNOWN_OS_ID
+}
+
+private class Boxes.OSDatabase {
+    private const int DEFAULT_VCPUS = 1;
+    private const int64 DEFAULT_RAM = 500 * (int64) MEBIBYTES;
+    private const int64 DEFAULT_STORAGE = 2 * (int64) GIBIBYTES;
+
+    private Db db;
+
+    private static Resources get_default_resources () {
+        var resources = new Resources ("whatever", "x86_64");
+
+        resources.n_cpus = DEFAULT_VCPUS;
+        resources.ram = DEFAULT_RAM;
+        resources.storage = DEFAULT_STORAGE;
+
+        return resources;
+    }
+
+    public OSDatabase () throws GLib.Error {
+        var loader = new Loader ();
+        loader.process_default_path ();
+        db = loader.get_db ();
+    }
+
+    public async Os? guess_os_from_install_media (string       media_path,
+                                                  Cancellable? cancellable,
+                                                  out Media    os_media) throws GLib.Error {
+        var media = yield Media.create_from_location_async (media_path, Priority.DEFAULT, cancellable);
+
+        var os = db.guess_os_from_media (media);
+        if (os != null) {
+            var medias = os.get_media_list ();
+            os_media = get_prefered_media (medias);
+        } else
+            os_media = null;
+
+        return os;
+    }
+
+    public Os? get_os_by_id (string id) {
+        return db.get_os (id);
+    }
+
+    public Resources get_resources_for_os (Os? os) {
+        if (os == null)
+            return get_default_resources ();
+
+        // First try recommended resources
+        var list = os.get_recommended_resources ();
+        var recommended = get_prefered_resources (list);
+
+        list = os.get_minimum_resources ();
+        var minimum = get_prefered_resources (list);
+
+        return get_resources_from_os_resources (minimum, recommended);
+    }
+
+    private Resources get_resources_from_os_resources (Resources? minimum, Resources? recommended) {
+        var resources = get_default_resources ();
+
+        // Number of CPUs
+        if (recommended != null && recommended.n_cpus > 0)
+            resources.n_cpus = recommended.n_cpus;
+        else if (minimum != null && minimum.n_cpus > 0)
+            resources.n_cpus = int.max (minimum.n_cpus, resources.n_cpus);
+
+        // RAM
+        if (recommended != null && recommended.ram > 0)
+            resources.ram = recommended.ram;
+        else if (minimum != null && minimum.ram > 0)
+            resources.ram = int64.max (minimum.ram, resources.ram);
+
+        // Storage
+        if (recommended != null && recommended.storage > 0)
+            resources.storage = recommended.storage;
+        else if (minimum != null && minimum.storage > 0)
+            resources.storage = int64.max (minimum.storage, resources.storage);
+
+        return resources;
+    }
+
+    private Resources? get_prefered_resources (ResourcesList list) {
+        // Prefer x86_64 resources
+        string[] prefs = {"x86_64", "i386", ARCHITECTURE_ALL};
+
+        return get_prefered_entity (list.new_filtered, RESOURCES_PROP_ARCHITECTURE, prefs) as Resources;
+    }
+
+    private Media? get_prefered_media (MediaList list) {
+        // Prefer x86_64 resources
+        string[] prefs = {"x86_64", "i386", ARCHITECTURE_ALL};
+
+        return get_prefered_entity (list.new_filtered, MEDIA_PROP_ARCHITECTURE, prefs) as Media;
+    }
+
+    private Entity? get_prefered_entity (ListFilterFunc filter_func, string property, string[] prefs) {
+        if (prefs.length <= 0)
+            return null;
+
+        var filter = new Filter ();
+        filter.add_constraint (property, prefs[0]);
+        var filtered = filter_func (filter);
+        if (filtered.get_length () <= 0)
+            return get_prefered_entity (filter_func, property, prefs[1:prefs.length]);
+        else
+            // Assumption: There is only one resources instance of each type
+            // (minimum/recommended) of each architecture for each OS.
+            return filtered.get_nth (0);
+    }
+
+    private delegate Osinfo.List ListFilterFunc (Filter filter);
+}
diff --git a/src/unattended-installer.vala b/src/unattended-installer.vala
new file mode 100644
index 0000000..3f0e9ab
--- /dev/null
+++ b/src/unattended-installer.vala
@@ -0,0 +1,140 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+public errordomain UnattendedInstallerError {
+    COMMAND_FAILED
+}
+
+private abstract class Boxes.UnattendedInstaller: InstallerMedia {
+    public string kernel_path;
+    public string initrd_path;
+
+    public string floppy_path;
+    protected File floppy_file;
+
+    protected string unattended_src_path;
+    protected string unattended_dest_name;
+
+    private bool created_floppy;
+
+    public UnattendedInstaller.copy (InstallerMedia media,
+                                     string         unattended_src_path,
+                                     string         unattended_dest_name) throws GLib.Error {
+        os = media.os;
+        os_media = media.os_media;
+        label = media.label;
+        device_file = media.device_file;
+        from_image = media.from_image;
+
+        floppy_path = get_pkgcache (os.short_id + "-unattended.img");
+        this.unattended_src_path = unattended_src_path;
+        this.unattended_dest_name = unattended_dest_name;
+    }
+
+    public async void setup (Cancellable? cancellable) throws GLib.Error {
+        try {
+            if (yield unattended_floppy_exists (cancellable))
+                debug ("Found previously created unattended floppy image for '%s', re-using..", os.short_id);
+            else {
+                yield create_floppy_image (cancellable);
+                yield copy_unattended_file (cancellable);
+            }
+
+            yield prepare_direct_boot (cancellable);
+        } catch (GLib.Error error) {
+            clean_up ();
+
+            throw error;
+        }
+    }
+
+    protected virtual void clean_up () throws GLib.Error {
+        if (!created_floppy)
+            return;
+
+        var floppy_file = File.new_for_path (floppy_path);
+
+        floppy_file.delete ();
+
+        debug ("Removed '%s'.", floppy_path);
+    }
+
+    protected virtual async void prepare_direct_boot (Cancellable? cancellable) throws GLib.Error {}
+
+    protected async void exec (string[] argv, Cancellable? cancellable) throws GLib.Error {
+        SourceFunc continuation = exec.callback;
+        GLib.Error error = null;
+        var context = MainContext.get_thread_default ();
+
+        g_io_scheduler_push_job ((job) => {
+            try {
+                exec_sync (argv);
+            } catch (GLib.Error err) {
+                error = err;
+            }
+
+            var source = new IdleSource ();
+            source.set_callback (() => {
+                continuation ();
+
+                return false;
+            });
+            source.attach (context);
+
+            return false;
+        });
+
+        yield;
+
+        if (error != null)
+            throw error;
+    }
+
+    private async void create_floppy_image (Cancellable? cancellable) throws GLib.Error {
+        var floppy_file = File.new_for_path (floppy_path);
+        var template_path = get_unattended_dir ("floppy.img");
+        var template_file = File.new_for_path (template_path);
+
+        debug ("Creating floppy image for unattended installation at '%s'..", floppy_path);
+        yield template_file.copy_async (floppy_file, 0, Priority.DEFAULT, cancellable);
+        debug ("Floppy image for unattended installation created at '%s'", floppy_path);
+
+        created_floppy = true;
+    }
+
+    private async void copy_unattended_file (Cancellable? cancellable) throws GLib.Error {
+        debug ("Putting unattended file: %s", unattended_dest_name);
+        // FIXME: Perhaps we should use libarchive for this?
+        string[] argv = { "mcopy", "-i", floppy_path,
+                                   unattended_src_path,
+                                   "::" + unattended_dest_name };
+        yield exec (argv, cancellable);
+        debug ("Put unattended file: %s", unattended_dest_name);
+    }
+
+    private async bool unattended_floppy_exists (Cancellable? cancellable) {
+        var file = File.new_for_path (floppy_path);
+
+        try {
+            yield file.read_async (Priority.DEFAULT, cancellable);
+        } catch (IOError.NOT_FOUND not_found_error) {
+            return false;
+        } catch (GLib.Error error) {}
+
+        return true;
+    }
+
+    private void exec_sync (string[] argv) throws GLib.Error {
+        int exit_status = -1;
+
+        Process.spawn_sync (null,
+                            argv,
+                            null,
+                            SpawnFlags.SEARCH_PATH,
+                            null,
+                            null,
+                            null,
+                            out exit_status);
+        if (exit_status != 0)
+            throw new UnattendedInstallerError.COMMAND_FAILED ("Failed to execute: %s", string.joinv (" ", argv));
+    }
+}
diff --git a/src/util.vala b/src/util.vala
index 7fa78de..40b0e8c 100644
--- a/src/util.vala
+++ b/src/util.vala
@@ -25,6 +25,22 @@ namespace Boxes {
         return Path.build_filename (get_pkgdata (), "pixmaps", file_name);
     }
 
+    private string get_unattended_dir (string? file_name = null) {
+        var dir = Path.build_filename (get_pkgdata (), "unattended");
+
+        ensure_directory (dir);
+
+        return Path.build_filename (dir, file_name);
+    }
+
+    private string get_user_unattended_dir (string? file_name = null) {
+        var dir = Path.build_filename (get_pkgconfig (), "unattended", file_name);
+
+        ensure_directory (dir);
+
+        return Path.build_filename (dir, file_name);
+    }
+
     private string get_pkgdata_source (string? file_name = null) {
         return Path.build_filename (get_pkgdata (), "sources", file_name);
     }
diff --git a/src/vm-creator.vala b/src/vm-creator.vala
new file mode 100644
index 0000000..439fbbb
--- /dev/null
+++ b/src/vm-creator.vala
@@ -0,0 +1,212 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+using Osinfo;
+using GVir;
+
+private class Boxes.VMCreator {
+    private Connection connection;
+
+    public VMCreator (string uri) throws GLib.Error {
+        connection = new Connection (uri);
+    }
+
+    public async GVir.Domain create_domain_for_installer (InstallerMedia install_media,
+                                                          Resources      resources,
+                                                          Cancellable?   cancellable) throws GLib.Error {
+        if (!connection.is_open ())
+            yield connect (cancellable);
+
+        if (install_media is UnattendedInstaller)
+            yield (install_media as UnattendedInstaller).setup (cancellable);
+
+        string name;
+        if (install_media.os != null)
+            name = install_media.os.short_id;
+        else
+            name = install_media.label.replace (" ", "-");
+
+        var target_path = yield create_target_volume (name, resources.storage);
+
+        var xml = get_virt_xml (install_media, name, target_path, resources);
+        var config = new GVirConfig.Domain (xml);
+
+        return connection.create_domain (config);
+    }
+
+    private async void connect (Cancellable? cancellable) throws GLib.Error {
+        yield connection.open_async (cancellable);
+        yield connection.fetch_domains_async (cancellable);
+        yield connection.fetch_storage_pools_async (cancellable);
+    }
+
+    private string get_virt_xml (InstallerMedia install_media, string name, string target_path, Resources resources) {
+        // FIXME: This information should come from libosinfo
+        var clock_offset = "utc";
+        if (install_media.os != null && install_media.os.short_id.contains ("win"))
+            clock_offset = "localtime";
+
+        var spice_port = 61000;
+
+        var domain_name = name;
+        for (var i = 1; connection.find_domain_by_name (domain_name) != null; i++)
+            domain_name = name + "-" + i.to_string ();
+
+        var ram = (resources.ram / KIBIBYTES).to_string ();
+        return "<domain type='kvm'>\n" +
+               "  <name>" +  domain_name + "</name>\n" +
+               "  <memory>" + ram + "</memory>\n" +
+               "  <vcpu>" + resources.n_cpus.to_string () + "</vcpu>\n" +
+               "  <os>\n" +
+               "    <type arch='x86_64'>hvm</type>\n" +
+               "    <boot dev='cdrom'/>\n" +
+               "    <boot dev='hd'/>\n" +
+               get_direct_boot_xml (install_media) +
+               "  </os>\n" +
+               "  <features>\n" +
+               "    <acpi/><apic/><pae/>\n" +
+               "  </features>\n" +
+               "  <clock offset='" + clock_offset + "'/>\n" +
+               "  <on_poweroff>destroy</on_poweroff>\n" +
+               "  <on_reboot>destroy</on_reboot>\n" +
+               "  <on_crash>destroy</on_crash>\n" +
+               "  <devices>\n" +
+               "    <disk type='file' device='disk'>\n" +
+               "      <driver name='qemu' type='qcow2'/>\n" +
+               "      <source file='" + target_path + "'/>\n" +
+               "      <target dev='hda' bus='ide'/>\n" +
+               "    </disk>\n" +
+               get_unattended_dir_floppy_xml (install_media) +
+               get_source_media_xml (install_media) +
+               "    <interface type='user'>\n" +
+               "      <mac address='00:11:22:33:44:55'/>\n" +
+               "    </interface>\n" +
+               "    <input type='tablet' bus='usb'/>\n" +
+               "    <graphics type='spice' port='" + spice_port.to_string () + "'/>\n" +
+               "    <console type='pty'/>\n" +
+               "    <video>\n" +
+               // FIXME: Should be 'qxl', work-around for a spice bug
+               "      <model type='vga'/>\n" +
+               "    </video>\n" +
+               "  </devices>\n" +
+               "</domain>";
+    }
+
+    private async string create_target_volume (string name, int64 storage) throws GLib.Error {
+        var pool = yield get_storage_pool ();
+
+        var volume_name = name + ".qcow2";
+        for (var i = 1; pool.get_volume (volume_name) != null; i++)
+            volume_name = name + "-" + i.to_string () + ".qcow2";
+
+        var storage_str = (storage / GIBIBYTES * 2).to_string ();
+        var xml = "<volume>\n" +
+                  "  <name>" + volume_name + "</name>\n" +
+                  "  <capacity unit='G'>" + storage_str + "</capacity>\n" +
+                  "  <target>\n" +
+                  "    <format type='qcow2'/>\n" +
+                  "    <permissions>\n" +
+                  "      <owner>" + get_uid () + "</owner>\n" +
+                  "      <group>" + get_gid () + "</group>\n" +
+                  "      <mode>0744</mode>\n" +
+                  "      <label>virt-image-" + name + "</label>\n" +
+                  "    </permissions>\n" +
+                  "  </target>\n" +
+                  "</volume>";
+        var config = new GVirConfig.StorageVol (xml);
+        var volume = pool.create_volume (config);
+
+        return volume.get_path ();
+    }
+
+    private string get_source_media_xml (InstallerMedia install_media) {
+        string type, source_attr;
+
+        if (install_media.from_image) {
+            type = "file";
+            source_attr = "file" ;
+        } else {
+            type = "block";
+            source_attr = "dev";
+        }
+
+        return "    <disk type='" + type + "'\n" +
+               "          device='cdrom'>\n" +
+               "      <driver name='qemu' type='raw'/>\n" +
+               "      <source " + source_attr + "='" +
+                                  install_media.device_file + "'/>\n" +
+               "      <target dev='hdc' bus='ide'/>\n" +
+               "      <readonly/>\n" +
+               "    </disk>\n";
+    }
+
+    private string get_unattended_dir_floppy_xml (InstallerMedia install_media) {
+        if (!(install_media is UnattendedInstaller))
+            return "";
+
+        var floppy_path = (install_media as UnattendedInstaller).floppy_path;
+        if (floppy_path == null)
+            return "";
+
+        return "    <disk type='file' device='floppy'>\n" +
+               "      <driver name='qemu' type='raw'/>\n" +
+               "      <source file='" + floppy_path + "'/>\n" +
+               "      <target dev='fd'/>\n" +
+               "    </disk>\n";
+    }
+
+    private string get_direct_boot_xml (InstallerMedia install_media) {
+        if (!(install_media is UnattendedInstaller))
+            return "";
+
+        var unattended = install_media as UnattendedInstaller;
+
+        var kernel_path = unattended.kernel_path;
+        var initrd_path = unattended.initrd_path;
+
+        if (kernel_path == null || initrd_path == null)
+            return "";
+
+        return "    <kernel>" + kernel_path + "</kernel>\n" +
+               "    <initrd>" + initrd_path + "</initrd>\n" +
+               "    <cmdline>ks=floppy</cmdline>\n";
+    }
+
+    private async StoragePool get_storage_pool () throws GLib.Error {
+        var pool = connection.find_storage_pool_by_name (Config.PACKAGE_TARNAME);
+        if (pool == null) {
+            var pool_path = get_pkgconfig ("images");
+            var xml = "<pool type='dir'>\n" +
+                      "<name>" + Config.PACKAGE_TARNAME + "</name>\n" +
+                      "  <source>\n" +
+                      "    <directory path='" + pool_path + "'/>\n" +
+                      "  </source>\n" +
+                      "  <target>\n" +
+                      "    <path>" + pool_path + "</path>\n" +
+                      "    <permissions>\n" +
+                      "      <owner>" + get_uid () + "</owner>\n" +
+                      "      <group>" + get_gid () + "</group>\n" +
+                      "      <mode>0744</mode>\n" +
+                      "      <label>" + Config.PACKAGE_TARNAME + "</label>\n" +
+                      "    </permissions>\n" +
+                      "  </target>\n" +
+                      "</pool>";
+            var config = new GVirConfig.StoragePool (xml);
+            pool = connection.create_storage_pool (config, 0);
+            yield pool.build_async (0, null);
+            yield pool.start_async (0, null);
+        }
+
+        // This should be async
+        pool.refresh (null);
+
+        return pool;
+    }
+
+    private string get_uid () {
+        return ((uint32) Posix.getuid ()).to_string ();
+    }
+
+    private string get_gid () {
+        return ((uint32) Posix.getgid ()).to_string ();
+    }
+}
diff --git a/src/win7-installer.vala b/src/win7-installer.vala
new file mode 100644
index 0000000..9db0c0b
--- /dev/null
+++ b/src/win7-installer.vala
@@ -0,0 +1,9 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+// Automated installer media for Windows 7 and 2008
+private class Boxes.Win7Installer: UnattendedInstaller {
+    public Win7Installer.copy (InstallerMedia media) throws GLib.Error {
+        var unattended_source = get_unattended_dir (media.os.short_id + ".xml");
+        base.copy (media, unattended_source, "Autounattend.xml");
+    }
+}
diff --git a/src/winxp-installer.vala b/src/winxp-installer.vala
new file mode 100644
index 0000000..37220e2
--- /dev/null
+++ b/src/winxp-installer.vala
@@ -0,0 +1,9 @@
+// This file is part of GNOME Boxes. License: LGPLv2+
+
+// Automated installer media for Windows XP, 2000 and 2003
+private class Boxes.WinXPInstaller: UnattendedInstaller {
+    public WinXPInstaller.copy (InstallerMedia media) throws GLib.Error {
+        var unattended_source = get_unattended_dir (media.os.short_id + ".sif");
+        base.copy (media, unattended_source, "Winnt.sif");
+    }
+}



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