github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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  	"encoding/xml"
     9  	"fmt"
    10  	"net"
    11  	"net/url"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/utils"
    18  	"github.com/juju/utils/set"
    19  	"labix.org/v2/mgo/bson"
    20  	"launchpad.net/gomaasapi"
    21  
    22  	"github.com/juju/juju/agent"
    23  	"github.com/juju/juju/cloudinit"
    24  	"github.com/juju/juju/constraints"
    25  	"github.com/juju/juju/environs"
    26  	"github.com/juju/juju/environs/config"
    27  	"github.com/juju/juju/environs/imagemetadata"
    28  	"github.com/juju/juju/environs/network"
    29  	"github.com/juju/juju/environs/simplestreams"
    30  	"github.com/juju/juju/environs/storage"
    31  	envtools "github.com/juju/juju/environs/tools"
    32  	"github.com/juju/juju/instance"
    33  	"github.com/juju/juju/provider/common"
    34  	"github.com/juju/juju/state"
    35  	"github.com/juju/juju/state/api"
    36  	"github.com/juju/juju/tools"
    37  )
    38  
    39  const (
    40  	// We're using v1.0 of the MAAS API.
    41  	apiVersion = "1.0"
    42  )
    43  
    44  // A request may fail to due "eventual consistency" semantics, which
    45  // should resolve fairly quickly.  A request may also fail due to a slow
    46  // state transition (for instance an instance taking a while to release
    47  // a security group after termination).  The former failure mode is
    48  // dealt with by shortAttempt, the latter by LongAttempt.
    49  var shortAttempt = utils.AttemptStrategy{
    50  	Total: 5 * time.Second,
    51  	Delay: 200 * time.Millisecond,
    52  }
    53  
    54  type maasEnviron struct {
    55  	common.SupportsUnitPlacementPolicy
    56  
    57  	name string
    58  
    59  	// archMutex gates access to supportedArchitectures
    60  	archMutex sync.Mutex
    61  	// supportedArchitectures caches the architectures
    62  	// for which images can be instantiated.
    63  	supportedArchitectures []string
    64  
    65  	// ecfgMutex protects the *Unlocked fields below.
    66  	ecfgMutex sync.Mutex
    67  
    68  	ecfgUnlocked       *maasEnvironConfig
    69  	maasClientUnlocked *gomaasapi.MAASObject
    70  	storageUnlocked    storage.Storage
    71  }
    72  
    73  var _ environs.Environ = (*maasEnviron)(nil)
    74  var _ imagemetadata.SupportsCustomSources = (*maasEnviron)(nil)
    75  var _ envtools.SupportsCustomSources = (*maasEnviron)(nil)
    76  
    77  func NewEnviron(cfg *config.Config) (*maasEnviron, error) {
    78  	env := new(maasEnviron)
    79  	err := env.SetConfig(cfg)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	env.name = cfg.Name()
    84  	env.storageUnlocked = NewStorage(env)
    85  	return env, nil
    86  }
    87  
    88  // Name is specified in the Environ interface.
    89  func (env *maasEnviron) Name() string {
    90  	return env.name
    91  }
    92  
    93  // Bootstrap is specified in the Environ interface.
    94  func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) error {
    95  	return common.Bootstrap(ctx, env, args)
    96  }
    97  
    98  // StateInfo is specified in the Environ interface.
    99  func (env *maasEnviron) StateInfo() (*state.Info, *api.Info, error) {
   100  	return common.StateInfo(env)
   101  }
   102  
   103  // ecfg returns the environment's maasEnvironConfig, and protects it with a
   104  // mutex.
   105  func (env *maasEnviron) ecfg() *maasEnvironConfig {
   106  	env.ecfgMutex.Lock()
   107  	defer env.ecfgMutex.Unlock()
   108  	return env.ecfgUnlocked
   109  }
   110  
   111  // Config is specified in the Environ interface.
   112  func (env *maasEnviron) Config() *config.Config {
   113  	return env.ecfg().Config
   114  }
   115  
   116  // SetConfig is specified in the Environ interface.
   117  func (env *maasEnviron) SetConfig(cfg *config.Config) error {
   118  	env.ecfgMutex.Lock()
   119  	defer env.ecfgMutex.Unlock()
   120  
   121  	// The new config has already been validated by itself, but now we
   122  	// validate the transition from the old config to the new.
   123  	var oldCfg *config.Config
   124  	if env.ecfgUnlocked != nil {
   125  		oldCfg = env.ecfgUnlocked.Config
   126  	}
   127  	cfg, err := env.Provider().Validate(cfg, oldCfg)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	ecfg, err := providerInstance.newConfig(cfg)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	env.ecfgUnlocked = ecfg
   138  
   139  	authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.maasServer(), ecfg.maasOAuth(), apiVersion)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient)
   144  
   145  	return nil
   146  }
   147  
   148  // SupportedArchitectures is specified on the EnvironCapability interface.
   149  func (env *maasEnviron) SupportedArchitectures() ([]string, error) {
   150  	env.archMutex.Lock()
   151  	defer env.archMutex.Unlock()
   152  	if env.supportedArchitectures != nil {
   153  		return env.supportedArchitectures, nil
   154  	}
   155  	// Create a filter to get all images from our region and for the correct stream.
   156  	imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   157  		Stream: env.Config().ImageStream(),
   158  	})
   159  	var err error
   160  	env.supportedArchitectures, err = common.SupportedArchitectures(env, imageConstraint)
   161  	return env.supportedArchitectures, err
   162  }
   163  
   164  // SupportNetworks is specified on the EnvironCapability interface.
   165  func (env *maasEnviron) SupportNetworks() bool {
   166  	caps, err := env.getCapabilities()
   167  	if err != nil {
   168  		logger.Debugf("getCapabilities failed: %v", err)
   169  		return false
   170  	}
   171  	return caps.Contains(capNetworksManagement)
   172  }
   173  
   174  func (env *maasEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error {
   175  	// We treat all placement directives as maas-name.
   176  	return nil
   177  }
   178  
   179  const capNetworksManagement = "networks-management"
   180  
   181  // getCapabilities asks the MAAS server for its capabilities, if
   182  // supported by the server.
   183  func (env *maasEnviron) getCapabilities() (caps set.Strings, err error) {
   184  	var result gomaasapi.JSONObject
   185  	caps = set.NewStrings()
   186  
   187  	for a := shortAttempt.Start(); a.Next(); {
   188  		client := env.getMAASClient().GetSubObject("version/")
   189  		result, err = client.CallGet("", nil)
   190  		if err != nil {
   191  			err0, ok := err.(*gomaasapi.ServerError)
   192  			if ok && err0.StatusCode == 404 {
   193  				return caps, fmt.Errorf("MAAS does not support version info")
   194  			} else {
   195  				return caps, err
   196  			}
   197  			continue
   198  		}
   199  	}
   200  	if err != nil {
   201  		return caps, err
   202  	}
   203  	info, err := result.GetMap()
   204  	if err != nil {
   205  		return caps, err
   206  	}
   207  	capsObj, ok := info["capabilities"]
   208  	if !ok {
   209  		return caps, fmt.Errorf("MAAS does not report capabilities")
   210  	}
   211  	items, err := capsObj.GetArray()
   212  	if err != nil {
   213  		return caps, err
   214  	}
   215  	for _, item := range items {
   216  		val, err := item.GetString()
   217  		if err != nil {
   218  			return set.NewStrings(), err
   219  		}
   220  		caps.Add(val)
   221  	}
   222  	return caps, nil
   223  }
   224  
   225  // getMAASClient returns a MAAS client object to use for a request, in a
   226  // lock-protected fashion.
   227  func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject {
   228  	env.ecfgMutex.Lock()
   229  	defer env.ecfgMutex.Unlock()
   230  
   231  	return env.maasClientUnlocked
   232  }
   233  
   234  // convertConstraints converts the given constraints into an url.Values
   235  // object suitable to pass to MAAS when acquiring a node.
   236  // CpuPower is ignored because it cannot translated into something
   237  // meaningful for MAAS right now.
   238  func convertConstraints(cons constraints.Value) url.Values {
   239  	params := url.Values{}
   240  	if cons.Arch != nil {
   241  		params.Add("arch", *cons.Arch)
   242  	}
   243  	if cons.CpuCores != nil {
   244  		params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores))
   245  	}
   246  	if cons.Mem != nil {
   247  		params.Add("mem", fmt.Sprintf("%d", *cons.Mem))
   248  	}
   249  	if cons.Tags != nil && len(*cons.Tags) > 0 {
   250  		params.Add("tags", strings.Join(*cons.Tags, ","))
   251  	}
   252  	// TODO(bug 1212689): ignore root-disk constraint for now.
   253  	if cons.RootDisk != nil {
   254  		logger.Warningf("ignoring unsupported constraint 'root-disk'")
   255  	}
   256  	if cons.CpuPower != nil {
   257  		logger.Warningf("ignoring unsupported constraint 'cpu-power'")
   258  	}
   259  	return params
   260  }
   261  
   262  // addNetworks converts networks include/exclude information into
   263  // url.Values object suitable to pass to MAAS when acquiring a node.
   264  func addNetworks(params url.Values, includeNetworks, excludeNetworks []string) {
   265  	// Network Inclusion/Exclusion setup
   266  	if len(includeNetworks) > 0 {
   267  		for _, name := range includeNetworks {
   268  			params.Add("networks", name)
   269  		}
   270  	}
   271  	if len(excludeNetworks) > 0 {
   272  		for _, name := range excludeNetworks {
   273  			params.Add("not_networks", name)
   274  		}
   275  	}
   276  }
   277  
   278  // acquireNode allocates a node from the MAAS.
   279  func (environ *maasEnviron) acquireNode(nodeName string, cons constraints.Value, includeNetworks, excludeNetworks []string, possibleTools tools.List) (gomaasapi.MAASObject, *tools.Tools, error) {
   280  	acquireParams := convertConstraints(cons)
   281  	addNetworks(acquireParams, includeNetworks, excludeNetworks)
   282  	acquireParams.Add("agent_name", environ.ecfg().maasAgentName())
   283  	if nodeName != "" {
   284  		acquireParams.Add("name", nodeName)
   285  	}
   286  	var result gomaasapi.JSONObject
   287  	var err error
   288  	for a := shortAttempt.Start(); a.Next(); {
   289  		client := environ.getMAASClient().GetSubObject("nodes/")
   290  		result, err = client.CallPost("acquire", acquireParams)
   291  		if err == nil {
   292  			break
   293  		}
   294  	}
   295  	if err != nil {
   296  		return gomaasapi.MAASObject{}, nil, err
   297  	}
   298  	node, err := result.GetMAASObject()
   299  	if err != nil {
   300  		msg := fmt.Errorf("unexpected result from 'acquire' on MAAS API: %v", err)
   301  		return gomaasapi.MAASObject{}, nil, msg
   302  	}
   303  	tools := possibleTools[0]
   304  	logger.Warningf("picked arbitrary tools %v", tools)
   305  	return node, tools, nil
   306  }
   307  
   308  // startNode installs and boots a node.
   309  func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) error {
   310  	userDataParam := base64.StdEncoding.EncodeToString(userdata)
   311  	params := url.Values{
   312  		"distro_series": {series},
   313  		"user_data":     {userDataParam},
   314  	}
   315  	// Initialize err to a non-nil value as a sentinel for the following
   316  	// loop.
   317  	err := fmt.Errorf("(no error)")
   318  	for a := shortAttempt.Start(); a.Next() && err != nil; {
   319  		_, err = node.CallPost("start", params)
   320  	}
   321  	return err
   322  }
   323  
   324  // createBridgeNetwork returns a string representing the upstart command to
   325  // create a bridged eth0.
   326  func createBridgeNetwork() string {
   327  	return `cat > /etc/network/eth0.config << EOF
   328  iface eth0 inet manual
   329  
   330  auto br0
   331  iface br0 inet dhcp
   332    bridge_ports eth0
   333  EOF
   334  `
   335  }
   336  
   337  // linkBridgeInInterfaces adds the file created by createBridgeNetwork to the
   338  // interfaces file.
   339  func linkBridgeInInterfaces() string {
   340  	return `sed -i "s/iface eth0 inet dhcp/source \/etc\/network\/eth0.config/" /etc/network/interfaces`
   341  }
   342  
   343  var unsupportedConstraints = []string{
   344  	constraints.CpuPower,
   345  	constraints.InstanceType,
   346  }
   347  
   348  // ConstraintsValidator is defined on the Environs interface.
   349  func (environ *maasEnviron) ConstraintsValidator() (constraints.Validator, error) {
   350  	validator := constraints.NewValidator()
   351  	validator.RegisterUnsupported(unsupportedConstraints)
   352  	supportedArches, err := environ.SupportedArchitectures()
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	validator.RegisterVocabulary(constraints.Arch, supportedArches)
   357  	return validator, nil
   358  }
   359  
   360  // setupNetworks prepares a []network.Info for the given instance, but
   361  // only interfaces on networks in networksToEnable will be configured
   362  // on the machine.
   363  func (environ *maasEnviron) setupNetworks(inst instance.Instance, networksToEnable set.Strings) ([]network.Info, error) {
   364  	// Get the instance network interfaces first.
   365  	interfaces, err := environ.getInstanceNetworkInterfaces(inst)
   366  	if err != nil {
   367  		return nil, fmt.Errorf("getInstanceNetworkInterfaces failed: %v", err)
   368  	}
   369  	logger.Debugf("node %q has network interfaces %v", inst.Id(), interfaces)
   370  	networks, err := environ.getInstanceNetworks(inst)
   371  	if err != nil {
   372  		return nil, fmt.Errorf("getInstanceNetworks failed: %v", err)
   373  	}
   374  	logger.Debugf("node %q has networks %v", inst.Id(), networks)
   375  	var tempNetworkInfo []network.Info
   376  	for _, netw := range networks {
   377  		disabled := !networksToEnable.Contains(netw.Name)
   378  		netCIDR := &net.IPNet{
   379  			IP:   net.ParseIP(netw.IP),
   380  			Mask: net.IPMask(net.ParseIP(netw.Mask)),
   381  		}
   382  		macs, err := environ.getNetworkMACs(netw.Name)
   383  		if err != nil {
   384  			return nil, fmt.Errorf("getNetworkMACs failed: %v", err)
   385  		}
   386  		logger.Debugf("network %q has MACs: %v", netw.Name, macs)
   387  		for _, mac := range macs {
   388  			if interfaceName, ok := interfaces[mac]; ok {
   389  				tempNetworkInfo = append(tempNetworkInfo, network.Info{
   390  					MACAddress:    mac,
   391  					InterfaceName: interfaceName,
   392  					CIDR:          netCIDR.String(),
   393  					VLANTag:       netw.VLANTag,
   394  					ProviderId:    network.Id(netw.Name),
   395  					NetworkName:   netw.Name,
   396  					IsVirtual:     netw.VLANTag > 0,
   397  					Disabled:      disabled,
   398  				})
   399  			}
   400  		}
   401  	}
   402  	// Verify we filled-in everything for all networks/interfaces
   403  	// and drop incomplete records.
   404  	var networkInfo []network.Info
   405  	for _, info := range tempNetworkInfo {
   406  		if info.ProviderId == "" || info.NetworkName == "" || info.CIDR == "" {
   407  			logger.Warningf("ignoring network interface %q: missing network information", info.InterfaceName)
   408  			continue
   409  		}
   410  		if info.MACAddress == "" || info.InterfaceName == "" {
   411  			logger.Warningf("ignoring network %q: missing network interface information", info.ProviderId)
   412  			continue
   413  		}
   414  		networkInfo = append(networkInfo, info)
   415  	}
   416  	logger.Debugf("node %q network information: %#v", inst.Id(), networkInfo)
   417  	return networkInfo, nil
   418  }
   419  
   420  // StartInstance is specified in the InstanceBroker interface.
   421  func (environ *maasEnviron) StartInstance(args environs.StartInstanceParams) (
   422  	instance.Instance, *instance.HardwareCharacteristics, []network.Info, error,
   423  ) {
   424  	var inst *maasInstance
   425  	var err error
   426  	nodeName := args.Placement
   427  	requestedNetworks := args.MachineConfig.Networks
   428  	includeNetworks := append(args.Constraints.IncludeNetworks(), requestedNetworks...)
   429  	excludeNetworks := args.Constraints.ExcludeNetworks()
   430  	node, tools, err := environ.acquireNode(
   431  		nodeName,
   432  		args.Constraints,
   433  		includeNetworks,
   434  		excludeNetworks,
   435  		args.Tools)
   436  	if err != nil {
   437  		return nil, nil, nil, fmt.Errorf("cannot run instances: %v", err)
   438  	} else {
   439  		inst = &maasInstance{maasObject: &node, environ: environ}
   440  		args.MachineConfig.Tools = tools
   441  	}
   442  	defer func() {
   443  		if err != nil {
   444  			if err := environ.StopInstances(inst.Id()); err != nil {
   445  				logger.Errorf("error releasing failed instance: %v", err)
   446  			}
   447  		}
   448  	}()
   449  	var networkInfo []network.Info
   450  	if args.MachineConfig.HasNetworks() {
   451  		networkInfo, err = environ.setupNetworks(inst, set.NewStrings(requestedNetworks...))
   452  		if err != nil {
   453  			return nil, nil, nil, err
   454  		}
   455  	}
   456  
   457  	hostname, err := inst.hostname()
   458  	if err != nil {
   459  		return nil, nil, nil, err
   460  	}
   461  	if err := environs.FinishMachineConfig(args.MachineConfig, environ.Config(), args.Constraints); err != nil {
   462  		return nil, nil, nil, err
   463  	}
   464  	// TODO(thumper): 2013-08-28 bug 1217614
   465  	// The machine envronment config values are being moved to the agent config.
   466  	// Explicitly specify that the lxc containers use the network bridge defined above.
   467  	args.MachineConfig.AgentEnvironment[agent.LxcBridge] = "br0"
   468  	cloudcfg, err := newCloudinitConfig(hostname, networkInfo)
   469  	if err != nil {
   470  		return nil, nil, nil, err
   471  	}
   472  	userdata, err := environs.ComposeUserData(args.MachineConfig, cloudcfg)
   473  	if err != nil {
   474  		msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err)
   475  		return nil, nil, nil, msg
   476  	}
   477  	logger.Debugf("maas user data; %d bytes", len(userdata))
   478  
   479  	series := args.Tools.OneSeries()
   480  	if err := environ.startNode(*inst.maasObject, series, userdata); err != nil {
   481  		return nil, nil, nil, err
   482  	}
   483  	logger.Debugf("started instance %q", inst.Id())
   484  	// TODO(bug 1193998) - return instance hardware characteristics as well
   485  	return inst, nil, networkInfo, nil
   486  }
   487  
   488  // newCloudinitConfig creates a cloudinit.Config structure
   489  // suitable as a base for initialising a MAAS node.
   490  func newCloudinitConfig(hostname string, networkInfo []network.Info) (*cloudinit.Config, error) {
   491  	info := machineInfo{hostname}
   492  	runCmd, err := info.cloudinitRunCmd()
   493  	if err != nil {
   494  		return nil, err
   495  	}
   496  	cloudcfg := cloudinit.New()
   497  	cloudcfg.SetAptUpdate(true)
   498  	cloudcfg.AddPackage("bridge-utils")
   499  	cloudcfg.AddScripts(
   500  		"set -xe",
   501  		runCmd,
   502  		"ifdown eth0",
   503  		createBridgeNetwork(),
   504  		linkBridgeInInterfaces(),
   505  		"ifup br0",
   506  	)
   507  	setupNetworksOnBoot(cloudcfg, networkInfo)
   508  	return cloudcfg, nil
   509  }
   510  
   511  // setupNetworksOnBoot prepares a script to enable and start all
   512  // enabled network interfaces on boot.
   513  func setupNetworksOnBoot(cloudcfg *cloudinit.Config, networkInfo []network.Info) {
   514  	const ifaceConfig = `cat >> /etc/network/interfaces << EOF
   515  
   516  auto %s
   517  iface %s inet dhcp
   518  EOF
   519  `
   520  	// We need the vlan package for the vconfig command.
   521  	cloudcfg.AddPackage("vlan")
   522  
   523  	script := func(line string, args ...interface{}) {
   524  		cloudcfg.AddScripts(fmt.Sprintf(line, args...))
   525  	}
   526  	// Because eth0 is already configured in the br0 bridge, we
   527  	// don't want to break that.
   528  	configured := set.NewStrings("eth0")
   529  
   530  	// In order to support VLANs, we need to include 8021q module
   531  	// configure vconfig's set_name_type, but due to bug #1316762,
   532  	// we need to first check if it's already loaded.
   533  	script("sh -c 'lsmod | grep -q 8021q || modprobe 8021q'")
   534  	script("sh -c 'grep -q 8021q /etc/modules || echo 8021q >> /etc/modules'")
   535  	script("vconfig set_name_type DEV_PLUS_VID_NO_PAD")
   536  	// Now prepare each interface configuration
   537  	for _, info := range networkInfo {
   538  		if !configured.Contains(info.InterfaceName) {
   539  			// TODO(dimitern): We should respect user's choice
   540  			// and skip interfaces marked as Disabled, but we
   541  			// are postponing this until we have the networker
   542  			// in place.
   543  
   544  			// Register and bring up the physical interface.
   545  			script(ifaceConfig, info.InterfaceName, info.InterfaceName)
   546  			script("ifup %s", info.InterfaceName)
   547  			configured.Add(info.InterfaceName)
   548  		}
   549  		if info.VLANTag > 0 {
   550  			// We have a VLAN and need to create and register it after
   551  			// its parent interface was brought up.
   552  			script("vconfig add %s %d", info.InterfaceName, info.VLANTag)
   553  			vlan := info.ActualInterfaceName()
   554  			script(ifaceConfig, vlan, vlan)
   555  			script("ifup %s", vlan)
   556  		}
   557  	}
   558  }
   559  
   560  // StopInstances is specified in the InstanceBroker interface.
   561  func (environ *maasEnviron) StopInstances(ids ...instance.Id) error {
   562  	// Shortcut to exit quickly if 'instances' is an empty slice or nil.
   563  	if len(ids) == 0 {
   564  		return nil
   565  	}
   566  	// TODO(axw) 2014-05-13 #1319016
   567  	// Nodes that have been removed out of band will cause
   568  	// the release call to fail. We should parse the error
   569  	// returned from MAAS and retry, or otherwise request
   570  	// an enhancement to MAAS to ignore unknown node IDs.
   571  	nodes := environ.getMAASClient().GetSubObject("nodes")
   572  	_, err := nodes.CallPost("release", getSystemIdValues("nodes", ids))
   573  	return err
   574  }
   575  
   576  // instances calls the MAAS API to list nodes.  The "ids" slice is a filter for
   577  // specific instance IDs.  Due to how this works in the HTTP API, an empty
   578  // "ids" matches all instances (not none as you might expect).
   579  func (environ *maasEnviron) instances(ids []instance.Id) ([]instance.Instance, error) {
   580  	nodeListing := environ.getMAASClient().GetSubObject("nodes")
   581  	filter := getSystemIdValues("id", ids)
   582  	filter.Add("agent_name", environ.ecfg().maasAgentName())
   583  	listNodeObjects, err := nodeListing.CallGet("list", filter)
   584  	if err != nil {
   585  		return nil, err
   586  	}
   587  	listNodes, err := listNodeObjects.GetArray()
   588  	if err != nil {
   589  		return nil, err
   590  	}
   591  	instances := make([]instance.Instance, len(listNodes))
   592  	for index, nodeObj := range listNodes {
   593  		node, err := nodeObj.GetMAASObject()
   594  		if err != nil {
   595  			return nil, err
   596  		}
   597  		instances[index] = &maasInstance{
   598  			maasObject: &node,
   599  			environ:    environ,
   600  		}
   601  	}
   602  	return instances, nil
   603  }
   604  
   605  // Instances returns the instance.Instance objects corresponding to the given
   606  // slice of instance.Id.  The error is ErrNoInstances if no instances
   607  // were found.
   608  func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
   609  	if len(ids) == 0 {
   610  		// This would be treated as "return all instances" below, so
   611  		// treat it as a special case.
   612  		// The interface requires us to return this particular error
   613  		// if no instances were found.
   614  		return nil, environs.ErrNoInstances
   615  	}
   616  	instances, err := environ.instances(ids)
   617  	if err != nil {
   618  		return nil, err
   619  	}
   620  	if len(instances) == 0 {
   621  		return nil, environs.ErrNoInstances
   622  	}
   623  
   624  	idMap := make(map[instance.Id]instance.Instance)
   625  	for _, instance := range instances {
   626  		idMap[instance.Id()] = instance
   627  	}
   628  
   629  	result := make([]instance.Instance, len(ids))
   630  	for index, id := range ids {
   631  		result[index] = idMap[id]
   632  	}
   633  
   634  	if len(instances) < len(ids) {
   635  		return result, environs.ErrPartialInstances
   636  	}
   637  	return result, nil
   638  }
   639  
   640  // AllocateAddress requests a new address to be allocated for the
   641  // given instance on the given network. This is not implemented on the
   642  // MAAS provider yet.
   643  func (*maasEnviron) AllocateAddress(_ instance.Id, _ network.Id) (instance.Address, error) {
   644  	// TODO(dimitern) 2014-05-06 bug #1316627
   645  	// Once MAAS API allows allocating an address,
   646  	// implement this using the API.
   647  	return instance.Address{}, errors.NotImplementedf("AllocateAddress")
   648  }
   649  
   650  // AllInstances returns all the instance.Instance in this provider.
   651  func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) {
   652  	return environ.instances(nil)
   653  }
   654  
   655  // Storage is defined by the Environ interface.
   656  func (env *maasEnviron) Storage() storage.Storage {
   657  	env.ecfgMutex.Lock()
   658  	defer env.ecfgMutex.Unlock()
   659  	return env.storageUnlocked
   660  }
   661  
   662  func (environ *maasEnviron) Destroy() error {
   663  	return common.Destroy(environ)
   664  }
   665  
   666  // MAAS does not do firewalling so these port methods do nothing.
   667  func (*maasEnviron) OpenPorts([]instance.Port) error {
   668  	logger.Debugf("unimplemented OpenPorts() called")
   669  	return nil
   670  }
   671  
   672  func (*maasEnviron) ClosePorts([]instance.Port) error {
   673  	logger.Debugf("unimplemented ClosePorts() called")
   674  	return nil
   675  }
   676  
   677  func (*maasEnviron) Ports() ([]instance.Port, error) {
   678  	logger.Debugf("unimplemented Ports() called")
   679  	return []instance.Port{}, nil
   680  }
   681  
   682  func (*maasEnviron) Provider() environs.EnvironProvider {
   683  	return &providerInstance
   684  }
   685  
   686  // GetImageSources returns a list of sources which are used to search for simplestreams image metadata.
   687  func (e *maasEnviron) GetImageSources() ([]simplestreams.DataSource, error) {
   688  	// Add the simplestreams source off the control bucket.
   689  	return []simplestreams.DataSource{
   690  		storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseImagesPath)}, nil
   691  }
   692  
   693  // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata.
   694  func (e *maasEnviron) GetToolsSources() ([]simplestreams.DataSource, error) {
   695  	// Add the simplestreams source off the control bucket.
   696  	return []simplestreams.DataSource{
   697  		storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)}, nil
   698  }
   699  
   700  // networkDetails holds information about a MAAS network.
   701  type networkDetails struct {
   702  	Name        string
   703  	IP          string
   704  	Mask        string
   705  	VLANTag     int
   706  	Description string
   707  }
   708  
   709  // getInstanceNetworks returns a list of all MAAS networks for a given node.
   710  func (environ *maasEnviron) getInstanceNetworks(inst instance.Instance) ([]networkDetails, error) {
   711  	maasInst := inst.(*maasInstance)
   712  	maasObj := maasInst.maasObject
   713  	client := environ.getMAASClient().GetSubObject("networks")
   714  	nodeId, err := maasObj.GetField("system_id")
   715  	if err != nil {
   716  		return nil, err
   717  	}
   718  	params := url.Values{"node": {nodeId}}
   719  	json, err := client.CallGet("", params)
   720  	if err != nil {
   721  		return nil, err
   722  	}
   723  	jsonNets, err := json.GetArray()
   724  	if err != nil {
   725  		return nil, err
   726  	}
   727  
   728  	networks := make([]networkDetails, len(jsonNets))
   729  	for i, jsonNet := range jsonNets {
   730  		fields, err := jsonNet.GetMap()
   731  		if err != nil {
   732  			return nil, err
   733  		}
   734  		name, err := fields["name"].GetString()
   735  		if err != nil {
   736  			return nil, fmt.Errorf("cannot get name: %v", err)
   737  		}
   738  		ip, err := fields["ip"].GetString()
   739  		if err != nil {
   740  			return nil, fmt.Errorf("cannot get ip: %v", err)
   741  		}
   742  		netmask, err := fields["netmask"].GetString()
   743  		if err != nil {
   744  			return nil, fmt.Errorf("cannot get netmask: %v", err)
   745  		}
   746  		vlanTag := 0
   747  		vlanTagField, ok := fields["vlan_tag"]
   748  		if ok && !vlanTagField.IsNil() {
   749  			// vlan_tag is optional, so assume it's 0 when missing or nil.
   750  			vlanTagFloat, err := vlanTagField.GetFloat64()
   751  			if err != nil {
   752  				return nil, fmt.Errorf("cannot get vlan_tag: %v", err)
   753  			}
   754  			vlanTag = int(vlanTagFloat)
   755  		}
   756  		description, err := fields["description"].GetString()
   757  		if err != nil {
   758  			return nil, fmt.Errorf("cannot get description: %v", err)
   759  		}
   760  
   761  		networks[i] = networkDetails{
   762  			Name:        name,
   763  			IP:          ip,
   764  			Mask:        netmask,
   765  			VLANTag:     vlanTag,
   766  			Description: description,
   767  		}
   768  	}
   769  	return networks, nil
   770  }
   771  
   772  // getNetworkMACs returns all MAC addresses connected to the given
   773  // network.
   774  func (environ *maasEnviron) getNetworkMACs(networkName string) ([]string, error) {
   775  	client := environ.getMAASClient().GetSubObject("networks").GetSubObject(networkName)
   776  	json, err := client.CallGet("list_connected_macs", nil)
   777  	if err != nil {
   778  		return nil, err
   779  	}
   780  	jsonMACs, err := json.GetArray()
   781  	if err != nil {
   782  		return nil, err
   783  	}
   784  
   785  	macs := make([]string, len(jsonMACs))
   786  	for i, jsonMAC := range jsonMACs {
   787  		fields, err := jsonMAC.GetMap()
   788  		if err != nil {
   789  			return nil, err
   790  		}
   791  		macAddress, err := fields["mac_address"].GetString()
   792  		if err != nil {
   793  			return nil, fmt.Errorf("cannot get mac_address: %v", err)
   794  		}
   795  		macs[i] = macAddress
   796  	}
   797  	return macs, nil
   798  }
   799  
   800  // getInstanceNetworkInterfaces returns a map of interface MAC address
   801  // to name for each network interface of the given instance, as
   802  // discovered during the commissioning phase.
   803  func (environ *maasEnviron) getInstanceNetworkInterfaces(inst instance.Instance) (map[string]string, error) {
   804  	maasInst := inst.(*maasInstance)
   805  	maasObj := maasInst.maasObject
   806  	result, err := maasObj.CallGet("details", nil)
   807  	if err != nil {
   808  		return nil, err
   809  	}
   810  	// Get the node's lldp / lshw details discovered at commissioning.
   811  	data, err := result.GetBytes()
   812  	if err != nil {
   813  		return nil, err
   814  	}
   815  	var parsed map[string]interface{}
   816  	if err := bson.Unmarshal(data, &parsed); err != nil {
   817  		return nil, err
   818  	}
   819  	lshwData, ok := parsed["lshw"]
   820  	if !ok {
   821  		return nil, fmt.Errorf("no hardware information available for node %q", inst.Id())
   822  	}
   823  	lshwXML, ok := lshwData.([]byte)
   824  	if !ok {
   825  		return nil, fmt.Errorf("invalid hardware information for node %q", inst.Id())
   826  	}
   827  	// Now we have the lshw XML data, parse it to extract and return NICs.
   828  	return extractInterfaces(inst, lshwXML)
   829  }
   830  
   831  // extractInterfaces parses the XML output of lswh and extracts all
   832  // network interfaces, returing a map MAC address to interface name.
   833  func extractInterfaces(inst instance.Instance, lshwXML []byte) (map[string]string, error) {
   834  	type Node struct {
   835  		Id          string `xml:"id,attr"`
   836  		Description string `xml:"description"`
   837  		Serial      string `xml:"serial"`
   838  		LogicalName string `xml:"logicalname"`
   839  		Children    []Node `xml:"node"`
   840  	}
   841  	type List struct {
   842  		Nodes []Node `xml:"node"`
   843  	}
   844  	var lshw List
   845  	if err := xml.Unmarshal(lshwXML, &lshw); err != nil {
   846  		return nil, fmt.Errorf("cannot parse lshw XML details for node %q: %v", inst.Id(), err)
   847  	}
   848  	interfaces := make(map[string]string)
   849  	var processNodes func(nodes []Node)
   850  	processNodes = func(nodes []Node) {
   851  		for _, node := range nodes {
   852  			if strings.HasPrefix(node.Id, "network") {
   853  				interfaces[node.Serial] = node.LogicalName
   854  			}
   855  			processNodes(node.Children)
   856  		}
   857  	}
   858  	processNodes(lshw.Nodes)
   859  	return interfaces, nil
   860  }