github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/worker/discoverspaces/discoverspaces.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package discoverspaces 5 6 import ( 7 "github.com/juju/errors" 8 "github.com/juju/loggo" 9 "github.com/juju/names" 10 "github.com/juju/utils/set" 11 12 "github.com/juju/juju/apiserver/params" 13 "github.com/juju/juju/environs" 14 "github.com/juju/juju/worker" 15 "github.com/juju/juju/worker/catacomb" 16 "github.com/juju/juju/worker/gate" 17 ) 18 19 // Facade exposes the relevant capabilities of a *discoverspaces.API; it's 20 // a bit raw but at least it's easily mockable. 21 type Facade interface { 22 CreateSpaces(params.CreateSpacesParams) (params.ErrorResults, error) 23 AddSubnets(params.AddSubnetsParams) (params.ErrorResults, error) 24 ListSpaces() (params.DiscoverSpacesResults, error) 25 ListSubnets(params.SubnetsFilters) (params.ListSubnetsResults, error) 26 } 27 28 // NameFunc returns a string derived from base that is not contained in used. 29 type NameFunc func(base string, used set.Strings) string 30 31 // Config defines the operation of a space discovery worker. 32 type Config struct { 33 34 // Facade exposes the capabilities of a controller. 35 Facade Facade 36 37 // Environ exposes the capabilities of a compute substrate. 38 Environ environs.Environ 39 40 // NewName is used to sanitise, and make unique, space names as 41 // reported by an Environ (for use in juju, via the Facade). You 42 // should probably set it to ConvertSpaceName. 43 NewName NameFunc 44 45 // Unlocker, if not nil, will be unlocked when the first discovery 46 // attempt completes successfully. 47 Unlocker gate.Unlocker 48 } 49 50 // Validate returns an error if the config cannot be expected to 51 // drive a functional worker. 52 func (config Config) Validate() error { 53 if config.Facade == nil { 54 return errors.NotValidf("nil Facade") 55 } 56 if config.Environ == nil { 57 return errors.NotValidf("nil Environ") 58 } 59 if config.NewName == nil { 60 return errors.NotValidf("nil NewName") 61 } 62 // missing Unlocker gate just means "don't bother notifying" 63 return nil 64 } 65 66 var logger = loggo.GetLogger("juju.worker.discoverspaces") 67 68 type discoverspacesWorker struct { 69 catacomb catacomb.Catacomb 70 config Config 71 } 72 73 // NewWorker returns a worker that will attempt to discover the 74 // configured Environ's spaces, and update the controller via the 75 // configured Facade. Names are sanitised with NewName, and any 76 // supplied Unlocker will be Unlock()ed when the first complete 77 // discovery and update succeeds. 78 // 79 // Once that update completes, the worker just waits to be Kill()ed. 80 // We should probably poll for changes, really, but I'm making an 81 // effort to preserve existing behaviour where possible. 82 func NewWorker(config Config) (worker.Worker, error) { 83 if err := config.Validate(); err != nil { 84 return nil, errors.Trace(err) 85 } 86 dw := &discoverspacesWorker{ 87 config: config, 88 } 89 err := catacomb.Invoke(catacomb.Plan{ 90 Site: &dw.catacomb, 91 Work: dw.loop, 92 }) 93 if err != nil { 94 return nil, errors.Trace(err) 95 } 96 return dw, nil 97 } 98 99 // Kill is part of the worker.Worker interface. 100 func (dw *discoverspacesWorker) Kill() { 101 dw.catacomb.Kill(nil) 102 } 103 104 // Wait is part of the worker.Worker interface. 105 func (dw *discoverspacesWorker) Wait() error { 106 return dw.catacomb.Wait() 107 } 108 109 func (dw *discoverspacesWorker) loop() (err error) { 110 111 // TODO(mfoord): we'll have a watcher here checking if we need to 112 // update the spaces/subnets definition. 113 // TODO(fwereade): for now, use a changes channel that apes the 114 // standard initial event behaviour, so we can make the loop 115 // follow the standard structure. 116 changes := make(chan struct{}, 1) 117 changes <- struct{}{} 118 119 gate := dw.config.Unlocker 120 for { 121 select { 122 case <-dw.catacomb.Dying(): 123 return dw.catacomb.ErrDying() 124 case <-changes: 125 if err := dw.handleSubnets(); err != nil { 126 return errors.Trace(err) 127 } 128 logger.Debugf("space discovery complete") 129 if gate != nil { 130 gate.Unlock() 131 gate = nil 132 } 133 } 134 } 135 } 136 137 func (dw *discoverspacesWorker) handleSubnets() error { 138 environ, ok := environs.SupportsNetworking(dw.config.Environ) 139 if !ok { 140 logger.Debugf("not a networking environ") 141 return nil 142 } 143 if supported, err := environ.SupportsSpaceDiscovery(); err != nil { 144 return errors.Trace(err) 145 } else if !supported { 146 logger.Debugf("environ does not support space discovery") 147 return nil 148 } 149 providerSpaces, err := environ.Spaces() 150 if err != nil { 151 return errors.Trace(err) 152 } 153 154 facade := dw.config.Facade 155 listSpacesResult, err := facade.ListSpaces() 156 if err != nil { 157 return errors.Trace(err) 158 } 159 stateSubnets, err := facade.ListSubnets(params.SubnetsFilters{}) 160 if err != nil { 161 return errors.Trace(err) 162 } 163 164 stateSubnetIds := make(set.Strings) 165 for _, subnet := range stateSubnets.Results { 166 stateSubnetIds.Add(subnet.ProviderId) 167 } 168 stateSpaceMap := make(map[string]params.ProviderSpace) 169 spaceNames := make(set.Strings) 170 for _, space := range listSpacesResult.Results { 171 stateSpaceMap[space.ProviderId] = space 172 spaceNames.Add(space.Name) 173 } 174 175 // TODO(mfoord): we need to delete spaces and subnets that no longer 176 // exist, so long as they're not in use. 177 var createSpacesArgs params.CreateSpacesParams 178 var addSubnetsArgs params.AddSubnetsParams 179 for _, space := range providerSpaces { 180 // Check if the space is already in state, in which case we know 181 // its name. 182 stateSpace, ok := stateSpaceMap[string(space.ProviderId)] 183 var spaceTag names.SpaceTag 184 if ok { 185 spaceName := stateSpace.Name 186 if !names.IsValidSpace(spaceName) { 187 // Can only happen if an invalid name is stored 188 // in state. 189 logger.Errorf("space %q has an invalid name, ignoring", spaceName) 190 continue 191 192 } 193 spaceTag = names.NewSpaceTag(spaceName) 194 195 } else { 196 // The space is new, we need to create a valid name for it 197 // in state. 198 spaceName := string(space.Name) 199 // Convert the name into a valid name that isn't already in 200 // use. 201 spaceName = dw.config.NewName(spaceName, spaceNames) 202 spaceNames.Add(spaceName) 203 spaceTag = names.NewSpaceTag(spaceName) 204 // We need to create the space. 205 createSpacesArgs.Spaces = append(createSpacesArgs.Spaces, params.CreateSpaceParams{ 206 Public: false, 207 SpaceTag: spaceTag.String(), 208 ProviderId: string(space.ProviderId), 209 }) 210 } 211 // TODO(mfoord): currently no way of removing subnets, or 212 // changing the space they're in, so we can only add ones we 213 // don't already know about. 214 for _, subnet := range space.Subnets { 215 if stateSubnetIds.Contains(string(subnet.ProviderId)) { 216 continue 217 } 218 zones := subnet.AvailabilityZones 219 if len(zones) == 0 { 220 logger.Tracef( 221 "provider does not specify zones for subnet %q; using 'default' zone as fallback", 222 subnet.CIDR, 223 ) 224 zones = []string{"default"} 225 } 226 addSubnetsArgs.Subnets = append(addSubnetsArgs.Subnets, params.AddSubnetParams{ 227 SubnetProviderId: string(subnet.ProviderId), 228 SpaceTag: spaceTag.String(), 229 Zones: zones, 230 }) 231 } 232 } 233 234 if err := dw.createSpacesFromArgs(createSpacesArgs); err != nil { 235 return errors.Trace(err) 236 } 237 238 if err := dw.addSubnetsFromArgs(addSubnetsArgs); err != nil { 239 return errors.Trace(err) 240 } 241 242 return nil 243 } 244 245 func (dw *discoverspacesWorker) createSpacesFromArgs(createSpacesArgs params.CreateSpacesParams) error { 246 facade := dw.config.Facade 247 248 expectedNumCreated := len(createSpacesArgs.Spaces) 249 if expectedNumCreated > 0 { 250 result, err := facade.CreateSpaces(createSpacesArgs) 251 if err != nil { 252 return errors.Annotate(err, "creating spaces failed") 253 } 254 if len(result.Results) != expectedNumCreated { 255 return errors.Errorf( 256 "unexpected response from CreateSpaces: expected %d results, got %d", 257 expectedNumCreated, len(result.Results), 258 ) 259 } 260 for _, res := range result.Results { 261 if res.Error != nil { 262 return errors.Annotate(res.Error, "creating space failed") 263 } 264 } 265 logger.Debugf("discovered and imported %d spaces: %v", expectedNumCreated, createSpacesArgs) 266 } else { 267 logger.Debugf("no unknown spaces discovered for import") 268 } 269 270 return nil 271 } 272 273 func (dw *discoverspacesWorker) addSubnetsFromArgs(addSubnetsArgs params.AddSubnetsParams) error { 274 facade := dw.config.Facade 275 276 expectedNumAdded := len(addSubnetsArgs.Subnets) 277 if expectedNumAdded > 0 { 278 result, err := facade.AddSubnets(addSubnetsArgs) 279 if err != nil { 280 return errors.Annotate(err, "adding subnets failed") 281 } 282 if len(result.Results) != expectedNumAdded { 283 return errors.Errorf( 284 "unexpected response from AddSubnets: expected %d results, got %d", 285 expectedNumAdded, len(result.Results), 286 ) 287 } 288 for _, res := range result.Results { 289 if res.Error != nil { 290 return errors.Annotate(res.Error, "adding subnet failed") 291 } 292 } 293 logger.Debugf("discovered and imported %d subnets: %v", expectedNumAdded, addSubnetsArgs) 294 } else { 295 logger.Debugf("no unknown subnets discovered for import") 296 } 297 298 return nil 299 }