// Copyright 2019 Yunion // // 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 cmdline import ( "fmt" "regexp" "strconv" "strings" "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/fileutils" "yunion.io/x/pkg/util/netutils" "yunion.io/x/pkg/util/osprofile" "yunion.io/x/pkg/util/regutils" "yunion.io/x/pkg/util/sets" "yunion.io/x/pkg/utils" billing_api "yunion.io/x/onecloud/pkg/apis/billing" "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/httperrors" ) var ( ErrorEmptyDesc = errors.Errorf("Empty description") ) // ParseSchedtagConfig desc format: :: func ParseSchedtagConfig(desc string) (*compute.SchedtagConfig, error) { if len(desc) == 0 { return nil, ErrorEmptyDesc } parts := strings.Split(desc, ":") if len(parts) < 2 { return nil, fmt.Errorf("Invalid desc: %s", desc) } strategy := parts[1] if !utils.IsInStringArray(strategy, compute.STRATEGY_LIST) { return nil, fmt.Errorf("Invalid strategy: %s", strategy) } conf := &compute.SchedtagConfig{ Id: parts[0], Strategy: parts[1], } if len(parts) == 3 { conf.ResourceType = parts[2] } return conf, nil } // ParseResourceSchedtagConfig desc format: :: func ParseResourceSchedtagConfig(desc string) (int, *compute.SchedtagConfig, error) { if len(desc) == 0 { return 0, nil, ErrorEmptyDesc } parts := strings.Split(desc, ":") if len(parts) != 3 { return 0, nil, fmt.Errorf("Invalid desc: %s", desc) } idx, err := strconv.Atoi(parts[0]) if err != nil { return 0, nil, err } tag, err := ParseSchedtagConfig(fmt.Sprintf("%s:%s", parts[1], parts[2])) if err != nil { return 0, nil, err } return idx, tag, nil } func ParseDiskConfig(diskStr string, idx int) (*compute.DiskConfig, error) { if len(diskStr) == 0 { return nil, ErrorEmptyDesc } diskConfig := new(compute.DiskConfig) diskConfig.Index = idx // default backend and medium type diskConfig.Backend = "" // STORAGE_LOCAL diskConfig.Medium = "" diskConfig.SizeMb = -1 oldPart, newPart := []string{}, []string{} for _, d0 := range strings.Split(diskStr, ",") { for _, d1 := range strings.Split(d0, ":") { if len(d1) == 0 { continue } if strings.Contains(d1, "=") { newPart = append(newPart, d1) continue } oldPart = append(oldPart, d1) } } for _, p := range oldPart { if regutils.MatchSize(p) { diskConfig.SizeMb, _ = fileutils.GetSizeMb(p, 'M', 1024) } else if utils.IsInStringArray(p, osprofile.FS_TYPES) { diskConfig.Fs = p } else if utils.IsInStringArray(p, osprofile.IMAGE_FORMAT_TYPES) { diskConfig.Format = p } else if utils.IsInStringArray(p, osprofile.DISK_DRIVERS) { diskConfig.Driver = p } else if utils.IsInStringArray(p, osprofile.DISK_CACHE_MODES) { diskConfig.Cache = p } else if utils.IsInStringArray(p, compute.DISK_TYPES) { diskConfig.Medium = p } else if utils.IsInStringArray(p, []string{compute.DISK_TYPE_VOLUME}) { diskConfig.DiskType = p } else if p[0] == '/' { diskConfig.Mountpoint = p } else if p == "autoextend" { diskConfig.SizeMb = -1 } else if p == "autoreset" { diskConfig.AutoReset = true } else if utils.IsInStringArray(p, compute.STORAGE_TYPES) { diskConfig.Backend = p } else if len(p) > 0 { diskConfig.ImageId = p } } for _, p := range newPart { info := strings.Split(p, "=") if len(info) != 2 { return nil, errors.Errorf("invalid disk description %s", p) } var err error desc, str := info[0], info[1] switch desc { case "size": diskConfig.SizeMb, err = fileutils.GetSizeMb(str, 'M', 1024) if err != nil { return nil, errors.Errorf("invalid disk size %s", str) } case "fs": if !utils.IsInStringArray(str, osprofile.FS_TYPES) { return nil, errors.Errorf("invalid disk fs %s, allow choices: %s", str, osprofile.FS_TYPES) } diskConfig.Fs = str case "fs_features": if diskConfig.Fs == "" { return nil, errors.Errorf("disk fs is required") } diskConfig.FsFeatures = &compute.DiskFsFeatures{} for _, feature := range strings.Split(str, ",") { if diskConfig.Fs == "ext4" { if diskConfig.FsFeatures.Ext4 == nil { diskConfig.FsFeatures.Ext4 = &compute.DiskFsExt4Features{} } if feature == "casefold" { diskConfig.FsFeatures.Ext4.CaseInsensitive = true } else { return nil, errors.Errorf("invalid feature %s of %s", feature, diskConfig.Fs) } } if diskConfig.Fs == "f2fs" { if diskConfig.FsFeatures.F2fs == nil { diskConfig.FsFeatures.F2fs = &compute.DiskFsF2fsFeatures{} } if feature == "casefold" { diskConfig.FsFeatures.F2fs.CaseInsensitive = true } else { return nil, errors.Errorf("invalid feature %s of %s", feature, diskConfig.Fs) } } } case "format": if !utils.IsInStringArray(str, osprofile.IMAGE_FORMAT_TYPES) { return nil, errors.Errorf("invalid disk format %s, allow choices: %s", str, osprofile.IMAGE_FORMAT_TYPES) } diskConfig.Format = str case "driver": if !utils.IsInStringArray(str, osprofile.DISK_DRIVERS) { return nil, errors.Errorf("invalid disk driver %s, allow choices: %s", str, osprofile.DISK_DRIVERS) } diskConfig.Driver = str case "cache", "cache_mode": if !utils.IsInStringArray(str, osprofile.DISK_CACHE_MODES) { return nil, errors.Errorf("invalid disk cache mode %s, allow choices: %s", str, osprofile.DISK_CACHE_MODES) } diskConfig.Cache = str case "medium": if !utils.IsInStringArray(str, compute.DISK_TYPES) { return nil, errors.Errorf("invalid disk medium type %s, allow choices: %s", str, compute.DISK_TYPES) } diskConfig.Medium = str case "type", "disk_type": diskTypes := []string{compute.DISK_TYPE_SYS, compute.DISK_TYPE_DATA} if !utils.IsInStringArray(str, diskTypes) { return nil, errors.Errorf("invalid disk type %s, allow choices: %s", str, diskTypes) } diskConfig.DiskType = str case "mountpoint": diskConfig.Mountpoint = str case "storage_type", "backend": diskConfig.Backend = str case "snapshot", "snapshot_id": diskConfig.SnapshotId = str case "disk", "disk_id": diskConfig.DiskId = str case "storage", "storage_id": diskConfig.Storage = str case "image", "image_id": diskConfig.ImageId = str case "existing_path": diskConfig.ExistingPath = str case "boot_index": bootIndex, err := strconv.Atoi(str) if err != nil { return nil, errors.Wrapf(err, "parse disk boot index %s", str) } bootIndex8 := int8(bootIndex) diskConfig.BootIndex = &bootIndex8 case "nvme-device-id": diskConfig.NVMEDevice = &compute.IsolatedDeviceConfig{ Id: str, } case "nvme-device-model": diskConfig.NVMEDevice = &compute.IsolatedDeviceConfig{ Model: str, } case "iops": diskConfig.Iops, _ = strconv.Atoi(str) if err != nil { return nil, errors.Wrapf(err, "parse disk iops %s", str) } case "throughput": diskConfig.Throughput, _ = strconv.Atoi(str) if err != nil { return nil, errors.Wrapf(err, "parse disk iops %s", str) } case "preallocation": if !utils.IsInStringArray(str, compute.DISK_PREALLOCATIONS) { return nil, errors.Errorf("invalid preallocation %s, allow choices: %s", str, compute.DISK_PREALLOCATIONS) } diskConfig.Preallocation = str case "auto_delete": v, err := strconv.ParseBool(str) if err != nil { return nil, errors.Wrapf(err, "parse disk auto_delete %s", str) } diskConfig.AutoDelete = &v default: return nil, errors.Errorf("invalid disk description %s", p) } } return diskConfig, nil } func ParseNetworkConfigByJSON(desc jsonutils.JSONObject, idx int) (*compute.NetworkConfig, error) { if _, ok := desc.(*jsonutils.JSONString); ok { descStr, _ := desc.GetString() return ParseNetworkConfig(descStr, idx) } conf := new(compute.NetworkConfig) conf.Index = idx err := desc.Unmarshal(conf) return conf, err } func isQuoteChar(ch byte) (bool, string) { switch ch { case '[': return true, "]" default: return false, "" } } func splitConfig(confStr string) ([]string, error) { return utils.FindWords2([]byte(confStr), 0, ":", isQuoteChar) } func ParseNetworkConfig(desc string, idx int) (*compute.NetworkConfig, error) { if len(desc) == 0 { return nil, ErrorEmptyDesc } parts, err := splitConfig(desc) if err != nil { return nil, errors.Wrap(err, "splitConfig") } netConfig := new(compute.NetworkConfig) netConfig.Index = idx for _, p := range parts { if len(p) == 0 { continue } if regutils.MatchIP4Addr(p) { netConfig.Address = p } else if regutils.MatchIP6Addr(p) { addr6, err := netutils.NewIPV6Addr(p) if err != nil { return nil, errors.Wrap(httperrors.ErrInvalidFormat, p) } netConfig.Address6 = addr6.String() } else if regutils.MatchCompactMacAddr(p) { netConfig.Mac = netutils.MacUnpackHex(p) } else if strings.HasPrefix(p, "wire=") { netConfig.Wire = p[len("wire="):] } else if strings.HasPrefix(p, "macs=") { macSegs := strings.Split(p[len("macs="):], ",") macs := make([]string, len(macSegs)) for i := range macSegs { macs[i] = netutils.MacUnpackHex(macSegs[i]) } netConfig.Macs = macs } else if strings.HasPrefix(p, "ips=") { netConfig.Addresses = strings.Split(p[len("ips="):], ",") for _, addr := range netConfig.Addresses { _, err := netutils.NewIPV4Addr(addr) if err != nil { return nil, errors.Wrap(err, p) } } } else if strings.HasPrefix(p, "ip6s=") { netConfig.Addresses6 = strings.Split(p[len("ip6s="):], ",") for i, addrStr := range netConfig.Addresses6 { addr6, err := netutils.NewIPV6Addr(addrStr) if err != nil { return nil, errors.Wrap(err, p) } netConfig.Addresses6[i] = addr6.String() } } else if strings.HasPrefix(p, "secgroups=") { netConfig.Secgroups = strings.Split(p[len("secgroups="):], ",") } else if p == "require_designated_ip" { netConfig.RequireDesignatedIP = true } else if p == "random_exit" { netConfig.Exit = true } else if p == "random" { netConfig.Exit = false } else if p == "private" { netConfig.Private = true } else if p == "reserved" { netConfig.Reserved = true } else if p == "teaming" { netConfig.RequireTeaming = true } else if p == "try-teaming" { netConfig.TryTeaming = true } else if p == "defaultgw" { netConfig.IsDefault = true } else if p == "ipv6" { netConfig.RequireIPv6 = true } else if p == "strict-ipv6" { netConfig.RequireIPv6 = true netConfig.StrictIPv6 = true } else if strings.HasPrefix(p, "standby-port=") { netConfig.StandbyPortCount, _ = strconv.Atoi(p[len("standby-port="):]) } else if strings.HasPrefix(p, "standby-addr=") { netConfig.StandbyAddrCount, _ = strconv.Atoi(p[len("standby-addr="):]) } else if utils.IsInStringArray(p, []string{"virtio", "e1000", "vmxnet3"}) { netConfig.Driver = p } else if strings.HasPrefix(p, "num-queues=") { netConfig.NumQueues, _ = strconv.Atoi(p[len("num-queues="):]) } else if strings.HasPrefix(p, "billing-type=") { netConfig.BillingType = billing_api.ParseBillingType(p[len("billing-type="):]) } else if strings.HasPrefix(p, "charge-type=") { netConfig.ChargeType = billing_api.ParseNetChargeType(p[len("charge-type="):]) } else if regutils.MatchSize(p) { bw, err := fileutils.GetSizeMb(p, 'M', 1000) if err != nil { return nil, err } netConfig.BwLimit = bw } else if p == "vip" { netConfig.Vip = true } else if strings.HasPrefix(p, "sriov-nic-id=") { netConfig.SriovDevice = &compute.IsolatedDeviceConfig{ Id: p[len("sriov-nic-id="):], } } else if strings.HasPrefix(p, "sriov-nic-model=") { netConfig.SriovDevice = &compute.IsolatedDeviceConfig{ Model: p[len("sriov-nic-model="):], } } else if strings.HasPrefix(p, "rx-traffic-limit=") { var err error netConfig.RxTrafficLimit, err = strconv.ParseInt(p[len("rx-traffic-limit="):], 10, 0) if err != nil { return nil, errors.Wrap(err, "parse rx-traffic-limit") } } else if strings.HasPrefix(p, "tx-traffic-limit=") { var err error netConfig.TxTrafficLimit, err = strconv.ParseInt(p[len("tx-traffic-limit="):], 10, 0) if err != nil { return nil, errors.Wrap(err, "parse tx-traffic-limit") } } else if compute.IsInNetworkTypes(compute.TNetworkType(p), compute.ALL_NETWORK_TYPES) { netConfig.NetType = compute.TNetworkType(p) } else { netConfig.Network = p } } return netConfig, nil } func ParseNetworkConfigPortMappings(descs []string) (map[int]compute.GuestPortMappings, error) { if len(descs) == 0 { return nil, ErrorEmptyDesc } pms := make(map[int]compute.GuestPortMappings, 0) for _, desc := range descs { idx, pm, err := parseNetworkConfigPortMapping(desc) if err != nil { return nil, errors.Wrapf(err, "parse port mapping: %s", desc) } mappings, ok := pms[idx] if !ok { mappings = make([]*compute.GuestPortMapping, 0) } mappings = append(mappings, pm) pms[idx] = mappings } return pms, nil } func parseNetworkConfigPortMapping(desc string) (int, *compute.GuestPortMapping, error) { pm := &compute.GuestPortMapping{ Protocol: compute.GuestPortMappingProtocolTCP, } idx := 0 for _, seg := range strings.Split(desc, ",") { info := strings.Split(seg, "=") if len(info) != 2 { return -1, nil, errors.Errorf("invalid option %s", seg) } key := info[0] val := info[1] switch key { case "index": valIdx, err := strconv.Atoi(val) if err != nil { return -1, nil, errors.Wrapf(err, "invalid index %s", val) } idx = valIdx case "host_port": hp, err := strconv.Atoi(val) if err != nil { return -1, nil, errors.Wrapf(err, "invalid host_port %s", val) } pm.HostPort = &hp case "container_port", "port": cp, err := strconv.Atoi(val) if err != nil { return -1, nil, errors.Wrapf(err, "invalid container_port %s", val) } pm.Port = cp case "proto", "protocol": pm.Protocol = compute.GuestPortMappingProtocol(val) case "host_port_range": rangeParts := strings.Split(val, "-") if len(rangeParts) != 2 { return -1, nil, errors.Errorf("invalid range string %s", val) } start, err := strconv.Atoi(rangeParts[0]) if err != nil { return -1, nil, errors.Wrapf(err, "invalid host_port_range %s", rangeParts[0]) } end, err := strconv.Atoi(rangeParts[1]) if err != nil { return -1, nil, errors.Wrapf(err, "invalid host_port_range %s", rangeParts[1]) } pm.HostPortRange = &compute.GuestPortMappingPortRange{ Start: start, End: end, } case "remote_ips", "remote_ip": ips := strings.Split(val, "|") pm.RemoteIps = ips } } if pm.Port == 0 { return -1, nil, errors.Error("container_port must specified") } if idx < 0 { return -1, nil, errors.Errorf("invalid index %d", idx) } return idx, pm, nil } func ParseIsolatedDevice(desc string, idx int) (*compute.IsolatedDeviceConfig, error) { if len(desc) == 0 { return nil, ErrorEmptyDesc } if idx < 0 { return nil, fmt.Errorf("Invalid index: %d", idx) } dev := new(compute.IsolatedDeviceConfig) parts := strings.Split(desc, ":") devTypes := sets.NewString(compute.VALID_PASSTHROUGH_TYPES...) devTypes.Insert(compute.VALID_CONTAINER_DEVICE_TYPES...) for _, p := range parts { if regutils.MatchUUIDExact(p) { dev.Id = p } else if devTypes.Has(p) { dev.DevType = p } else if strings.HasPrefix(p, "vendor=") { dev.Vendor = p[len("vendor="):] } else if strings.HasPrefix(p, "device_path=") { dev.DevicePath = p[len("device_path="):] } else { dev.Model = p } } return dev, nil } func ParseBaremetalRootDiskMatcher(line string) (*compute.BaremetalRootDiskMatcher, error) { ret := new(compute.BaremetalRootDiskMatcher) for _, seg := range strings.Split(line, ",") { info := strings.Split(seg, "=") if len(info) != 2 { return nil, errors.Errorf("invalid option %s", seg) } key := info[0] val := info[1] switch key { case "size": sizeMB, err := fileutils.GetSizeMb(val, 'M', 1024) if err != nil { return nil, errors.Wrapf(err, "parse size %s", val) } ret.SizeMB = int64(sizeMB) case "device", "dev": ret.Device = val case "size_start": sizeMB, err := fileutils.GetSizeMb(val, 'M', 1024) if err != nil { return nil, errors.Wrapf(err, "parse size_start %s", val) } if ret.SizeMBRange == nil { ret.SizeMBRange = new(compute.RootDiskMatcherSizeMBRange) } ret.SizeMBRange.Start = int64(sizeMB) case "size_end": sizeMB, err := fileutils.GetSizeMb(val, 'M', 1024) if err != nil { return nil, errors.Wrapf(err, "parse size_end %s", val) } if ret.SizeMBRange == nil { ret.SizeMBRange = new(compute.RootDiskMatcherSizeMBRange) } ret.SizeMBRange.End = int64(sizeMB) } } return ret, nil } func ParseBaremetalDiskConfig(desc string) (*compute.BaremetalDiskConfig, error) { bdc := new(compute.BaremetalDiskConfig) bdc.Type = compute.DISK_TYPE_HYBRID bdc.Conf = compute.DISK_CONF_NONE bdc.Count = 0 desc = strings.ToLower(desc) if len(desc) == 0 { return bdc, nil } parts := strings.Split(desc, ":") drvMap := make(map[string]string) for _, drv := range compute.DISK_DRIVERS.List() { drvMap[strings.ToLower(drv)] = drv } for _, p := range parts { if len(p) == 0 { continue } else if utils.IsInStringArray(p, compute.DISK_TYPES) { bdc.Type = p } else if compute.DISK_CONFS.Has(p) { bdc.Conf = p } else if drv, ok := drvMap[p]; ok { bdc.Driver = drv } else if utils.IsMatchInteger(p) { bdc.Count, _ = strconv.ParseInt(p, 0, 0) } else if len(p) > 2 && p[0] == '[' && p[len(p)-1] == ']' { rg, err1 := ParseRange(p[1:(len(p) - 1)]) if err1 != nil { return nil, err1 } bdc.Range = rg } else if len(p) > 2 && p[0] == '(' && p[len(p)-1] == ')' { bdc.Splits = p[1 : len(p)-1] } else if utils.HasPrefix(p, "strip") { strip := parseStrip(p[len("strip"):], "k") bdc.Strip = &strip } else if utils.HasPrefix(p, "adapter") { ada, _ := strconv.ParseInt(p[len("adapter"):], 0, 64) pada := int(ada) bdc.Adapter = &pada } else if p == "ra" { hasRA := true bdc.RA = &hasRA } else if p == "nora" { noRA := false bdc.RA = &noRA } else if p == "wt" { wt := true bdc.WT = &wt } else if p == "wb" { wt := false bdc.WT = &wt } else if p == "direct" { direct := true bdc.Direct = &direct } else if p == "cached" { direct := false bdc.Direct = &direct } else if p == "cachedbadbbu" { cached := true bdc.Cachedbadbbu = &cached } else if p == "nocachedbadbbu" { cached := false bdc.Cachedbadbbu = &cached } else { return nil, fmt.Errorf("ParseDiskConfig unkown option %q", p) } } return bdc, nil } func ParseRange(rangeStr string) (ret []int64, err error) { rss := regexp.MustCompile(`[\s,]+`).Split(rangeStr, -1) intSet := sets.NewInt64() for _, rs := range rss { r, err1 := _parseRange(rs) if err1 != nil { err = err1 return } intSet.Insert(r...) } ret = intSet.List() return } // range string should be: "1-3", "3" func _parseRange(str string) (ret []int64, err error) { if len(str) == 0 { return } // exclude "," symbol if len(str) == 1 && !utils.IsMatchInteger(str) { return } // add int string if utils.IsMatchInteger(str) { i, _ := strconv.ParseInt(str, 10, 64) ret = append(ret, i) return } // add rang like string, "2-10" etc. ret, err = parseRangeStr(str) return } // return KB func parseStrip(stripStr string, defaultSize string) int64 { size, _ := utils.GetSize(stripStr, defaultSize, 1024) return size / 1024 } func parseRangeStr(str string) (ret []int64, err error) { im := utils.IsMatchInteger errGen := func(e string) error { return fmt.Errorf("Incorrect range str: %q", e) } rs := strings.Split(str, "-") if len(rs) != 2 { err = errGen(str) return } bs, es := rs[0], rs[1] if !im(bs) { err = errGen(str) return } if !im(es) { err = errGen(str) return } begin, _ := strconv.ParseInt(bs, 10, 64) end, _ := strconv.ParseInt(es, 10, 64) if begin > end { begin, end = end, begin } for i := begin; i <= end; i++ { ret = append(ret, i) } return }