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  }