This appends a single line to a file, with some cleverness
involving guessing the right line endings to use.
Also adds a test.
---
builder/test-virt-builder.sh | 57 +++++++++++++++++++++++++++++++++++-
customize/Makefile.am | 2 ++
customize/append_line.ml | 70 ++++++++++++++++++++++++++++++++++++++++++++
customize/append_line.mli | 20 +++++++++++++
customize/customize_run.ml | 11 +++++++
generator/customize.ml | 34 +++++++++++++++++++++
6 files changed, 193 insertions(+), 1 deletion(-)
create mode 100644 customize/append_line.ml
create mode 100644 customize/append_line.mli
diff --git a/builder/test-virt-builder.sh b/builder/test-virt-builder.sh
index 2a9227b..80dcd98 100755
--- a/builder/test-virt-builder.sh
+++ b/builder/test-virt-builder.sh
@@ -70,6 +70,19 @@ $VG virt-builder phony-fedora \
--delete /Makefile \
--link /etc/foo/bar/baz/foo:/foo \
--link /etc/foo/bar/baz/foo:/foo1:/foo2:/foo3 \
+ --append-line '/etc/append1:hello' \
+ --append-line '/etc/append2:line1' \
+ --append-line '/etc/append2:line2' \
+ --write '/etc/append3:line1' \
+ --append-line '/etc/append3:line2' \
+ --write '/etc/append4:line1
+' \
+ --append-line '/etc/append4:line2' \
+ --touch /etc/append5 \
+ --append-line '/etc/append5:line1' \
+ --write '/etc/append6:
+' \
+ --append-line '/etc/append6:line2' \
--firstboot Makefile --firstboot-command 'echo "hello"' \
--firstboot-install "minicom,inkscape"
@@ -97,6 +110,24 @@ echo -----
# Password
is-file /etc/shadow
cat /etc/shadow | sed -r '/^root:/!d;s,^(root:\\\$6\\\$).*,\\1,g'
+
+echo -----
+# Line appending
+# Note that the guestfish 'cat' command appends a newline
+echo append1:
+cat /etc/append1
+echo append2:
+cat /etc/append2
+echo append3:
+cat /etc/append3
+echo append4:
+cat /etc/append4
+echo append5:
+cat /etc/append5
+echo append6:
+cat /etc/append6
+
+echo -----
EOF
if [ "$(cat test-virt-builder.out)" != "true
@@ -113,7 +144,31 @@ true
/usr/share/zoneinfo/Europe/London
-----
true
-root:\$6\$" ]; then
+root:\$6\$
+-----
+append1:
+hello
+
+append2:
+line1
+line2
+
+append3:
+line1
+line2
+
+append4:
+line1
+line2
+
+append5:
+line1
+
+append6:
+
+line2
+
+-----" ]; then
echo "$0: unexpected output:"
cat test-virt-builder.out
exit 1
diff --git a/customize/Makefile.am b/customize/Makefile.am
index ae37b51..ce5c662 100644
--- a/customize/Makefile.am
+++ b/customize/Makefile.am
@@ -34,6 +34,7 @@ generator_built = \
customize-synopsis.pod
SOURCES_MLI = \
+ append_line.mli \
crypt.mli \
customize_cmdline.mli \
customize_run.mli \
@@ -51,6 +52,7 @@ SOURCES_MLI = \
# This list must be in dependency order.
SOURCES_ML = \
customize_utils.ml \
+ append_line.ml \
crypt.ml \
firstboot.ml \
hostname.ml \
diff --git a/customize/append_line.ml b/customize/append_line.ml
new file mode 100644
index 0000000..0095ff6
--- /dev/null
+++ b/customize/append_line.ml
@@ -0,0 +1,70 @@
+(* virt-customize
+ * Copyright (C) 2016 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_utils
+open Common_gettext.Gettext
+
+module G = Guestfs
+
+let append_line (g : G.guestfs) root path line =
+ (* The default line ending for this guest type. This is only
+ * used when we don't know anything more about the file.
+ *)
+ let default_newline () =
+ match g#inspect_get_type root with
+ | "windows" -> "\r\n"
+ | _ -> "\n"
+ in
+
+ if not (g#exists path) then (
+ g#write path (line ^ default_newline ())
+ )
+ else (
+ (* Stat the file. We want to know it's a regular file, and
+ * also its size.
+ *)
+ let { G.st_mode = mode; st_size = size } = g#statns path in
+ if Int64.logand mode 0o170000_L <> 0o100000_L then
+ error (f_"append_line: %s is not a file") path;
+
+ (* Guess the line ending from the first part of the file, else
+ * use the default for this guest type.
+ *)
+ let newline =
+ let content = g#pread path 8192 0L in
+ if String.find content "\r\n" >= 0 then "\r\n"
+ else if String.find content "\n" >= 0 then "\n"
+ else default_newline () in
+
+ let line = line ^ newline in
+
+ (* Do we need to append a newline to the existing file? *)
+ let last_chars =
+ let len = String.length newline in
+ if size <= 0L then newline (* empty file ends in virtual newline *)
+ else if size >= Int64.of_int len then
+ g#pread path len (size -^ Int64.of_int len)
+ else
+ g#pread path len 0L in
+ let line =
+ if last_chars = newline then line
+ else newline ^ line in
+
+ (* Finally, append. *)
+ g#write_append path line
+ )
diff --git a/customize/append_line.mli b/customize/append_line.mli
new file mode 100644
index 0000000..11c2da5
--- /dev/null
+++ b/customize/append_line.mli
@@ -0,0 +1,20 @@
+(* virt-customize
+ * Copyright (C) 2016 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 append_line : Guestfs.guestfs -> string -> string -> string -> unit
+(** append_line [g root file line] appends a single line to a text file. *)
diff --git a/customize/customize_run.ml b/customize/customize_run.ml
index 3e759a2..9ec7b5a 100644
--- a/customize/customize_run.ml
+++ b/customize/customize_run.ml
@@ -25,6 +25,7 @@ open Common_utils
open Customize_utils
open Customize_cmdline
open Password
+open Append_line
let run (g : Guestfs.guestfs) root (ops : ops) =
(* Is the host_cpu compatible with the guest arch? ie. Can we
@@ -204,6 +205,16 @@ exec >>%s 2>&1
(* Perform the remaining customizations in command-line order. *)
List.iter (
function
+ | `AppendLine (path, line) ->
+ (* It's an error if it's not a single line. This is
+ * to prevent incorrect line endings being added to a file.
+ *)
+ if String.contains line '\n' then
+ error (f_"--append-line: line must not contain newline characters. Use the
--append-line option multiple times to add several lines.");
+
+ message (f_"Appending line to %s") path;
+ append_line g root path line
+
| `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
diff --git a/generator/customize.ml b/generator/customize.ml
index 259cd26..d3a1946 100644
--- a/generator/customize.ml
+++ b/generator/customize.ml
@@ -49,6 +49,40 @@ and op_type =
| SMPoolSelector of string (* pool selector *)
let ops = [
+ { op_name = "append-line";
+ op_type = StringPair "FILE:LINE";
+ op_discrim = "`AppendLine";
+ op_shortdesc = "Append line(s) to the file";
+ op_pod_longdesc = "\
+Append a single line of text to the C<FILE>. If the file does not already
+end with a newline, then one is added before the appended
+line. Also a newline is added to the end of the C<LINE> string
+automatically.
+
+For example (assuming ordinary shell quoting) this command:
+
+ --append-line '/etc/hosts:10.0.0.1 foo'
+
+will add either C<10.0.0.1 foo⏎> or C<⏎10.0.0.1 foo⏎> to
+the file, the latter only if the existing file does not
+already end with a newline.
+
+C<⏎> represents a newline character, which is guessed by
+looking at the existing content of the file, so this command
+does the right thing for files using Unix or Windows line endings.
+It also works for empty or non-existent files.
+
+To insert several lines, use the same option several times:
+
+ --append-line '/etc/hosts:10.0.0.1 foo' \
+ --append-line '/etc/hosts:10.0.0.2 bar'
+
+To insert a blank line before the appended line, do:
+
+ --append-line '/etc/hosts:'
+ --append-line '/etc/hosts:10.0.0.1 foo'";
+ };
+
{ op_name = "chmod";
op_type = StringPair "PERMISSIONS:FILE";
op_discrim = "`Chmod";
--
2.9.3