cli_tools/common/disk/inspect.go (129 lines of code) (raw):

// Copyright 2020 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 disk import ( "encoding/base64" "errors" "fmt" "path" "strings" "time" daisy "github.com/GoogleCloudPlatform/compute-daisy" "google.golang.org/protobuf/proto" "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/distro" "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/daisyutils" "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/logging" "github.com/GoogleCloudPlatform/compute-image-tools/proto/go/pb" ) const ( workflowFile = "image_import/inspection/boot-inspect.wf.json" ) // Inspector finds partition and boot-related properties for a disk. // //go:generate go run github.com/golang/mock/mockgen -package diskmocks -source $GOFILE -destination mocks/mock_inspect.go type Inspector interface { // Inspect finds partition and boot-related properties for a disk and // returns an InspectionResult. The reference is implementation specific. Inspect(reference string) (*pb.InspectionResults, error) Cancel(reason string) bool } // NewInspector creates an Inspector that can inspect GCP disks. // A GCE instance runs the inspection; network and subnet are used // for its network interface. func NewInspector(env daisyutils.EnvironmentSettings, logger logging.Logger) (Inspector, error) { wfProvider := daisyutils.WorkflowProvider(func() (*daisy.Workflow, error) { wf, err := daisy.NewFromFile(path.Join(env.WorkflowDirectory, workflowFile)) if err != nil { return nil, err } if env.DaisyLogLinePrefix != "" { env.DaisyLogLinePrefix += "-" } env.DaisyLogLinePrefix += "inspect" return wf, err }) return &bootInspector{daisyutils.NewDaisyWorker(wfProvider, env, logger), logger}, nil } // bootInspector implements disk.Inspector using the Python boot-inspect package, // executed on a worker VM using Daisy. type bootInspector struct { worker daisyutils.DaisyWorker logger logging.Logger } func (i *bootInspector) Cancel(reason string) bool { i.logger.Debug(fmt.Sprintf("Canceling inspection with reason: %q", reason)) return i.worker.Cancel(reason) } // Inspect finds partition and boot-related properties for a GCP persistent disk, and // returns an InspectionResult. `reference` is a fully-qualified PD URI, such as // "projects/project-name/zones/us-central1-a/disks/disk-name". func (i *bootInspector) Inspect(reference string) (*pb.InspectionResults, error) { startTime := time.Now() results := &pb.InspectionResults{} // Run the inspection worker. vars := map[string]string{ "pd_uri": reference, } encodedProto, err := i.worker.RunAndReadSerialValue("inspect_pb", vars) if err != nil { return i.assembleErrors(reference, results, pb.InspectionResults_RUNNING_WORKER, err, startTime) } // Decode the base64-encoded proto. bytes, err := base64.StdEncoding.DecodeString(encodedProto) if err == nil { err = proto.Unmarshal(bytes, results) } if err != nil { return i.assembleErrors(reference, results, pb.InspectionResults_DECODING_WORKER_RESPONSE, err, startTime) } i.logger.Debug(fmt.Sprintf("Detection results: %s", results.String())) // Validate the results. if err = i.validate(results); err != nil { return i.assembleErrors(reference, results, pb.InspectionResults_INTERPRETING_INSPECTION_RESULTS, err, startTime) } if err = i.populate(results); err != nil { return i.assembleErrors(reference, results, pb.InspectionResults_INTERPRETING_INSPECTION_RESULTS, err, startTime) } results.ElapsedTimeMs = time.Since(startTime).Milliseconds() i.logger.Metric(&pb.OutputInfo{InspectionResults: results}) return results, nil } // assembleErrors sets the errorWhen field, and generates an error object. func (i *bootInspector) assembleErrors(reference string, results *pb.InspectionResults, errorWhen pb.InspectionResults_ErrorWhen, err error, startTime time.Time) (*pb.InspectionResults, error) { results.ErrorWhen = errorWhen if err != nil { err = fmt.Errorf("failed to inspect %v: %w", reference, err) } else { err = fmt.Errorf("failed to inspect %v", reference) } results.ElapsedTimeMs = time.Since(startTime).Milliseconds() return results, err } // validate checks the fields from a pb.InspectionResults object for consistency, returning // an error if an issue is found. func (i *bootInspector) validate(results *pb.InspectionResults) error { // Only populate OsRelease when one OS is found. if results.OsCount != 1 { if results.OsRelease != nil { return fmt.Errorf( "worker should not return OsRelease when NumOsFound != 1: NumOsFound=%d", results.OsCount) } return nil } if results.OsRelease == nil { return errors.New("worker should return OsRelease when OsCount == 1") } if results.OsRelease.CliFormatted != "" { return errors.New("worker should not return CliFormatted") } if results.OsRelease.Distro != "" { return errors.New("worker should not return Distro name, only DistroId") } if results.OsRelease.MajorVersion == "" { return errors.New("missing MajorVersion") } if results.OsRelease.Architecture == pb.Architecture_ARCHITECTURE_UNKNOWN { return errors.New("missing Architecture") } if results.OsRelease.DistroId == pb.Distro_DISTRO_UNKNOWN { return errors.New("missing DistroId") } return nil } // populate fills the fields in the pb.InspectionResults that are not returned by the worker. // This is required since the worker is unaware of import-specific idioms, such as the formatting // used by gcloud's --os argument. func (i *bootInspector) populate(results *pb.InspectionResults) error { if results.ErrorWhen == pb.InspectionResults_NO_ERROR && results.OsCount == 1 { distroEnum, major, minor := results.OsRelease.DistroId, results.OsRelease.MajorVersion, results.OsRelease.MinorVersion distroName := strings.ReplaceAll(strings.ToLower(results.OsRelease.GetDistroId().String()), "_", "-") results.OsRelease.Distro = distroName version, err := distro.FromComponents(distroName, major, minor, results.OsRelease.Architecture.String()) if err != nil { i.logger.Trace( fmt.Sprintf("Failed to interpret version distro=%q, major=%q, minor=%q: %v", distroEnum, major, minor, err)) } else { results.OsRelease.CliFormatted = version.AsGcloudArg() } } return nil }