This adds a customize option:
virt-customize --ssh-inject USER
virt-customize --ssh-inject USER:string:KEY_STRING
virt-customize --ssh-inject USER:file:FILENAME
(ditto for virt-builder and virt-sysprep)
In each case this injects into the guest user USER
a) the current (host) user's ssh pubkey
b) the key specified as KEY_STRING
c) the key in FILENAME
adding it to ~USER/.ssh/authorized_keys in the guest.
For example:
virt-builder fedora-20 --ssh-inject root
will add the local user's ssh pubkey into the root account of the
newly created guest. Or:
virt-customize -a disk.img \
--ssh-inject 'mary:string:ssh-rsa AAAA.... mary@localhost'
adds the given ssh pubkey to mary's account in the guest.
This doesn't set the SELinux labels correctly on newly created files
and directories, so you have to use --selinux-relabel (probably we
should fix this as part of the general effort to fix SELinux
relabelling). However it should preserve the labels if the
~/.ssh/authorized_keys file already exists.
Most of this work is based on a patch sent to the mailing list by
Richard W.M. Jones <rjones(a)redhat.com>:
https://www.redhat.com/archives/libguestfs/2014-November/msg00000.html
---
builder/Makefile.am | 1 +
builder/cmdline.ml | 4 +-
builder/virt-builder.pod | 31 +++++++++++
customize/Makefile.am | 3 +
customize/customize_run.ml | 8 +++
customize/ssh_key.ml | 133 +++++++++++++++++++++++++++++++++++++++++++++
customize/ssh_key.mli | 31 +++++++++++
generator/customize.ml | 38 ++++++++++++-
po/POTFILES-ml | 1 +
sysprep/Makefile.am | 1 +
v2v/Makefile.am | 1 +
11 files changed, 248 insertions(+), 4 deletions(-)
create mode 100644 customize/ssh_key.ml
create mode 100644 customize/ssh_key.mli
diff --git a/builder/Makefile.am b/builder/Makefile.am
index 414279f..206abce 100644
--- a/builder/Makefile.am
+++ b/builder/Makefile.am
@@ -111,6 +111,7 @@ deps = \
$(top_builddir)/customize/crypt-c.o \
$(top_builddir)/customize/crypt.cmx \
$(top_builddir)/customize/password.cmx \
+ $(top_builddir)/customize/ssh_key.cmx \
$(top_builddir)/customize/customize_cmdline.cmx \
$(top_builddir)/customize/customize_run.cmx \
$(top_builddir)/fish/guestfish-uri.o \
diff --git a/builder/cmdline.ml b/builder/cmdline.ml
index c0584f7..e21d5bb 100644
--- a/builder/cmdline.ml
+++ b/builder/cmdline.ml
@@ -306,8 +306,8 @@ read the man page virt-builder(1).
| `Command _ | `InstallPackages _ | `Script _ | `Update -> true
| `Delete _ | `Edit _ | `FirstbootCommand _ | `FirstbootPackages _
| `FirstbootScript _ | `Hostname _ | `Link _ | `Mkdir _
- | `Password _ | `RootPassword _ | `Scrub _ | `Timezone _ | `Upload _
- | `Write _ | `Chmod _ -> false
+ | `Password _ | `RootPassword _ | `Scrub _ | `SSHInject _
+ | `Timezone _ | `Upload _ | `Write _ | `Chmod _ -> false
) ops.ops in
if requires_execute_on_guest then
error (f_"sorry, cannot run commands on a guest with a different
architecture");
diff --git a/builder/virt-builder.pod b/builder/virt-builder.pod
index 993e92c..82533d6 100644
--- a/builder/virt-builder.pod
+++ b/builder/virt-builder.pod
@@ -774,6 +774,37 @@ If C</tmp> or C<C:\Temp> is missing.
If you don't want the log file to appear in the final image, then
use the I<--no-logfile> command line option.
+=head2 SSH KEYS
+
+The I<--ssh-inject> option is used to inject ssh keys for users in
+the guest, so they can login without supplying a password.
+
+The C<SELECTOR> part of the option value is optional; in this case,
+I<--ssh-inject> C<USER> means that we look in the I<current>
+user's C<~/.ssh> directory to find the default public ID file. That
+key is uploaded. "default public ID" is the I<default_ID_file> file
+described in L<ssh-copy-id(1)>.
+
+If specified, the C<SELECTOR> can be in one of the following formats:
+
+=over 4
+
+=item B<--ssh-inject> USER:file:FILENAME
+
+Read the ssh key from C<FILENAME>. C<FILENAME> is usually a I<.pub>
+file.
+
+=item B<--ssh-inject> USER:string:KEY_STRING
+
+Use the specified C<KEY_STRING>. C<KEY_STRING> is usually a public
+string like I<ssh-rsa AAAA.... user@localhost>.
+
+=back
+
+In any case, the C<~USER/.ssh> directory and the
+C<~USER/.ssh/authorized_keys> file will be created if not existing
+already.
+
=head2 INSTALLATION PROCESS
When you invoke virt-builder, installation proceeds as follows:
diff --git a/customize/Makefile.am b/customize/Makefile.am
index 60e2091..56c5ad5 100644
--- a/customize/Makefile.am
+++ b/customize/Makefile.am
@@ -55,6 +55,8 @@ SOURCES = \
perl_edit.mli \
random_seed.ml \
random_seed.mli \
+ ssh_key.ml \
+ ssh_key.mli \
timezone.ml \
timezone.mli \
urandom.ml \
@@ -92,6 +94,7 @@ ocaml_modules = \
password \
perl_edit \
random_seed \
+ ssh_key \
timezone \
customize_cmdline \
customize_run \
diff --git a/customize/customize_run.ml b/customize/customize_run.ml
index 51b218a..09ada7d 100644
--- a/customize/customize_run.ml
+++ b/customize/customize_run.ml
@@ -232,6 +232,14 @@ exec >>%s 2>&1
msg (f_"Scrubbing: %s") path;
g#scrub_file path
+ | `SSHInject (user, selector) ->
+ (match g#inspect_get_type root with
+ | "linux" | "freebsd" | "netbsd" |
"openbsd" | "hurd" ->
+ msg (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"))
+
| `Timezone tz ->
msg (f_"Setting the timezone: %s") tz;
if not (Timezone.set_timezone g root tz) then
diff --git a/customize/ssh_key.ml b/customize/ssh_key.ml
new file mode 100644
index 0000000..09664bf
--- /dev/null
+++ b/customize/ssh_key.ml
@@ -0,0 +1,133 @@
+(* 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 Common_gettext.Gettext
+open Common_utils
+
+open Customize_utils
+
+open Printf
+open Sys
+open Unix
+
+module G = Guestfs
+
+type ssh_key_selector =
+| SystemKey
+| KeyFile of string
+| KeyString of string
+
+let rec parse_selector arg =
+ parse_selector_list arg (string_nsplit ":" arg)
+
+and parse_selector_list orig_arg = function
+ | [] | [ "" ] ->
+ SystemKey
+ | [ "file"; f ] ->
+ KeyFile f
+ | [ "string"; s ] ->
+ KeyString s
+ | _ ->
+ error (f_"invalid ssh-inject selector '%s'; see the man page")
orig_arg
+
+(* Find the local [on the host] user's SSH public key. See
+ * ssh-copy-id(1) default_ID_file for rationale.
+ *)
+let pubkey_re = Str.regexp "^id.*\\.pub$"
+let pubkey_ignore_re = Str.regexp ".*-cert\\.pub$"
+
+let local_user_ssh_pubkey () =
+ let home_dir =
+ try getenv "HOME"
+ with Not_found ->
+ error (f_"ssh-inject: $HOME environment variable is not set") in
+ let ssh_dir = home_dir // ".ssh" in
+ let files = Sys.readdir ssh_dir in
+ let files = Array.to_list files in
+ let files = List.filter (
+ fun file ->
+ Str.string_match pubkey_re file 0 &&
+ not (Str.string_match pubkey_ignore_re file 0)
+ ) files in
+ if files = [] then
+ error (f_"ssh-inject: no public key file found in %s") ssh_dir;
+
+ (* Newest file. *)
+ let files = List.map (
+ fun file ->
+ let file = ssh_dir // file in
+ let stat = stat file in
+ (file, stat.st_mtime)
+ ) files in
+ let files = List.sort (fun (_,m1) (_,m2) -> compare m2 m1) files in
+
+ fst (List.hd files)
+
+let read_key file =
+ (* Read and return the public key. *)
+ let key = read_whole_file file in
+ if key = "" then
+ error (f_"ssh-inject: public key file (%s) is empty") file;
+ key
+
+let key_string_from_selector = function
+ | SystemKey ->
+ read_key (local_user_ssh_pubkey ())
+ | KeyFile f ->
+ read_key f
+ | KeyString s ->
+ if String.length s < 1 then
+ error (f_"ssh-inject: key is an empty string");
+ s
+
+(* Inject SSH key, where possible. *)
+let do_ssh_inject_unix (g : Guestfs.guestfs) user selector =
+ let key = key_string_from_selector selector in
+ assert (String.length key > 0);
+
+ (* If the key doesn't have \n at the end, add it. *)
+ let len = String.length key in
+ let key = if key.[len-1] = '\n' then key else key ^ "\n" in
+
+ (* Get user's home directory. *)
+ g#aug_init "/" 0;
+ let home_dir =
+ try
+ let expr = sprintf "/files/etc/passwd/%s/home" user in
+ g#aug_get expr
+ with G.Error _ ->
+ error (f_"ssh-inject: the user %s does not exist on the guest")
+ user in
+ g#aug_close ();
+
+ (* Create ~user/.ssh if it doesn't exist. *)
+ let ssh_dir = sprintf "%s/.ssh" home_dir in
+ if not (g#exists ssh_dir) then (
+ g#mkdir ssh_dir;
+ g#chmod 0o755 ssh_dir
+ );
+
+ (* Create ~user/.ssh/authorized_keys if it doesn't exist. *)
+ let auth_keys = sprintf "%s/authorized_keys" ssh_dir in
+ if not (g#exists auth_keys) then (
+ g#touch auth_keys;
+ g#chmod 0o644 auth_keys
+ );
+
+ (* Append the key. *)
+ g#write_append auth_keys key
diff --git a/customize/ssh_key.mli b/customize/ssh_key.mli
new file mode 100644
index 0000000..3223e55
--- /dev/null
+++ b/customize/ssh_key.mli
@@ -0,0 +1,31 @@
+(* 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.
+ *)
+
+type ssh_key_selector =
+| SystemKey (* Default key from the user in the system, in
+ * the style of ssh-copy-id(1)/default_ID_file.
+ *)
+| KeyFile of string (* Key from the specified file. *)
+| KeyString of string (* Key specified as string. *)
+
+val parse_selector : string -> ssh_key_selector
+(** Parse the selector field in --ssh-ibject. Note this
+ doesn't parse the username part. Exits if the format is not valid. *)
+
+val do_ssh_inject_unix : Guestfs.guestfs -> string -> ssh_key_selector -> unit
+(** ... *)
diff --git a/generator/customize.ml b/generator/customize.ml
index 8642a54..82ecb79 100644
--- a/generator/customize.ml
+++ b/generator/customize.ml
@@ -42,6 +42,7 @@ and op_type =
| TargetLinks of string (* target:link[:link...] *)
| PasswordSelector of string (* password selector *)
| UserPasswordSelector of string (* user:selector *)
+| SSHKeySelector of string (* user:selector *)
let ops = [
{ op_name = "chmod";
@@ -260,6 +261,22 @@ It cannot delete directories, only regular files.
=back";
};
+ { op_name = "ssh-inject";
+ op_type = SSHKeySelector "USER[:SELECTOR]";
+ op_discrim = "`SSHInject";
+ op_shortdesc = "Inject a public key into the guest";
+ op_pod_longdesc = "\
+Inject an ssh key so the given C<USER> will be able to log in over
+ssh without supplying a password. The C<USER> must exist already
+in the guest.
+
+See L<virt-builder(1)/SSH KEYS> for the format of
+the C<SELECTOR> field.
+
+You can have multiple I<--ssh-inject> options, for different users
+and also for more keys for each user."
+ };
+
{ op_name = "timezone";
op_type = String "TIMEZONE";
op_discrim = "`Timezone";
@@ -539,6 +556,19 @@ let rec argspec () =
pr " s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v
shortdesc;
pr " ),\n";
pr " Some %S, %S;\n" v longdesc
+ | { op_type = SSHKeySelector v; op_name = name; op_discrim = discrim;
+ op_shortdesc = shortdesc; op_pod_longdesc = longdesc } ->
+ pr " (\n";
+ pr " \"--%s\",\n" name;
+ pr " Arg.String (\n";
+ pr " fun s ->\n";
+ pr " let user, selstr = string_split \":\" s in\n";
+ pr " let sel = Ssh_key.parse_selector selstr in\n";
+ pr " ops := %s (user, sel) :: !ops\n" discrim;
+ pr " ),\n";
+ pr " s_\"%s\" ^ \" \" ^ s_\"%s\"\n" v
shortdesc;
+ pr " ),\n";
+ pr " Some %S, %S;\n" v longdesc
) ops;
List.iter (
@@ -606,6 +636,10 @@ type ops = {
op_name = name } ->
pr " | %s of string * Password.password_selector\n (* --%s %s
*)\n"
discrim name v
+ | { op_type = SSHKeySelector v; op_discrim = discrim;
+ op_name = name } ->
+ pr " | %s of string * Ssh_key.ssh_key_selector\n (* --%s %s *)\n"
+ discrim name v
) ops;
pr "]\n";
@@ -631,7 +665,7 @@ let generate_customize_synopsis_pod () =
| { op_type = Unit; op_name = n } ->
n, sprintf "[--%s]" n
| { op_type = String v | StringPair v | StringList v | TargetLinks v
- | PasswordSelector v | UserPasswordSelector v;
+ | PasswordSelector v | UserPasswordSelector v | SSHKeySelector v;
op_name = n } ->
n, sprintf "[--%s %s]" n v
) ops @
@@ -671,7 +705,7 @@ let generate_customize_options_pod () =
| { op_type = Unit; op_name = n; op_pod_longdesc = ld } ->
n, sprintf "B<--%s>" n, ld
| { op_type = String v | StringPair v | StringList v | TargetLinks v
- | PasswordSelector v | UserPasswordSelector v;
+ | PasswordSelector v | UserPasswordSelector v | SSHKeySelector v;
op_name = n; op_pod_longdesc = ld } ->
n, sprintf "B<--%s> %s" n v, ld
) ops @
diff --git a/po/POTFILES-ml b/po/POTFILES-ml
index a3086eb..b6d88b0 100644
--- a/po/POTFILES-ml
+++ b/po/POTFILES-ml
@@ -25,6 +25,7 @@ customize/hostname.ml
customize/password.ml
customize/perl_edit.ml
customize/random_seed.ml
+customize/ssh_key.ml
customize/timezone.ml
customize/urandom.ml
mllib/JSON.ml
diff --git a/sysprep/Makefile.am b/sysprep/Makefile.am
index 6553c9c..17fe612 100644
--- a/sysprep/Makefile.am
+++ b/sysprep/Makefile.am
@@ -102,6 +102,7 @@ deps = \
$(top_builddir)/customize/firstboot.cmx \
$(top_builddir)/customize/perl_edit-c.o \
$(top_builddir)/customize/perl_edit.cmx \
+ $(top_builddir)/customize/ssh_key.cmx \
$(top_builddir)/customize/customize_cmdline.cmx \
$(top_builddir)/customize/customize_run.cmx \
$(top_builddir)/fish/guestfish-uri.o \
diff --git a/v2v/Makefile.am b/v2v/Makefile.am
index b4bb9cc..9217777 100644
--- a/v2v/Makefile.am
+++ b/v2v/Makefile.am
@@ -139,6 +139,7 @@ BOBJECTS = \
$(top_builddir)/customize/perl_edit.cmo \
$(top_builddir)/customize/crypt.cmo \
$(top_builddir)/customize/password.cmo \
+ $(top_builddir)/customize/ssh_key.cmo \
$(top_builddir)/customize/customize_run.cmo \
$(SOURCES_ML:.ml=.cmo)
XOBJECTS = $(BOBJECTS:.cmo=.cmx)
--
1.9.3