github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/gce_windows_upgrade/upgrader/workflows.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 upgrader 16 17 import ( 18 "fmt" 19 "os" 20 21 daisy "github.com/GoogleCloudPlatform/compute-daisy" 22 "google.golang.org/api/compute/v1" 23 24 "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/daisyutils" 25 "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/path" 26 ) 27 28 var ( 29 upgradeSteps = map[string]func(*upgrader, *daisy.Workflow) error{ 30 versionWindows2012r2: populateUpgradeStepsFrom2008r2To2012r2, 31 versionWindows2016: populateUpgradeStepsTo2016, 32 versionWindows2019: populateUpgradeStepsTo2019, 33 versionWindows2022: populateUpgradeStepsTo2022, 34 } 35 retryUpgradeSteps = map[string]func(*upgrader, *daisy.Workflow) error{versionWindows2012r2: populateRetryUpgradeStepsFrom2008r2To2012r2} 36 ) 37 38 func (u *upgrader) prepare() (daisyutils.DaisyWorker, error) { 39 if u.prepareFn != nil { 40 return u.prepareFn() 41 } 42 43 return u.runWorkflowWithSteps("windows-upgrade-preparation", u.Timeout, populatePrepareSteps) 44 } 45 46 func populatePrepareSteps(u *upgrader, w *daisy.Workflow) error { 47 currentExecutablePath := os.Args[0] 48 w.Sources = map[string]string{"upgrade_script.ps1": path.ToWorkingDir("upgrade_script.ps1", currentExecutablePath)} 49 50 stepStopInstance, err := daisyutils.NewStep(w, "stop-instance") 51 if err != nil { 52 return err 53 } 54 stepStopInstance.StopInstances = &daisy.StopInstances{ 55 Instances: []string{u.instanceURI}, 56 } 57 prevStep := stepStopInstance 58 59 if u.CreateMachineBackup { 60 stepBackupMachineImage, err := daisyutils.NewStep(w, "backup-machine-image", stepStopInstance) 61 if err != nil { 62 return err 63 } 64 stepBackupMachineImage.CreateMachineImages = &daisy.CreateMachineImages{ 65 &daisy.MachineImage{ 66 MachineImage: compute.MachineImage{ 67 Name: u.machineImageBackupName, 68 SourceInstance: u.instanceURI, 69 }, 70 Resource: daisy.Resource{ 71 ExactName: true, 72 NoCleanup: true, 73 }, 74 }, 75 } 76 prevStep = stepBackupMachineImage 77 } 78 79 stepBackupOSDiskSnapshot, err := daisyutils.NewStep(w, "backup-os-disk-snapshot", prevStep) 80 if err != nil { 81 return err 82 } 83 stepBackupOSDiskSnapshot.CreateSnapshots = &daisy.CreateSnapshots{ 84 &daisy.Snapshot{ 85 Snapshot: compute.Snapshot{ 86 Name: u.osDiskSnapshotName, 87 SourceDisk: u.osDiskURI, 88 }, 89 Resource: daisy.Resource{ 90 ExactName: true, 91 NoCleanup: true, 92 }, 93 }, 94 } 95 96 stepCreateNewOSDisk, err := daisyutils.NewStep(w, "create-new-os-disk", stepBackupOSDiskSnapshot) 97 if err != nil { 98 return err 99 } 100 stepCreateNewOSDisk.CreateDisks = &daisy.CreateDisks{ 101 &daisy.Disk{ 102 Disk: compute.Disk{ 103 Name: u.newOSDiskName, 104 Zone: u.instanceZone, 105 Type: u.osDiskType, 106 SourceSnapshot: u.osDiskSnapshotName, 107 Licenses: []string{upgradePaths[u.SourceOS][u.TargetOS].licenseToAdd}, 108 }, 109 Resource: daisy.Resource{ 110 ExactName: true, 111 NoCleanup: true, 112 }, 113 }, 114 } 115 116 stepDetachOldOSDisk, err := daisyutils.NewStep(w, "detach-old-os-disk", stepCreateNewOSDisk) 117 if err != nil { 118 return err 119 } 120 stepDetachOldOSDisk.DetachDisks = &daisy.DetachDisks{ 121 &daisy.DetachDisk{ 122 Instance: u.instanceURI, 123 DeviceName: daisyutils.GetDeviceURI(u.instanceProject, u.instanceZone, u.osDiskDeviceName), 124 }, 125 } 126 127 stepAttachNewOSDisk, err := daisyutils.NewStep(w, "attach-new-os-disk", stepDetachOldOSDisk) 128 if err != nil { 129 return err 130 } 131 stepAttachNewOSDisk.AttachDisks = &daisy.AttachDisks{ 132 &daisy.AttachDisk{ 133 Instance: u.instanceURI, 134 AttachedDisk: compute.AttachedDisk{ 135 Source: u.newOSDiskName, 136 DeviceName: u.osDiskDeviceName, 137 AutoDelete: u.osDiskAutoDelete, 138 Boot: true, 139 }, 140 }, 141 } 142 143 stepCreateInstallDisk, err := daisyutils.NewStep(w, "create-install-disk", stepAttachNewOSDisk) 144 if err != nil { 145 return err 146 } 147 stepCreateInstallDisk.CreateDisks = &daisy.CreateDisks{ 148 &daisy.Disk{ 149 Disk: compute.Disk{ 150 Name: u.installMediaDiskName, 151 Zone: u.instanceZone, 152 Type: "pd-ssd", 153 SourceImage: "projects/compute-image-tools/global/images/family/windows-install-media", 154 }, 155 Resource: daisy.Resource{ 156 ExactName: true, 157 NoCleanup: true, 158 }, 159 }, 160 } 161 if u.UseStagingInstallMedia { 162 (*stepCreateInstallDisk.CreateDisks)[0].SourceImage = "projects/bct-prod-images/global/images/family/windows-install-media" 163 } 164 165 stepAttachInstallDisk, err := daisyutils.NewStep(w, "attach-install-disk", stepCreateInstallDisk) 166 if err != nil { 167 return err 168 } 169 stepAttachInstallDisk.AttachDisks = &daisy.AttachDisks{ 170 &daisy.AttachDisk{ 171 Instance: u.instanceURI, 172 AttachedDisk: compute.AttachedDisk{ 173 Source: u.installMediaDiskName, 174 AutoDelete: true, 175 }, 176 }, 177 } 178 prevStep = stepAttachInstallDisk 179 180 // If there isn't an original url, just skip the backup step. 181 if u.originalWindowsStartupScriptURL != nil { 182 fmt.Printf("\nDetected an existing metadata for key '%v', value='%v'. Will backup to '%v'.\n\n", metadataWindowsStartupScriptURL, 183 *u.originalWindowsStartupScriptURL, metadataWindowsStartupScriptURLBackup) 184 185 stepBackupScript, err := daisyutils.NewStep(w, "backup-script", stepAttachInstallDisk) 186 if err != nil { 187 return err 188 } 189 stepBackupScript.UpdateInstancesMetadata = &daisy.UpdateInstancesMetadata{ 190 &daisy.UpdateInstanceMetadata{ 191 Instance: u.instanceURI, 192 Metadata: map[string]string{metadataWindowsStartupScriptURLBackup: *u.originalWindowsStartupScriptURL}, 193 }, 194 } 195 prevStep = stepBackupScript 196 } 197 198 stepSetScript, err := daisyutils.NewStep(w, "set-script", prevStep) 199 if err != nil { 200 return err 201 } 202 stepSetScript.UpdateInstancesMetadata = &daisy.UpdateInstancesMetadata{ 203 &daisy.UpdateInstanceMetadata{ 204 Instance: u.instanceURI, 205 Metadata: map[string]string{ 206 metadataWindowsStartupScriptURL: "${SOURCESPATH}/upgrade_script.ps1", 207 "expected-current-version": upgradePaths[u.SourceOS][u.TargetOS].expectedCurrentVersion, 208 "expected-new-version": upgradePaths[u.SourceOS][u.TargetOS].expectedNewVersion, 209 "install-folder": upgradePaths[u.SourceOS][u.TargetOS].installFolder, 210 }, 211 }, 212 } 213 return nil 214 } 215 216 func (u *upgrader) upgrade() (daisyutils.DaisyWorker, error) { 217 if u.upgradeFn != nil { 218 return u.upgradeFn() 219 } 220 221 return u.runWorkflowWithSteps("upgrade", u.Timeout, upgradeSteps[u.TargetOS]) 222 } 223 224 func populateUpgradeStepsFrom2008r2To2012r2(u *upgrader, w *daisy.Workflow) error { 225 cleanupWorkflow, err := u.generateWorkflowWithSteps("cleanup", "10m", populateCleanupSteps) 226 if err != nil { 227 return nil 228 } 229 230 w.Steps = map[string]*daisy.Step{ 231 "start-instance": { 232 StartInstances: &daisy.StartInstances{ 233 Instances: []string{u.instanceURI}, 234 }, 235 }, 236 "wait-for-boot": { 237 Timeout: "15m", 238 WaitForInstancesSignal: &daisy.WaitForInstancesSignal{ 239 { 240 Name: u.instanceURI, 241 SerialOutput: &daisy.SerialOutput{ 242 Port: 1, 243 SuccessMatch: "Beginning upgrade startup script.", 244 }, 245 }, 246 }, 247 }, 248 "wait-for-upgrade": { 249 WaitForAnyInstancesSignal: &daisy.WaitForAnyInstancesSignal{ 250 { 251 Name: u.instanceURI, 252 SerialOutput: &daisy.SerialOutput{ 253 Port: 1, 254 SuccessMatch: "windows_upgrade_current_version='Windows Server 2012 R2 Datacenter'", 255 FailureMatch: []string{"UpgradeFailed:"}, 256 StatusMatch: "GCEMetadataScripts:", 257 }, 258 }, 259 { 260 Name: u.instanceURI, 261 SerialOutput: &daisy.SerialOutput{ 262 Port: 3, 263 // These errors were thrown from setup.exe. 264 FailureMatch: []string{"Windows needs to be restarted", "CheckDiskSpaceRequirements not satisfied"}, 265 // This is the prefix of error log emitted from install media. Catch it and write to daisy log for debugging. 266 StatusMatch: "$WINDOWS.~BT setuperr$", 267 }, 268 }, 269 }, 270 }, 271 "cleanup-temp-resources": { 272 IncludeWorkflow: &daisy.IncludeWorkflow{ 273 Workflow: cleanupWorkflow, 274 }, 275 }, 276 } 277 w.Dependencies = map[string][]string{ 278 "wait-for-boot": {"start-instance"}, 279 "wait-for-upgrade": {"start-instance"}, 280 "cleanup-temp-resources": {"wait-for-upgrade"}, 281 } 282 return nil 283 } 284 285 func populateUpgradeStepsTo2016(u *upgrader, w *daisy.Workflow) error { 286 return populateUpgradeStepsTemplate(u, w, versionStringForWindows2016) 287 } 288 289 func populateUpgradeStepsTo2019(u *upgrader, w *daisy.Workflow) error { 290 return populateUpgradeStepsTemplate(u, w, versionStringForWindows2019) 291 } 292 293 func populateUpgradeStepsTo2022(u *upgrader, w *daisy.Workflow) error { 294 return populateUpgradeStepsTemplate(u, w, versionStringForWindows2022) 295 } 296 297 func populateUpgradeStepsTemplate(u *upgrader, w *daisy.Workflow, newVersionString string) error { 298 cleanupWorkflow, err := u.generateWorkflowWithSteps("cleanup", "10m", populateCleanupSteps) 299 if err != nil { 300 return nil 301 } 302 303 w.Steps = map[string]*daisy.Step{ 304 "start-instance": { 305 StartInstances: &daisy.StartInstances{ 306 Instances: []string{u.instanceURI}, 307 }, 308 }, 309 "wait-for-boot": { 310 Timeout: "15m", 311 WaitForInstancesSignal: &daisy.WaitForInstancesSignal{ 312 { 313 Name: u.instanceURI, 314 SerialOutput: &daisy.SerialOutput{ 315 Port: 1, 316 SuccessMatch: "Beginning upgrade startup script.", 317 }, 318 }, 319 }, 320 }, 321 "wait-for-upgrade": { 322 WaitForAnyInstancesSignal: &daisy.WaitForAnyInstancesSignal{ 323 { 324 Name: u.instanceURI, 325 SerialOutput: &daisy.SerialOutput{ 326 Port: 1, 327 SuccessMatch: fmt.Sprintf("windows_upgrade_current_version='%v'", newVersionString), 328 FailureMatch: []string{"UpgradeFailed:"}, 329 StatusMatch: "GCEMetadataScripts:", 330 }, 331 }, 332 { 333 Name: u.instanceURI, 334 SerialOutput: &daisy.SerialOutput{ 335 Port: 3, 336 // These errors were thrown from setup.exe. 337 FailureMatch: []string{"CheckDiskSpaceRequirements not satisfied"}, //TODO: disk issue, CPU/mem issue 338 // This is the prefix of error log emitted from install media. Catch it and write to daisy log for debugging. 339 StatusMatch: "$WINDOWS.~BT setuperr$", 340 }, 341 }, 342 }, 343 }, 344 "cleanup-temp-resources": { 345 IncludeWorkflow: &daisy.IncludeWorkflow{ 346 Workflow: cleanupWorkflow, 347 }, 348 }, 349 } 350 w.Dependencies = map[string][]string{ 351 "wait-for-boot": {"start-instance"}, 352 "wait-for-upgrade": {"start-instance"}, 353 "cleanup-temp-resources": {"wait-for-upgrade"}, 354 } 355 return nil 356 } 357 358 func (u *upgrader) retryUpgrade() (daisyutils.DaisyWorker, error) { 359 if u.retryUpgradeFn != nil { 360 return u.retryUpgradeFn() 361 } 362 363 return u.runWorkflowWithSteps("retry-upgrade", u.Timeout, retryUpgradeSteps[u.TargetOS]) 364 } 365 366 func populateRetryUpgradeStepsFrom2008r2To2012r2(u *upgrader, w *daisy.Workflow) error { 367 cleanupWorkflow, err := u.generateWorkflowWithSteps("cleanup", "10m", populateCleanupSteps) 368 if err != nil { 369 return nil 370 } 371 372 w.Steps = map[string]*daisy.Step{ 373 "wait-for-boot": { 374 Timeout: "15m", 375 WaitForInstancesSignal: &daisy.WaitForInstancesSignal{ 376 { 377 Name: u.instanceURI, 378 SerialOutput: &daisy.SerialOutput{ 379 Port: 1, 380 SuccessMatch: "Beginning upgrade startup script.", 381 }, 382 }, 383 }, 384 }, 385 "wait-for-upgrade": { 386 WaitForAnyInstancesSignal: &daisy.WaitForAnyInstancesSignal{ 387 { 388 Name: u.instanceURI, 389 SerialOutput: &daisy.SerialOutput{ 390 Port: 1, 391 SuccessMatch: "windows_upgrade_current_version='Windows Server 2012 R2 Datacenter'", 392 FailureMatch: []string{"UpgradeFailed:"}, 393 StatusMatch: "GCEMetadataScripts:", 394 }, 395 }, 396 { 397 Name: u.instanceURI, 398 SerialOutput: &daisy.SerialOutput{ 399 Port: 3, 400 // These errors were thrown from setup.exe. 401 FailureMatch: []string{"Windows needs to be restarted", "CheckDiskSpaceRequirements not satisfied"}, 402 }, 403 }, 404 }, 405 }, 406 "cleanup-temp-resources": { 407 IncludeWorkflow: &daisy.IncludeWorkflow{ 408 Workflow: cleanupWorkflow, 409 }, 410 }, 411 } 412 w.Dependencies = map[string][]string{ 413 "cleanup-temp-resources": {"wait-for-upgrade"}, 414 } 415 return nil 416 } 417 418 func (u *upgrader) reboot() (daisyutils.DaisyWorker, error) { 419 if u.rebootFn != nil { 420 return u.rebootFn() 421 } 422 423 return u.runWorkflowWithSteps("reboot", "15m", populateRebootSteps) 424 } 425 426 func populateRebootSteps(u *upgrader, w *daisy.Workflow) error { 427 w.Steps = map[string]*daisy.Step{ 428 "stop-instance": { 429 StopInstances: &daisy.StopInstances{ 430 Instances: []string{u.instanceURI}, 431 }, 432 }, 433 "start-instance": { 434 StartInstances: &daisy.StartInstances{ 435 Instances: []string{u.instanceURI}, 436 }, 437 }, 438 } 439 w.Dependencies = map[string][]string{ 440 "start-instance": {"stop-instance"}, 441 } 442 return nil 443 } 444 445 func (u *upgrader) cleanup() (daisyutils.DaisyWorker, error) { 446 if u.cleanupFn != nil { 447 return u.cleanupFn() 448 } 449 450 return u.runWorkflowWithSteps("cleanup", "20m", populateCleanupSteps) 451 } 452 453 func populateCleanupSteps(u *upgrader, w *daisy.Workflow) error { 454 w.Steps = map[string]*daisy.Step{ 455 "restore-script": { 456 UpdateInstancesMetadata: &daisy.UpdateInstancesMetadata{ 457 { 458 Instance: u.instanceURI, 459 Metadata: map[string]string{ 460 metadataWindowsStartupScriptURL: u.getOriginalStartupScriptURL(), 461 metadataWindowsStartupScriptURLBackup: "", 462 }, 463 }, 464 }, 465 }, 466 "detach-install-media-disk": { 467 DetachDisks: &daisy.DetachDisks{ 468 { 469 Instance: u.instanceURI, 470 DeviceName: daisyutils.GetDeviceURI(u.instanceProject, u.instanceZone, u.installMediaDiskName), 471 }, 472 }, 473 }, 474 "delete-install-media-disk": { 475 DeleteResources: &daisy.DeleteResources{ 476 Disks: []string{ 477 daisyutils.GetDiskURI(u.instanceProject, u.instanceZone, u.installMediaDiskName), 478 }, 479 }, 480 }, 481 // TODO: use a flag to determine whether to stop the instance. b/156668741 482 "stop-instance": { 483 StopInstances: &daisy.StopInstances{ 484 Instances: []string{u.instanceURI}, 485 }, 486 }, 487 } 488 w.Dependencies = map[string][]string{ 489 "delete-install-media-disk": {"detach-install-media-disk"}, 490 } 491 return nil 492 } 493 494 func (u *upgrader) rollback() (daisyutils.DaisyWorker, error) { 495 if u.rollbackFn != nil { 496 return u.rollbackFn() 497 } 498 499 return u.runWorkflowWithSteps("rollback", u.Timeout, populateRollbackSteps) 500 } 501 502 func populateRollbackSteps(u *upgrader, w *daisy.Workflow) error { 503 stepStopInstance, err := daisyutils.NewStep(w, "stop-instance") 504 if err != nil { 505 return err 506 } 507 stepStopInstance.StopInstances = &daisy.StopInstances{ 508 Instances: []string{u.instanceURI}, 509 } 510 511 stepDetachNewOSDisk, err := daisyutils.NewStep(w, "detach-new-os-disk", stepStopInstance) 512 if err != nil { 513 return err 514 } 515 stepDetachNewOSDisk.DetachDisks = &daisy.DetachDisks{ 516 { 517 Instance: u.instanceURI, 518 DeviceName: daisyutils.GetDeviceURI(u.instanceProject, u.instanceZone, u.osDiskDeviceName), 519 }, 520 } 521 522 stepAttachOldOSDisk, err := daisyutils.NewStep(w, "attach-old-os-disk", stepDetachNewOSDisk) 523 if err != nil { 524 return err 525 } 526 stepAttachOldOSDisk.AttachDisks = &daisy.AttachDisks{ 527 { 528 Instance: u.instanceURI, 529 AttachedDisk: compute.AttachedDisk{ 530 Source: u.osDiskURI, 531 DeviceName: u.osDiskDeviceName, 532 AutoDelete: u.osDiskAutoDelete, 533 Boot: true, 534 }, 535 }, 536 } 537 538 stepDetachInstallMediaDisk, err := daisyutils.NewStep(w, "detach-install-media-disk", stepAttachOldOSDisk) 539 if err != nil { 540 return err 541 } 542 stepDetachInstallMediaDisk.DetachDisks = &daisy.DetachDisks{ 543 { 544 Instance: u.instanceURI, 545 DeviceName: daisyutils.GetDeviceURI(u.instanceProject, u.instanceZone, u.installMediaDiskName), 546 }, 547 } 548 549 stepRestoreScript, err := daisyutils.NewStep(w, "restore-script", stepDetachInstallMediaDisk) 550 if err != nil { 551 return err 552 } 553 stepRestoreScript.UpdateInstancesMetadata = &daisy.UpdateInstancesMetadata{ 554 { 555 Instance: u.instanceURI, 556 Metadata: map[string]string{ 557 metadataWindowsStartupScriptURL: u.getOriginalStartupScriptURL(), 558 metadataWindowsStartupScriptURLBackup: "", 559 }, 560 }, 561 } 562 563 // TODO: use a flag to determine whether to start the instance. b/156668741 564 565 stepDeleteNewOSDiskAndInstallMediaDisk, err := daisyutils.NewStep(w, "delete-new-os-disk-and-install-media-disk", stepRestoreScript) 566 if err != nil { 567 return err 568 } 569 stepDeleteNewOSDiskAndInstallMediaDisk.DeleteResources = &daisy.DeleteResources{ 570 Disks: []string{ 571 daisyutils.GetDiskURI(u.instanceProject, u.instanceZone, u.newOSDiskName), 572 daisyutils.GetDiskURI(u.instanceProject, u.instanceZone, u.installMediaDiskName), 573 }, 574 } 575 return nil 576 } 577 578 func (u *upgrader) getOriginalStartupScriptURL() string { 579 originalStartupScriptURL := "" 580 if u.originalWindowsStartupScriptURL != nil { 581 originalStartupScriptURL = *u.originalWindowsStartupScriptURL 582 } 583 return originalStartupScriptURL 584 } 585 586 func (u *upgrader) runWorkflowWithSteps(workflowName string, timeout string, populateStepsFunc func(*upgrader, *daisy.Workflow) error) (daisyutils.DaisyWorker, error) { 587 588 workflowProvider := func() (*daisy.Workflow, error) { 589 return u.generateWorkflowWithSteps(workflowName, timeout, populateStepsFunc) 590 } 591 592 env := daisyutils.EnvironmentSettings{ 593 Project: u.instanceProject, 594 Zone: u.instanceZone, 595 GCSPath: u.ScratchBucketGcsPath, 596 OAuth: u.Oauth, 597 Timeout: u.Timeout, 598 ComputeEndpoint: u.Ce, 599 DisableGCSLogs: u.GcsLogsDisabled, 600 DisableCloudLogs: u.CloudLogsDisabled, 601 DisableStdoutLogs: u.StdoutLogsDisabled, 602 ExecutionID: u.executionID, 603 Tool: daisyutils.Tool{ 604 HumanReadableName: "windows upgrade", 605 ResourceLabelName: "windows-upgrade", 606 }, 607 } 608 609 worker := daisyutils.NewDaisyWorker(workflowProvider, env, u.logger) 610 err := worker.Run(map[string]string{}) 611 return worker, err 612 } 613 614 func (u *upgrader) generateWorkflowWithSteps(workflowName string, timeout string, populateStepsFunc func(*upgrader, *daisy.Workflow) error) (*daisy.Workflow, error) { 615 w := daisy.New() 616 w.Name = workflowName 617 w.DefaultTimeout = timeout 618 err := populateStepsFunc(u, w) 619 return w, err 620 }