github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/gce/google/raw.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package google 5 6 import ( 7 "fmt" 8 "net/http" 9 "path" 10 "strings" 11 "time" 12 13 "github.com/juju/clock" 14 "github.com/juju/errors" 15 "github.com/juju/retry" 16 "golang.org/x/net/context" 17 "google.golang.org/api/compute/v1" 18 "google.golang.org/api/googleapi" 19 ) 20 21 const diskTypesBase = "https://www.googleapis.com/compute/v1/projects/%s/zones/%s/diskTypes/%s" 22 23 // These are attempt strategies used in waitOperation. 24 var ( 25 // TODO(ericsnow) Tune the timeouts and delays. 26 longRetryStrategy = retry.CallArgs{ 27 Clock: clock.WallClock, 28 Delay: 2 * time.Second, 29 MaxDuration: 5 * time.Minute, 30 } 31 ) 32 33 func convertRawAPIError(err error) error { 34 if err2, ok := err.(*googleapi.Error); ok { 35 if err2.Code == http.StatusNotFound { 36 return errors.NewNotFound(err, "") 37 } 38 } 39 return err 40 } 41 42 type rawConn struct { 43 *compute.Service 44 } 45 46 func (rc *rawConn) GetProject(projectID string) (*compute.Project, error) { 47 call := rc.Projects.Get(projectID) 48 proj, err := call.Do() 49 return proj, errors.Trace(err) 50 } 51 52 func (rc *rawConn) GetInstance(projectID, zone, id string) (*compute.Instance, error) { 53 call := rc.Instances.Get(projectID, zone, id) 54 inst, err := call.Do() 55 return inst, errors.Trace(err) 56 } 57 58 func (rc *rawConn) ListInstances(projectID, prefix string, statuses ...string) ([]*compute.Instance, error) { 59 call := rc.Instances.AggregatedList(projectID) 60 call = call.Filter("name eq " + prefix + ".*") 61 62 var results []*compute.Instance 63 for { 64 rawResult, err := call.Do() 65 if err != nil { 66 return nil, errors.Trace(err) 67 } 68 69 for _, instList := range rawResult.Items { 70 for _, inst := range instList.Instances { 71 if !checkInstStatus(inst, statuses) { 72 continue 73 } 74 results = append(results, inst) 75 } 76 } 77 if rawResult.NextPageToken == "" { 78 break 79 } 80 call = call.PageToken(rawResult.NextPageToken) 81 } 82 return results, nil 83 } 84 85 func checkInstStatus(inst *compute.Instance, statuses []string) bool { 86 if len(statuses) == 0 { 87 return true 88 } 89 for _, status := range statuses { 90 if inst.Status == status { 91 return true 92 } 93 } 94 return false 95 } 96 97 func (rc *rawConn) AddInstance(projectID, zoneName string, spec *compute.Instance) error { 98 call := rc.Instances.Insert(projectID, zoneName, spec) 99 operation, err := call.Do() 100 if err != nil { 101 // We are guaranteed the insert failed at the point. 102 return errors.Annotate(err, "sending new instance request") 103 } 104 err = rc.waitOperation(projectID, operation, longRetryStrategy, logOperationErrors) 105 return errors.Trace(err) 106 } 107 108 func (rc *rawConn) RemoveInstance(projectID, zone, id string) error { 109 call := rc.Instances.Delete(projectID, zone, id) 110 operation, err := call.Do() 111 if err != nil { 112 return errors.Trace(err) 113 } 114 err = rc.waitOperation(projectID, operation, longRetryStrategy, returnNotFoundOperationErrors) 115 return errors.Trace(err) 116 } 117 118 func matchesPrefix(firewallName, namePrefix string) bool { 119 return firewallName == namePrefix || strings.HasPrefix(firewallName, namePrefix+"-") 120 } 121 122 func (rc *rawConn) GetFirewalls(projectID, namePrefix string) ([]*compute.Firewall, error) { 123 call := rc.Firewalls.List(projectID) 124 firewallList, err := call.Do() 125 if err != nil { 126 return nil, errors.Annotate(err, "while getting firewall from GCE") 127 } 128 129 if len(firewallList.Items) == 0 { 130 return nil, errors.NotFoundf("firewall %q", namePrefix) 131 } 132 var result []*compute.Firewall 133 for _, fw := range firewallList.Items { 134 if matchesPrefix(fw.Name, namePrefix) { 135 result = append(result, fw) 136 } 137 } 138 return result, nil 139 } 140 141 func (rc *rawConn) AddFirewall(projectID string, firewall *compute.Firewall) error { 142 call := rc.Firewalls.Insert(projectID, firewall) 143 operation, err := call.Do() 144 if err != nil { 145 return errors.Trace(err) 146 } 147 err = rc.waitOperation(projectID, operation, longRetryStrategy, logOperationErrors) 148 return errors.Trace(err) 149 } 150 151 func (rc *rawConn) UpdateFirewall(projectID, name string, firewall *compute.Firewall) error { 152 call := rc.Firewalls.Update(projectID, name, firewall) 153 operation, err := call.Do() 154 if err != nil { 155 return errors.Trace(err) 156 } 157 err = rc.waitOperation(projectID, operation, longRetryStrategy, logOperationErrors) 158 return errors.Trace(err) 159 } 160 161 type handleOperationErrors func(operation *compute.Operation) error 162 163 func returnNotFoundOperationErrors(operation *compute.Operation) error { 164 if operation.Error != nil { 165 result := waitError{operation, nil} 166 for _, err := range operation.Error.Errors { 167 if err.Code == "RESOURCE_NOT_FOUND" { 168 result.cause = errors.NotFoundf("%v: resource", err.Message) 169 continue 170 } 171 logger.Errorf("GCE operation error: (%s) %s", err.Code, err.Message) 172 } 173 return result 174 } 175 return nil 176 } 177 178 func logOperationErrors(operation *compute.Operation) error { 179 if operation.Error != nil { 180 for _, err := range operation.Error.Errors { 181 logger.Errorf("GCE operation error: (%s) %s", err.Code, err.Message) 182 } 183 return waitError{operation, nil} 184 } 185 return nil 186 } 187 188 func (rc *rawConn) RemoveFirewall(projectID, name string) error { 189 call := rc.Firewalls.Delete(projectID, name) 190 operation, err := call.Do() 191 if err != nil { 192 return errors.Trace(convertRawAPIError(err)) 193 } 194 195 err = rc.waitOperation(projectID, operation, longRetryStrategy, returnNotFoundOperationErrors) 196 return errors.Trace(convertRawAPIError(err)) 197 } 198 199 func (rc *rawConn) ListAvailabilityZones(projectID, region string) ([]*compute.Zone, error) { 200 call := rc.Zones.List(projectID) 201 if region != "" { 202 call = call.Filter("name eq " + region + "-.*") 203 } 204 205 var results []*compute.Zone 206 for { 207 zoneList, err := call.Do() 208 if err != nil { 209 return nil, errors.Trace(err) 210 } 211 212 for _, zone := range zoneList.Items { 213 results = append(results, zone) 214 } 215 if zoneList.NextPageToken == "" { 216 break 217 } 218 call = call.PageToken(zoneList.NextPageToken) 219 } 220 return results, nil 221 } 222 223 func formatDiskType(project, zone string, spec *compute.Disk) { 224 // empty will default in pd-standard 225 if spec.Type == "" { 226 return 227 } 228 // see https://cloud.google.com/compute/docs/reference/latest/disks#resource 229 if strings.HasPrefix(spec.Type, "http") || strings.HasPrefix(spec.Type, "projects") || strings.HasPrefix(spec.Type, "global") { 230 return 231 } 232 spec.Type = fmt.Sprintf(diskTypesBase, project, zone, spec.Type) 233 } 234 235 func (rc *rawConn) CreateDisk(project, zone string, spec *compute.Disk) error { 236 ds := rc.Service.Disks 237 formatDiskType(project, zone, spec) 238 call := ds.Insert(project, zone, spec) 239 op, err := call.Do() 240 if err != nil { 241 return errors.Annotate(err, "could not create a new disk") 242 } 243 return errors.Trace(rc.waitOperation(project, op, longRetryStrategy, logOperationErrors)) 244 } 245 246 func (rc *rawConn) ListDisks(project string) ([]*compute.Disk, error) { 247 ds := rc.Service.Disks 248 call := ds.AggregatedList(project) 249 var results []*compute.Disk 250 for { 251 diskList, err := call.Do() 252 if err != nil { 253 return nil, errors.Trace(err) 254 } 255 for _, list := range diskList.Items { 256 results = append(results, list.Disks...) 257 } 258 if diskList.NextPageToken == "" { 259 break 260 } 261 call = call.PageToken(diskList.NextPageToken) 262 } 263 return results, nil 264 } 265 266 func (rc *rawConn) RemoveDisk(project, zone, id string) error { 267 ds := rc.Disks 268 call := ds.Delete(project, zone, id) 269 op, err := call.Do() 270 if err != nil { 271 return errors.Annotatef(err, "could not delete disk %q", id) 272 } 273 return errors.Trace(rc.waitOperation(project, op, longRetryStrategy, returnNotFoundOperationErrors)) 274 } 275 276 func (rc *rawConn) GetDisk(project, zone, id string) (*compute.Disk, error) { 277 ds := rc.Disks 278 call := ds.Get(project, zone, id) 279 disk, err := call.Do() 280 if err != nil { 281 return nil, errors.Annotatef(err, "cannot get disk %q at zone %q in project %q", id, zone, project) 282 } 283 return disk, nil 284 } 285 286 func (rc *rawConn) SetDiskLabels(project, zone, id, labelFingerprint string, labels map[string]string) error { 287 ds := rc.Service.Disks 288 call := ds.SetLabels(project, zone, id, &compute.ZoneSetLabelsRequest{ 289 LabelFingerprint: labelFingerprint, 290 Labels: labels, 291 }) 292 _, err := call.Do() 293 return errors.Trace(err) 294 } 295 296 func (rc *rawConn) AttachDisk(project, zone, instanceId string, disk *compute.AttachedDisk) error { 297 call := rc.Instances.AttachDisk(project, zone, instanceId, disk) 298 _, err := call.Do() // Perhaps return something from the Op 299 if err != nil { 300 return errors.Annotatef(err, "cannot attach volume into %q", instanceId) 301 } 302 return nil 303 } 304 305 func (rc *rawConn) DetachDisk(project, zone, instanceId, diskDeviceName string) error { 306 call := rc.Instances.DetachDisk(project, zone, instanceId, diskDeviceName) 307 _, err := call.Do() 308 if err != nil { 309 return errors.Annotatef(err, "cannot detach volume from %q", instanceId) 310 } 311 return nil 312 } 313 314 func (rc *rawConn) InstanceDisks(project, zone, instanceId string) ([]*compute.AttachedDisk, error) { 315 instance, err := rc.GetInstance(project, zone, instanceId) 316 if err != nil { 317 return nil, errors.Annotatef(err, "cannot get instance %q to list its disks", instanceId) 318 } 319 return instance.Disks, nil 320 } 321 322 type waitError struct { 323 op *compute.Operation 324 cause error 325 } 326 327 func (err waitError) Error() string { 328 if err.cause != nil { 329 return fmt.Sprintf("GCE operation %q failed: %v", err.op.Name, err.cause) 330 } 331 return fmt.Sprintf("GCE operation %q failed", err.op.Name) 332 } 333 334 func (err waitError) Cause() error { 335 if err.cause != nil { 336 return err.cause 337 } 338 return err 339 } 340 341 func isWaitError(err error) bool { 342 _, ok := err.(*waitError) 343 return ok 344 } 345 346 type opDoer interface { 347 Do(...googleapi.CallOption) (*compute.Operation, error) 348 } 349 350 // checkOperation requests a new copy of the given operation from the 351 // GCE API and returns it. The new copy will have the operation's 352 // current status. 353 func (rc *rawConn) checkOperation(projectID string, op *compute.Operation) (*compute.Operation, error) { 354 var call opDoer 355 if op.Zone != "" { 356 zoneName := path.Base(op.Zone) 357 call = rc.ZoneOperations.Get(projectID, zoneName, op.Name) 358 } else if op.Region != "" { 359 region := path.Base(op.Region) 360 call = rc.RegionOperations.Get(projectID, region, op.Name) 361 } else { 362 call = rc.GlobalOperations.Get(projectID, op.Name) 363 } 364 365 operation, err := doOpCall(call) 366 if err != nil { 367 return nil, errors.Annotatef(err, "request for GCE operation %q failed", op.Name) 368 } 369 return operation, nil 370 } 371 372 var doOpCall = func(call opDoer) (*compute.Operation, error) { 373 return call.Do() 374 } 375 376 // waitOperation waits for the provided operation to reach the "done" 377 // status. It follows the given attempt strategy (e.g. wait time between 378 // attempts) and may time out. 379 func (rc *rawConn) waitOperation(projectID string, op *compute.Operation, retryStrategy retry.CallArgs, f handleOperationErrors) error { 380 // TODO(perrito666) 2016-05-02 lp:1558657 381 started := time.Now() 382 logger.Infof("GCE operation %q, waiting...", op.Name) 383 384 retryStrategy.IsFatalError = func(err error) bool { 385 return !errors.IsNotProvisioned(err) 386 } 387 retryStrategy.Func = func() error { 388 var err error 389 op, err = rc.checkOperation(projectID, op) 390 if err != nil { 391 return err 392 } 393 if op.Status == StatusDone { 394 return nil 395 } 396 return errors.NewNotProvisioned(nil, "GCE operation not done yet") 397 } 398 var err error 399 if op.Status != StatusDone { 400 err = retry.Call(retryStrategy) 401 } 402 403 if retry.IsAttemptsExceeded(err) || retry.IsDurationExceeded(err) { 404 // lp:1558657 405 err := errors.Annotatef(err, "timed out after %d seconds", time.Now().Sub(started)/time.Second) 406 return waitError{op, err} 407 } 408 if err != nil { 409 return errors.Trace(err) 410 } 411 if err := f(op); err != nil { 412 return err 413 } 414 415 logger.Infof("GCE operation %q finished", op.Name) 416 return nil 417 } 418 419 // ListMachineTypes returns a list of machines available in the project and zone provided. 420 func (rc *rawConn) ListMachineTypes(projectID, zone string) (*compute.MachineTypeList, error) { 421 op := rc.MachineTypes.List(projectID, zone) 422 machines, err := op.Do() 423 if err != nil { 424 return nil, errors.Annotatef(err, "listing machine types for project %q and zone %q", projectID, zone) 425 } 426 return machines, nil 427 } 428 429 func (rc *rawConn) SetMetadata(projectID, zone, instanceID string, metadata *compute.Metadata) error { 430 call := rc.Instances.SetMetadata(projectID, zone, instanceID, metadata) 431 op, err := call.Do() 432 if err != nil { 433 return errors.Trace(err) 434 } 435 err = rc.waitOperation(projectID, op, longRetryStrategy, logOperationErrors) 436 return errors.Trace(err) 437 } 438 439 func (rc *rawConn) ListSubnetworks(projectID, region string) ([]*compute.Subnetwork, error) { 440 ctx := context.Background() 441 call := rc.Subnetworks.List(projectID, region) 442 var results []*compute.Subnetwork 443 err := call.Pages(ctx, func(page *compute.SubnetworkList) error { 444 results = append(results, page.Items...) 445 return nil 446 }) 447 if err != nil { 448 return nil, errors.Trace(err) 449 } 450 return results, nil 451 } 452 453 func (rc *rawConn) ListNetworks(projectID string) ([]*compute.Network, error) { 454 ctx := context.Background() 455 call := rc.Networks.List(projectID) 456 var results []*compute.Network 457 err := call.Pages(ctx, func(page *compute.NetworkList) error { 458 results = append(results, page.Items...) 459 return nil 460 }) 461 if err != nil { 462 return nil, errors.Trace(err) 463 } 464 return results, nil 465 }