Skip to content

Commit 1a534bb

Browse files
committed
gce_install_cuttlefish_packages tool.
- Given a base image, create an amended one installing cuttlefish debian packages. Bug: b/441328183
1 parent 1392f42 commit 1a534bb

3 files changed

Lines changed: 226 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright (C) 2025 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"flag"
19+
"fmt"
20+
"log"
21+
"path/filepath"
22+
"time"
23+
24+
"github.com/google/android-cuttlefish/tools/baseimage/pkg/gce"
25+
"github.com/google/android-cuttlefish/tools/baseimage/pkg/gce/scripts"
26+
)
27+
28+
const (
29+
outImageName = "amended-image"
30+
)
31+
32+
var (
33+
project = flag.String("project", "", "GCP project whose resources will be used for creating the amended image")
34+
zone = flag.String("zone", "us-central1-a", "GCP zone used for creating relevant resources")
35+
source_image_project = flag.String("source-image-project", "", "Source image GCP project")
36+
source_image = flag.String("source-image", "", "Source image name")
37+
cuttlefish_debs_zip_source = flag.String("cuttlefish-debs-zip-source", "", "Local path to zip file containing cuttlefish debian packages")
38+
)
39+
40+
type amendImageOpts struct {
41+
SourceImageProject string
42+
SourceImage string
43+
CuttlefishDebsZipSource string
44+
}
45+
46+
func installCuttlefishDebs(project, zone, insName, zipSrc string) error {
47+
dstSrc := "/tmp/" + filepath.Base(zipSrc)
48+
if err := gce.UploadFile(project, zone, insName, zipSrc, dstSrc); err != nil {
49+
return fmt.Errorf("error uploading %s: %v", zipSrc, err)
50+
}
51+
if err := gce.UploadBashScript(project, zone, insName, "install.sh", scripts.InstallCuttlefishPackages); err != nil {
52+
return fmt.Errorf("error uploading script: %v", err)
53+
}
54+
if err := gce.RunCmd(project, zone, insName, "./install.sh "+dstSrc); err != nil {
55+
return err
56+
}
57+
return nil
58+
}
59+
60+
func cleanupDeleteDisk(h *gce.GceHelper, disk string) {
61+
log.Printf("cleanup: deleting disk %q...", disk)
62+
if err := h.DeleteDisk(disk); err != nil {
63+
log.Printf("cleanup: error deleting disk: %v", err)
64+
} else {
65+
log.Println("cleanup: disk deleted")
66+
}
67+
}
68+
69+
func cleanupDeleteInstance(h *gce.GceHelper, ins string) {
70+
log.Printf("cleanup: deleting instance %q...", ins)
71+
if err := h.DeleteInstance(ins); err != nil {
72+
log.Printf("cleanup: error deleting instance: %v", err)
73+
} else {
74+
log.Println("cleanup: instance deleted")
75+
}
76+
}
77+
78+
func cleanupDetachDisk(h *gce.GceHelper, ins, disk string) {
79+
log.Printf("cleanup: detaching disk %q from instance %q...", ins, disk)
80+
if err := h.DetachDisk(ins, disk); err != nil {
81+
log.Printf("cleanup: error detaching disk: %v", err)
82+
} else {
83+
log.Println("cleanup: disk detached")
84+
}
85+
}
86+
87+
func amendImageMain(project, zone string, opts amendImageOpts) error {
88+
h, err := gce.NewGceHelper(project, zone)
89+
if err != nil {
90+
return fmt.Errorf("failed to create GCE helper: %w", err)
91+
}
92+
insName := outImageName
93+
attachedDiskName := fmt.Sprintf("%s-attached-disk", insName)
94+
95+
log.Println("creating disk...")
96+
_, err = h.CreateDisk(opts.SourceImageProject, opts.SourceImage, attachedDiskName)
97+
if err != nil {
98+
return fmt.Errorf("failed to create disk: %w", err)
99+
}
100+
log.Printf("disk created: %q", attachedDiskName)
101+
defer cleanupDeleteDisk(h, attachedDiskName)
102+
103+
log.Println("creating instance...")
104+
_, err = h.CreateInstance(insName)
105+
if err != nil {
106+
return fmt.Errorf("failed to create instance: %w", err)
107+
}
108+
log.Printf("instance created: %q", insName)
109+
defer cleanupDeleteInstance(h, insName)
110+
111+
log.Println("attaching disk...")
112+
if err := h.AttachDisk(insName, attachedDiskName); err != nil {
113+
log.Fatalf("failed to attach disk %q to instance %q: %v", attachedDiskName, insName, err)
114+
}
115+
log.Println("disk attached")
116+
defer cleanupDetachDisk(h, insName, attachedDiskName)
117+
118+
if err := gce.WaitForInstance(project, zone, insName); err != nil {
119+
return fmt.Errorf("waiting for instance error: %v", err)
120+
}
121+
122+
installCuttlefishDebs(project, zone, insName, opts.CuttlefishDebsZipSource)
123+
124+
// Reboot the instance to force a clean umount of the attached disk's file system.
125+
if err := gce.RunCmd(project, zone, insName, "sudo reboot"); err != nil {
126+
return err
127+
}
128+
time.Sleep(2 * time.Minute)
129+
if err := gce.WaitForInstance(project, zone, insName); err != nil {
130+
return fmt.Errorf("waiting for instance error: %v", err)
131+
}
132+
log.Printf("deleting instance %q...", insName)
133+
if err := h.StopInstance(insName); err != nil {
134+
return fmt.Errorf("error deleting instance: %v", err)
135+
}
136+
log.Println("instance deleted")
137+
138+
log.Printf("creating image %q...", outImageName)
139+
if err := h.CreateImage(insName, attachedDiskName, outImageName); err != nil {
140+
return fmt.Errorf("failed to create image: %w", err)
141+
}
142+
log.Println("image created")
143+
return nil
144+
}
145+
146+
func main() {
147+
flag.Parse()
148+
149+
if *project == "" {
150+
log.Fatal("usage: `-project` must not be empty")
151+
}
152+
if *zone == "" {
153+
log.Fatal("usage: `-zone` must not be empty")
154+
}
155+
if *source_image_project == "" {
156+
log.Fatal("usage: `-source-image-project` must not be empty")
157+
}
158+
if *source_image == "" {
159+
log.Fatal("usage: `-source_image` must not be empty")
160+
}
161+
if *cuttlefish_debs_zip_source == "" {
162+
log.Fatal("usage: `-cuttlefish-debs-zip-source` must not be empty")
163+
}
164+
165+
opts := amendImageOpts{
166+
SourceImageProject: *source_image_project,
167+
SourceImage: *source_image,
168+
CuttlefishDebsZipSource: *cuttlefish_debs_zip_source,
169+
}
170+
if err := amendImageMain(*project, *zone, opts); err != nil {
171+
log.Fatal(err)
172+
}
173+
fmt.Printf(`Copy the image somewhere else:
174+
gcloud compute images create \
175+
--source-image-project=%s \
176+
--source-image=%s \
177+
--project=[DEST_PROJECT] \
178+
--family=[DEST_IMAGE_FAMILY] [DEST_IMAGE_NAME]
179+
`,
180+
*project,
181+
outImageName,
182+
)
183+
}

tools/baseimage/pkg/gce/helper.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ func UploadBashScript(project, zone, ins, scriptName, scriptContent string) erro
209209
return nil
210210
}
211211

212+
func UploadFile(project, zone, ins, src string, dst string) error {
213+
return runCmd("gcloud", "compute", "scp", "--project", project, "--zone", zone, src, ins+":"+dst)
214+
}
215+
212216
func RunCmd(project, zone, ins, cmd string) error {
213217
return runCmd("gcloud", "compute", "ssh", "--project", project, "--zone", zone, ins, "--command", cmd)
214218
}

tools/baseimage/pkg/gce/scripts/scripts.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,42 @@ chmod a+x NVIDIA-Linux-${nvidia_arch}-${nvidia_version}.run
165165
./NVIDIA-Linux-${nvidia_arch}-${nvidia_version}.run -x
166166
NVIDIA-Linux-${nvidia_arch}-${nvidia_version}/nvidia-installer --silent --no-install-compat32-libs --no-backup --no-wine-files --install-libglvnd --dkms -k "${kmodver}"
167167
`
168+
169+
const InstallCuttlefishPackages = `#!/usr/bin/env bash
170+
set -o errexit -o nounset -o pipefail
171+
172+
if [[ $# -eq 0 ]] ; then
173+
echo "usage: $0 /path/to/zip"
174+
exit 1
175+
fi
176+
177+
# Prepare chroot environment for attached disk.
178+
sudo mkdir -p /mnt/image
179+
sudo mount /dev/sdb1 /mnt/image
180+
sudo mount -t sysfs none /mnt/image/sys
181+
sudo mount -t proc none /mnt/image/proc
182+
sudo mount --bind /boot/efi /mnt/image/boot/efi
183+
sudo mount --bind /dev/ /mnt/image/dev
184+
sudo mount --bind /dev/pts /mnt/image/dev/pts
185+
sudo mount --bind /run /mnt/image/run
186+
if [ ! -f /mnt/image/etc/resolv.conf ]; then
187+
sudo cp /etc/resolv.conf /mnt/image/etc/
188+
fi
189+
190+
# Unzip
191+
sudo apt install -y unzip
192+
rm -rf /mnt/image/tmp/install
193+
unzip -d /mnt/image/tmp/install $1
194+
195+
readonly PKG_NAMES=("cuttlefish-base" "cuttlefish-user" "cuttlefish-orchestration")
196+
197+
# Install packages
198+
for name in "${PKG_NAMES[@]}"; do
199+
path=("/mnt/image/tmp/install/${name}"*.deb)
200+
path="${path[0]}"
201+
name=$(basename "${path}")
202+
echo "Installing: ${name}"
203+
204+
sudo chroot /mnt/image /usr/bin/apt install -y "/tmp/install/${name}"
205+
done
206+
`

0 commit comments

Comments
 (0)