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 }