github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/instance/hardwarecharacteristics.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package instance
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"strconv"
    10  	"strings"
    11  	"text/scanner"
    12  	"unicode"
    13  
    14  	"github.com/juju/errors"
    15  
    16  	"github.com/juju/juju/core/arch"
    17  )
    18  
    19  // HardwareCharacteristics represents the characteristics of the instance (if known).
    20  // Attributes that are nil are unknown or not supported.
    21  type HardwareCharacteristics struct {
    22  	// Arch is the architecture of the processor.
    23  	Arch *string `json:"arch,omitempty" yaml:"arch,omitempty"`
    24  
    25  	// Mem is the size of RAM in megabytes.
    26  	Mem *uint64 `json:"mem,omitempty" yaml:"mem,omitempty"`
    27  
    28  	// RootDisk is the size of the disk in megabytes.
    29  	RootDisk *uint64 `json:"root-disk,omitempty" yaml:"rootdisk,omitempty"`
    30  
    31  	// RootDiskSource is where the disk storage resides.
    32  	RootDiskSource *string `json:"root-disk-source,omitempty" yaml:"rootdisksource,omitempty"`
    33  
    34  	// CpuCores is the number of logical cores the processor has.
    35  	CpuCores *uint64 `json:"cpu-cores,omitempty" yaml:"cpucores,omitempty"`
    36  
    37  	// CpuPower is a relative representation of the speed of the processor.
    38  	CpuPower *uint64 `json:"cpu-power,omitempty" yaml:"cpupower,omitempty"`
    39  
    40  	// Tags is a list of strings that identify the machine.
    41  	Tags *[]string `json:"tags,omitempty" yaml:"tags,omitempty"`
    42  
    43  	// AvailabilityZone defines the zone in which the machine resides.
    44  	AvailabilityZone *string `json:"availability-zone,omitempty" yaml:"availabilityzone,omitempty"`
    45  
    46  	// VirtType is the virtualisation type of the instance.
    47  	VirtType *string `json:"virt-type,omitempty" yaml:"virttype,omitempty"`
    48  }
    49  
    50  // quoteIfNeeded quotes s (according to Go string quoting rules) if it
    51  // contains a comma or quote or whitespace character, otherwise it returns the
    52  // original string.
    53  func quoteIfNeeded(s string) string {
    54  	i := strings.IndexFunc(s, func(c rune) bool {
    55  		return c == ',' || c == '"' || unicode.IsSpace(c)
    56  	})
    57  	if i < 0 {
    58  		// No space or comma or quote in string, return as is
    59  		return s
    60  	}
    61  	return strconv.Quote(s)
    62  }
    63  
    64  func (hc HardwareCharacteristics) String() string {
    65  	var strs []string
    66  	if hc.Arch != nil {
    67  		strs = append(strs, fmt.Sprintf("arch=%s", quoteIfNeeded(*hc.Arch)))
    68  	}
    69  	if hc.CpuCores != nil {
    70  		strs = append(strs, fmt.Sprintf("cores=%d", *hc.CpuCores))
    71  	}
    72  	if hc.CpuPower != nil {
    73  		strs = append(strs, fmt.Sprintf("cpu-power=%d", *hc.CpuPower))
    74  	}
    75  	if hc.Mem != nil {
    76  		strs = append(strs, fmt.Sprintf("mem=%dM", *hc.Mem))
    77  	}
    78  	if hc.RootDisk != nil {
    79  		strs = append(strs, fmt.Sprintf("root-disk=%dM", *hc.RootDisk))
    80  	}
    81  	if hc.RootDiskSource != nil {
    82  		strs = append(strs, fmt.Sprintf("root-disk-source=%s", quoteIfNeeded(*hc.RootDiskSource)))
    83  	}
    84  	if hc.Tags != nil && len(*hc.Tags) > 0 {
    85  		escapedTags := make([]string, len(*hc.Tags))
    86  		for i, tag := range *hc.Tags {
    87  			escapedTags[i] = quoteIfNeeded(tag)
    88  		}
    89  		strs = append(strs, fmt.Sprintf("tags=%s", strings.Join(escapedTags, ",")))
    90  	}
    91  	if hc.AvailabilityZone != nil && *hc.AvailabilityZone != "" {
    92  		strs = append(strs, fmt.Sprintf("availability-zone=%s", quoteIfNeeded(*hc.AvailabilityZone)))
    93  	}
    94  	if hc.VirtType != nil && *hc.VirtType != "" {
    95  		strs = append(strs, fmt.Sprintf("virt-type=%s", quoteIfNeeded(*hc.VirtType)))
    96  	}
    97  	return strings.Join(strs, " ")
    98  }
    99  
   100  // Clone returns a copy of the hardware characteristics.
   101  func (hc *HardwareCharacteristics) Clone() *HardwareCharacteristics {
   102  	if hc == nil {
   103  		return nil
   104  	}
   105  	clone := *hc
   106  	if hc.Tags != nil {
   107  		tags := make([]string, len(*hc.Tags))
   108  		copy(tags, *hc.Tags)
   109  		clone.Tags = &tags
   110  	}
   111  	return &clone
   112  }
   113  
   114  // MustParseHardware constructs a HardwareCharacteristics from the supplied arguments,
   115  // as Parse, but panics on failure.
   116  func MustParseHardware(args ...string) HardwareCharacteristics {
   117  	hc, err := ParseHardware(args...)
   118  	if err != nil {
   119  		panic(err)
   120  	}
   121  	return hc
   122  }
   123  
   124  // ParseHardware constructs a HardwareCharacteristics from the supplied arguments,
   125  // each of which must contain only spaces and name=value pairs. If any
   126  // name is specified more than once, an error is returned.
   127  func ParseHardware(args ...string) (HardwareCharacteristics, error) {
   128  	hc := HardwareCharacteristics{}
   129  	for _, arg := range args {
   130  		arg = strings.TrimSpace(arg)
   131  		for arg != "" {
   132  			var err error
   133  			arg, err = hc.parseField(arg)
   134  			if err != nil {
   135  				return hc, errors.Trace(err)
   136  			}
   137  			arg = strings.TrimSpace(arg)
   138  		}
   139  	}
   140  	return hc, nil
   141  }
   142  
   143  // parseField parses a single name=value (or name="value") field into the
   144  // corresponding field of the receiver.
   145  func (hc *HardwareCharacteristics) parseField(s string) (rest string, err error) {
   146  	eq := strings.IndexByte(s, '=')
   147  	if eq <= 0 {
   148  		return s, errors.Errorf("malformed characteristic %q", s)
   149  	}
   150  	name, rest := s[:eq], s[eq+1:]
   151  
   152  	switch name {
   153  	case "tags":
   154  		// Tags is a multi-valued field (comma separated)
   155  		var values []string
   156  		values, rest, err = parseMulti(rest)
   157  		if err != nil {
   158  			return rest, errors.Errorf("%s: %v", name, err)
   159  		}
   160  		err = hc.setTags(values)
   161  	default:
   162  		// All other fields are single-valued
   163  		var value string
   164  		value, rest, err = parseSingle(rest, " ")
   165  		if err != nil {
   166  			return rest, errors.Errorf("%s: %v", name, err)
   167  		}
   168  		switch name {
   169  		case "arch":
   170  			err = hc.setArch(value)
   171  		case "cores":
   172  			err = hc.setCpuCores(value)
   173  		case "cpu-power":
   174  			err = hc.setCpuPower(value)
   175  		case "mem":
   176  			err = hc.setMem(value)
   177  		case "root-disk":
   178  			err = hc.setRootDisk(value)
   179  		case "root-disk-source":
   180  			err = hc.setRootDiskSource(value)
   181  		case "availability-zone":
   182  			err = hc.setAvailabilityZone(value)
   183  		case "virt-type":
   184  			err = hc.setVirtType(value)
   185  		default:
   186  			return rest, errors.Errorf("unknown characteristic %q", name)
   187  		}
   188  	}
   189  	if err != nil {
   190  		return rest, errors.Errorf("bad %q characteristic: %v", name, err)
   191  	}
   192  	return rest, nil
   193  }
   194  
   195  // parseSingle parses a single (optionally quoted) value from s and returns
   196  // the value and the remainder of the string.
   197  func parseSingle(s string, seps string) (value, rest string, err error) {
   198  	if len(s) > 0 && s[0] == '"' {
   199  		value, rest, err = parseQuotedString(s)
   200  		if err != nil {
   201  			return "", rest, errors.Trace(err)
   202  		}
   203  	} else {
   204  		sepPos := strings.IndexAny(s, seps)
   205  		value = s
   206  		if sepPos >= 0 {
   207  			value, rest = value[:sepPos], value[sepPos:]
   208  		}
   209  	}
   210  	return value, rest, nil
   211  }
   212  
   213  // parseMulti parses multiple (optionally quoted) comma-separated values from s
   214  // and returns the values and the remainder of the string.
   215  func parseMulti(s string) (values []string, rest string, err error) {
   216  	needComma := false
   217  	rest = s
   218  	for rest != "" && rest[0] != ' ' {
   219  		if needComma {
   220  			if rest[0] != ',' {
   221  				return values, rest, errors.New("expected comma after quoted value")
   222  			}
   223  			rest = rest[1:]
   224  		}
   225  		needComma = true
   226  
   227  		var value string
   228  		value, rest, err = parseSingle(rest, ", ")
   229  		if err != nil {
   230  			return values, rest, errors.Trace(err)
   231  		}
   232  		if value != "" {
   233  			values = append(values, value)
   234  		}
   235  	}
   236  	return values, rest, nil
   237  }
   238  
   239  // parseQuotedString parses a string name=value argument, returning the
   240  // unquoted value and the remainder of the string.
   241  func parseQuotedString(input string) (value, rest string, err error) {
   242  	// Use text/scanner to find end of quoted string
   243  	var s scanner.Scanner
   244  	s.Init(strings.NewReader(input))
   245  	s.Mode = scanner.ScanStrings
   246  	s.Whitespace = 0
   247  	var errMsg string
   248  	s.Error = func(s *scanner.Scanner, msg string) {
   249  		// Record first error
   250  		if errMsg == "" {
   251  			errMsg = msg
   252  		}
   253  	}
   254  	tok := s.Scan()
   255  	rest = input[s.Pos().Offset:]
   256  	if s.ErrorCount > 0 {
   257  		return "", rest, errors.Errorf("parsing quoted string: %s", errMsg)
   258  	}
   259  	if tok != scanner.String {
   260  		// Shouldn't happen; we only asked for strings
   261  		return "", rest, errors.Errorf("parsing quoted string: unexpected token %s", scanner.TokenString(tok))
   262  	}
   263  
   264  	// And then strconv to unquote it (oddly, text/scanner doesn't unquote)
   265  	unquoted, err := strconv.Unquote(s.TokenText())
   266  	if err != nil {
   267  		// Shouldn't happen; scanner should only return valid quoted strings
   268  		return "", rest, errors.Errorf("parsing quoted string: %v", err)
   269  	}
   270  	return unquoted, rest, nil
   271  }
   272  
   273  func (hc *HardwareCharacteristics) setArch(str string) error {
   274  	if hc.Arch != nil {
   275  		return errors.Errorf("already set")
   276  	}
   277  	if str != "" && !arch.IsSupportedArch(str) {
   278  		return errors.Errorf("%q not recognized", str)
   279  	}
   280  	hc.Arch = &str
   281  	return nil
   282  }
   283  
   284  func (hc *HardwareCharacteristics) setCpuCores(str string) (err error) {
   285  	if hc.CpuCores != nil {
   286  		return errors.Errorf("already set")
   287  	}
   288  	hc.CpuCores, err = parseUint64(str)
   289  	return
   290  }
   291  
   292  func (hc *HardwareCharacteristics) setCpuPower(str string) (err error) {
   293  	if hc.CpuPower != nil {
   294  		return errors.Errorf("already set")
   295  	}
   296  	hc.CpuPower, err = parseUint64(str)
   297  	return
   298  }
   299  
   300  func (hc *HardwareCharacteristics) setMem(str string) (err error) {
   301  	if hc.Mem != nil {
   302  		return errors.Errorf("already set")
   303  	}
   304  	hc.Mem, err = parseSize(str)
   305  	return
   306  }
   307  
   308  func (hc *HardwareCharacteristics) setRootDisk(str string) (err error) {
   309  	if hc.RootDisk != nil {
   310  		return errors.Errorf("already set")
   311  	}
   312  	hc.RootDisk, err = parseSize(str)
   313  	return
   314  }
   315  
   316  func (hc *HardwareCharacteristics) setRootDiskSource(str string) (err error) {
   317  	if hc.RootDiskSource != nil {
   318  		return errors.Errorf("already set")
   319  	}
   320  	if str != "" {
   321  		hc.RootDiskSource = &str
   322  	}
   323  	return
   324  }
   325  
   326  func (hc *HardwareCharacteristics) setTags(strs []string) (err error) {
   327  	if hc.Tags != nil {
   328  		return errors.Errorf("already set")
   329  	}
   330  	if len(strs) > 0 {
   331  		hc.Tags = &strs
   332  	}
   333  	return
   334  }
   335  
   336  func (hc *HardwareCharacteristics) setAvailabilityZone(str string) error {
   337  	if hc.AvailabilityZone != nil {
   338  		return errors.Errorf("already set")
   339  	}
   340  	if str != "" {
   341  		hc.AvailabilityZone = &str
   342  	}
   343  	return nil
   344  }
   345  
   346  func (hc *HardwareCharacteristics) setVirtType(str string) error {
   347  	if hc.VirtType != nil {
   348  		return errors.Errorf("already set")
   349  	}
   350  	// TODO (stickupkid): We potentially will want to allow "" to be a valid
   351  	// container virt-type, converting all empty strings to the default instance
   352  	// type. For now, allow LXD to fallback to the default instance type.
   353  	if str == "" {
   354  		return nil
   355  	}
   356  	if _, err := ParseVirtType(str); err != nil {
   357  		return errors.Trace(err)
   358  	}
   359  	hc.VirtType = &str
   360  	return nil
   361  }
   362  
   363  func parseUint64(str string) (*uint64, error) {
   364  	var value uint64
   365  	if str != "" {
   366  		val, err := strconv.ParseUint(str, 10, 64)
   367  		if err != nil {
   368  			return nil, errors.Errorf("must be a non-negative integer")
   369  		}
   370  		value = val
   371  	}
   372  	return &value, nil
   373  }
   374  
   375  func parseSize(str string) (*uint64, error) {
   376  	var value uint64
   377  	if str != "" {
   378  		mult := 1.0
   379  		if m, ok := mbSuffixes[str[len(str)-1:]]; ok {
   380  			str = str[:len(str)-1]
   381  			mult = m
   382  		}
   383  		val, err := strconv.ParseFloat(str, 64)
   384  		if err != nil || val < 0 {
   385  			return nil, errors.Errorf("must be a non-negative float with optional M/G/T/P suffix")
   386  		}
   387  		val *= mult
   388  		value = uint64(math.Ceil(val))
   389  	}
   390  	return &value, nil
   391  }
   392  
   393  var mbSuffixes = map[string]float64{
   394  	"M": 1,
   395  	"G": 1024,
   396  	"T": 1024 * 1024,
   397  	"P": 1024 * 1024 * 1024,
   398  }