blob: 13ea28053c346e6afb079e192f4c656e335d694b [file] [log] [blame]
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")
}