[Libguestfs] [PATCH v2 4/5] v2v: ova: don't extract files from OVA if it's not needed

Pino Toscano ptoscano at redhat.com
Mon Nov 21 15:21:02 UTC 2016


On Saturday, 12 November 2016 16:37:52 CET Tomáš Golembiovský wrote:
> We don't have to always extract all files from the OVA archive. The OVA,
> as defined in the standard, is plain tar. We can work directly over the
> tar archive if we use correct 'offset' and 'size' options when defining
> the backing file for QEMU.
> 
> This leads to improvements in speed and puts much lower requirement on
> available disk space.
> 
> Signed-off-by: Tomáš Golembiovský <tgolembi at redhat.com>
> ---
>  v2v/input_ova.ml | 177 +++++++++++++++++++++++++++++++++++++++++++++++--------
>  1 file changed, 154 insertions(+), 23 deletions(-)
> 
> diff --git a/v2v/input_ova.ml b/v2v/input_ova.ml
> index f76fe82..7a3e27b 100644
> --- a/v2v/input_ova.ml
> +++ b/v2v/input_ova.ml
> @@ -39,17 +39,23 @@ object
>  
>    method source () =
>  
> -    let untar ?(format = "") file outdir =
> -      let cmd = [ "tar"; sprintf "-x%sf" format; file; "-C"; outdir ] in
> +    (* Untar part or all files from tar archive. If [path] is specified it is
> +     * a path in the tar archive.
> +     *)
> +    let untar ?(format = "") ?(path) file outdir =

No need for parenthesis around "path".
Also, tar supports extracting more files at the same time, so I'd
change "path" into "paths", and just call this once later on ...

> +      let cmd =
> +        [ "tar"; sprintf "-x%sf" format; file; "-C"; outdir ]
> +        @ (match path with None -> [] | Some p -> [p])
> +      in
>        if run_command cmd <> 0 then
>          error (f_"error unpacking %s, see earlier error messages") ova in
>  
>      (* Extract ova file. *)
> -    let exploded =
> +    let exploded, partial =
>        (* The spec allows a directory to be specified as an ova.  This
>         * is also pretty convenient.
>         *)
> -      if is_directory ova then ova
> +      if is_directory ova then ova, false
>        else (
>          let uncompress_head zcat file =
>            let cmd = sprintf "%s %s" zcat (quote file) in
> @@ -67,11 +73,53 @@ object
>  
>            tmpfile in
>  
> +        (* Untar only ovf and manifest from the archive *)
> +        let untar_partial file outdir =

I'd rename this as "untar_metadata", as it seems a better fitting name.
YMMV.

> +          let files =
> +            external_command (sprintf "tar -tf %s" (Filename.quote file)) in
> +          List.iter (fun f ->
> +            if Filename.check_suffix f ".ovf" ||
> +              Filename.check_suffix f ".mf" then
> +                untar ~path:f file outdir
> +          ) files in

... i.e. here: this can be changed to:

  let files =
    filter_map (
      fun f ->
        if Filename.check_suffix f ".ovf" ||
           Filename.check_suffix f ".mf" then
          Some f
        else None
    ) files in
  untar ~paths:files file outdir

> +        let qemu_img_version () =
> +          let cmd = "qemu-img --version" in
> +          let lines = external_command cmd in
> +          match lines with
> +          | [] -> error ("'qemu-img --version' returned no output")
> +          | line :: _ ->
> +              let rex = Str.regexp "qemu-img version \\([0-9]+\\)\\.\\([0-9]+\\)" in
> +              if Str.string_match rex line 0 then (
> +                try
> +                  int_of_string (Str.matched_group 1 line),
> +                  int_of_string (Str.matched_group 2 line)
> +                with Failure _ ->
> +                  warning (f_"warning: failed to read qemu-img version! Line: %S")
> +                  line;

"warning" is already printed by the warning function; also I'd simplify
the message as:

  warning (f_"failed to parse the version of qemu-img (%S), assuming 0.9")
    line

> +                  0, 9
> +              ) else (
> +                warning (f_"warning: failed to read qemu-img version! Line: %S")
> +                  line;

Ditto.

> +                0, 9
> +              )
> +        in
> +
>          match detect_file_type ova with
>          | `Tar ->
>            (* Normal ovas are tar file (not compressed). *)
> -          untar ova tmpdir;
> -          tmpdir
> +          let qmajor, qminor = qemu_img_version () in
> +          if qmajor > 2 || (qmajor == 2 && qminor >= 8) then (
> +            (* If QEMU is recent enough we don't have to extract everything. We
> +             * can access disks inside the tar archive.
> +             *)
> +            untar_partial ova tmpdir;
> +            tmpdir, true
> +          ) else (
> +            untar ova tmpdir;
> +            tmpdir, false
> +          )
> +
>          | `Zip ->
>            (* However, although not permitted by the spec, people ship
>             * zip files as ova too.
> @@ -81,7 +129,7 @@ object
>              [ "-j"; "-d"; tmpdir; ova ] in
>            if run_command cmd <> 0 then
>              error (f_"error unpacking %s, see earlier error messages") ova;
> -          tmpdir
> +          tmpdir, false
>          | (`GZip|`XZ) as format ->
>            let zcat, tar_fmt =
>              match format with
> @@ -94,7 +142,7 @@ object
>            (match tmpfiletype with
>            | `Tar ->
>              untar ~format:tar_fmt ova tmpdir;
> -            tmpdir
> +            tmpdir, false
>            | `Zip | `GZip | `XZ | `Unknown ->
>              error (f_"%s: unsupported file format\n\nFormats which we currently understand for '-i ova' are: tar (uncompressed, compress with gzip or xz), zip") ova
>            )
> @@ -135,6 +183,59 @@ object
>        loop [dir]
>      in
>  
> +    (* Find file in [tar] archive and return at which byte it starts and how
> +     * long it is.
> +     *)
> +    let find_file_in_tar tar filename =
> +      let lines = external_command (stringify_args [ "tar"; "tRvf"; tar ]) in

stringify_args is meant to be used for displaying, rather than passing
it back as shell command. Please just build a command string as done in
the rest of the places.

> +      let rec loop lines =
> +        match lines with
> +        | [] -> raise Not_found
> +        | line :: lines -> (
> +          (* Lines have the form:
> +           * block <offset>: <perms> <owner>/<group> <size> <mdate> <mtime> <file>
> +           *)
> +          let rex = Str.regexp "^block \\([0-9]+\\): \\([^ \\t]+\\) \\([^ \\t]+\\) +\\([^ \\t]+\\) \\([^ \\t]+\\) \\([^ \\t]+\\) \\(.+\\)$" in

Whoa how unreadable :/ The alternative would be using String.nsplit,
but it would play badly with filenames with spaces inside the archive.
Maybe a faster alternative could be:
a) find the 6th empty space, and pick the substring of all the rest
   of the string after it
b) filter the lines by picking only the wanted file, checking using (a)
c) once a single line is found, use String.nsplit and parse only parts
   1 (removing the ':' suffix) and 4

It feels slightly more complex, but this way we can avoid running the
regex until we find the right filename.

> +          if Str.string_match rex line 0 then (
> +            let offset = Str.matched_group 1 line in
> +            let size = Str.matched_group 4 line in
> +            let fname = Str.matched_group 7 line in
> +
> +            let offset =
> +              try Int64.of_string offset
> +              with Failure _ ->
> +                error (f_"invalid offset returned by tar: %S") offset in
> +
> +            let size =
> +              try Int64.of_string size
> +              with Failure _ ->
> +                error (f_"invalid size returned by tar: %S") size in
> +
> +            if fname = filename then
> +              (* Note: Offset is actualy block number and there is a single
> +               * block with tar header at the beginning of the file. So skip
> +               * the header and convert the block number to bytes before
> +               * returning.
> +               *)
> +              (offset +^ 1L) *^ 512L, size
> +            else loop lines
> +          ) else
> +            error (f_"failed to parse line returned by tar: %S") line
> +        )
> +      in
> +      loop lines
> +    in
> +
> +    let subfolder folder parent =
> +      if String.is_prefix folder (parent // "") then
> +        let len = String.length parent in
> +        String.sub folder (len+1) (String.length folder-len-1)
> +      else if folder = parent then
> +        ""

I'd invert the two checks (first the = one, then is_prefix).

> +      else
> +        assert false
> +    in
> +
>      (* Search for the ovf file. *)
>      let ovf = find_files exploded ".ovf" in
>      let ovf =
> @@ -152,6 +253,7 @@ object
>        fun mf ->
>          debug "processing manifest %s" mf;
>          let mf_folder = Filename.dirname mf in
> +        let mf_subfolder = subfolder mf_folder exploded in
>          let chan = open_in mf in
>          let rec loop () =
>            let line = input_line chan in
> @@ -160,7 +262,11 @@ object
>              let disk = Str.matched_group 2 line in
>              let expected = Str.matched_group 3 line in
>              let csum = Checksums.of_string mode expected in
> -            try Checksums.verify_checksum csum (mf_folder // disk)
> +            try
> +              if partial then
> +                Checksums.verify_checksum csum ~tar:ova (mf_subfolder // disk)
> +              else
> +                Checksums.verify_checksum csum (mf_folder // disk)
>              with Checksums.Mismatched_checksum (_, actual) ->
>                error (f_"checksum of disk %s does not match manifest %s (actual %s(%s) = %s, expected %s(%s) = %s)")
>                  disk mf mode disk actual mode disk expected;
> @@ -283,25 +389,50 @@ object
>              | Some "gzip" -> true
>              | Some s -> error (f_"unsupported comprression in OVF: %s") s in
>  
> -          let filename = if compressed then (
> -            let new_filename = tmpdir // String.random8 () ^ ".vmdk" in
> -            let cmd =
> -              sprintf "zcat %s > %s" (quote ovf_folder // filename) (quote new_filename) in
> -            if shell_command cmd <> 0 then
> -              error (f_"error uncompressing %s, see earlier error messages")
> -                filename;
> -            new_filename
> -          )
> -          else
> -            ovf_folder // filename
> +          let filename, partial =
> +            if compressed then (
> +              if partial then
> +                untar ~path:((subfolder ovf_folder exploded) // filename)
> +                  ova tmpdir;
> +              let new_filename = tmpdir // String.random8 () ^ ".vmdk" in
> +              let cmd =
> +                sprintf "zcat %s > %s" (quote ovf_folder // filename) (quote new_filename) in
> +              if shell_command cmd <> 0 then
> +                error (f_"error uncompressing %s, see earlier error messages")
> +                  filename;

I guess parts of this block could be shared/factored out with the same
bits used earlier.

Thanks,
-- 
Pino Toscano
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 819 bytes
Desc: This is a digitally signed message part.
URL: <http://listman.redhat.com/archives/libguestfs/attachments/20161121/0341ab0f/attachment.sig>


More information about the Libguestfs mailing list