Files
libguestfs/customize/customize_run.ml
Richard W.M. Jones 669e25049e sysprep: Add --network to enable the network (RHBZ#1345813).
In commit ae6f726ecc we started to use
the virt-customize code to replace various virt-sysprep operations.
This had the effect of adding many more possible operations to
virt-sysprep, but some of them (specifically --install) did not work
unless the appliance network is enabled.  It was not enabled in
virt-sysprep, so these operations never worked.

This change does NOT enable the network by default.  However it adds a
--network flag which may be used in conjunction with --install etc to
make those commands work.

In addition we now emit a warning for certain customize operations if
they fail and if the network is not enabled.  It will print:

  [   4.5] Installing packages: tcpdump
  Error: Cannot retrieve repository metadata (repomd.xml) for repository: base. Please verify its path and try again
  Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=os&infra=stock error was

  virt-sysprep: warning: the command may have failed because the network is
  disabled.  Try either removing '--no-network' or adding '--network' on the
  command line.

  virt-sysprep: error: yum -y install 'tcpdump': command exited with an error

(If the network is already enabled, or if the command is successful,
then the warning is not printed.)

Thanks: Xianghua Chen
2016-06-13 15:39:34 +01:00

458 lines
16 KiB
OCaml

(* virt-customize
* Copyright (C) 2014 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 Unix
open Printf
open Common_gettext.Gettext
open Common_utils
open Customize_utils
open Customize_cmdline
open Password
let run (g : Guestfs.guestfs) root (ops : ops) =
(* Is the host_cpu compatible with the guest arch? ie. Can we
* run commands in this guest?
*)
let guest_arch = g#inspect_get_arch root in
let guest_arch_compatible = guest_arch_compatible guest_arch in
(* Based on the guest type, choose a log file location. *)
let logfile =
match g#inspect_get_type root with
| "windows" | "dos" ->
if g#is_dir ~followsymlinks:true "/Temp" then "/Temp/builder.log"
else "/builder.log"
| _ ->
if g#is_dir ~followsymlinks:true "/tmp" then "/tmp/builder.log"
else "/builder.log" in
(* Function to cat the log file, for debugging and error messages. *)
let debug_logfile () =
try g#download logfile "/dev/stderr"
with exn ->
warning (f_"log file %s: %s (ignored)") logfile (Printexc.to_string exn) in
(* Useful wrapper for scripts. *)
let do_run ~display ?(warn_failed_no_network = false) cmd =
if not guest_arch_compatible then
error (f_"host cpu (%s) and guest arch (%s) are not compatible, so you cannot use command line options that involve running commands in the guest. Use --firstboot scripts instead.")
Guestfs_config.host_cpu guest_arch;
(* Add a prologue to the scripts:
* - Pass environment variables through from the host.
* - Send stdout and stderr to a log file so we capture all output
* in error messages.
* - Use setarch when running x86_64 host + i686 guest.
* Also catch errors and dump the log file completely on error.
*)
let env_vars =
filter_map (
fun name ->
try Some (sprintf "export %s=%s" name (quote (Sys.getenv name)))
with Not_found -> None
) [ "http_proxy"; "https_proxy"; "ftp_proxy"; "no_proxy" ] in
let env_vars = String.concat "\n" env_vars ^ "\n" in
let cmd =
match Guestfs_config.host_cpu, guest_arch with
| "x86_64", ("i386"|"i486"|"i586"|"i686") ->
sprintf "setarch i686 <<\"__EOCMD\"
%s
__EOCMD
" cmd
| _ -> cmd in
let cmd = sprintf "\
exec >>%s 2>&1
%s
%s
" (quote logfile) env_vars cmd in
debug "running command:\n%s" cmd;
try ignore (g#sh cmd)
with
Guestfs.Error msg ->
debug_logfile ();
if warn_failed_no_network && not (g#get_network ()) then (
prerr_newline ();
warning (f_"the command may have failed because the network is disabled. Try either removing '--no-network' or adding '--network' on the command line.");
prerr_newline ()
);
error (f_"%s: command exited with an error") display
in
(* http://distrowatch.com/dwres.php?resource=package-management *)
let rec guest_install_command packages =
let quoted_args = String.concat " " (List.map quote packages) in
match g#inspect_get_package_management root with
| "apk" ->
sprintf "
apk update
apk add %s
" quoted_args
| "apt" ->
(* http://unix.stackexchange.com/questions/22820 *)
sprintf "
export DEBIAN_FRONTEND=noninteractive
apt_opts='-q -y -o Dpkg::Options::=--force-confnew'
apt-get $apt_opts update
apt-get $apt_opts install %s
" quoted_args
| "dnf" -> sprintf "dnf -y install %s" quoted_args
| "pisi" -> sprintf "pisi it %s" quoted_args
| "pacman" -> sprintf "pacman -S %s" quoted_args
| "urpmi" -> sprintf "urpmi %s" quoted_args
| "xbps" -> sprintf "xbps-install -Sy %s" quoted_args
| "yum" -> sprintf "yum -y install %s" quoted_args
| "zypper" -> sprintf "zypper -n in -l %s" quoted_args
| "unknown" ->
error_unknown_package_manager (s_"--install")
| pm ->
error_unimplemented_package_manager (s_"--install") pm
and guest_update_command () =
match g#inspect_get_package_management root with
| "apk" ->
"
apk update
apk upgrade
"
| "apt" ->
(* http://unix.stackexchange.com/questions/22820 *)
"
export DEBIAN_FRONTEND=noninteractive
apt_opts='-q -y -o Dpkg::Options::=--force-confnew'
apt-get $apt_opts update
apt-get $apt_opts upgrade
"
| "dnf" -> "dnf -y --best upgrade"
| "pisi" -> "pisi upgrade"
| "pacman" -> "pacman -Su"
| "urpmi" -> "urpmi --auto-select"
| "xbps" -> "xbps-install -Suy"
| "yum" -> "yum -y update"
| "zypper" -> "zypper -n update -l"
| "unknown" ->
error_unknown_package_manager (s_"--update")
| pm ->
error_unimplemented_package_manager (s_"--update") pm
and guest_uninstall_command packages =
let quoted_args = String.concat " " (List.map quote packages) in
match g#inspect_get_package_management root with
| "apk" -> sprintf "apk del %s" quoted_args
| "apt" ->
(* http://unix.stackexchange.com/questions/22820 *)
sprintf "
export DEBIAN_FRONTEND=noninteractive
apt_opts='-q -y -o Dpkg::Options::=--force-confnew'
apt-get $apt_opts remove %s
" quoted_args
| "dnf" -> sprintf "dnf -y remove %s" quoted_args
| "pisi" -> sprintf "pisi rm %s" quoted_args
| "pacman" -> sprintf "pacman -R %s" quoted_args
| "urpmi" -> sprintf "urpme %s" quoted_args
| "xbps" -> sprintf "xbps-remove -Sy %s" quoted_args
| "yum" -> sprintf "yum -y remove %s" quoted_args
| "zypper" -> sprintf "zypper -n rm -l %s" quoted_args
| "unknown" ->
error_unknown_package_manager (s_"--uninstall")
| pm ->
error_unimplemented_package_manager (s_"--uninstall") pm
(* Windows has package_management == "unknown". *)
and error_unknown_package_manager flag =
error (f_"cannot use '%s' because no package manager has been detected for this guest OS.\n\nIf this guest OS is a common one with ordinary package management then this may have been caused by a failure of libguestfs inspection.\n\nFor OSes such as Windows that lack package management, this is not possible. Try using one of the '--firstboot*' flags instead (described in the manual).") flag
and error_unimplemented_package_manager flag pm =
error (f_"sorry, '%s' with the '%s' package manager has not been implemented yet.\n\nYou can work around this by using one of the '--run*' or '--firstboot*' options instead (described in the manual).") flag pm
in
(* Set the random seed. *)
message (f_"Setting a random seed");
if not (Random_seed.set_random_seed g root) then
warning (f_"random seed could not be set for this type of guest");
(* Store the passwords and set them all at the end. *)
let passwords = Hashtbl.create 13 in
let set_password user pw =
if Hashtbl.mem passwords user then
error (f_"multiple --root-password/--password options set the password for user '%s' twice") user;
Hashtbl.replace passwords user pw
in
(* Perform the remaining customizations in command-line order. *)
List.iter (
function
| `Chmod (mode, path) ->
message (f_"Changing permissions of %s to %s") path mode;
(* If the mode string is octal, add the OCaml prefix for octal values
* so it is properly converted as octal integer.
*)
let mode = if String.is_prefix mode "0" then "0o" ^ mode else mode in
g#chmod (int_of_string mode) path
| `Command cmd ->
message (f_"Running: %s") cmd;
do_run ~display:cmd cmd
| `CommandsFromFile _ ->
(* Nothing to do, the files with customize commands are already
* read when their arguments are met. *)
()
| `Copy (src, dest) ->
message (f_"Copying (in image): %s to %s") src dest;
g#cp_a src dest
| `CopyIn (localpath, remotedir) ->
message (f_"Copying: %s to %s") localpath remotedir;
g#copy_in localpath remotedir
| `Delete path ->
message (f_"Deleting: %s") path;
Array.iter g#rm_rf (g#glob_expand ~directoryslash:false path)
| `Edit (path, expr) ->
message (f_"Editing: %s") path;
if not (g#exists path) then
error (f_"%s does not exist in the guest") path;
if not (g#is_file ~followsymlinks:true path) then
error (f_"%s is not a regular file in the guest") path;
Perl_edit.edit_file g#ocaml_handle path expr
| `FirstbootCommand cmd ->
message (f_"Installing firstboot command: %s") cmd;
Firstboot.add_firstboot_script g root cmd cmd
| `FirstbootPackages pkgs ->
message (f_"Installing firstboot packages: %s")
(String.concat " " pkgs);
let cmd = guest_install_command pkgs in
let name = String.concat " " ("install" :: pkgs) in
Firstboot.add_firstboot_script g root name cmd
| `FirstbootScript script ->
message (f_"Installing firstboot script: %s") script;
let cmd = read_whole_file script in
Firstboot.add_firstboot_script g root script cmd
| `Hostname hostname ->
message (f_"Setting the hostname: %s") hostname;
if not (Hostname.set_hostname g root hostname) then
warning (f_"hostname could not be set for this type of guest")
| `InstallPackages pkgs ->
message (f_"Installing packages: %s") (String.concat " " pkgs);
let cmd = guest_install_command pkgs in
do_run ~display:cmd ~warn_failed_no_network:true cmd
| `Link (target, links) ->
List.iter (
fun link ->
message (f_"Linking: %s -> %s") link target;
g#ln_sf target link
) links
| `Mkdir dir ->
message (f_"Making directory: %s") dir;
g#mkdir_p dir
| `Move (src, dest) ->
message (f_"Moving: %s -> %s") src dest;
g#mv src dest
| `Password (user, pw) ->
set_password user pw
| `RootPassword pw ->
set_password "root" pw
| `Script script ->
message (f_"Running: %s") script;
let cmd = read_whole_file script in
do_run ~display:script cmd
| `Scrub path ->
message (f_"Scrubbing: %s") path;
g#scrub_file path
| `SMAttach pool ->
(match pool with
| Subscription_manager.PoolAuto ->
message (f_"Attaching to compatible subscriptions");
let cmd = "subscription-manager attach --auto" in
do_run ~display:cmd ~warn_failed_no_network:true cmd
| Subscription_manager.PoolId id ->
message (f_"Attaching to the pool %s") id;
let cmd = sprintf "subscription-manager attach --pool=%s" (quote id) in
do_run ~display:cmd ~warn_failed_no_network:true cmd
)
| `SMRegister ->
message (f_"Registering with subscription-manager");
let creds =
match ops.flags.sm_credentials with
| None ->
error (f_"subscription-manager credentials required for --sm-register")
| Some c -> c in
let cmd = sprintf "subscription-manager register --username=%s --password=%s"
(quote creds.Subscription_manager.sm_username)
(quote creds.Subscription_manager.sm_password) in
do_run ~display:"subscription-manager register"
~warn_failed_no_network:true cmd
| `SMRemove ->
message (f_"Removing all the subscriptions");
let cmd = "subscription-manager remove --all" in
do_run ~display:cmd ~warn_failed_no_network:true cmd
| `SMUnregister ->
message (f_"Unregistering with subscription-manager");
let cmd = "subscription-manager unregister" in
do_run ~display:cmd ~warn_failed_no_network:true cmd
| `SSHInject (user, selector) ->
(match g#inspect_get_type root with
| "linux" | "freebsd" | "netbsd" | "openbsd" | "hurd" ->
message (f_"SSH key inject: %s") user;
Ssh_key.do_ssh_inject_unix g user selector
| _ ->
warning (f_"SSH key could be injected for this type of guest"))
| `Truncate path ->
message (f_"Truncating: %s") path;
g#truncate path
| `TruncateRecursive path ->
message (f_"Recursively truncating: %s") path;
truncate_recursive g path
| `Timezone tz ->
message (f_"Setting the timezone: %s") tz;
if not (Timezone.set_timezone g root tz) then
warning (f_"timezone could not be set for this type of guest")
| `Touch path ->
message (f_"Running touch: %s") path;
g#touch path
| `UninstallPackages pkgs ->
message (f_"Uninstalling packages: %s") (String.concat " " pkgs);
let cmd = guest_uninstall_command pkgs in
do_run ~display:cmd cmd
| `Update ->
message (f_"Updating packages");
let cmd = guest_update_command () in
do_run ~display:cmd ~warn_failed_no_network:true cmd
| `Upload (path, dest) ->
message (f_"Uploading: %s to %s") path dest;
let dest =
if g#is_dir ~followsymlinks:true dest then
dest ^ "/" ^ Filename.basename path
else
dest in
(* Do the file upload. *)
g#upload path dest;
(* Copy (some of) the permissions from the local file to the
* uploaded file.
*)
let statbuf = stat path in
let perms = statbuf.st_perm land 0o7777 (* sticky & set*id *) in
g#chmod perms dest;
let uid, gid = statbuf.st_uid, statbuf.st_gid in
let chown () =
try g#chown uid gid dest
with Guestfs.Error m as e ->
if g#last_errno () = Guestfs.Errno.errno_EPERM
then warning "%s" m
else raise e in
chown ()
| `Write (path, content) ->
message (f_"Writing: %s") path;
g#write path content
) ops.ops;
(* Set all the passwords at the end. *)
if Hashtbl.length passwords > 0 then (
match g#inspect_get_type root with
| "linux" ->
message (f_"Setting passwords");
let password_crypto = ops.flags.password_crypto in
set_linux_passwords ?password_crypto g root passwords
| _ ->
warning (f_"passwords could not be set for this type of guest")
);
if ops.flags.selinux_relabel then (
message (f_"SELinux relabelling");
if guest_arch_compatible then (
let cmd = sprintf "
if load_policy && fixfiles restore; then
rm -f /.autorelabel
else
touch /.autorelabel
echo '%s: SELinux relabelling failed, will relabel at boot instead.'
fi
" prog in
do_run ~display:"load_policy && fixfiles restore" cmd
) else (
g#touch "/.autorelabel"
)
);
(* Clean up the log file:
*
* If debugging, dump out the log file.
* Then if asked, scrub the log file.
*)
if verbose () then debug_logfile ();
if ops.flags.scrub_logfile && g#exists logfile then (
message (f_"Scrubbing the log file");
(* Try various methods with decreasing complexity. *)
try g#scrub_file logfile
with _ -> g#rm_f logfile
);
(* Kill any daemons (eg. started by newly installed packages) using
* the sysroot.
* XXX How to make this nicer?
* XXX fuser returns an error if it doesn't kill any processes, which
* is not very useful.
*)
(try ignore (g#debug "sh" [| "fuser"; "-k"; "/sysroot" |])
with exn ->
if verbose () then
warning (f_"%s (ignored)") (Printexc.to_string exn)
);
g#ping_daemon () (* tiny delay after kill *)