Richard W.M. Jones
2016-Sep-30 14:04 UTC
[Libguestfs] [PATCH] customize: Add --append-line.
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
On Friday, 30 September 2016 15:04:18 CEST Richard W.M. Jones wrote:> This appends a single line to a file, with some cleverness > involving guessing the right line endings to use. > > Also adds a test. > ---Makes sense, just a couple of notes.> + 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 thenI guess maybe it could be better to use g#is_file and g#filesize, to avoid having to deal at application side with the file mode got in the appliance.> + (* 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 () inShould this also check for the Mac end line ('\r')? (If so, the API documentation should mention that too.) Thanks, -- Pino Toscano
Richard W.M. Jones
2016-Oct-01 12:08 UTC
Re: [Libguestfs] [PATCH] customize: Add --append-line.
On Fri, Sep 30, 2016 at 05:01:23PM +0200, Pino Toscano wrote:> On Friday, 30 September 2016 15:04:18 CEST Richard W.M. Jones wrote: > > This appends a single line to a file, with some cleverness > > involving guessing the right line endings to use. > > > > Also adds a test. > > --- > > Makes sense, just a couple of notes. > > > + 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 > > I guess maybe it could be better to use g#is_file and g#filesize, to > avoid having to deal at application side with the file mode got in the > appliance.These hard-coded constants aren't really a problem. It's what the guestfs_statns ABI defines. We also use them in cat/visit.c.> > + (* 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 > > Should this also check for the Mac end line ('\r')? (If so, the API > documentation should mention that too.)OK I added that, retested it, and pushed it. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com libguestfs lets you edit virtual machines. Supports shell scripting, bindings from many languages. http://libguestfs.org