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  }