github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/common/disk/inspect.go (about)

     1  //  Copyright 2020 Google Inc. All Rights Reserved.
     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 disk
    16  
    17  import (
    18  	"encoding/base64"
    19  	"errors"
    20  	"fmt"
    21  	"path"
    22  	"strings"
    23  	"time"
    24  
    25  	daisy "github.com/GoogleCloudPlatform/compute-daisy"
    26  	"google.golang.org/protobuf/proto"
    27  
    28  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/distro"
    29  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/daisyutils"
    30  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/logging"
    31  	"github.com/GoogleCloudPlatform/compute-image-tools/proto/go/pb"
    32  )
    33  
    34  const (
    35  	workflowFile = "image_import/inspection/boot-inspect.wf.json"
    36  )
    37  
    38  // Inspector finds partition and boot-related properties for a disk.
    39  //
    40  //go:generate go run github.com/golang/mock/mockgen -package diskmocks -source $GOFILE -destination mocks/mock_inspect.go
    41  type Inspector interface {
    42  	// Inspect finds partition and boot-related properties for a disk and
    43  	// returns an InspectionResult. The reference is implementation specific.
    44  	Inspect(reference string) (*pb.InspectionResults, error)
    45  	Cancel(reason string) bool
    46  }
    47  
    48  // NewInspector creates an Inspector that can inspect GCP disks.
    49  // A GCE instance runs the inspection; network and subnet are used
    50  // for its network interface.
    51  func NewInspector(env daisyutils.EnvironmentSettings, logger logging.Logger) (Inspector, error) {
    52  	wfProvider := daisyutils.WorkflowProvider(func() (*daisy.Workflow, error) {
    53  		wf, err := daisy.NewFromFile(path.Join(env.WorkflowDirectory, workflowFile))
    54  		if err != nil {
    55  			return nil, err
    56  		}
    57  
    58  		if env.DaisyLogLinePrefix != "" {
    59  			env.DaisyLogLinePrefix += "-"
    60  		}
    61  		env.DaisyLogLinePrefix += "inspect"
    62  		return wf, err
    63  	})
    64  
    65  	return &bootInspector{daisyutils.NewDaisyWorker(wfProvider, env, logger), logger}, nil
    66  }
    67  
    68  // bootInspector implements disk.Inspector using the Python boot-inspect package,
    69  // executed on a worker VM using Daisy.
    70  type bootInspector struct {
    71  	worker daisyutils.DaisyWorker
    72  	logger logging.Logger
    73  }
    74  
    75  func (i *bootInspector) Cancel(reason string) bool {
    76  	i.logger.Debug(fmt.Sprintf("Canceling inspection with reason: %q", reason))
    77  	return i.worker.Cancel(reason)
    78  }
    79  
    80  // Inspect finds partition and boot-related properties for a GCP persistent disk, and
    81  // returns an InspectionResult. `reference` is a fully-qualified PD URI, such as
    82  // "projects/project-name/zones/us-central1-a/disks/disk-name".
    83  func (i *bootInspector) Inspect(reference string) (*pb.InspectionResults, error) {
    84  	startTime := time.Now()
    85  	results := &pb.InspectionResults{}
    86  
    87  	// Run the inspection worker.
    88  	vars := map[string]string{
    89  		"pd_uri": reference,
    90  	}
    91  	encodedProto, err := i.worker.RunAndReadSerialValue("inspect_pb", vars)
    92  	if err != nil {
    93  		return i.assembleErrors(reference, results, pb.InspectionResults_RUNNING_WORKER, err, startTime)
    94  	}
    95  
    96  	// Decode the base64-encoded proto.
    97  	bytes, err := base64.StdEncoding.DecodeString(encodedProto)
    98  	if err == nil {
    99  		err = proto.Unmarshal(bytes, results)
   100  	}
   101  	if err != nil {
   102  		return i.assembleErrors(reference, results, pb.InspectionResults_DECODING_WORKER_RESPONSE, err, startTime)
   103  	}
   104  	i.logger.Debug(fmt.Sprintf("Detection results: %s", results.String()))
   105  
   106  	// Validate the results.
   107  	if err = i.validate(results); err != nil {
   108  		return i.assembleErrors(reference, results, pb.InspectionResults_INTERPRETING_INSPECTION_RESULTS, err, startTime)
   109  	}
   110  
   111  	if err = i.populate(results); err != nil {
   112  		return i.assembleErrors(reference, results, pb.InspectionResults_INTERPRETING_INSPECTION_RESULTS, err, startTime)
   113  	}
   114  
   115  	results.ElapsedTimeMs = time.Since(startTime).Milliseconds()
   116  	i.logger.Metric(&pb.OutputInfo{InspectionResults: results})
   117  	return results, nil
   118  }
   119  
   120  // assembleErrors sets the errorWhen field, and generates an error object.
   121  func (i *bootInspector) assembleErrors(reference string, results *pb.InspectionResults,
   122  	errorWhen pb.InspectionResults_ErrorWhen, err error, startTime time.Time) (*pb.InspectionResults, error) {
   123  	results.ErrorWhen = errorWhen
   124  	if err != nil {
   125  		err = fmt.Errorf("failed to inspect %v: %w", reference, err)
   126  	} else {
   127  		err = fmt.Errorf("failed to inspect %v", reference)
   128  	}
   129  	results.ElapsedTimeMs = time.Since(startTime).Milliseconds()
   130  	return results, err
   131  }
   132  
   133  // validate checks the fields from a pb.InspectionResults object for consistency, returning
   134  // an error if an issue is found.
   135  func (i *bootInspector) validate(results *pb.InspectionResults) error {
   136  	// Only populate OsRelease when one OS is found.
   137  	if results.OsCount != 1 {
   138  		if results.OsRelease != nil {
   139  			return fmt.Errorf(
   140  				"worker should not return OsRelease when NumOsFound != 1: NumOsFound=%d", results.OsCount)
   141  		}
   142  		return nil
   143  	}
   144  
   145  	if results.OsRelease == nil {
   146  		return errors.New("worker should return OsRelease when OsCount == 1")
   147  	}
   148  
   149  	if results.OsRelease.CliFormatted != "" {
   150  		return errors.New("worker should not return CliFormatted")
   151  	}
   152  
   153  	if results.OsRelease.Distro != "" {
   154  		return errors.New("worker should not return Distro name, only DistroId")
   155  	}
   156  
   157  	if results.OsRelease.MajorVersion == "" {
   158  		return errors.New("missing MajorVersion")
   159  	}
   160  
   161  	if results.OsRelease.Architecture == pb.Architecture_ARCHITECTURE_UNKNOWN {
   162  		return errors.New("missing Architecture")
   163  	}
   164  
   165  	if results.OsRelease.DistroId == pb.Distro_DISTRO_UNKNOWN {
   166  		return errors.New("missing DistroId")
   167  	}
   168  
   169  	return nil
   170  }
   171  
   172  // populate fills the fields in the pb.InspectionResults that are not returned by the worker.
   173  // This is required since the worker is unaware of import-specific idioms, such as the formatting
   174  // used by gcloud's --os argument.
   175  func (i *bootInspector) populate(results *pb.InspectionResults) error {
   176  	if results.ErrorWhen == pb.InspectionResults_NO_ERROR && results.OsCount == 1 {
   177  		distroEnum, major, minor := results.OsRelease.DistroId,
   178  			results.OsRelease.MajorVersion, results.OsRelease.MinorVersion
   179  
   180  		distroName := strings.ReplaceAll(strings.ToLower(results.OsRelease.GetDistroId().String()), "_", "-")
   181  
   182  		results.OsRelease.Distro = distroName
   183  		version, err := distro.FromComponents(distroName, major, minor,
   184  			results.OsRelease.Architecture.String())
   185  		if err != nil {
   186  			i.logger.Trace(
   187  				fmt.Sprintf("Failed to interpret version distro=%q, major=%q, minor=%q: %v",
   188  					distroEnum, major, minor, err))
   189  		} else {
   190  			results.OsRelease.CliFormatted = version.AsGcloudArg()
   191  		}
   192  	}
   193  	return nil
   194  }