| package input | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "io/ioutil" | |
| "log" | |
| "net/http" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "strings" | |
| "cos.googlesource.com/cos/tools.git/src/cmd/cos_image_analyzer/internal/utilities" | |
| ) | |
| const gcsObjFormat = ".tar.gz" | |
| const makeDirFilemode = 0700 | |
| const timeOut = "7200s" | |
| const imageFormat = "vmdk" | |
| const name = "gcr.io/compute-image-tools/gce_vm_image_export:release" | |
| const pathToKernelConfigs = "usr/src/linux-headers-4.19.112+/.config" | |
| const pathToSysctlSettings = "/etc/sysctl.d/00-sysctl.conf" // Located in partition 3 Root-A | |
| // ImageInfo stores all relevant information on a COS image | |
| type ImageInfo struct { | |
| // Input Overhead | |
| TempDir string // Temporary directory holding the mounted image and disk file | |
| DiskFile string // Path to the DOS/MBR disk partition file | |
| StatePartition1 string // Path to mounted directory of partition #1, stateful partition | |
| RootfsPartition3 string // Path to mounted directory of partition #3, Rootfs-A | |
| EFIPartition12 string // Path to mounted directory of partition #12, EFI-System | |
| LoopDevice1 string // Active loop device for mounted image | |
| LoopDevice3 string // Active loop device for mounted image | |
| LoopDevice12 string // Active loop device for mounted image | |
| // Binary info | |
| Version string // Major cos version | |
| BuildID string // Minor cos version | |
| PartitionFile string // Path to the file storing the disk partition structure from "sgdisk" | |
| SysctlSettingsFile string // Path to the /etc/sysctrl.d/00-sysctl.conf file of an image | |
| KernelCommandLine string // The kernel command line boot-time parameters stored in partition 12 efi/boot/grub.cfg | |
| KernelConfigsFile string // Path to the ".config" file downloaded from GCS that holds a build's kernel configs | |
| } | |
| // Rename temporary directory and its contents once Version and BuildID are known | |
| func (image *ImageInfo) Rename(flagInfo *FlagInfo) error { | |
| if image.Version != "" && image.BuildID != "" { | |
| fullImageName := "cos-" + image.Version + "-" + image.BuildID | |
| if err := os.Rename(image.TempDir, fullImageName); err != nil { | |
| return fmt.Errorf("failed to rename directory %v to %v: %v", image.TempDir, fullImageName, err) | |
| } | |
| image.TempDir = fullImageName | |
| if !flagInfo.LocalPtr { | |
| image.DiskFile = filepath.Join(fullImageName, "disk.raw") | |
| } | |
| if image.StatePartition1 != "" { | |
| image.StatePartition1 = filepath.Join(fullImageName, "stateful") | |
| } | |
| if image.RootfsPartition3 != "" { | |
| image.RootfsPartition3 = filepath.Join(fullImageName, "rootfs") | |
| } | |
| if image.EFIPartition12 != "" { | |
| image.EFIPartition12 = filepath.Join(fullImageName, "efi") | |
| } | |
| image.PartitionFile = filepath.Join(fullImageName, "partitions.txt") | |
| image.KernelConfigsFile = filepath.Join(fullImageName, pathToKernelConfigs) | |
| image.SysctlSettingsFile = filepath.Join(image.RootfsPartition3, pathToSysctlSettings) | |
| } | |
| return nil | |
| } | |
| // MountImage is an ImagInfo method that mounts partitions 1,3 and 12 of | |
| // the image into the temporary directory | |
| // Input: | |
| // (string) arr - List of binary types selected from the user | |
| // Output: nil on success, else error | |
| func (image *ImageInfo) MountImage(arr []string) error { | |
| if image.TempDir == "" { | |
| return nil | |
| } | |
| if utilities.InArray("Stateful-partition", arr) { | |
| stateful := filepath.Join(image.TempDir, "stateful") | |
| if err := os.Mkdir(stateful, makeDirFilemode); err != nil { | |
| return fmt.Errorf("failed to create make directory %v: %v", stateful, err) | |
| } | |
| image.StatePartition1 = stateful | |
| loopDevice1, err := utilities.MountDisk(image.DiskFile, image.StatePartition1, "1") | |
| if err != nil { | |
| return fmt.Errorf("Failed to mount %v's partition #1 onto %v: %v", image.DiskFile, image.StatePartition1, err) | |
| } | |
| image.LoopDevice1 = loopDevice1 | |
| } | |
| if utilities.InArray("Version", arr) || utilities.InArray("BuildID", arr) || utilities.InArray("Rootfs", arr) || utilities.InArray("Sysctl-settings", arr) || utilities.InArray("OS-config", arr) || utilities.InArray("Kernel-configs", arr) { | |
| rootfs := filepath.Join(image.TempDir, "rootfs") | |
| if err := os.Mkdir(rootfs, makeDirFilemode); err != nil { | |
| return fmt.Errorf("failed to create make directory %v: %v", rootfs, err) | |
| } | |
| image.RootfsPartition3 = rootfs | |
| loopDevice3, err := utilities.MountDisk(image.DiskFile, image.RootfsPartition3, "3") | |
| if err != nil { | |
| return fmt.Errorf("Failed to mount %v's partition #3 onto %v: %v", image.DiskFile, image.RootfsPartition3, err) | |
| } | |
| image.LoopDevice3 = loopDevice3 | |
| } | |
| if utilities.InArray("Kernel-command-line", arr) { | |
| efi := filepath.Join(image.TempDir, "efi") | |
| if err := os.Mkdir(efi, makeDirFilemode); err != nil { | |
| return fmt.Errorf("failed to create make directory %v: %v", efi, err) | |
| } | |
| image.EFIPartition12 = efi | |
| loopDevice12, err := utilities.MountDisk(image.DiskFile, image.EFIPartition12, "12") | |
| if err != nil { | |
| return fmt.Errorf("Failed to mount %v's partition #12 onto %v: %v", image.DiskFile, image.EFIPartition12, err) | |
| } | |
| image.LoopDevice12 = loopDevice12 | |
| } | |
| return nil | |
| } | |
| // GetGcsImage is an ImagInfo method that calls the GCS client api to | |
| // download a COS image from a GCS bucket, unzips it, and mounts relevant | |
| // partitions. ADC is used for authorization | |
| // Input: | |
| // (string) gcsPath - GCS "bucket/object" path for stored COS Image (.tar.gz file) | |
| // Output: nil on success, else error | |
| func (image *ImageInfo) GetGcsImage(gcsPath string) error { | |
| if gcsPath == "" { | |
| return nil | |
| } | |
| var gcsBucket, gcsObject string | |
| if startOfBucket := strings.Index(gcsPath, "gs://"); startOfBucket < len(gcsPath)-5 { | |
| gcsPath = gcsPath[startOfBucket+5:] | |
| } else { | |
| printUsage() | |
| return errors.New("Error: Argument " + gcsPath + " is not a valid gcs path \"gs://<bucket>/<object_path>.tar.gz\"") | |
| } | |
| if startOfObject := strings.Index(gcsPath, "/"); startOfObject > 0 && startOfObject < len(gcsPath)-1 { | |
| gcsBucket = gcsPath[:startOfObject] | |
| gcsObject = gcsPath[startOfObject+1:] | |
| } else { | |
| printUsage() | |
| return errors.New("Error: Argument " + gcsPath + " is not a valid gcs path \"gs://<bucket>/<object_path>.tar.gz\"") | |
| } | |
| tempDir, err := ioutil.TempDir(".", "tempDir") // Removed at end | |
| if err != nil { | |
| return fmt.Errorf("failed to create temporary directory: %v", err) | |
| } | |
| image.TempDir = tempDir | |
| tarFile, err := utilities.GcsDowndload(gcsBucket, gcsObject, image.TempDir, filepath.Base(gcsObject), true) | |
| if err != nil { | |
| return fmt.Errorf("failed to download GCS object %v from bucket %v: %v", gcsObject, gcsBucket, err) | |
| } | |
| _, err = exec.Command("tar", "-xzf", tarFile, "-C", image.TempDir).Output() | |
| if err != nil { | |
| return fmt.Errorf("failed to unzip %v into %v: %v", tarFile, image.TempDir, err) | |
| } | |
| image.DiskFile = filepath.Join(image.TempDir, "disk.raw") | |
| return nil | |
| } | |
| // GetLocalImage is an ImageInfo method that creates a temporary directory | |
| // to loop device mount the disk.raw file stored on the local file system | |
| // Input: | |
| // (string) localPath - Local path to the disk.raw file | |
| // Output: nil on success, else error | |
| func (image *ImageInfo) GetLocalImage(localPath string) error { | |
| if localPath == "" { | |
| return nil | |
| } | |
| image.DiskFile = localPath | |
| tempDir, err := ioutil.TempDir(".", "tempDir") // Removed at end | |
| if err != nil { | |
| return fmt.Errorf("failed to create temporary directory: %v", err) | |
| } | |
| image.TempDir = tempDir | |
| return nil | |
| } | |
| // steps holds GCE payload meta data | |
| type steps struct { | |
| Args [5]string `json:"args"` | |
| Name string `json:"name"` | |
| Env [1]string `json:"env"` | |
| } | |
| // gcePayload holds GCE's rest API payload | |
| type gcePayload struct { | |
| Timeout string `json:"timeout"` | |
| Steps [1]steps `json:"steps"` | |
| Tags [2]string `json:"tags"` | |
| } | |
| // gceExport calls the cloud build REST api that exports a public compute | |
| // image to a specific GCS bucket. | |
| // Input: | |
| // (string) projectID - project ID of the cloud project holding the image | |
| // (string) bucket - name of the GCS bucket holding the COS Image | |
| // (string) image - name of the source image to be exported | |
| // Output: nil on success, else error | |
| func gceExport(projectID, bucket, image string) error { | |
| // API Variables | |
| gceURL := "https://cloudbuild.googleapis.com/v1/projects/" + projectID + "/builds" | |
| destURI := "gs://" + bucket + "/" + image + "." + imageFormat | |
| args := [5]string{"-timeout=" + timeOut, "-source_image=" + image, "-client_id=api", "-format=" + imageFormat, "-destination_uri=" + destURI} | |
| env := [1]string{"BUILD_ID=$BUILD_ID"} | |
| tags := [2]string{"gce-daisy", "gce-daisy-image-export"} | |
| // Build API bodies | |
| steps := [1]steps{{Args: args, Name: name, Env: env}} | |
| payload := gcePayload{ | |
| Timeout: timeOut, | |
| Steps: steps, | |
| Tags: tags} | |
| requestBody, err := json.Marshal(payload) | |
| if err != nil { | |
| return fmt.Errorf("failed to json marshal GCE payload: %v", err) | |
| } | |
| log.Println(string(requestBody)) | |
| resp, err := http.Post(gceURL, "application/json", bytes.NewBuffer(requestBody)) | |
| if err != nil { | |
| return fmt.Errorf("failed to make POST request: %v", err) | |
| } | |
| defer resp.Body.Close() | |
| body, err := ioutil.ReadAll(resp.Body) | |
| if err != nil { | |
| return fmt.Errorf("failed to read returned POST request: %v", err) | |
| } | |
| log.Println(string(body)) | |
| return nil | |
| } | |
| // GetCosImage calls the cloud build api to export a public COS image to a | |
| // a GCS bucket and then calls GetGcsImage() to download that image from GCS. | |
| // ADC is used for authorization. | |
| // Input: | |
| // (*ImageInfo) image - A struct that holds the relevent | |
| // CosCloudPath "bucket/image" and projectID for the stored COS Image | |
| // Output: nil on success, else error | |
| func (image *ImageInfo) GetCosImage(cosCloudPath, projectID string) error { | |
| if cosCloudPath == "" { | |
| return nil | |
| } | |
| cosArray := strings.Split(cosCloudPath, "/") | |
| if len(cosArray) != 2 { | |
| printUsage() | |
| return errors.New("Error: Argument " + cosCloudPath + " is not a valid cos-cloud path (\"/\" separators)") | |
| } | |
| gcsBucket := cosArray[0] | |
| publicCosImage := cosArray[1] | |
| if err := gceExport(projectID, gcsBucket, publicCosImage); err != nil { | |
| return fmt.Errorf("failed to export %v cos image to GCS bucket %v: %v", publicCosImage, gcsBucket, err) | |
| } | |
| gcsPath := filepath.Join(gcsBucket, publicCosImage, gcsObjFormat) | |
| if err := image.GetGcsImage(gcsPath); err != nil { | |
| return fmt.Errorf("failed to download image stored on GCS for %v: %v", gcsPath, err) | |
| } | |
| return nil | |
| } | |
| // Cleanup is a ImageInfo method that removes a mounted directory & loop device | |
| // Input: | |
| // (*ImageInfo) image - A struct that holds the relevent info to clean up | |
| // Output: nil on success, else error | |
| func (image *ImageInfo) Cleanup() error { | |
| if image.TempDir == "" { | |
| return nil | |
| } | |
| if image.LoopDevice1 != "" { | |
| if err := utilities.Unmount(image.StatePartition1, image.LoopDevice1); err != nil { | |
| return fmt.Errorf("failed to unmount mount directory %v and/or loop device %v: %v", image.StatePartition1, image.LoopDevice1, err) | |
| } | |
| } | |
| if image.LoopDevice3 != "" { | |
| if err := utilities.Unmount(image.RootfsPartition3, image.LoopDevice3); err != nil { | |
| return fmt.Errorf("failed to unmount mount directory %v and/or loop device %v: %v", image.RootfsPartition3, image.LoopDevice3, err) | |
| } | |
| } | |
| if image.LoopDevice12 != "" { | |
| if err := utilities.Unmount(image.EFIPartition12, image.LoopDevice12); err != nil { | |
| return fmt.Errorf("failed to unmount mount directory %v and/or loop device %v: %v", image.EFIPartition12, image.LoopDevice12, err) | |
| } | |
| } | |
| if err := os.RemoveAll(image.TempDir); err != nil { | |
| return fmt.Errorf("failed to delete directory %v: %v", image.TempDir, err) | |
| } | |
| return nil | |
| } |