github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/constraints.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package storage
     5  
     6  import (
     7  	"fmt"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils/v3"
    14  )
    15  
    16  // Constraints describes a set of storage constraints.
    17  type Constraints struct {
    18  	// Pool is the name of the storage pool (ebs, ceph, custompool, ...)
    19  	// that must provide the storage, or "" if the default pool should be
    20  	// used.
    21  	Pool string
    22  
    23  	// Size is the minimum size of the storage in MiB.
    24  	Size uint64
    25  
    26  	// Count is the number of instances of the storage to create.
    27  	Count uint64
    28  }
    29  
    30  var (
    31  	poolRE  = regexp.MustCompile("^[a-zA-Z]+[-?a-zA-Z0-9]*$")
    32  	countRE = regexp.MustCompile("^-?[0-9]+$")
    33  	sizeRE  = regexp.MustCompile("^-?[0-9]+(?:\\.[0-9]+)?[MGTPEZY](?:i?B)?$")
    34  )
    35  
    36  // ParseConstraints parses the specified string and creates a
    37  // Constraints structure.
    38  //
    39  // The acceptable format for storage constraints is a comma separated
    40  // sequence of: POOL, COUNT, and SIZE, where
    41  //
    42  //	POOL identifies the storage pool. POOL can be a string
    43  //	starting with a letter, followed by zero or more digits
    44  //	or letters optionally separated by hyphens.
    45  //
    46  //	COUNT is a positive integer indicating how many instances
    47  //	of the storage to create. If unspecified, and SIZE is
    48  //	specified, COUNT defaults to 1.
    49  //
    50  //	SIZE describes the minimum size of the storage instances to
    51  //	create. SIZE is a floating point number and multiplier from
    52  //	the set (M, G, T, P, E, Z, Y), which are all treated as
    53  //	powers of 1024.
    54  func ParseConstraints(s string) (Constraints, error) {
    55  	var cons Constraints
    56  	fields := strings.Split(s, ",")
    57  	for _, field := range fields {
    58  		if field == "" {
    59  			continue
    60  		}
    61  		if IsValidPoolName(field) {
    62  			if cons.Pool != "" {
    63  				return cons, errors.NotValidf("pool name is already set to %q, new value %q", cons.Pool, field)
    64  			} else {
    65  				cons.Pool = field
    66  			}
    67  			continue
    68  		}
    69  		if count, ok, err := parseCount(field); ok {
    70  			if err != nil {
    71  				return cons, errors.Annotate(err, "cannot parse count")
    72  			}
    73  			cons.Count = count
    74  			continue
    75  		}
    76  		if size, ok, err := parseSize(field); ok {
    77  			if err != nil {
    78  				return cons, errors.Annotate(err, "cannot parse size")
    79  			}
    80  			cons.Size = size
    81  			continue
    82  		}
    83  		return cons, errors.NotValidf("unrecognized storage constraint %q", field)
    84  	}
    85  	if cons.Count == 0 && cons.Size == 0 && cons.Pool == "" {
    86  		return Constraints{}, errors.New("storage constraints require at least one field to be specified")
    87  	}
    88  	if cons.Count == 0 {
    89  		cons.Count = 1
    90  	}
    91  	return cons, nil
    92  }
    93  
    94  // IsValidPoolName checks if given string is a valid pool name.
    95  func IsValidPoolName(s string) bool {
    96  	return poolRE.MatchString(s)
    97  }
    98  
    99  // ParseConstraintsMap parses string representation of
   100  // storage constraints into a map keyed on storage names
   101  // with constraints as values.
   102  //
   103  // Storage constraints may be specified as
   104  //
   105  //	<name>=<constraints>
   106  //
   107  // or as
   108  //
   109  //	<name>
   110  //
   111  // where latter is equivalent to <name>=1.
   112  //
   113  // Duplicate storage names cause an error to be returned.
   114  // Constraints presence can be enforced.
   115  func ParseConstraintsMap(args []string, mustHaveConstraints bool) (map[string]Constraints, error) {
   116  	results := make(map[string]Constraints, len(args))
   117  	for _, kv := range args {
   118  		parts := strings.SplitN(kv, "=", -1)
   119  		name := parts[0]
   120  		if len(parts) > 2 || len(name) == 0 {
   121  			return nil, errors.Errorf(`expected "name=constraints" or "name", got %q`, kv)
   122  		}
   123  
   124  		if mustHaveConstraints && len(parts) == 1 {
   125  			return nil, errors.Errorf(`expected "name=constraints" where "constraints" must be specified, got %q`, kv)
   126  		}
   127  
   128  		if _, exists := results[name]; exists {
   129  			return nil, errors.Errorf("storage %q specified more than once", name)
   130  		}
   131  		consString := "1"
   132  		if len(parts) > 1 {
   133  			consString = parts[1]
   134  		}
   135  		cons, err := ParseConstraints(consString)
   136  		if err != nil {
   137  			return nil, errors.Annotatef(err, "cannot parse constraints for storage %q", name)
   138  		}
   139  
   140  		results[name] = cons
   141  	}
   142  	return results, nil
   143  }
   144  
   145  func parseCount(s string) (uint64, bool, error) {
   146  	if !countRE.MatchString(s) {
   147  		return 0, false, nil
   148  	}
   149  	var n uint64
   150  	var err error
   151  	if s[0] == '-' {
   152  		goto bad
   153  	}
   154  	n, err = strconv.ParseUint(s, 10, 64)
   155  	if err != nil {
   156  		return 0, false, nil
   157  	}
   158  	if n > 0 {
   159  		return n, true, nil
   160  	}
   161  bad:
   162  	return 0, true, errors.Errorf("count must be greater than zero, got %q", s)
   163  }
   164  
   165  func parseSize(s string) (uint64, bool, error) {
   166  	if !sizeRE.MatchString(s) {
   167  		return 0, false, nil
   168  	}
   169  	size, err := utils.ParseSize(s)
   170  	if err != nil {
   171  		return 0, true, err
   172  	}
   173  	return size, true, nil
   174  }
   175  
   176  // ToString returns a parsable string representation of the storage constraints.
   177  func ToString(c Constraints) (string, error) {
   178  	if c.Pool == "" && c.Size <= 0 && c.Count <= 0 {
   179  		return "", errors.Errorf("must provide one of pool or size or count")
   180  	}
   181  
   182  	var parts []string
   183  	if c.Pool != "" {
   184  		parts = append(parts, c.Pool)
   185  	}
   186  	if c.Count > 0 {
   187  		parts = append(parts, fmt.Sprintf("%d", c.Count))
   188  	}
   189  	if c.Size > 0 {
   190  		parts = append(parts, fmt.Sprintf("%dM", c.Size))
   191  	}
   192  	return strings.Join(parts, ","), nil
   193  }