| package input | |
| import ( | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "cos.googlesource.com/cos/tools.git/src/cmd/cos_image_analyzer/internal/utilities" | |
| ) | |
| // BinaryDiffTypes is a list of all valid binary differnce types | |
| var BinaryDiffTypes = []string{"Version", "BuildID", "Rootfs", "Kernel-command-line", "Stateful-partition", "Partition-structure", "Sysctl-settings", "OS-config", "Kernel-configs"} | |
| // Default Rootfs entires that are overridden by the "compress-rootfs" flag | |
| var defaultCompressRootfs = []string{"/bin/", "/lib/modules/", "/lib64/", "/usr/libexec/", "/usr/bin/", "/usr/sbin/", "/usr/lib64/", "/usr/share/zoneinfo/", "/usr/share/git/", "/usr/lib/", "/sbin/", "/etc/ssh/", "/etc/os-release/", "/etc/package_list/"} | |
| // Default Stateful entires that are overridden by the "compress-stateful" flag | |
| var defaultCompressStateful = []string{"/var_overlay/db/"} | |
| // Custom usage function. See -h flag | |
| func printUsage() { | |
| usageTemplate := `NAME | |
| cos_image_analyzer - finds all meaningful differences of two COS Images (binary and package differences). | |
| If only one image is passed in, its binary info and package info will be returned. | |
| SYNOPSIS | |
| %s [-local] FILE-1 [FILE-2] (default true) | |
| FILE - the local file path to the DOS/MBR boot sector file of your image (Ex: disk.raw) | |
| Ex: %s image-cos-77-12371-273-0/disk.raw image-cos-81-12871-119-0/disk.raw | |
| %s -local -binary=Sysctl-settings,OS-config -package=false image-cos-77-12371-273-0/disk.raw | |
| %s -gcs GCS-PATH-1 [GCS-PATH-2] | |
| GCS-PATH - the GCS "gs://bucket/object" path for the COS Image ("object" is type .tar.gz) | |
| Ex: %s -gcs gs://my-bucket/cos-images/cos-77-12371-273-0.tar.gz gs://my-bucket/cos-images/cos-81-12871-119-0.tar.gz | |
| DESCRIPTION | |
| Input Flags: | |
| -local (default true, flag is optional) | |
| input is one or two DOS/MBR disk file on the local filesystem. If the images are downloaded from | |
| Google Cloud as a tarball, decompress the tarball first then pass the disk.raw file to the program. | |
| -gcs | |
| input is one or two objects stored on Google Cloud Storage of type (.tar.gz). This flag temporarily downloads, | |
| unzips, and loop device mounts the images into this tool's directory. | |
| To download images from Google Cloud Storage, you need to pass a service account credential to the program. | |
| Folllow https://cloud.google.com/docs/authentication/production#create_service_account to create a service account and | |
| download the service account key. Then point environment variable GOOGLE_APPLICATION_CREDENTIALS to the key file then | |
| run the program. | |
| Difference Flags: | |
| -binary (string) | |
| specify which type of binary difference to show. Types "Version", "BuildID", "Kernel-command-line", | |
| "Partition-structure", "Sysctl-settings", and "Kernel-configs" are supported for one and two image. "Rootfs", | |
| "Stateful-partition", and "OS-config" are only supported for two images. To list multiple types separate by | |
| comma. To NOT list any binary difference, set flag to "false". (default all types) | |
| -package | |
| specify whether to show package difference. Shows addition/removal of packages and package version updates. | |
| To NOT list any package difference, set flag to false. (default false) | |
| Attribute Flags | |
| -verbose | |
| include flag to increase verbosity of Rootfs, Stateful-partition, and OS-config differences. See -compress-rootfs and | |
| -compress-stateful flags descriptions for the directories that are compressed by default. | |
| -compress-rootfs (string) | |
| to customize which directories are compressed in a non-verbose Rootfs and OS-config difference output, provide a local | |
| file path to a .txt file. Format of the file must be one root file path per line with an ending back slash and no commas. | |
| By default the directory(s) that are compressed during a diff are /bin/, /lib/modules/, /lib64/, /usr/libexec/, /usr/bin/, | |
| /usr/sbin/, /usr/lib64/, /usr/share/zoneinfo/, /usr/share/git/, /usr/lib/, /sbin/, /etc/ssh/, /etc/os-release/ and | |
| /etc/package_list/. | |
| -compress-stateful (string) | |
| to customize which directories are compressed in a non-verbose Stateful-partition difference output, provide a local | |
| file path to a .txt file. Format of file must be one root file path per line with no commas. By default the directory(s) | |
| that are compressed during a diff are /var_overlay/db/. | |
| Output Flags: | |
| -output (string) | |
| Specify format of output. Only "terminal" stdout or "json" object is supported. (default "terminal") | |
| OUTPUT | |
| Based on the "-output" flag. Either "terminal" stdout or machine readable "json" format. | |
| NOTE | |
| The root permission is needed for this program because it needs to mount images into your local filesystem to calculate difference. | |
| ` | |
| cmd := filepath.Base(os.Args[0]) | |
| usage := fmt.Sprintf(usageTemplate, cmd, cmd, cmd, cmd, cmd) | |
| fmt.Printf("%s", usage) | |
| } | |
| // FlagErrorChecking validates command-line flags stored in the FlagInfo struct | |
| // Input: | |
| // (*FlagInfo) flagInfo - A struct that stores all flag input | |
| // Output: nil on success, else error | |
| func FlagErrorChecking(flagInfo *FlagInfo) error { | |
| // Error Checking | |
| if (flagInfo.LocalPtr && flagInfo.GcsPtr) || (flagInfo.LocalPtr && flagInfo.CosCloudPtr) || (flagInfo.CosCloudPtr && flagInfo.GcsPtr) { | |
| return errors.New("Error: Only one input flag is allowed. Multiple appeared") | |
| } | |
| if !(flagInfo.GcsPtr) && !(flagInfo.CosCloudPtr) { | |
| flagInfo.LocalPtr = true | |
| } | |
| if flagInfo.BinaryDiffPtr == "" { | |
| flagInfo.BinaryTypesSelected = BinaryDiffTypes | |
| } else { | |
| binaryTypesSelected := strings.Split(flagInfo.BinaryDiffPtr, ",") | |
| for _, elem := range binaryTypesSelected { | |
| if utilities.InArray(elem, BinaryDiffTypes) { | |
| flagInfo.BinaryTypesSelected = append(flagInfo.BinaryTypesSelected, elem) | |
| } else if elem != "false" { | |
| return errors.New("Error: Invalid option for \"-binary\" flag") | |
| } | |
| } | |
| } | |
| if flagInfo.CompressRootfsFile != "" { | |
| if res := utilities.FileExists(flagInfo.CompressRootfsFile, "txt"); res == -1 { | |
| return errors.New("Error: " + flagInfo.CompressRootfsFile + " file does not exist") | |
| } else if res == 0 { | |
| return errors.New("Error: " + flagInfo.CompressRootfsFile + " is not a \".txt\" file") | |
| } | |
| } | |
| if flagInfo.CompressStatefulFile != "" { | |
| if res := utilities.FileExists(flagInfo.CompressStatefulFile, "txt"); res == -1 { | |
| return errors.New("Error: " + flagInfo.CompressStatefulFile + " file does not exist") | |
| } else if res == 0 { | |
| return errors.New("Error: " + flagInfo.CompressStatefulFile + " is not a \".txt\" file") | |
| } | |
| } | |
| if flagInfo.OutputSelected != "terminal" && flagInfo.OutputSelected != "json" { | |
| return errors.New("Error: \"-output\" flag must be ethier \"terminal\" or \"json\"") | |
| } | |
| if len(flag.Args()) < 1 || len(flag.Args()) > 2 { | |
| return errors.New("Error: Input must be one or two arguments") | |
| } | |
| flagInfo.Image1 = flag.Arg(0) | |
| if len(flag.Args()) == 2 { | |
| if flag.Arg(0) == flag.Arg(1) { | |
| return errors.New("Error: Identical image passed in. To analyze single image, pass in one argument") | |
| } | |
| flagInfo.Image2 = flag.Arg(1) | |
| } | |
| return nil | |
| } | |
| // ParseFlags reads and validates the flags from the command-line | |
| // Input: None (Command-line flags and args) | |
| // Output: | |
| // (*FlagInfo) flagInfo - A struct that holds input preference from the user | |
| func ParseFlags() (*FlagInfo, error) { | |
| flagInfo := &FlagInfo{} | |
| flag.Usage = printUsage | |
| flag.BoolVar(&flagInfo.LocalPtr, "local", false, "See printUsage for description") | |
| flag.BoolVar(&flagInfo.GcsPtr, "gcs", false, "") | |
| flag.BoolVar(&flagInfo.CosCloudPtr, "cos-cloud", false, "") | |
| flag.StringVar(&flagInfo.ProjectIDPtr, "projectID", "", "") | |
| flag.StringVar(&flagInfo.BinaryDiffPtr, "binary", "", "") | |
| flag.BoolVar(&flagInfo.PackageSelected, "package", false, "") | |
| flag.BoolVar(&flagInfo.CommitSelected, "commit", true, "") | |
| flag.BoolVar(&flagInfo.ReleaseNotesSelected, "release-notes", true, "") | |
| flag.BoolVar(&flagInfo.Verbose, "verbose", false, "") | |
| flag.StringVar(&flagInfo.CompressRootfsFile, "compress-rootfs", "", "") | |
| flag.StringVar(&flagInfo.CompressStatefulFile, "compress-stateful", "", "") | |
| flag.StringVar(&flagInfo.OutputSelected, "output", "terminal", "") | |
| flag.Parse() | |
| if err := FlagErrorChecking(flagInfo); err != nil { | |
| printUsage() | |
| return &FlagInfo{}, err | |
| } | |
| if flagInfo.CompressRootfsFile != "" { // Get CompressRootfsslice | |
| compressRootsBytes, err := ioutil.ReadFile(flagInfo.CompressRootfsFile) | |
| if err != nil { | |
| return &FlagInfo{}, fmt.Errorf("failed to read compress-rootfs file %v: %v", flagInfo.CompressRootfsFile, err) | |
| } | |
| flagInfo.CompressRootfsSlice = strings.Split(string(compressRootsBytes), "\n") | |
| } else { | |
| flagInfo.CompressRootfsSlice = defaultCompressRootfs | |
| } | |
| if flagInfo.CompressStatefulFile != "" { // Get CompressStatefulFileSlice | |
| compressedStatefulBytes, err := ioutil.ReadFile(flagInfo.CompressStatefulFile) | |
| if err != nil { | |
| return &FlagInfo{}, fmt.Errorf("failed to read compress-stateful file %v: %v", flagInfo.CompressStatefulFile, err) | |
| } | |
| flagInfo.CompressStatefulSlice = strings.Split(string(compressedStatefulBytes), "\n") | |
| } else { | |
| flagInfo.CompressStatefulSlice = defaultCompressStateful | |
| } | |
| return flagInfo, nil | |
| } | |
| // validateLocalImages ensures the two images are one or two unique boot files | |
| // Input: | |
| // (string) localPath1 - Local path to the first disk.raw file | |
| // (string) localPath2 - Local path to the second disk.raw file | |
| // Output: nil on success, else error | |
| func validateLocalImages(localPath1, localPath2 string) error { | |
| if localPath2 == "" { | |
| if res := utilities.FileExists(localPath1, "raw"); res == -1 { | |
| return errors.New("Error: " + localPath1 + " file does not exist") | |
| } else if res == 0 { | |
| return errors.New("Error: " + localPath1 + " is not a \".raw\" file") | |
| } | |
| return nil | |
| } | |
| if res := utilities.FileExists(localPath2, "raw"); res == -1 { | |
| return errors.New("Error: " + localPath2 + " file does not exist") | |
| } else if res == 0 { | |
| return errors.New("Error: " + localPath2 + " is not a \".raw\" file") | |
| } | |
| info1, _ := os.Stat(localPath1) | |
| info2, _ := os.Stat(localPath2) | |
| if os.SameFile(info1, info2) { | |
| return errors.New("Error: Identical image passed in. To analyze single image, pass in one argument") | |
| } | |
| return nil | |
| } | |
| // GetImages reads in all the flags and handles the input based on its type. | |
| // Input: | |
| // (*FlagInfo) flagInfo - A struct that holds input preference from the user | |
| // Output: | |
| // (*ImageInfo) image1 - A struct that stores relevent info for image1 | |
| // (*ImageInfo) image2 - A struct that stores relevent info for image2 | |
| func GetImages(flagInfo *FlagInfo) (*ImageInfo, *ImageInfo, error) { | |
| image1, image2 := &ImageInfo{}, &ImageInfo{} | |
| // Input Selection | |
| if flagInfo.GcsPtr { | |
| gcsPath1, gcsPath2 := flagInfo.Image1, flagInfo.Image2 | |
| if err := image1.GetGcsImage(gcsPath1); err != nil { | |
| return image1, image2, fmt.Errorf("failed to download image stored on GCS for %s: %v", gcsPath1, err) | |
| } | |
| if err := image2.GetGcsImage(gcsPath2); err != nil { | |
| return image1, image2, fmt.Errorf("failed to download image stored on GCS for %s: %v", gcsPath2, err) | |
| } | |
| return image1, image2, nil | |
| } else if flagInfo.CosCloudPtr { | |
| if flagInfo.ProjectIDPtr == "" { | |
| return image1, image2, errors.New("Error: COS-cloud input requires the \"projectID\" flag to be set") | |
| } | |
| cosCloudPath1, cosCloudPath2 := flagInfo.Image1, flagInfo.Image2 | |
| if err := image1.GetCosImage(cosCloudPath1, flagInfo.ProjectIDPtr); err != nil { | |
| return image1, image2, fmt.Errorf("failed to get cos image for %s: %v", cosCloudPath1, err) | |
| } | |
| if err := image2.GetCosImage(cosCloudPath2, flagInfo.ProjectIDPtr); err != nil { | |
| return image1, image2, fmt.Errorf("failed to get cos image for %s: %v", cosCloudPath2, err) | |
| } | |
| return image1, image2, nil | |
| } else if flagInfo.LocalPtr { | |
| localPath1, localPath2 := flagInfo.Image1, flagInfo.Image2 | |
| if err := validateLocalImages(localPath1, localPath2); err != nil { | |
| return image1, image2, fmt.Errorf("failed to validate local images: %v", err) | |
| } | |
| if err := image1.GetLocalImage(localPath1); err != nil { | |
| return image1, image2, fmt.Errorf("failed to get local image for %s: %v", localPath1, err) | |
| } | |
| if err := image2.GetLocalImage(localPath2); err != nil { | |
| return image1, image2, fmt.Errorf("failed to get local image for %s: %v", localPath2, err) | |
| } | |
| return image1, image2, nil | |
| } | |
| return image1, image2, errors.New("Error: At least one flag needs to be true") | |
| } |