cli_tools/gce_windows_upgrade/upgrader/validators.go (168 lines of code) (raw):
// 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 upgrader
import (
"fmt"
"regexp"
"strings"
daisy "github.com/GoogleCloudPlatform/compute-daisy"
daisyCompute "github.com/GoogleCloudPlatform/compute-daisy/compute"
"google.golang.org/api/compute/v1"
"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/domain"
"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/daisyutils"
"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/param"
"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/path"
)
const (
rfc1035 = "[a-z]([-a-z0-9]*[a-z0-9])?"
projectRgxStr = "[a-z]([-.:a-z0-9]*[a-z0-9])?"
)
var (
instanceURLRgx = regexp.MustCompile(fmt.Sprintf(`^(projects/(?P<project>%[1]s)/)?zones/(?P<zone>%[2]s)/instances/(?P<instance>%[2]s)$`, projectRgxStr, rfc1035))
computeClient daisyCompute.Client
mgce domain.MetadataGCEInterface
)
// validateAndDeriveParams validates input params, and infers derived params
// from input params. For example, project and zone can be derived from the
// instance URI.
func (u *upgrader) validateAndDeriveParams() error {
if u.validateAndDeriveParamsFn != nil {
return u.validateAndDeriveParamsFn()
}
if u.derivedVars == nil {
u.derivedVars = &derivedVars{}
}
if err := validateOSVersion(u.SourceOS, u.TargetOS); err != nil {
return err
}
if err := validateAndDeriveInstanceURI(u.Instance, u.ProjectPtr, u.Zone, u.derivedVars); err != nil {
return err
}
if err := validateAndDeriveInstance(u.derivedVars, u.SourceOS, u.TargetOS); err != nil {
return err
}
if u.Timeout == "" {
u.Timeout = DefaultTimeout
}
// Prepare resource names with a random suffix
suffix := path.RandString(8)
u.machineImageBackupName = fmt.Sprintf("windows-upgrade-backup-%v", suffix)
u.osDiskSnapshotName = fmt.Sprintf("windows-upgrade-backup-os-%v", suffix)
u.newOSDiskName = fmt.Sprintf("windows-upgraded-os-%v", suffix)
u.installMediaDiskName = fmt.Sprintf("windows-install-media-%v", suffix)
// Update '-project' flag value for logging purpose.
// Since '-project' may not be input by user explicitly, we need to populate it
// when it's referred in order to track usage by project number.
*u.ProjectPtr = u.instanceProject
return nil
}
func validateOSVersion(sourceOS, targetOS string) error {
if sourceOS == "" {
return daisy.Errf("Flag -source-os must be provided. Please choose a supported version from {%v}.", strings.Join(SupportedVersions, ", "))
}
if !isSupportedOSVersion(sourceOS) {
return daisy.Errf("Flag -source-os value '%v' unsupported. Please choose a supported version from {%v}.", sourceOS, strings.Join(SupportedVersions, ", "))
}
if targetOS == "" {
return daisy.Errf("Flag -target-os must be provided. Please choose a supported version from {%v}.", strings.Join(SupportedVersions, ", "))
}
if !isSupportedOSVersion(targetOS) {
return daisy.Errf("Flag -target-os value '%v' unsupported. Please choose a supported version from {%v}.", targetOS, strings.Join(SupportedVersions, ", "))
}
if !isSupportedUpgradePath(sourceOS, targetOS) {
return daisy.Errf("Can't upgrade from %v to %v. Supported upgrade paths are: %v.", sourceOS, targetOS, strings.Join(getAllUpgradePaths(), ", "))
}
return nil
}
func getAllUpgradePaths() []string {
paths := []string{}
for sourceOS, targets := range upgradePaths {
for targetOS, upgradePath := range targets {
if upgradePath.enabled {
paths = append(paths, fmt.Sprintf("%v => %v", sourceOS, targetOS))
}
}
}
return paths
}
func validateAndDeriveInstanceURI(instance string, projectPtr *string, inputZone string, derivedVars *derivedVars) error {
if instance == "" {
return daisy.Errf("Flag -instance must be provided")
}
derivedVars.instanceURI = instance
if !strings.Contains(instance, "/") {
if err := param.PopulateProjectIfMissing(mgce, projectPtr); err != nil {
return err
}
if inputZone == "" {
return daisy.Errf("--zone must be provided when --instance is not a URI with zone info.")
}
derivedVars.instanceURI = daisyutils.GetInstanceURI(*projectPtr, inputZone, instance)
}
m := daisy.NamedSubexp(instanceURLRgx, derivedVars.instanceURI)
if m == nil {
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)
}
derivedVars.instanceProject = m["project"]
derivedVars.instanceZone = m["zone"]
derivedVars.instanceName = m["instance"]
return nil
}
func validateAndDeriveInstance(derivedVars *derivedVars, sourceOS, targetOS string) error {
inst, err := computeClient.GetInstance(derivedVars.instanceProject, derivedVars.instanceZone, derivedVars.instanceName)
if err != nil {
return daisy.Errf("Failed to get instance: %v", err)
}
if len(inst.Disks) == 0 {
return daisy.Errf("No disks attached to the instance.")
}
// Boot disk is always with index=0: https://cloud.google.com/compute/docs/reference/rest/v1/instances/attachDisk
// "0 is reserved for the boot disk"
bootDisk := inst.Disks[0]
if err := validateAndDeriveOSDisk(bootDisk, derivedVars); err != nil {
return err
}
if err := validateLicense(bootDisk, sourceOS, targetOS); err != nil {
return err
}
// We need to launch upgrade by a startup script, whose URL is set by a metadata
// 'windows-startup-script-url'.
// If that metadata key has been used by the customer before the upgrade, we need
// to backup it and restore after the upgrade finished. We backup it to metadata
// 'windows-startup-script-url-backup'.
// There are 3 possible scenarios:
// 1. 'windows-startup-script-url' doesn't exist originally. Which means, the customer
// doesn't set it. In that case, we don't need to backup anything.
// 2. 'windows-startup-script-url' exists. Which means, the customer set it for
// their purposes. We should backup it in order to restore from it when cleanup
// or rollback.
// 3. 'windows-startup-script-url' exists, but 'windows-startup-script-url-backup'
// also exists. That means the customer tried to run upgrade before but got
// interrupted for some reason. In that case, 'windows-startup-script-url'
// must have been modified, so we should backup 'windows-startup-script-url-backup'
// instead.
if inst.Metadata != nil && inst.Metadata.Items != nil {
originalURL := getMetadataValue(inst.Metadata.Items, metadataWindowsStartupScriptURLBackup)
if originalURL == nil {
originalURL = getMetadataValue(inst.Metadata.Items, metadataWindowsStartupScriptURL)
}
derivedVars.originalWindowsStartupScriptURL = originalURL
}
return nil
}
func getMetadataValue(items []*compute.MetadataItems, key string) *string {
for _, metadataItem := range items {
if metadataItem.Key == key && metadataItem.Value != nil && *metadataItem.Value != "" {
return metadataItem.Value
}
}
return nil
}
func validateAndDeriveOSDisk(osDisk *compute.AttachedDisk, derivedVars *derivedVars) error {
if osDisk.Boot == false {
return daisy.Errf("The instance has no boot disk.")
}
osDiskName := daisyutils.GetResourceID(osDisk.Source)
d, err := computeClient.GetDisk(derivedVars.instanceProject, derivedVars.instanceZone, osDiskName)
if err != nil {
return daisy.Errf("Failed to get boot disk info: %v", err)
}
derivedVars.osDiskURI = param.GetZonalResourcePath(derivedVars.instanceZone, "disks", osDisk.Source)
derivedVars.osDiskDeviceName = osDisk.DeviceName
derivedVars.osDiskAutoDelete = osDisk.AutoDelete
derivedVars.osDiskType = daisyutils.GetResourceID(d.Type)
return nil
}
func validateLicense(osDisk *compute.AttachedDisk, sourceOS, targetOS string) error {
matchSourceOSVersion := false
upgraded := false
for _, lic := range osDisk.Licenses {
for _, expectedLic := range upgradePaths[sourceOS][targetOS].expectedCurrentLicense {
if strings.HasSuffix(lic, expectedLic) {
matchSourceOSVersion = true
} else if strings.HasSuffix(lic, upgradePaths[sourceOS][targetOS].licenseToAdd) {
upgraded = true
}
}
}
if !matchSourceOSVersion {
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))
}
if upgraded {
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))
}
return nil
}