[Libguestfs] [PATCH 1/4] sysprep: Add --firstboot functionality.

Richard W.M. Jones rjones at redhat.com
Thu Aug 16 17:34:23 UTC 2012


From: "Richard W.M. Jones" <rjones at redhat.com>

This allows you to add scripts that run in the context of
the guest the first time it boots.
---
 TODO                                   |    2 -
 po/POTFILES-ml                         |    2 +
 sysprep/Makefile.am                    |   40 +++++++++++--
 sysprep/firstboot.ml                   |  102 ++++++++++++++++++++++++++++++++
 sysprep/firstboot.mli                  |   27 +++++++++
 sysprep/sysprep_operation_firstboot.ml |   86 +++++++++++++++++++++++++++
 sysprep/sysprep_operation_script.ml    |    6 +-
 sysprep/utils.ml                       |   12 ++++
 sysprep/utils.mli                      |    3 +
 sysprep/virt-sysprep.pod               |   26 ++++++++
 10 files changed, 297 insertions(+), 9 deletions(-)
 create mode 100644 sysprep/firstboot.ml
 create mode 100644 sysprep/firstboot.mli
 create mode 100644 sysprep/sysprep_operation_firstboot.ml

diff --git a/TODO b/TODO
index 3832371..1232e4d 100644
--- a/TODO
+++ b/TODO
@@ -383,7 +383,6 @@ virt-sysprep ideas
  - Windows sysprep
    (see: https://github.com/clalancette/oz/blob/e74ce83283d468fd987583d6837b441608e5f8f0/oz/Windows.py )
  - (librarian suggests ...)
-   . install a firstboot script      virt-sysprep --script=/tmp/foo.sh
    . run external guestfish script   virt-sysprep --fish=/tmp/foo.fish
  - if drives are encrypted, then dm-crypt key should be changed
    and drives all re-encrypted
@@ -421,7 +420,6 @@ customized with the organization logo etc.  Some ideas:
 
  - change the background image to some custom desktop
  - change the sign-on messages (/etc/issue.net etc)
- - firstboot script (as suggested by librarian above)
  - Windows login script/service
 
 Launch remote sessions over ssh
diff --git a/po/POTFILES-ml b/po/POTFILES-ml
index 7f75dc8..76043a0 100644
--- a/po/POTFILES-ml
+++ b/po/POTFILES-ml
@@ -7,6 +7,7 @@ sparsify/progress.ml
 sparsify/sparsify.ml
 sparsify/sparsify_gettext.ml
 sparsify/utils.ml
+sysprep/firstboot.ml
 sysprep/main.ml
 sysprep/sysprep_gettext.ml
 sysprep/sysprep_operation.ml
@@ -18,6 +19,7 @@ sysprep/sysprep_operation_cron_spool.ml
 sysprep/sysprep_operation_dhcp_client_state.ml
 sysprep/sysprep_operation_dhcp_server_state.ml
 sysprep/sysprep_operation_dovecot_data.ml
+sysprep/sysprep_operation_firstboot.ml
 sysprep/sysprep_operation_flag_reconfiguration.ml
 sysprep/sysprep_operation_hostname.ml
 sysprep/sysprep_operation_kerberos_data.ml
diff --git a/sysprep/Makefile.am b/sysprep/Makefile.am
index 50c6e11..5551c2d 100644
--- a/sysprep/Makefile.am
+++ b/sysprep/Makefile.am
@@ -39,16 +39,43 @@ CLEANFILES = \
 
 # Filenames sysprep_operation_<name>.ml in alphabetical order.
 operations = \
-	abrt_data bash_history blkid_tab ca_certificates cron_spool \
-	dhcp_client_state dhcp_server_state dovecot_data flag_reconfiguration \
-	hostname kerberos_data lvm_uuids logfiles machine_id mail_spool \
-	net_hwaddr pacct_log package_manager_cache pam_data puppet_data_log \
-	random_seed rhn_systemid samba_db_log script smolt_uuid ssh_hostkeys \
-	ssh_userdir sssd_db_log udev_persistent_net user_account \
+	abrt_data \
+	bash_history \
+	blkid_tab \
+	ca_certificates \
+	cron_spool \
+	dhcp_client_state \
+	dhcp_server_state \
+	dovecot_data \
+	flag_reconfiguration \
+	firstboot \
+	hostname \
+	kerberos_data \
+	lvm_uuids \
+	logfiles \
+	machine_id \
+	mail_spool \
+	net_hwaddr \
+	pacct_log \
+	package_manager_cache \
+	pam_data \
+	puppet_data_log \
+	random_seed \
+	rhn_systemid \
+	samba_db_log \
+	script \
+	smolt_uuid \
+	ssh_hostkeys \
+	ssh_userdir \
+	sssd_db_log \
+	udev_persistent_net \
+	user_account \
 	utmp yum_uuid
 
 # Alphabetical order.
 SOURCES = \
+	firstboot.ml \
+	firstboot.mli \
 	main.ml \
 	sysprep_gettext.ml \
 	sysprep_operation.ml \
@@ -63,6 +90,7 @@ if HAVE_OCAML
 OBJECTS = \
 	sysprep_gettext.cmx \
 	utils.cmx \
+	firstboot.cmx \
 	sysprep_operation.cmx \
 	$(patsubst %,sysprep_operation_%.cmx,$(operations)) \
 	main.cmx
diff --git a/sysprep/firstboot.ml b/sysprep/firstboot.ml
new file mode 100644
index 0000000..c551bd5
--- /dev/null
+++ b/sysprep/firstboot.ml
@@ -0,0 +1,102 @@
+(* virt-sysprep
+ * Copyright (C) 2012 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+open Printf
+
+open Utils
+open Sysprep_operation
+open Sysprep_gettext.Gettext
+
+(* For Linux guests. *)
+let firstboot_dir = "/usr/lib/virt-sysprep"
+
+let firstboot_sh = sprintf "\
+#!/bin/sh -
+
+d=%s/scripts
+logfile=~root/virt-sysprep-firstboot.log
+
+for f in $d/* ; do
+  echo '=== Running' $f '===' >>$logfile
+  $f >>$logfile 2>&1
+  rm $f
+done
+" firstboot_dir
+
+let firstboot_service = sprintf "\
+[Unit]
+Description=virt-sysprep firstboot service
+After=syslog.target network.target
+Before=prefdm.service
+
+[Service]
+Type=oneshot
+ExecStart=%s/firstboot.sh
+RemainAfterExit=yes
+
+[Install]
+WantedBy=default.target
+" firstboot_dir
+
+let failed fs =
+  ksprintf (fun msg -> failwith (s_"firstboot: failed: " ^ msg)) fs
+
+let rec install_service g root =
+  g#mkdir_p firstboot_dir;
+  g#mkdir_p (sprintf "%s/scripts" firstboot_dir);
+  g#write (sprintf "%s/firstboot.sh" firstboot_dir) firstboot_sh;
+  g#chmod 0o755 (sprintf "%s/firstboot.sh" firstboot_dir);
+
+  (* systemd, else assume sysvinit *)
+  if g#is_dir "/etc/systemd" then
+    install_systemd_service g root
+  else
+    install_sysvinit_service g root
+
+(* Install the systemd firstboot service, if not installed already. *)
+and install_systemd_service g root =
+  g#write (sprintf "%s/firstboot.service" firstboot_dir) firstboot_service;
+  g#mkdir_p "/etc/systemd/system/default.target.wants";
+  g#ln_sf (sprintf "%s/firstboot.service" firstboot_dir)
+    "/etc/systemd/system/default.target.wants"
+
+and install_sysvinit_service g root =
+  g#mkdir_p "/etc/rc.d/rc2.d";
+  g#mkdir_p "/etc/rc.d/rc3.d";
+  g#mkdir_p "/etc/rc.d/rc5.d";
+  g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
+    "/etc/rc.d/rc2.d/99virt-sysprep-firstboot";
+  g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
+    "/etc/rc.d/rc3.d/99virt-sysprep-firstboot";
+  g#ln_sf (sprintf "%s/firstboot.sh" firstboot_dir)
+    "/etc/rc.d/rc5.d/99virt-sysprep-firstboot"
+
+let add_firstboot_script g root id content =
+  let typ = g#inspect_get_type root in
+  let distro = g#inspect_get_distro root in
+  match typ, distro with
+  | "linux", _ ->
+    install_service g root;
+    let filename =
+      sprintf "%s/scripts/%g-%s-%s"
+        firstboot_dir (Unix.time ()) (string_random8 ()) id in
+    g#write filename content;
+    g#chmod 0o755 filename
+
+  | _ ->
+    failed "guest type %s/%s is not supported" typ distro
diff --git a/sysprep/firstboot.mli b/sysprep/firstboot.mli
new file mode 100644
index 0000000..910dd75
--- /dev/null
+++ b/sysprep/firstboot.mli
@@ -0,0 +1,27 @@
+(* virt-sysprep
+ * Copyright (C) 2012 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+val add_firstboot_script : Guestfs.guestfs -> string -> string -> string -> unit
+  (** [add_firstboot_script g root id content] adds a firstboot
+      script called [shortname] containing [content].
+
+      NB. [content] is the contents of the script, {b not} a filename.
+
+      [id] should be a short name containing only 7 bit ASCII [-a-z0-9].
+
+      You should make sure the filesystem is relabelled after calling this. *)
diff --git a/sysprep/sysprep_operation_firstboot.ml b/sysprep/sysprep_operation_firstboot.ml
new file mode 100644
index 0000000..d0f3293
--- /dev/null
+++ b/sysprep/sysprep_operation_firstboot.ml
@@ -0,0 +1,86 @@
+(* virt-sysprep
+ * Copyright (C) 2012 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+open Printf
+
+open Utils
+open Sysprep_operation
+open Sysprep_gettext.Gettext
+
+module G = Guestfs
+
+let files = ref []
+
+let make_id_from_filename filename =
+  let ret = String.copy filename in
+  for i = 0 to String.length ret - 1 do
+    let c = String.unsafe_get ret i in
+    if not ((c >= 'a' && c <= 'z') ||
+               (c >= 'A' && c <= 'Z') ||
+               (c >= '0' && c <= '9')) then
+      String.unsafe_set ret i '-'
+  done;
+  ret
+
+let firstboot_perform g root =
+  (* Read the files and add them using the {!Firstboot} module. *)
+  List.iter (
+    fun filename ->
+      let content = read_whole_file filename in
+      let basename = Filename.basename filename in
+      let id = make_id_from_filename basename in
+      Firstboot.add_firstboot_script g root id content
+  ) !files;
+  [ `Created_files ]
+
+let firstboot_op = {
+  name = "firstboot";
+
+  (* enabled_by_default because we only do anything if the
+   * --firstboot parameter is used.
+   *)
+  enabled_by_default = true;
+
+  heading = s_"Add scripts to run once at next boot";
+  pod_description = Some (s_"\
+Supply one of more shell scripts (using the I<--firstboot> option).
+
+These are run the first time the guest boots, and then are
+deleted.  So these are useful for performing last minute
+configuration that must run in the context of the guest
+operating system, for example C<yum update>.
+
+Output or errors from the scripts are written to
+C<~root/virt-sysprep-firstboot.log> (in the guest).
+
+Currently this is only implemented for Linux guests using
+either System V init, or systemd.");
+
+  extra_args = [
+    ("--firstboot", Arg.String (fun s -> files := s :: !files),
+     s_"script" ^ " " ^ s_"run script once next time guest boots"),
+    s_"\
+Run script(s) once next time the guest boots.  You can supply
+the I<--firstboot> option as many times as needed."
+  ];
+
+  perform_on_filesystems = Some firstboot_perform;
+  perform_on_devices = None;
+}
+
+let () = register_operation firstboot_op
diff --git a/sysprep/sysprep_operation_script.ml b/sysprep/sysprep_operation_script.ml
index 9337701..a49bc3c 100644
--- a/sysprep/sysprep_operation_script.ml
+++ b/sysprep/sysprep_operation_script.ml
@@ -134,7 +134,11 @@ guest's DNS configuration file, but C<rm /etc/resolv.conf> would
 (try to) remove the host's file.
 
 Normally a temporary mount point for the guest is used, but you
-can choose a specific one by using the I<--scriptdir> parameter.");
+can choose a specific one by using the I<--scriptdir> parameter.
+
+B<Note:> This is different from I<--firstboot> scripts (which run
+in the context of the guest when it is booting first time).
+I<--script> scripts run on the host, not in the guest.");
   extra_args = [
     ("--scriptdir", Arg.String set_scriptdir, s_"dir" ^ " " ^ s_"Mount point on host"),
     s_"\
diff --git a/sysprep/utils.ml b/sysprep/utils.ml
index 3b3ad8a..6f76713 100644
--- a/sysprep/utils.ml
+++ b/sysprep/utils.ml
@@ -86,3 +86,15 @@ let skip_dashes str =
 
 let compare_command_line_args a b =
   compare (String.lowercase (skip_dashes a)) (String.lowercase (skip_dashes b))
+
+let read_whole_file path =
+  let buf = Buffer.create 1024 in
+  let chan = open_in path in
+  let rec loop () =
+    let line = input_line chan in
+    Buffer.add_string buf line;
+    loop ()
+  in
+  (try loop () with End_of_file -> ());
+  close_in chan;
+  Buffer.contents buf
diff --git a/sysprep/utils.mli b/sysprep/utils.mli
index 0ecb8da..351b936 100644
--- a/sysprep/utils.mli
+++ b/sysprep/utils.mli
@@ -52,3 +52,6 @@ val compare_command_line_args : string -> string -> int
 (** Compare two command line arguments (eg. ["-a"] and ["--V"]),
     ignoring leading dashes and case.  Note this assumes the
     strings are 7 bit ASCII. *)
+
+val read_whole_file : string -> string
+(** Read whole file into memory. *)
diff --git a/sysprep/virt-sysprep.pod b/sysprep/virt-sysprep.pod
index 66bc710..71900ca 100755
--- a/sysprep/virt-sysprep.pod
+++ b/sysprep/virt-sysprep.pod
@@ -383,6 +383,32 @@ to pay for disk space), then instead of copying the template, you can
 run L<virt-resize(1)>.  Virt-resize performs a copy and resize, and
 thus is ideal for cloning guests from a template.
 
+=head1 FIRSTBOOT VS SCRIPT
+
+The two options I<--firstboot> and I<--script> both supply shell
+scripts that are run against the guest.  However these two options are
+significantly different.
+
+I<--firstboot script> uploads the file C<script> into the guest
+and arranges that it will run, in the guest, when the guest is
+next booted.  (The script will only run once, at the "first boot").
+
+I<--script script> runs the shell C<script> I<on the host>, with its
+current directory inside the guest filesystem.
+
+If you needed, for example, to C<yum install> new packages, then you
+I<must not> use I<--script> for this, since that would (a) run the
+C<yum> command on the host and (b) wouldn't have access to the same
+resources (repositories, keys, etc.) as the guest.  Any command that
+needs to run on the guest I<must> be run via I<--firstboot>.
+
+On the other hand if you need to make adjustments to the guest
+filesystem (eg. copying in files), then I<--script> is ideal since (a)
+it has access to the host filesystem and (b) you will get immediate
+feedback on errors.
+
+Either or both options can be used multiple times on the command line.
+
 =head1 SECURITY
 
 Although virt-sysprep removes some sensitive information from the
-- 
1.7.10.4




More information about the Libguestfs mailing list