github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/common/availabilityzones.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package common
     5  
     6  import (
     7  	"sort"
     8  
     9  	"github.com/juju/errors"
    10  
    11  	"github.com/juju/juju/core/instance"
    12  	"github.com/juju/juju/environs"
    13  	"github.com/juju/juju/environs/context"
    14  )
    15  
    16  // AvailabilityZone describes a provider availability zone.
    17  //go:generate mockgen -package mocks -destination mocks/availability_zone.go github.com/juju/juju/provider/common AvailabilityZone
    18  type AvailabilityZone interface {
    19  	// Name returns the name of the availability zone.
    20  	Name() string
    21  
    22  	// Available reports whether the availability zone is currently available.
    23  	Available() bool
    24  }
    25  
    26  // ZonedEnviron is an environs.Environ that has support for availability zones.
    27  //go:generate mockgen -package mocks -destination mocks/zoned_environ.go github.com/juju/juju/provider/common ZonedEnviron
    28  type ZonedEnviron interface {
    29  	environs.Environ
    30  
    31  	// AvailabilityZones returns all availability zones in the environment.
    32  	AvailabilityZones(ctx context.ProviderCallContext) ([]AvailabilityZone, error)
    33  
    34  	// InstanceAvailabilityZoneNames returns the names of the availability
    35  	// zones for the specified instances. The error returned follows the same
    36  	// rules as Environ.Instances.
    37  	InstanceAvailabilityZoneNames(ctx context.ProviderCallContext, ids []instance.Id) ([]string, error)
    38  
    39  	// DeriveAvailabilityZones attempts to derive availability zones from
    40  	// the specified StartInstanceParams.
    41  	//
    42  	// The parameters for starting an instance may imply (or explicitly
    43  	// specify) availability zones, e.g. due to placement, or due to the
    44  	// attachment of existing volumes, or due to subnet placement. If
    45  	// there is no such restriction, then DeriveAvailabilityZones should
    46  	// return an empty string slice to indicate that the caller should
    47  	// choose an availability zone.
    48  	DeriveAvailabilityZones(ctx context.ProviderCallContext, args environs.StartInstanceParams) ([]string, error)
    49  }
    50  
    51  // AvailabilityZoneInstances describes an availability zone and
    52  // a set of instances in that zone.
    53  type AvailabilityZoneInstances struct {
    54  	// ZoneName is the name of the availability zone.
    55  	ZoneName string
    56  
    57  	// Instances is a set of instances within the availability zone.
    58  	Instances []instance.Id
    59  }
    60  
    61  type byPopulationThenName []AvailabilityZoneInstances
    62  
    63  func (b byPopulationThenName) Len() int {
    64  	return len(b)
    65  }
    66  
    67  func (b byPopulationThenName) Less(i, j int) bool {
    68  	switch {
    69  	case len(b[i].Instances) < len(b[j].Instances):
    70  		return true
    71  	case len(b[i].Instances) == len(b[j].Instances):
    72  		return b[i].ZoneName < b[j].ZoneName
    73  	}
    74  	return false
    75  }
    76  
    77  func (b byPopulationThenName) Swap(i, j int) {
    78  	b[i], b[j] = b[j], b[i]
    79  }
    80  
    81  // AvailabilityZoneAllocations returns the availability zones and their
    82  // instance allocations from the specified group, in ascending order of
    83  // population. Availability zones with the same population size are
    84  // ordered by name.
    85  //
    86  // If the specified group is empty, then it will behave as if the result of
    87  // AllInstances were provided.
    88  func AvailabilityZoneAllocations(
    89  	env ZonedEnviron, ctx context.ProviderCallContext, group []instance.Id,
    90  ) ([]AvailabilityZoneInstances, error) {
    91  	if len(group) == 0 {
    92  		instances, err := env.AllInstances(ctx)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		group = make([]instance.Id, len(instances))
    97  		for i, inst := range instances {
    98  			group[i] = inst.Id()
    99  		}
   100  	}
   101  	instanceZones, err := env.InstanceAvailabilityZoneNames(ctx, group)
   102  	switch err {
   103  	case nil, environs.ErrPartialInstances:
   104  	case environs.ErrNoInstances:
   105  		group = nil
   106  	default:
   107  		return nil, err
   108  	}
   109  
   110  	// Get the list of all "available" availability zones,
   111  	// and then initialise a tally for each one.
   112  	zones, err := env.AvailabilityZones(ctx)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	instancesByZoneName := make(map[string][]instance.Id)
   117  	for _, zone := range zones {
   118  		if !zone.Available() {
   119  			continue
   120  		}
   121  		name := zone.Name()
   122  		instancesByZoneName[name] = nil
   123  	}
   124  	if len(instancesByZoneName) == 0 {
   125  		return nil, nil
   126  	}
   127  
   128  	for i, id := range group {
   129  		zone := instanceZones[i]
   130  		if zone == "" {
   131  			continue
   132  		}
   133  		if _, ok := instancesByZoneName[zone]; !ok {
   134  			// zone is not available
   135  			continue
   136  		}
   137  		instancesByZoneName[zone] = append(instancesByZoneName[zone], id)
   138  	}
   139  
   140  	zoneInstances := make([]AvailabilityZoneInstances, 0, len(instancesByZoneName))
   141  	for zoneName, instances := range instancesByZoneName {
   142  		zoneInstances = append(zoneInstances, AvailabilityZoneInstances{
   143  			ZoneName:  zoneName,
   144  			Instances: instances,
   145  		})
   146  	}
   147  	sort.Sort(byPopulationThenName(zoneInstances))
   148  	return zoneInstances, nil
   149  }
   150  
   151  var internalAvailabilityZoneAllocations = AvailabilityZoneAllocations
   152  
   153  // DistributeInstances is a common function for implement the
   154  // state.InstanceDistributor policy based on availability zone spread.
   155  // TODO (manadart 2018-11-27) This method signature has grown to the point
   156  // where the argument list should be replaced with a struct.
   157  // At that time limitZones could be transformed to a map so that lookups in the
   158  // filtering below are more efficient.
   159  func DistributeInstances(
   160  	env ZonedEnviron, ctx context.ProviderCallContext, candidates, group []instance.Id, limitZones []string,
   161  ) ([]instance.Id, error) {
   162  	// Determine availability zone distribution for the group.
   163  	zoneInstances, err := internalAvailabilityZoneAllocations(env, ctx, group)
   164  	if err != nil || len(zoneInstances) == 0 {
   165  		return nil, err
   166  	}
   167  
   168  	// If there any zones supplied for limitation,
   169  	// filter to distribution data so that only those zones are considered.
   170  	filteredZoneInstances := zoneInstances[:0]
   171  	if len(limitZones) > 0 {
   172  		for _, zi := range zoneInstances {
   173  			for _, zone := range limitZones {
   174  				if zi.ZoneName == zone {
   175  					filteredZoneInstances = append(filteredZoneInstances, zi)
   176  					break
   177  				}
   178  			}
   179  		}
   180  	} else {
   181  		filteredZoneInstances = zoneInstances
   182  	}
   183  
   184  	// Determine which of the candidates are eligible based on whether
   185  	// they are allocated in one of the least-populated availability zones.
   186  	var allEligible []string
   187  	for i := range filteredZoneInstances {
   188  		if i > 0 && len(filteredZoneInstances[i].Instances) > len(filteredZoneInstances[i-1].Instances) {
   189  			break
   190  		}
   191  		for _, id := range filteredZoneInstances[i].Instances {
   192  			allEligible = append(allEligible, string(id))
   193  		}
   194  	}
   195  	sort.Strings(allEligible)
   196  
   197  	eligible := make([]instance.Id, 0, len(candidates))
   198  	for _, candidate := range candidates {
   199  		n := sort.SearchStrings(allEligible, string(candidate))
   200  		if n >= 0 && n < len(allEligible) {
   201  			eligible = append(eligible, candidate)
   202  		}
   203  	}
   204  	return eligible, nil
   205  }
   206  
   207  // ValidateAvailabilityZone returns nil iff the availability
   208  // zone exists and is available, otherwise returns a NotValid
   209  // error.
   210  func ValidateAvailabilityZone(env ZonedEnviron, ctx context.ProviderCallContext, zone string) error {
   211  	zones, err := env.AvailabilityZones(ctx)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	for _, z := range zones {
   216  		if z.Name() == zone {
   217  			if z.Available() {
   218  				return nil
   219  			}
   220  			return errors.Errorf("availability zone %q is unavailable", zone)
   221  		}
   222  	}
   223  	return errors.NotValidf("availability zone %q", zone)
   224  }