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 }