cli_tools/common/distro/distro.go (292 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 distro import ( "errors" "fmt" "regexp" "strconv" "strings" "github.com/GoogleCloudPlatform/compute-image-import/cli_tools/common/assert" ) const ( centosStream = "centos-stream" centos = "centos" debian = "debian" opensuse = "opensuse" rhel = "rhel" rocky = "rocky" sles = "sles" slesSAP = "sles-sap" ubuntu = "ubuntu" windows = "windows" archX86 = "x86" archX64 = "x64" r2 = "r2" ) var osFlagExpression = regexp.MustCompile( // Distro is required and is comprised of at least one letter. // There may be two segments, separated by a hyphen. // Examples: ubuntu, sles, sles-sap, windows "^(?P<distro>[a-z]+(?:-[a-z]+)?)" + // Version is required, and is at least one word character. // Examples: 2004, 2008, 2008r2 "-(?P<version>[a-z0-9]+)" + // Architecture is optional. The only options are `x86` and `x64`. "(?:-(?P<arch>x86|x64))?" + // License is optional. The only value is `byol`. "(?:-(?P<license>byol))?$") // standardArchitectures contains the architectures that we support, along with possible synonyms. var standardArchitectures = []struct { arch string synonyms []string }{ { archX86, []string{"amd64", "x86_64"}, }, { archX64, []string{"i386", "i686", "x86_32"}, }, } // Flags that don't follow `osFlagExpression` and have been // replaced with newer versions. Mapping is from legacy to modern. var legacyFlags = map[string]string{ // windows-8-1-x64-byol includes an extra hyphen between its // major and minor version. The non-legacy flag is windows-8-x64-byol. "windows-8-1-x64-byol": "windows-8-x64-byol", } // Release encapsulates product and version information // about the operating systems we support. type Release interface { // ImportCompatible returns whether a distro considers two releases // compatible. As an example, different minor versions in Ubuntu are typically // not compatible, while different minor versions in centOS are. ImportCompatible(other Release) bool // AsGcloudArg returns a hyphenated identifier, as used in the `--os` flag // of `gcloud compute images import`. It is intended for // showing to users in a help message. Notably, it lacks support for // the -byol suffix, as that is not currently modeled. AsGcloudArg() string } // FromComponents interprets the (distro, major, minor) tuple and returns // a Release if the arguments are syntactically correct, and represent a // release that we *may* support. The caller is responsible for verifying // whether a translator is available elsewhere in the system. func FromComponents(distro string, major string, minor string, architecture string) (r Release, e error) { standardArch, err := standardizeArchitecture(architecture) if err != nil { return nil, err } standardDistro, err := standardizeDistro(distro) if err != nil { return nil, err } if standardDistro == windows { return newWindowsRelease(major, minor, standardArch) } return newLinuxRelease(standardDistro, major, minor) } // standardizeArchitecture maps a raw string to a known architecture. It's not an error to // have an empty architecture, but if it's specified, it has to match one of the // architectures in standardArchitectures. func standardizeArchitecture(architecture string) (string, error) { if architecture == "" { return "", nil } lowered := strings.ToLower(architecture) for _, standardArch := range standardArchitectures { if standardArch.arch == lowered { return standardArch.arch, nil } for _, synonym := range standardArch.synonyms { if synonym == lowered { return standardArch.arch, nil } } } return "", fmt.Errorf("Unrecognized architecture `%s`", architecture) } // standardizeDistro maps a raw string to a known distro. // It's an error if distro is empty, or if a match isn't found. func standardizeDistro(distro string) (string, error) { if distro == "" { return "", errors.New("distro name required") } d := strings.ReplaceAll(strings.ToLower(distro), "_", "-") for _, known := range []string{centosStream, centos, debian, opensuse, rhel, rocky, slesSAP, sles, ubuntu, windows} { if strings.Contains(d, known) { return known, nil } } return "", fmt.Errorf("Unrecognized distro `%s`", distro) } func newLinuxRelease(distro string, major string, minor string) (Release, error) { majorInt, e := strconv.Atoi(major) if e != nil || majorInt < 1 { return nil, fmt.Errorf( "major version required to be an integer greater than zero. Received: `%s`", major) } var minorInt int if minor == "" { minorInt = 0 } else { minorInt, e = strconv.Atoi(minor) if e != nil || minorInt < 0 { return nil, errors.New( "minor version required to be an integer greater than or equal to zero. Received: " + minor) } } switch distro { case ubuntu: return newUbuntuRelease(majorInt, minorInt) case centos: fallthrough case centosStream: fallthrough case debian: fallthrough case opensuse: fallthrough case rhel: fallthrough case rocky: return newCommonLinuxRelease(distro, majorInt, minorInt) case sles: fallthrough case slesSAP: return newSLESRelease(distro, majorInt, minorInt) default: return nil, fmt.Errorf("Unrecognized distro `%s`", distro) } } // FromGcloudOSArgumentMustParse parses the argument provided to the `--os` flag of // `gcloud compute images import`, and returns a Release if it represents a // release we *may* support. If osFlagValue does not parse, the call panics. func FromGcloudOSArgumentMustParse(osFlagValue string) Release { r, err := FromGcloudOSArgument(osFlagValue) if err != nil { panic(err) } return r } // FromGcloudOSArgument parses the argument provided to the `--os` flag of // `gcloud compute images import`, and returns a Release if it represents a // release we *may* support. The caller is responsible for verifying // whether a translator is available elsewhere in the system. // // https://cloud.google.com/sdk/gcloud/reference/compute/images/import#--os func FromGcloudOSArgument(osFlagValue string) (r Release, e error) { if legacyFlags[osFlagValue] != "" { osFlagValue = legacyFlags[osFlagValue] } match := osFlagExpression.FindStringSubmatch(strings.ToLower(osFlagValue)) if match == nil { return r, fmt.Errorf("expected pattern of `distro-version`. Actual: `%s`", osFlagValue) } components := make(map[string]string) for i, name := range osFlagExpression.SubexpNames() { if i != 0 && name != "" { components[name] = match[i] } } distro, version, arch := components["distro"], components["version"], components["arch"] var major, minor string if distro == ubuntu { // In gcloud, major and minor are combined as MMmm, such as ubuntu-1804 if len(version) != 4 { return r, fmt.Errorf("expected version with length four. Actual: `%s`", version) } major, minor = version[:2], version[2:] } else if distro == windows && strings.HasSuffix(version, r2) { major, minor = version[:len(version)-2], r2 } else { major, minor = version, "" } return FromComponents(distro, major, minor, arch) } // commonLinuxRelease is a Release that: // 1. Has integer major and minor versions. // 2. Compatibility is determined by the major version. // 3. There are no variants. type commonLinuxRelease struct { distro string major int minor int } func (r commonLinuxRelease) AsGcloudArg() string { return fmt.Sprintf("%s-%d", r.distro, r.major) } func (r commonLinuxRelease) ImportCompatible(other Release) bool { realOther, ok := other.(commonLinuxRelease) return ok && r.distro == realOther.distro && r.major == realOther.major } func commonLinuxDistros() []string { return []string{centosStream, centos, debian, opensuse, rhel, rocky} } // The caller is responsible for verifying the syntax of the arguments. // Verify the following before calling: // - distro is one of the distros returned by commonLinuxDistros(). // - major is >= 1 and minor is >= 0 func newCommonLinuxRelease(distro string, major, minor int) (Release, error) { assert.GreaterThanOrEqualTo(major, 1) assert.GreaterThanOrEqualTo(minor, 0) assert.Contains(distro, commonLinuxDistros()) return commonLinuxRelease{ distro: distro, major: major, minor: minor, }, nil } // windowsRelease uses marketing versions rather than NT versions. // Currently the only minor version is "r2". For example, the versions // 2012 and 2012r2 and *not* import compatible, and have different // minor versions. In the first case, the minor version is empty. // In the second it is "r2". // // In terms of import compatibility, currently r2 is the only minor version // that is treated separately. Other minor versions (eg: 8 vs 8.1) are treated // imported using the same logic. type windowsRelease struct { major, minor, architecture string } func (w windowsRelease) ImportCompatible(other Release) bool { actualOther, ok := other.(windowsRelease) compatible := ok && w.major == actualOther.major && w.architecture == actualOther.architecture if w.minor == r2 || actualOther.minor == r2 { compatible = compatible && (w.minor == actualOther.minor) } return compatible } func (w windowsRelease) AsGcloudArg() string { arg := fmt.Sprintf("windows-%s", w.major) if w.minor == r2 { arg += r2 } // Architecture is only included for desktop versions (eg: 7, 8, 10); // Server versions are assumed to be x64, so that's not included in the // osID. if w.architecture != "" && len(w.major) <= 2 { arg += "-" + w.architecture } return arg } func newWindowsRelease(major string, minor string, architecture string) (Release, error) { if !regexp.MustCompile("^\\d+$").MatchString(major) { return nil, fmt.Errorf("`%s` is not a valid major version for Windows", major) } return windowsRelease{major, minor, architecture}, nil } // WindowsServerVersionforNTVersion returns the marketing version corresponding to an NT version. // // Careful: NT versions are shared across multiple Windows products, so this function is a // rough heuristic. It follows these rules for a collision: // - Only returns server versions (not desktop). // - If multiple server versions are possible, return the earliest. func WindowsServerVersionforNTVersion(major string, minor string) (marketingMajor, marketingMinor string, err error) { // Mappings of NT version to marketing versions. // Source: https://wikipedia.org/wiki/List_of_Microsoft_Windows_versions for _, t := range []struct { ntMajor string ntMinor string marketingMajor string marketingMinor string }{ {"6", "0", "2008", ""}, {"6", "1", "2008", "r2"}, {"6", "2", "2012", ""}, {"6", "3", "2012", "r2"}, {"10", "0", "2016", ""}, // NT 10.0 is also 2019 & 2022 } { if major == t.ntMajor && minor == t.ntMinor { return t.marketingMajor, t.marketingMinor, nil } } return "", "", fmt.Errorf("`%s.%s` is not a recognized Windows NT version", major, minor) } // slesRelease is a Release that represents the SLES distro and its variants (such as SLES for SAP). // Compatibility requires the same variant and major version. type slesRelease struct { variant string major int minor int } // The caller is responsible for verifying the syntax of the arguments. // Verify the following before calling: // - distroAndVariant starts with "sles" or "sles-" // - major is >= 1 and minor is >= 0 // // A non-nil error is returned if the syntax is correct, but the // arguments do not follow SLES's naming system. Specifically: // - variant may not have a hyphen in its name func newSLESRelease(distroAndVariant string, major, minor int) (Release, error) { assert.GreaterThanOrEqualTo(major, 1) assert.GreaterThanOrEqualTo(minor, 0) var variant string switch distroAndVariant { case slesSAP: variant = "sap" case sles: variant = "" default: panic(fmt.Sprintf("%q is not valid for SLES", distroAndVariant)) } return slesRelease{variant, major, minor}, nil } func (r slesRelease) ImportCompatible(other Release) bool { actualOther, ok := other.(slesRelease) return ok && r.variant == actualOther.variant && r.major == actualOther.major } func (r slesRelease) AsGcloudArg() string { if r.variant != "" { return fmt.Sprintf("sles-%s-%d", r.variant, r.major) } return fmt.Sprintf("sles-%d", r.major) } // ubuntuRelease is a Release that represents Ubuntu. // Compatibility requires the same major and minor versions. type ubuntuRelease struct { major int minor int } // The caller is responsible for verifying the syntax of the arguments. // Verify the following before calling: // - major is >= 1 and minor is >= 0 // // A non-nil error is returned if the syntax is correct, but the // arguments do not follow Ubuntu's naming system. Specifically: // - minor version must be 4 or 10 func newUbuntuRelease(major, minor int) (Release, error) { assert.GreaterThanOrEqualTo(major, 1) assert.GreaterThanOrEqualTo(minor, 0) if minor == 4 || minor == 10 { return ubuntuRelease{major, minor}, nil } return nil, fmt.Errorf("Ubuntu version `%d.%d` is not importable", major, minor) } func (u ubuntuRelease) ImportCompatible(other Release) bool { actualOther, ok := other.(ubuntuRelease) return ok && u.major == actualOther.major && u.minor == actualOther.minor } func (u ubuntuRelease) AsGcloudArg() string { return fmt.Sprintf("ubuntu-%d%02d", u.major, u.minor) }