launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/provider/maas/environ.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package maas
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"net/url"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"launchpad.net/gomaasapi"
    15  
    16  	"launchpad.net/juju-core/agent"
    17  	"launchpad.net/juju-core/constraints"
    18  	"launchpad.net/juju-core/environs"
    19  	"launchpad.net/juju-core/environs/cloudinit"
    20  	"launchpad.net/juju-core/environs/config"
    21  	"launchpad.net/juju-core/environs/imagemetadata"
    22  	"launchpad.net/juju-core/environs/simplestreams"
    23  	"launchpad.net/juju-core/environs/storage"
    24  	envtools "launchpad.net/juju-core/environs/tools"
    25  	"launchpad.net/juju-core/instance"
    26  	"launchpad.net/juju-core/provider/common"
    27  	"launchpad.net/juju-core/state"
    28  	"launchpad.net/juju-core/state/api"
    29  	"launchpad.net/juju-core/tools"
    30  	"launchpad.net/juju-core/utils"
    31  )
    32  
    33  const (
    34  	// We're using v1.0 of the MAAS API.
    35  	apiVersion = "1.0"
    36  )
    37  
    38  // A request may fail to due "eventual consistency" semantics, which
    39  // should resolve fairly quickly.  A request may also fail due to a slow
    40  // state transition (for instance an instance taking a while to release
    41  // a security group after termination).  The former failure mode is
    42  // dealt with by shortAttempt, the latter by LongAttempt.
    43  var shortAttempt = utils.AttemptStrategy{
    44  	Total: 5 * time.Second,
    45  	Delay: 200 * time.Millisecond,
    46  }
    47  
    48  type maasEnviron struct {
    49  	name string
    50  
    51  	// ecfgMutex protects the *Unlocked fields below.
    52  	ecfgMutex sync.Mutex
    53  
    54  	ecfgUnlocked       *maasEnvironConfig
    55  	maasClientUnlocked *gomaasapi.MAASObject
    56  	storageUnlocked    storage.Storage
    57  }
    58  
    59  var _ environs.Environ = (*maasEnviron)(nil)
    60  var _ imagemetadata.SupportsCustomSources = (*maasEnviron)(nil)
    61  var _ envtools.SupportsCustomSources = (*maasEnviron)(nil)
    62  
    63  func NewEnviron(cfg *config.Config) (*maasEnviron, error) {
    64  	env := new(maasEnviron)
    65  	err := env.SetConfig(cfg)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	env.name = cfg.Name()
    70  	env.storageUnlocked = NewStorage(env)
    71  	return env, nil
    72  }
    73  
    74  // Name is specified in the Environ interface.
    75  func (env *maasEnviron) Name() string {
    76  	return env.name
    77  }
    78  
    79  // Bootstrap is specified in the Environ interface.
    80  func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error {
    81  	return common.Bootstrap(ctx, env, cons)
    82  }
    83  
    84  // StateInfo is specified in the Environ interface.
    85  func (env *maasEnviron) StateInfo() (*state.Info, *api.Info, error) {
    86  	return common.StateInfo(env)
    87  }
    88  
    89  // ecfg returns the environment's maasEnvironConfig, and protects it with a
    90  // mutex.
    91  func (env *maasEnviron) ecfg() *maasEnvironConfig {
    92  	env.ecfgMutex.Lock()
    93  	defer env.ecfgMutex.Unlock()
    94  	return env.ecfgUnlocked
    95  }
    96  
    97  // Config is specified in the Environ interface.
    98  func (env *maasEnviron) Config() *config.Config {
    99  	return env.ecfg().Config
   100  }
   101  
   102  // SetConfig is specified in the Environ interface.
   103  func (env *maasEnviron) SetConfig(cfg *config.Config) error {
   104  	env.ecfgMutex.Lock()
   105  	defer env.ecfgMutex.Unlock()
   106  
   107  	// The new config has already been validated by itself, but now we
   108  	// validate the transition from the old config to the new.
   109  	var oldCfg *config.Config
   110  	if env.ecfgUnlocked != nil {
   111  		oldCfg = env.ecfgUnlocked.Config
   112  	}
   113  	cfg, err := env.Provider().Validate(cfg, oldCfg)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	ecfg, err := providerInstance.newConfig(cfg)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	env.ecfgUnlocked = ecfg
   124  
   125  	authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.maasServer(), ecfg.maasOAuth(), apiVersion)
   126  	if err != nil {
   127  		return err
   128  	}
   129  	env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient)
   130  
   131  	return nil
   132  }
   133  
   134  // getMAASClient returns a MAAS client object to use for a request, in a
   135  // lock-protected fashion.
   136  func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject {
   137  	env.ecfgMutex.Lock()
   138  	defer env.ecfgMutex.Unlock()
   139  
   140  	return env.maasClientUnlocked
   141  }
   142  
   143  // convertConstraints converts the given constraints into an url.Values
   144  // object suitable to pass to MAAS when acquiring a node.
   145  // CpuPower is ignored because it cannot translated into something
   146  // meaningful for MAAS right now.
   147  func convertConstraints(cons constraints.Value) url.Values {
   148  	params := url.Values{}
   149  	if cons.Arch != nil {
   150  		params.Add("arch", *cons.Arch)
   151  	}
   152  	if cons.CpuCores != nil {
   153  		params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores))
   154  	}
   155  	if cons.Mem != nil {
   156  		params.Add("mem", fmt.Sprintf("%d", *cons.Mem))
   157  	}
   158  	if cons.Tags != nil && len(*cons.Tags) > 0 {
   159  		params.Add("tags", strings.Join(*cons.Tags, ","))
   160  	}
   161  	// TODO(bug 1212689): ignore root-disk constraint for now.
   162  	if cons.RootDisk != nil {
   163  		logger.Warningf("ignoring unsupported constraint 'root-disk'")
   164  	}
   165  	if cons.CpuPower != nil {
   166  		logger.Warningf("ignoring unsupported constraint 'cpu-power'")
   167  	}
   168  	return params
   169  }
   170  
   171  // acquireNode allocates a node from the MAAS.
   172  func (environ *maasEnviron) acquireNode(cons constraints.Value, possibleTools tools.List) (gomaasapi.MAASObject, *tools.Tools, error) {
   173  	acquireParams := convertConstraints(cons)
   174  	acquireParams.Add("agent_name", environ.ecfg().maasAgentName())
   175  	var result gomaasapi.JSONObject
   176  	var err error
   177  	for a := shortAttempt.Start(); a.Next(); {
   178  		client := environ.getMAASClient().GetSubObject("nodes/")
   179  		result, err = client.CallPost("acquire", acquireParams)
   180  		if err == nil {
   181  			break
   182  		}
   183  	}
   184  	if err != nil {
   185  		return gomaasapi.MAASObject{}, nil, err
   186  	}
   187  	node, err := result.GetMAASObject()
   188  	if err != nil {
   189  		msg := fmt.Errorf("unexpected result from 'acquire' on MAAS API: %v", err)
   190  		return gomaasapi.MAASObject{}, nil, msg
   191  	}
   192  	tools := possibleTools[0]
   193  	logger.Warningf("picked arbitrary tools %q", tools)
   194  	return node, tools, nil
   195  }
   196  
   197  // startNode installs and boots a node.
   198  func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) error {
   199  	userDataParam := base64.StdEncoding.EncodeToString(userdata)
   200  	params := url.Values{
   201  		"distro_series": {series},
   202  		"user_data":     {userDataParam},
   203  	}
   204  	// Initialize err to a non-nil value as a sentinel for the following
   205  	// loop.
   206  	err := fmt.Errorf("(no error)")
   207  	for a := shortAttempt.Start(); a.Next() && err != nil; {
   208  		_, err = node.CallPost("start", params)
   209  	}
   210  	return err
   211  }
   212  
   213  // createBridgeNetwork returns a string representing the upstart command to
   214  // create a bridged eth0.
   215  func createBridgeNetwork() string {
   216  	return `cat > /etc/network/eth0.config << EOF
   217  iface eth0 inet manual
   218  
   219  auto br0
   220  iface br0 inet dhcp
   221    bridge_ports eth0
   222  EOF
   223  `
   224  }
   225  
   226  // linkBridgeInInterfaces adds the file created by createBridgeNetwork to the
   227  // interfaces file.
   228  func linkBridgeInInterfaces() string {
   229  	return `sed -i "s/iface eth0 inet dhcp/source \/etc\/network\/eth0.config/" /etc/network/interfaces`
   230  }
   231  
   232  // StartInstance is specified in the InstanceBroker interface.
   233  func (environ *maasEnviron) StartInstance(cons constraints.Value, possibleTools tools.List,
   234  	machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) {
   235  
   236  	var inst *maasInstance
   237  	var err error
   238  	if node, tools, err := environ.acquireNode(cons, possibleTools); err != nil {
   239  		return nil, nil, fmt.Errorf("cannot run instances: %v", err)
   240  	} else {
   241  		inst = &maasInstance{maasObject: &node, environ: environ}
   242  		machineConfig.Tools = tools
   243  	}
   244  	defer func() {
   245  		if err != nil {
   246  			if err := environ.releaseInstance(inst); err != nil {
   247  				logger.Errorf("error releasing failed instance: %v", err)
   248  			}
   249  		}
   250  	}()
   251  
   252  	hostname, err := inst.DNSName()
   253  	if err != nil {
   254  		return nil, nil, err
   255  	}
   256  	info := machineInfo{hostname}
   257  	runCmd, err := info.cloudinitRunCmd()
   258  	if err != nil {
   259  		return nil, nil, err
   260  	}
   261  	if err := environs.FinishMachineConfig(machineConfig, environ.Config(), cons); err != nil {
   262  		return nil, nil, err
   263  	}
   264  	// TODO(thumper): 2013-08-28 bug 1217614
   265  	// The machine envronment config values are being moved to the agent config.
   266  	// Explicitly specify that the lxc containers use the network bridge defined above.
   267  	machineConfig.AgentEnvironment[agent.LxcBridge] = "br0"
   268  	userdata, err := environs.ComposeUserData(
   269  		machineConfig,
   270  		runCmd,
   271  		createBridgeNetwork(),
   272  		linkBridgeInInterfaces(),
   273  		"service networking restart",
   274  	)
   275  	if err != nil {
   276  		msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err)
   277  		return nil, nil, msg
   278  	}
   279  	logger.Debugf("maas user data; %d bytes", len(userdata))
   280  
   281  	series := possibleTools.OneSeries()
   282  	if err := environ.startNode(*inst.maasObject, series, userdata); err != nil {
   283  		return nil, nil, err
   284  	}
   285  	logger.Debugf("started instance %q", inst.Id())
   286  	// TODO(bug 1193998) - return instance hardware characteristics as well
   287  	return inst, nil, nil
   288  }
   289  
   290  // StartInstance is specified in the InstanceBroker interface.
   291  func (environ *maasEnviron) StopInstances(instances []instance.Instance) error {
   292  	// Shortcut to exit quickly if 'instances' is an empty slice or nil.
   293  	if len(instances) == 0 {
   294  		return nil
   295  	}
   296  	// Tell MAAS to release each of the instances.  If there are errors,
   297  	// return only the first one (but release all instances regardless).
   298  	// Note that releasing instances also turns them off.
   299  	var firstErr error
   300  	for _, instance := range instances {
   301  		err := environ.releaseInstance(instance)
   302  		if firstErr == nil {
   303  			firstErr = err
   304  		}
   305  	}
   306  	return firstErr
   307  }
   308  
   309  // releaseInstance releases a single instance.
   310  func (environ *maasEnviron) releaseInstance(inst instance.Instance) error {
   311  	maasInst := inst.(*maasInstance)
   312  	maasObj := maasInst.maasObject
   313  	_, err := maasObj.CallPost("release", nil)
   314  	if err != nil {
   315  		logger.Debugf("error releasing instance %v", maasInst)
   316  	}
   317  	return err
   318  }
   319  
   320  // instances calls the MAAS API to list nodes.  The "ids" slice is a filter for
   321  // specific instance IDs.  Due to how this works in the HTTP API, an empty
   322  // "ids" matches all instances (not none as you might expect).
   323  func (environ *maasEnviron) instances(ids []instance.Id) ([]instance.Instance, error) {
   324  	nodeListing := environ.getMAASClient().GetSubObject("nodes")
   325  	filter := getSystemIdValues(ids)
   326  	filter.Add("agent_name", environ.ecfg().maasAgentName())
   327  	listNodeObjects, err := nodeListing.CallGet("list", filter)
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  	listNodes, err := listNodeObjects.GetArray()
   332  	if err != nil {
   333  		return nil, err
   334  	}
   335  	instances := make([]instance.Instance, len(listNodes))
   336  	for index, nodeObj := range listNodes {
   337  		node, err := nodeObj.GetMAASObject()
   338  		if err != nil {
   339  			return nil, err
   340  		}
   341  		instances[index] = &maasInstance{
   342  			maasObject: &node,
   343  			environ:    environ,
   344  		}
   345  	}
   346  	return instances, nil
   347  }
   348  
   349  // Instances returns the instance.Instance objects corresponding to the given
   350  // slice of instance.Id.  The error is ErrNoInstances if no instances
   351  // were found.
   352  func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
   353  	if len(ids) == 0 {
   354  		// This would be treated as "return all instances" below, so
   355  		// treat it as a special case.
   356  		// The interface requires us to return this particular error
   357  		// if no instances were found.
   358  		return nil, environs.ErrNoInstances
   359  	}
   360  	instances, err := environ.instances(ids)
   361  	if err != nil {
   362  		return nil, err
   363  	}
   364  	if len(instances) == 0 {
   365  		return nil, environs.ErrNoInstances
   366  	}
   367  
   368  	idMap := make(map[instance.Id]instance.Instance)
   369  	for _, instance := range instances {
   370  		idMap[instance.Id()] = instance
   371  	}
   372  
   373  	result := make([]instance.Instance, len(ids))
   374  	for index, id := range ids {
   375  		result[index] = idMap[id]
   376  	}
   377  
   378  	if len(instances) < len(ids) {
   379  		return result, environs.ErrPartialInstances
   380  	}
   381  	return result, nil
   382  }
   383  
   384  // AllInstances returns all the instance.Instance in this provider.
   385  func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) {
   386  	return environ.instances(nil)
   387  }
   388  
   389  // Storage is defined by the Environ interface.
   390  func (env *maasEnviron) Storage() storage.Storage {
   391  	env.ecfgMutex.Lock()
   392  	defer env.ecfgMutex.Unlock()
   393  	return env.storageUnlocked
   394  }
   395  
   396  func (environ *maasEnviron) Destroy() error {
   397  	return common.Destroy(environ)
   398  }
   399  
   400  // MAAS does not do firewalling so these port methods do nothing.
   401  func (*maasEnviron) OpenPorts([]instance.Port) error {
   402  	logger.Debugf("unimplemented OpenPorts() called")
   403  	return nil
   404  }
   405  
   406  func (*maasEnviron) ClosePorts([]instance.Port) error {
   407  	logger.Debugf("unimplemented ClosePorts() called")
   408  	return nil
   409  }
   410  
   411  func (*maasEnviron) Ports() ([]instance.Port, error) {
   412  	logger.Debugf("unimplemented Ports() called")
   413  	return []instance.Port{}, nil
   414  }
   415  
   416  func (*maasEnviron) Provider() environs.EnvironProvider {
   417  	return &providerInstance
   418  }
   419  
   420  // GetImageSources returns a list of sources which are used to search for simplestreams image metadata.
   421  func (e *maasEnviron) GetImageSources() ([]simplestreams.DataSource, error) {
   422  	// Add the simplestreams source off the control bucket.
   423  	return []simplestreams.DataSource{
   424  		storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseImagesPath)}, nil
   425  }
   426  
   427  // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata.
   428  func (e *maasEnviron) GetToolsSources() ([]simplestreams.DataSource, error) {
   429  	// Add the simplestreams source off the control bucket.
   430  	return []simplestreams.DataSource{
   431  		storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)}, nil
   432  }