tools: add cos_ova_converter to the tools
To simplify handling OVA images to and from GCE during preloading
process using COS Customizer, adding a simple tools.
BUG=b/188836459
TEST=manual
RELEASE_NOTE=Add cos_ova_converter tools
Change-Id: Ie4a16e7d79de61048c2e0e5a21946584cb964008
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/24390
Reviewed-by: Robert Kolchmeyer <rkolchmeyer@google.com>
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Tested-by: Varsha Teratipally <teratipally@google.com>
diff --git a/go.mod b/go.mod
index 69ee986..0644b3b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,8 @@
require (
cloud.google.com/go v0.81.0
cloud.google.com/go/logging v1.4.2
- cloud.google.com/go/storage v1.13.0
+ cloud.google.com/go/storage v1.14.0
+ github.com/GoogleCloudPlatform/compute-image-tools/daisy v0.0.0-20211102200636-e7e49ca6dac0 // indirect
github.com/andygrunwald/go-gerrit v0.0.0-20201231163137-46815e48bfe0
github.com/beevik/etree v1.1.0
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
diff --git a/go.sum b/go.sum
index e1756e9..653fd08 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
@@ -30,6 +31,7 @@
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/logging v1.0.0/go.mod h1:V1cc3ogwobYzQq5f2R7DS/GvRIrI4FKj01Gs5glwAls=
cloud.google.com/go/logging v1.4.2 h1:Mu2Q75VBDQlW1HlBMjTX4X84UFR73G1TiLlRYc/b7tA=
cloud.google.com/go/logging v1.4.2/go.mod h1:jco9QZSx8HiVVqLJReq7z7bVdj0P1Jb9PDFs63T+axo=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -43,11 +45,15 @@
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.13.0 h1:amPvhCOI+Hltp6rPu+62YdwhIrjf+34PKVAL4HwgYwk=
cloud.google.com/go/storage v1.13.0/go.mod h1:pqFyBUK3zZqMIIU5+8NaZq6/Ma3ClgUg9Hv5jfuJnvo=
+cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/GoogleCloudPlatform/compute-image-tools/daisy v0.0.0-20211102200636-e7e49ca6dac0 h1:b4WGGawVW/lMUywy4/hapK01bJ+QB8twMKwe0A0Gw3E=
+github.com/GoogleCloudPlatform/compute-image-tools/daisy v0.0.0-20211102200636-e7e49ca6dac0/go.mod h1:Z9jsyfegJlbSvjxGJnLf0vFf5yn20YyYkFaANYscwCU=
github.com/andygrunwald/go-gerrit v0.0.0-20201231163137-46815e48bfe0 h1:1IlIh8TmY+eAX17cPIUzT4e5R5bQoEngAO5QFcGHbrA=
github.com/andygrunwald/go-gerrit v0.0.0-20201231163137-46815e48bfe0/go.mod h1:soxaYLbAFToS0OelBriItCts/mtUZOuLBkCk1Xv4ZSo=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
@@ -173,6 +179,7 @@
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -229,6 +236,7 @@
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -305,6 +313,7 @@
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
@@ -354,10 +363,12 @@
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
@@ -452,6 +463,7 @@
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/api v0.46.0 h1:jkDWHOBIoNSD0OQpq4rtBVu+Rh325MPjXG1rakAp8JU=
google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -467,6 +479,8 @@
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
@@ -499,6 +513,7 @@
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210203152818-3206188e46ba/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
diff --git a/src/cmd/cos_ova_converter/Dockerfile b/src/cmd/cos_ova_converter/Dockerfile
new file mode 100644
index 0000000..ba1445b
--- /dev/null
+++ b/src/cmd/cos_ova_converter/Dockerfile
@@ -0,0 +1,32 @@
+FROM golang:1.16 as cosovaconverter
+
+COPY . /work/
+WORKDIR /work/src/cmd/cos_ova_converter
+RUN go build -o cos_ova_converter .
+
+FROM gcr.io/compute-image-tools/daisy as daisyworkflow
+
+FROM debian:buster-slim
+
+RUN apt-get update && DEBIAN_FRONTEND=noninteractive \
+ apt-get install --no-install-recommends -y -qq \
+ ca-certificates \
+ apt-transport-https \
+ gnupg \
+ curl \
+ qemu-utils \
+ python3 \
+ python3-pip python3-setuptools\
+ xmlstarlet \
+ git
+
+RUN pip3 install cot
+
+RUN git clone https://cos.googlesource.com/third_party/platform/crosutils.git
+RUN cd crosutils && git checkout 74d0afda96dc8c58863f76b2e144c373f92451f6
+
+COPY --from=cosovaconverter /work/src/cmd/cos_ova_converter/cos_ova_converter /cos_ova_converter
+COPY --from=daisyworkflow /daisy /daisy
+COPY --from=daisyworkflow /workflows /workflows
+
+ENTRYPOINT ["/cos_ova_converter"]
\ No newline at end of file
diff --git a/src/cmd/cos_ova_converter/Makefile b/src/cmd/cos_ova_converter/Makefile
new file mode 100644
index 0000000..8f9e02e
--- /dev/null
+++ b/src/cmd/cos_ova_converter/Makefile
@@ -0,0 +1,11 @@
+COS_CONVERTER_BINARY=cos_ova_converter
+
+build:
+ go build -o ${COS_CONVERTER_BINARY} .
+
+clean:
+ go clean
+ rm -f ${COS_CONVERTER_BINARY}
+
+build-image:
+ cd ../../../ && docker build -t ${COS_CONVERTER_BINARY} -f src/cmd/cos_ova_converter/Dockerfile .
diff --git a/src/cmd/cos_ova_converter/README.md b/src/cmd/cos_ova_converter/README.md
new file mode 100644
index 0000000..6c66ca2
--- /dev/null
+++ b/src/cmd/cos_ova_converter/README.md
@@ -0,0 +1,54 @@
+# COS Ova Converter
+
+COS Ova Converter is a simple interface to convert the OVA images to GCE image in
+a GCP project and also exports the GCE image to OVA. It is available as a Docker image.
+
+The main motivation is to provide a simple tool for handling the OVA images
+during the COS preloading process using the COS Customizer as it exclusively
+deals with the GCE images.
+
+## How to get started
+
+### Compile COS OVA Converter
+
+``` shell
+make
+```
+will build the COS OVA Converter application
+
+### Try COS OVA Converter
+
+COS OVA Converter is available as a docker image (cos_ova_converter). It can be
+run as one of the steps in the Cloud build workflow for converting the OVA images
+to GCE and back to OVA from GCE images.
+
+
+To convert the OVA to GCE image,
+
+```shell
+steps:
+- name: 'cos-ova-converter'
+ args: ['to-gce',
+ '-image-name=cos-ova-converted-image',
+ '-image-project=${PROJECT_ID}',
+ '-gcs-bucket=${PROJECT_ID}_cloudbuild',
+ '-input-url=gs://sample-gcs/cos.ova']
+```
+
+This will download the image specified in the `input-url` and creates and image with
+name `image-name` in `image-project`. `gcs-bucket` here is a workspace.
+
+To convert the OVA from the GCE image
+
+```shell
+steps:
+- name: 'cos-ova-converter'
+ args: ['from-gce',
+ '-source-image=cos-preloaded-image',
+ '-image-project=${PROJECT_ID}',
+ '-gcs-bucket=${PROJECT_ID}_cloudbuild',
+ '-destination-path=gs://sample-gcs/cos.ova']
+```
+
+This will export the image with name `source-image` in `image-project` to `destination-path`
+as OVA. `gcs-bucket` here is a workspace.
diff --git a/src/cmd/cos_ova_converter/main.go b/src/cmd/cos_ova_converter/main.go
new file mode 100644
index 0000000..683c478
--- /dev/null
+++ b/src/cmd/cos_ova_converter/main.go
@@ -0,0 +1,39 @@
+// Copyright 2021 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "flag"
+ "os"
+
+ "cos.googlesource.com/cos/tools.git/src/pkg/ovaconverter"
+ "github.com/google/subcommands"
+)
+
+func main() {
+
+ flag.Parse()
+ //register commands
+ subcommands.Register(subcommands.HelpCommand(), "")
+ subcommands.Register(subcommands.FlagsCommand(), "")
+ subcommands.Register(new(ToGCECmd), "")
+ subcommands.Register(new(FromGCECmd), "")
+
+ ctx := context.Background()
+ converterConfig := ovaconverter.GetDefaultOVAConverterConfig()
+ ret := int(subcommands.Execute(ctx, converterConfig))
+ os.Exit(ret)
+}
diff --git a/src/cmd/cos_ova_converter/ova_from_gce.go b/src/cmd/cos_ova_converter/ova_from_gce.go
new file mode 100644
index 0000000..eb7e2b5
--- /dev/null
+++ b/src/cmd/cos_ova_converter/ova_from_gce.go
@@ -0,0 +1,107 @@
+// Copyright 2021 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "strings"
+
+ "github.com/golang/glog"
+ "github.com/google/subcommands"
+
+ "cos.googlesource.com/cos/tools.git/src/pkg/ovaconverter"
+)
+
+type FromGCECmd struct {
+ DestinationPath string
+ ImageProject string
+ SourceImage string
+ GcsBucket string
+ Zone string
+}
+
+// Name returns the name of the command.
+func (fgc *FromGCECmd) Name() string {
+ return "from-gce"
+}
+
+// Synopsis returns short description of the command.
+func (fgc *FromGCECmd) Synopsis() string {
+ return "Converts the Input GCE image " +
+ "to OVA format and uploads to destination path"
+}
+
+// Usage returns instructions on how to use the command.
+func (fgc *FromGCECmd) Usage() string {
+ return "Converts the Input GCE image " +
+ "to OVA format and uploads to destination path"
+}
+
+// SetFlags adds the flags to the specified set.
+func (fgc *FromGCECmd) SetFlags(fs *flag.FlagSet) {
+ fs.StringVar(&fgc.GcsBucket, "gcs-bucket", "",
+ "GCS bucket for the working dir. It is mandatory. Example: sample-bucket")
+ fs.StringVar(&fgc.Zone, "zone", "us-west1-b",
+ "Zone is required when exporting a GCE image. It is optional, by default it is us-west1-b. Example: us-west1-b")
+ fs.StringVar(&fgc.DestinationPath, "destination-path", "",
+ "URL to the save the OVA Image. It is mandatory. Example: gs://output-bucket/output.ova")
+ fs.StringVar(&fgc.ImageProject, "image-project", "",
+ "Project in which the source image is present. It is mandatory. Example: input-project")
+ fs.StringVar(&fgc.SourceImage, "source-image", "",
+ "Name of the Source Image. It is mandatory. Example: input-gce")
+}
+
+// Execute executes the command (converts GCE to OVA) and returns the CommandStatus
+func (fgc *FromGCECmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+ if err := fgc.validateInput(); err != nil {
+ glog.Errorf("failed to parse flags: %v", err)
+ return subcommands.ExitFailure
+ }
+ exitStatus := subcommands.ExitSuccess
+
+ gceToOVAConverterConfig := args[0].(*ovaconverter.GCEToOVAConverterConfig)
+ converter := ovaconverter.NewConverter(ctx)
+
+ if err := converter.ConvertOVAFromGCE(ctx, fgc.SourceImage, fgc.DestinationPath,
+ fgc.GcsBucket, fgc.ImageProject, fgc.Zone, gceToOVAConverterConfig); err != nil {
+ glog.Errorf("failed to create the OVA image: %v", err)
+ exitStatus = subcommands.ExitFailure
+ }
+ return exitStatus
+}
+
+// validateInput validates the input from flags parsed and returns error when the
+// mandatory input values are not present
+func (tgc *FromGCECmd) validateInput() error {
+ var errMsgs []string
+ if tgc.GcsBucket == "" {
+ errMsgs = append(errMsgs, "gcs-bucket is mandatory")
+ }
+ if tgc.DestinationPath == "" {
+ errMsgs = append(errMsgs, "destination-path is mandatory")
+ }
+ if tgc.SourceImage == "" {
+ errMsgs = append(errMsgs, "source-image is mandatory")
+ }
+ if tgc.ImageProject == "" {
+ errMsgs = append(errMsgs, "image-project is mandatory")
+ }
+ if len(errMsgs) > 0 {
+ return errors.New(strings.Join(errMsgs, ";"))
+ }
+ return nil
+}
diff --git a/src/cmd/cos_ova_converter/ova_to_gce.go b/src/cmd/cos_ova_converter/ova_to_gce.go
new file mode 100644
index 0000000..1b92040
--- /dev/null
+++ b/src/cmd/cos_ova_converter/ova_to_gce.go
@@ -0,0 +1,103 @@
+// Copyright 2021 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "strings"
+
+ "github.com/golang/glog"
+ "github.com/google/subcommands"
+
+ "cos.googlesource.com/cos/tools.git/src/pkg/ovaconverter"
+)
+
+type ToGCECmd struct {
+ InputURL string
+ ImageProject string
+ ImageName string
+ GcsBucket string
+}
+
+// Name returns the name of the command.
+func (tgc *ToGCECmd) Name() string {
+ return "to-gce"
+}
+
+// Synopsis returns short description of the command.
+func (tgc *ToGCECmd) Synopsis() string {
+ return "Converts the Input OVA image " +
+ "to raw format and uploads to GCE Project"
+}
+
+// Usage returns instructions on how to use the command.
+func (tgc *ToGCECmd) Usage() string {
+ return "Converts the Input OVA image " +
+ "to raw format and uploads to GCE Project"
+}
+
+// SetFlags adds the flags to the specified set.
+func (tgc *ToGCECmd) SetFlags(fs *flag.FlagSet) {
+ fs.StringVar(&tgc.GcsBucket, "gcs-bucket", "",
+ "GCS bucket for the working dir. It is mandatory. Example: sample-bucket")
+ fs.StringVar(&tgc.InputURL, "input-url", "",
+ "URL to the Input OVA Image. It is mandatory. Example: gs://sample-bucket/input.ova")
+ fs.StringVar(&tgc.ImageProject, "image-project", "",
+ "Project in which the image is to be created. It is mandatory. Example: test-project")
+ fs.StringVar(&tgc.ImageName, "image-name", "",
+ "Name of the Image to be create. It is mandatory. Example: input-gce")
+}
+
+// Execute executes the command (converts OVA to GCE) and returns the CommandStatus
+func (otgc *ToGCECmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+ if err := otgc.validateInput(); err != nil {
+ glog.Errorf("failed to parse flags: %v", err)
+ return subcommands.ExitFailure
+ }
+ exitStatus := subcommands.ExitSuccess
+
+ converter := ovaconverter.NewConverter(ctx)
+
+ if err := converter.ConvertOVAToGCE(ctx, otgc.InputURL, otgc.ImageName,
+ otgc.GcsBucket, otgc.ImageProject); err != nil {
+ glog.Errorf("failed to convert to the gce image: %v", err)
+ exitStatus = subcommands.ExitFailure
+ }
+ return exitStatus
+}
+
+// validateInput validates the input from flags parsed and returns error when the
+// mandatory input values are not present
+func (tgc *ToGCECmd) validateInput() error {
+ var errMsgs []string
+ if tgc.GcsBucket == "" {
+ errMsgs = append(errMsgs, "gcs-bucket is mandatory")
+ }
+ if tgc.InputURL == "" {
+ errMsgs = append(errMsgs, "input-url is mandatory")
+ }
+ if tgc.ImageName == "" {
+ errMsgs = append(errMsgs, "image-name is mandatory")
+ }
+ if tgc.ImageProject == "" {
+ errMsgs = append(errMsgs, "image-project is mandatory")
+ }
+ if len(errMsgs) > 0 {
+ return errors.New(strings.Join(errMsgs, ";"))
+ }
+ return nil
+}
diff --git a/src/pkg/fs/BUILD.bazel b/src/pkg/fs/BUILD.bazel
index 91d5218..c12f230 100644
--- a/src/pkg/fs/BUILD.bazel
+++ b/src/pkg/fs/BUILD.bazel
@@ -17,6 +17,7 @@
go_library(
name = "fs",
srcs = [
+ "archiver.go",
"build_context.go",
"copy.go",
"file_system.go",
diff --git a/src/pkg/fs/archiver.go b/src/pkg/fs/archiver.go
new file mode 100644
index 0000000..64dbbff
--- /dev/null
+++ b/src/pkg/fs/archiver.go
@@ -0,0 +1,125 @@
+package fs
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "cos.googlesource.com/cos/tools.git/src/pkg/utils"
+)
+
+const gzipFileExt = ".gz"
+
+// TarFile compresses the file at src to dst.
+func TarFile(src, dst string) error {
+ args := []string{"cf", dst}
+ dirPath := filepath.Dir(src)
+ baseName := filepath.Base(src)
+ // Add the compression type based on the dst, if the
+ // file type is not supported tar using default compression.
+ if filepath.Ext(dst) == gzipFileExt {
+ args = append(args, "-I", "/bin/gzip")
+ }
+ // add inputFilePath args
+ args = append(args, "-C", dirPath, baseName)
+ cmd := exec.Command("tar", args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+// TarDir compresses the directory at root to dst.
+func TarDir(root, dst string) error {
+ args := []string{"cf", dst, "-C", root}
+ inputFiles, err := filepath.Glob(filepath.Join(root, "*"))
+ if err != nil {
+ return err
+ }
+ var relInputFiles []string
+ for _, path := range inputFiles {
+ relPath, err := filepath.Rel(root, path)
+ if err != nil {
+ return err
+ }
+ relInputFiles = append(relInputFiles, relPath)
+ }
+ if relInputFiles == nil {
+ relInputFiles = []string{"."}
+ }
+ args = append(args, relInputFiles...)
+ cmd := exec.Command("tar", args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+// ExtractFile decompresses the tar file at inputFile to destDir.
+func ExtractFile(inputFile, destDir string) error {
+ var reader io.Reader
+ fileReader, err := os.Open(inputFile)
+ if err != nil {
+ return err
+ }
+ defer utils.CheckClose(fileReader, "error closing the file reader", &err)
+ if filepath.Ext(inputFile) == ".gz" {
+ reader, err = gzip.NewReader(fileReader)
+ if err != nil {
+ return err
+ }
+ } else {
+ reader = fileReader
+ }
+ return extractFile(reader, destDir)
+}
+
+// extractFile decompresses the tar file reader at inputFile to destDir.
+func extractFile(reader io.Reader, destDir string) error {
+ // Open the file for read
+
+ // Use gzip to read from the file
+ tarReader := tar.NewReader(reader)
+ // Read the file sequentially
+ for {
+ fileHeader, err := tarReader.Next()
+ switch {
+ // If no more files are found, return
+ case err == io.EOF:
+ return nil
+ // Return if hit any error
+ case err != nil:
+ return err
+ // If next file's header is nil, just skip it.
+ case fileHeader == nil:
+ continue
+ }
+ // Create a target file locally
+ localTarget := filepath.Join(destDir, fileHeader.Name)
+ switch fileHeader.Typeflag {
+ case tar.TypeDir:
+ if err := os.MkdirAll(localTarget, 0755); err != nil {
+ return err
+ }
+ // This should be tar.TypeReg, e.g regular file.
+ default:
+ localDir := filepath.Dir(localTarget)
+ // Create a dir if it doesn't exist but it should have created dir already.
+ if err := os.MkdirAll(localDir, 0755); err != nil {
+ return err
+ }
+ localFile, err := os.Create(localTarget)
+ if err != nil {
+ return err
+ }
+ // Copy over the contents.
+ if _, err = io.Copy(localFile, tarReader); err != nil {
+ localFile.Close()
+ return err
+ }
+ localFile.Close()
+ }
+ }
+ return nil
+}
diff --git a/src/pkg/fs/build_context.go b/src/pkg/fs/build_context.go
index 44e0269..b531ea2 100644
--- a/src/pkg/fs/build_context.go
+++ b/src/pkg/fs/build_context.go
@@ -19,43 +19,9 @@
"fmt"
"io"
"os"
- "os/exec"
"path/filepath"
)
-func tarFile(src, dst string) error {
- dirPath := filepath.Dir(src)
- baseName := filepath.Base(src)
- cmd := exec.Command("tar", "cf", dst, "-C", dirPath, baseName)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- return cmd.Run()
-}
-
-func tarDir(root, dst string) error {
- args := []string{"cf", dst, "-C", root}
- inputFiles, err := filepath.Glob(filepath.Join(root, "*"))
- if err != nil {
- return err
- }
- var relInputFiles []string
- for _, path := range inputFiles {
- relPath, err := filepath.Rel(root, path)
- if err != nil {
- return err
- }
- relInputFiles = append(relInputFiles, relPath)
- }
- if relInputFiles == nil {
- relInputFiles = []string{"."}
- }
- args = append(args, relInputFiles...)
- cmd := exec.Command("tar", args...)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- return cmd.Run()
-}
-
// CreateBuildContextArchive creates a tar archive of the given build context.
func CreateBuildContextArchive(src, dst string) error {
if _, err := os.Stat(dst); !os.IsNotExist(err) {
@@ -70,9 +36,9 @@
}
switch {
case info.IsDir():
- return tarDir(src, dst)
+ return TarDir(src, dst)
case info.Mode().IsRegular():
- return tarFile(src, dst)
+ return TarFile(src, dst)
default:
return fmt.Errorf("input path %s is neither a directory nor a regular file", src)
}
diff --git a/src/pkg/gce/gce.go b/src/pkg/gce/gce.go
index a71b320..2205f64 100644
--- a/src/pkg/gce/gce.go
+++ b/src/pkg/gce/gce.go
@@ -35,6 +35,7 @@
const (
defaultOperationTimeout = time.Duration(600) * time.Second
defaultRetryInterval = time.Duration(5) * time.Second
+ gcsURLPrefix = "https://storage.googleapis.com"
)
type timePkg struct {
@@ -152,6 +153,23 @@
return true, nil
}
+// CreateImage creates an image with imageName with the source-url from gcs storage
+func CreateImage(svc *compute.Service, sourceURL, imageName, imageProject string) error {
+ gcsImageURL := fmt.Sprintf("%s/%s", gcsURLPrefix, sourceURL)
+ image := &compute.Image{
+ Name: imageName,
+ RawDisk: &compute.ImageRawDisk{
+ Source: gcsImageURL,
+ },
+ }
+ createImageOp, err := svc.Images.Insert(imageProject, image).Do()
+ if err != nil {
+ return err
+ }
+ deadline := time.Now().Add(defaultOperationTimeout)
+ return waitForOp(svc, imageProject, createImageOp, deadline, realTime)
+}
+
type decodedImageName struct {
name string
milestone int
diff --git a/src/pkg/gcs/gcs_client.go b/src/pkg/gcs/gcs_client.go
new file mode 100644
index 0000000..df833a0
--- /dev/null
+++ b/src/pkg/gcs/gcs_client.go
@@ -0,0 +1,93 @@
+// Copyright 2021 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gcs
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+ "strings"
+
+ "cloud.google.com/go/storage"
+)
+
+const schemeGCS = "gs"
+
+// DownloadGCSObject downloads the object at inputURL and saves it at destinationPath
+func DownloadGCSObject(ctx context.Context,
+ gcsClient *storage.Client, inputURL, destinationPath string) error {
+ gcsBucket, name, err := getGCSVariables(inputURL)
+ if err != nil {
+ return err
+ }
+ r, err := gcsClient.Bucket(gcsBucket).Object(name).NewReader(ctx)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+
+ f, err := os.Create(destinationPath)
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(f, r); err != nil {
+ return fmt.Errorf("error copying file from gcs bucket: %v", err)
+ }
+ return nil
+}
+
+// UploadGCSObject uploads an object at inputPath to destination URL
+func UploadGCSObject(ctx context.Context,
+ gcsClient *storage.Client, inputPath, destinationURL string) error {
+
+ gcsBucket, name, err := getGCSVariables(destinationURL)
+ if err != nil {
+ return fmt.Errorf("error parsing destination URL: %v", err)
+ }
+ fileReader, err := os.Open(inputPath)
+ if err != nil {
+ return err
+ }
+
+ w := gcsClient.Bucket(gcsBucket).Object(name).NewWriter(ctx)
+ defer w.Close()
+
+ if _, err := io.Copy(w, fileReader); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DeleteGCSObject deletes an object at the input URL
+func DeleteGCSObject(ctx context.Context,
+ gcsClient *storage.Client, inputURL string) error {
+ gcsBucket, name, err := getGCSVariables(inputURL)
+ if err != nil {
+ return fmt.Errorf("error parsing input URL: %v", err)
+ }
+ return gcsClient.Bucket(gcsBucket).Object(name).Delete(ctx)
+}
+
+// Returns the getGCSVariables(GCSBucket, GCSPath, fileName) based on the input.
+func getGCSVariables(gcsPath string) (string, string, error) {
+ url, err := url.Parse(gcsPath)
+ if err != nil || url.Scheme != schemeGCS {
+ return "", "", fmt.Errorf("error parsing the input GCS path: %s", gcsPath)
+ }
+ // url.EscapedPath returns with the leading /.
+ return url.Hostname(), strings.TrimLeft(url.EscapedPath(), "/"), nil
+}
diff --git a/src/pkg/ovaconverter/converter.go b/src/pkg/ovaconverter/converter.go
new file mode 100644
index 0000000..16bb6a9
--- /dev/null
+++ b/src/pkg/ovaconverter/converter.go
@@ -0,0 +1,199 @@
+// Copyright 2021 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ovaconverter
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "cloud.google.com/go/storage"
+ "github.com/golang/glog"
+ "google.golang.org/api/compute/v1"
+
+ "cos.googlesource.com/cos/tools.git/src/pkg/fs"
+ "cos.googlesource.com/cos/tools.git/src/pkg/gce"
+ "cos.googlesource.com/cos/tools.git/src/pkg/gcs"
+ "cos.googlesource.com/cos/tools.git/src/pkg/utils"
+)
+
+const (
+ vmdkFileExtension = ".vmdk"
+)
+
+type Converter struct {
+ GCSClient *storage.Client
+ ComputeService *compute.Service
+}
+
+func NewConverter(ctx context.Context) *Converter {
+ gcsClient, err := storage.NewClient(ctx)
+ if err != nil {
+ return nil
+ }
+ svc, err := compute.NewService(ctx)
+ if err != nil {
+ return nil
+ }
+ return &Converter{
+ GCSClient: gcsClient,
+ ComputeService: svc,
+ }
+}
+
+// ConvertOVAToGCE converts the OVA file at GCS Location to a GCE image.
+func (converter *Converter) ConvertOVAToGCE(ctx context.Context, inputURL, imageName, gcsBucket, imageProject string) error {
+ // Create a temporary working directory
+ tempWorkDir, err := os.MkdirTemp("", "ova_dir")
+ if err != nil {
+ return err
+ }
+ defer utils.RemoveDir(tempWorkDir, "error on removing the temporary working directory", nil)
+
+ glog.Info("Downloading OVA from the input GCS URL")
+ inputFile := filepath.Join(tempWorkDir, "input.ova")
+ if err = gcs.DownloadGCSObject(ctx, converter.GCSClient,
+ inputURL, inputFile); err != nil {
+ return err
+ }
+
+ extractWorkDir := filepath.Join(tempWorkDir, "extractWorkDir")
+ glog.Info("Converting OVA to VMDK...")
+ if err = fs.ExtractFile(inputFile, extractWorkDir); err != nil {
+ return err
+ }
+
+ var vmdkFile string
+ files, _ := ioutil.ReadDir(extractWorkDir)
+ for _, file := range files {
+ if filepath.Ext(file.Name()) == vmdkFileExtension {
+ vmdkFile = file.Name()
+ break
+ }
+ }
+
+ glog.Info("Converting VMDK to Raw...")
+ tempRawImage := filepath.Join(tempWorkDir, "disk.raw")
+ if err = utils.ConvertImageToRaw(filepath.Join(extractWorkDir,
+ vmdkFile), tempRawImage); err != nil {
+ return err
+ }
+
+ glog.Info("Compressing disk.raw to tar.gz...")
+ if err = fs.TarFile(tempRawImage, filepath.Join(tempWorkDir,
+ "cos_gce.tar.gz")); err != nil {
+ return err
+ }
+
+ cosGCETar := "cos_gce.tar.gz"
+ cosTarURL := fmt.Sprintf("gs://%s/%s", gcsBucket, cosGCETar)
+
+ glog.Info("Uploading tar.gz file to a remote GCS location...")
+ if err = gcs.UploadGCSObject(ctx, converter.GCSClient, filepath.Join(tempWorkDir, cosGCETar), cosTarURL); err != nil {
+ return err
+ }
+ // delete the image staged temporarily before creating a GCE image
+ defer func() {
+ if gcs.DeleteGCSObject(ctx, converter.GCSClient, cosTarURL); err != nil {
+ glog.Warningf("error deleting the GCS temporary Object: %v", err)
+ }
+ }()
+
+ glog.Info("Creating a GCS Image...")
+ return gce.CreateImage(converter.ComputeService, filepath.Join(gcsBucket, cosGCETar),
+ imageName, imageProject)
+
+}
+
+// ConvertOVAFromGCE converts GCE Image to OVA Format.
+func (converter *Converter) ConvertOVAFromGCE(ctx context.Context, sourceImage, destinationPath, gcsBucket, imageProject, zone string,
+ gceToOVAConverterConfig *GCEToOVAConverterConfig) error {
+ tempWorkDir, err := ioutil.TempDir("", "ovaDir")
+ if err != nil {
+ return err
+ }
+ defer utils.RemoveDir(tempWorkDir, "error on removing the temporary working directory", nil)
+
+ tempExportedImageName := "cos-exported-image.tar.gz"
+ tempExportImageURL := fmt.Sprintf("gs://%s/%s", gcsBucket, tempExportedImageName)
+
+ glog.Info("Exporting image in tar.gz format to a temporary GCS location")
+ // Export the GCE image to a temporary location
+ if err = exportImageFromGCEUsingDaisy(sourceImage, imageProject, tempExportImageURL, zone,
+ gceToOVAConverterConfig.DaisyBin, gceToOVAConverterConfig.DaisyWorkflowPath); err != nil {
+ return err
+ }
+
+ glog.Info("Downloading the image in tar.gz...")
+ downloadImagePath := filepath.Join(tempWorkDir, tempExportedImageName)
+ if err = gcs.DownloadGCSObject(ctx, converter.GCSClient,
+ tempExportImageURL, downloadImagePath); err != nil {
+ return err
+ }
+
+ defer func() {
+ if err = gcs.DeleteGCSObject(ctx, converter.GCSClient, tempExportImageURL); err != nil {
+ glog.Warningf("error deleting the GCS temporary Object: %v", err)
+
+ }
+ }()
+
+ glog.Info("Extracting the disk.raw image from the tar file...")
+ extractWorkDir := filepath.Join(tempWorkDir, "extractDir")
+ if err = fs.ExtractFile(downloadImagePath, extractWorkDir); err != nil {
+ return err
+ }
+
+ glog.Info("Converting the disk.raw image to vmdk format...")
+ tempVMDKImageName := filepath.Join(tempWorkDir, "chromiumos_image.vmdk")
+ if err = utils.ConvertImageToVMDK(filepath.Join(extractWorkDir, "disk.raw"), tempVMDKImageName); err != nil {
+ return err
+ }
+
+ // convert to OVA image
+ glog.Info("Converting the VMDK to OVA image...")
+ tempOVAImage := filepath.Join(tempWorkDir, filepath.Base(destinationPath))
+ oVAImageName := strings.ReplaceAll(filepath.Base(destinationPath),
+ filepath.Ext(filepath.Base(destinationPath)), "")
+ if err = utils.RunCommand([]string{
+ gceToOVAConverterConfig.MakeOVAScript,
+ "-d", tempVMDKImageName, "-o", tempOVAImage, "-p", "GKE On-Prem", "-n",
+ oVAImageName, "-t", gceToOVAConverterConfig.OVATemplate,
+ }, "", nil); err != nil {
+ return err
+ }
+
+ glog.Info("Uploading the OVA file to the GCS URL...")
+ if err = gcs.UploadGCSObject(ctx, converter.GCSClient, tempOVAImage,
+ destinationPath); err != nil {
+ return err
+ }
+ return nil
+}
+
+// exportImageFromGCEUsingDaisy exports an image to the gce.tar.gz file by initiating a
+// daisy workflow.
+// Input: daisyBin - path to the daisy binary, daisyWorkflowPath - path to the image exporter workflow.
+func exportImageFromGCEUsingDaisy(imageName, imageProject, destinationFile, zone, daisyBin, daisyWorkflowPath string) error {
+ sourceImage := fmt.Sprintf("-var:source_image=projects/%s/global/images/%s", imageProject, imageName)
+ destination := fmt.Sprintf("-var:destination=%s", destinationFile)
+ exportImageUsingDaisyCommand := []string{
+ daisyBin, "-project", imageProject, "-zone", zone, sourceImage, destination, daisyWorkflowPath,
+ }
+ return utils.RunCommand(exportImageUsingDaisyCommand, "", nil)
+}
diff --git a/src/pkg/ovaconverter/ova_converter_config.go b/src/pkg/ovaconverter/ova_converter_config.go
new file mode 100644
index 0000000..f5114b1
--- /dev/null
+++ b/src/pkg/ovaconverter/ova_converter_config.go
@@ -0,0 +1,44 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ovaconverter
+
+const (
+ // makeOVAScriptPath is the location of make_ova.sh script
+ makeOVAScriptPath = "/crosutils/cos/make_ova.sh"
+ // ovaTemplate is the location of template used for OVA
+ ovaTemplatePath = "/crosutils/cos/template.ovf"
+ // daisyBin is the location of the Daisy binary.
+ daisyBin = "/daisy"
+ // daisyWorkflowPath is the location to the image_export.wf.json workflow.
+ daisyWorkflowPath = "/workflows/export/image_export.wf.json"
+)
+
+// GCEToOVAConverterConfig holds the required configuration about the
+// daisy bin path, make OVA script path and template path
+type GCEToOVAConverterConfig struct {
+ DaisyBin string
+ MakeOVAScript string
+ OVATemplate string
+ DaisyWorkflowPath string
+}
+
+func GetDefaultOVAConverterConfig() *GCEToOVAConverterConfig {
+ return &GCEToOVAConverterConfig{
+ DaisyBin: daisyBin,
+ MakeOVAScript: makeOVAScriptPath,
+ OVATemplate: ovaTemplatePath,
+ DaisyWorkflowPath: daisyWorkflowPath,
+ }
+}
diff --git a/src/pkg/utils/qemu_converter.go b/src/pkg/utils/qemu_converter.go
new file mode 100644
index 0000000..d31ae4f
--- /dev/null
+++ b/src/pkg/utils/qemu_converter.go
@@ -0,0 +1,37 @@
+// Copyright 2021 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package utils
+
+// ConvertImageToVMDK converts the image at inputFile to vmdk format
+// and saves it at destFile
+func ConvertImageToVMDK(inputFile, destFile string) error {
+ vmdkConvertCmd := []string{
+ "qemu-img", "convert", "-O", "vmdk", "-o", "subformat=streamOptimized",
+ inputFile,
+ destFile,
+ }
+ return RunCommand(vmdkConvertCmd, "", nil)
+}
+
+// ConvertImageToRaw converts the image at inputFile to raw format
+// and saves it at destFile
+func ConvertImageToRaw(inputFile, destFile string) error {
+ rawConvertCommand := []string{
+ "qemu-img", "convert", "-O", "raw",
+ inputFile,
+ destFile,
+ }
+ return RunCommand(rawConvertCommand, "", nil)
+}
diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go
index bc975d5..7bc9126 100644
--- a/src/pkg/utils/utils.go
+++ b/src/pkg/utils/utils.go
@@ -31,9 +31,8 @@
"syscall"
"time"
- "github.com/pkg/errors"
-
"github.com/golang/glog"
+ "github.com/pkg/errors"
)
var (
@@ -379,6 +378,24 @@
}
}
+// RemoveDir removes the directory at inputPath and checks its error. Useful for checking the
+// errors on deferred remove().
+func RemoveDir(inputPath string, errMsgOnRemove string, err *error) {
+ if removeErr := os.RemoveAll(inputPath); removeErr != nil {
+ var fullErr error
+ if errMsgOnRemove != "" {
+ fullErr = fmt.Errorf("%s: %v", errMsgOnRemove, fullErr)
+ } else {
+ fullErr = removeErr
+ }
+ if *err == nil {
+ *err = fullErr
+ } else {
+ log.Println(fullErr)
+ }
+ }
+}
+
func runCommand(args []string, dir string, env []string) error {
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout