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 }