github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/gce_windows_upgrade/upgrader/validators.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 "regexp" 20 "strings" 21 22 daisy "github.com/GoogleCloudPlatform/compute-daisy" 23 daisyCompute "github.com/GoogleCloudPlatform/compute-daisy/compute" 24 "google.golang.org/api/compute/v1" 25 26 "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/domain" 27 "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/daisyutils" 28 "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/param" 29 "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/path" 30 ) 31 32 const ( 33 rfc1035 = "[a-z]([-a-z0-9]*[a-z0-9])?" 34 projectRgxStr = "[a-z]([-.:a-z0-9]*[a-z0-9])?" 35 ) 36 37 var ( 38 instanceURLRgx = regexp.MustCompile(fmt.Sprintf(`^(projects/(?P<project>%[1]s)/)?zones/(?P<zone>%[2]s)/instances/(?P<instance>%[2]s)$`, projectRgxStr, rfc1035)) 39 40 computeClient daisyCompute.Client 41 mgce domain.MetadataGCEInterface 42 ) 43 44 // validateAndDeriveParams validates input params, and infers derived params 45 // from input params. For example, project and zone can be derived from the 46 // instance URI. 47 func (u *upgrader) validateAndDeriveParams() error { 48 if u.validateAndDeriveParamsFn != nil { 49 return u.validateAndDeriveParamsFn() 50 } 51 52 if u.derivedVars == nil { 53 u.derivedVars = &derivedVars{} 54 } 55 56 if err := validateOSVersion(u.SourceOS, u.TargetOS); err != nil { 57 return err 58 } 59 if err := validateAndDeriveInstanceURI(u.Instance, u.ProjectPtr, u.Zone, u.derivedVars); err != nil { 60 return err 61 } 62 if err := validateAndDeriveInstance(u.derivedVars, u.SourceOS, u.TargetOS); err != nil { 63 return err 64 } 65 66 if u.Timeout == "" { 67 u.Timeout = DefaultTimeout 68 } 69 70 // Prepare resource names with a random suffix 71 suffix := path.RandString(8) 72 u.machineImageBackupName = fmt.Sprintf("windows-upgrade-backup-%v", suffix) 73 u.osDiskSnapshotName = fmt.Sprintf("windows-upgrade-backup-os-%v", suffix) 74 u.newOSDiskName = fmt.Sprintf("windows-upgraded-os-%v", suffix) 75 u.installMediaDiskName = fmt.Sprintf("windows-install-media-%v", suffix) 76 77 // Update '-project' flag value for logging purpose. 78 // Since '-project' may not be input by user explicitly, we need to populate it 79 // when it's referred in order to track usage by project number. 80 *u.ProjectPtr = u.instanceProject 81 82 return nil 83 } 84 85 func validateOSVersion(sourceOS, targetOS string) error { 86 if sourceOS == "" { 87 return daisy.Errf("Flag -source-os must be provided. Please choose a supported version from {%v}.", strings.Join(SupportedVersions, ", ")) 88 } 89 if !isSupportedOSVersion(sourceOS) { 90 return daisy.Errf("Flag -source-os value '%v' unsupported. Please choose a supported version from {%v}.", sourceOS, strings.Join(SupportedVersions, ", ")) 91 } 92 if targetOS == "" { 93 return daisy.Errf("Flag -target-os must be provided. Please choose a supported version from {%v}.", strings.Join(SupportedVersions, ", ")) 94 } 95 if !isSupportedOSVersion(targetOS) { 96 return daisy.Errf("Flag -target-os value '%v' unsupported. Please choose a supported version from {%v}.", targetOS, strings.Join(SupportedVersions, ", ")) 97 } 98 if !isSupportedUpgradePath(sourceOS, targetOS) { 99 return daisy.Errf("Can't upgrade from %v to %v. Supported upgrade paths are: %v.", sourceOS, targetOS, strings.Join(getAllUpgradePaths(), ", ")) 100 } 101 return nil 102 } 103 104 func getAllUpgradePaths() []string { 105 paths := []string{} 106 for sourceOS, targets := range upgradePaths { 107 for targetOS, upgradePath := range targets { 108 if upgradePath.enabled { 109 paths = append(paths, fmt.Sprintf("%v => %v", sourceOS, targetOS)) 110 } 111 } 112 } 113 return paths 114 } 115 116 func validateAndDeriveInstanceURI(instance string, projectPtr *string, inputZone string, derivedVars *derivedVars) error { 117 if instance == "" { 118 return daisy.Errf("Flag -instance must be provided") 119 } 120 derivedVars.instanceURI = instance 121 if !strings.Contains(instance, "/") { 122 if err := param.PopulateProjectIfMissing(mgce, projectPtr); err != nil { 123 return err 124 } 125 if inputZone == "" { 126 return daisy.Errf("--zone must be provided when --instance is not a URI with zone info.") 127 } 128 derivedVars.instanceURI = daisyutils.GetInstanceURI(*projectPtr, inputZone, instance) 129 } 130 131 m := daisy.NamedSubexp(instanceURLRgx, derivedVars.instanceURI) 132 if m == nil { 133 return daisy.Errf("Please provide the instance flag either with the name of the instance or in the form of 'projects/<project>/zones/<zone>/instances/<instance>', not %s", instance) 134 } 135 derivedVars.instanceProject = m["project"] 136 derivedVars.instanceZone = m["zone"] 137 derivedVars.instanceName = m["instance"] 138 return nil 139 } 140 141 func validateAndDeriveInstance(derivedVars *derivedVars, sourceOS, targetOS string) error { 142 inst, err := computeClient.GetInstance(derivedVars.instanceProject, derivedVars.instanceZone, derivedVars.instanceName) 143 if err != nil { 144 return daisy.Errf("Failed to get instance: %v", err) 145 } 146 147 if len(inst.Disks) == 0 { 148 return daisy.Errf("No disks attached to the instance.") 149 } 150 // Boot disk is always with index=0: https://cloud.google.com/compute/docs/reference/rest/v1/instances/attachDisk 151 // "0 is reserved for the boot disk" 152 bootDisk := inst.Disks[0] 153 if err := validateAndDeriveOSDisk(bootDisk, derivedVars); err != nil { 154 return err 155 } 156 if err := validateLicense(bootDisk, sourceOS, targetOS); err != nil { 157 return err 158 } 159 160 // We need to launch upgrade by a startup script, whose URL is set by a metadata 161 // 'windows-startup-script-url'. 162 // If that metadata key has been used by the customer before the upgrade, we need 163 // to backup it and restore after the upgrade finished. We backup it to metadata 164 // 'windows-startup-script-url-backup'. 165 // There are 3 possible scenarios: 166 // 1. 'windows-startup-script-url' doesn't exist originally. Which means, the customer 167 // doesn't set it. In that case, we don't need to backup anything. 168 // 2. 'windows-startup-script-url' exists. Which means, the customer set it for 169 // their purposes. We should backup it in order to restore from it when cleanup 170 // or rollback. 171 // 3. 'windows-startup-script-url' exists, but 'windows-startup-script-url-backup' 172 // also exists. That means the customer tried to run upgrade before but got 173 // interrupted for some reason. In that case, 'windows-startup-script-url' 174 // must have been modified, so we should backup 'windows-startup-script-url-backup' 175 // instead. 176 if inst.Metadata != nil && inst.Metadata.Items != nil { 177 originalURL := getMetadataValue(inst.Metadata.Items, metadataWindowsStartupScriptURLBackup) 178 if originalURL == nil { 179 originalURL = getMetadataValue(inst.Metadata.Items, metadataWindowsStartupScriptURL) 180 } 181 derivedVars.originalWindowsStartupScriptURL = originalURL 182 } 183 184 return nil 185 } 186 187 func getMetadataValue(items []*compute.MetadataItems, key string) *string { 188 for _, metadataItem := range items { 189 if metadataItem.Key == key && metadataItem.Value != nil && *metadataItem.Value != "" { 190 return metadataItem.Value 191 } 192 } 193 return nil 194 } 195 196 func validateAndDeriveOSDisk(osDisk *compute.AttachedDisk, derivedVars *derivedVars) error { 197 if osDisk.Boot == false { 198 return daisy.Errf("The instance has no boot disk.") 199 } 200 osDiskName := daisyutils.GetResourceID(osDisk.Source) 201 d, err := computeClient.GetDisk(derivedVars.instanceProject, derivedVars.instanceZone, osDiskName) 202 if err != nil { 203 return daisy.Errf("Failed to get boot disk info: %v", err) 204 } 205 206 derivedVars.osDiskURI = param.GetZonalResourcePath(derivedVars.instanceZone, "disks", osDisk.Source) 207 derivedVars.osDiskDeviceName = osDisk.DeviceName 208 derivedVars.osDiskAutoDelete = osDisk.AutoDelete 209 derivedVars.osDiskType = daisyutils.GetResourceID(d.Type) 210 return nil 211 } 212 213 func validateLicense(osDisk *compute.AttachedDisk, sourceOS, targetOS string) error { 214 matchSourceOSVersion := false 215 upgraded := false 216 for _, lic := range osDisk.Licenses { 217 for _, expectedLic := range upgradePaths[sourceOS][targetOS].expectedCurrentLicense { 218 if strings.HasSuffix(lic, expectedLic) { 219 matchSourceOSVersion = true 220 } else if strings.HasSuffix(lic, upgradePaths[sourceOS][targetOS].licenseToAdd) { 221 upgraded = true 222 } 223 } 224 } 225 if !matchSourceOSVersion { 226 return daisy.Errf(fmt.Sprintf("No valid Windows Server PayG license can be found. Any of the following licenses are required: %v", upgradePaths[sourceOS][targetOS].expectedCurrentLicense)) 227 } 228 if upgraded { 229 return daisy.Errf(fmt.Sprintf("The GCE instance has the %v license attached. This likely means the instance either has been upgraded or has started an upgrade in the past.", upgradePaths[sourceOS][targetOS].licenseToAdd)) 230 } 231 return nil 232 }