-
Notifications
You must be signed in to change notification settings - Fork 147
/
inspect.go
194 lines (165 loc) · 6.71 KB
/
inspect.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// 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
}