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 <[email protected]>:
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

_______________________________________________
Libguestfs mailing list
[email protected]
https://www.redhat.com/mailman/listinfo/libguestfs

Reply via email to