github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/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/simplestreams"
    29  	"github.com/juju/juju/environs/storage"
    30  	envtools "github.com/juju/juju/environs/tools"
    31  	"github.com/juju/juju/instance"
    32  	"github.com/juju/juju/network"
    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  					Disabled:      disabled,
   397  				})
   398  			}
   399  		}
   400  	}
   401  	// Verify we filled-in everything for all networks/interfaces
   402  	// and drop incomplete records.
   403  	var networkInfo []network.Info
   404  	for _, info := range tempNetworkInfo {
   405  		if info.ProviderId == "" || info.NetworkName == "" || info.CIDR == "" {
   406  			logger.Warningf("ignoring network interface %q: missing network information", info.InterfaceName)
   407  			continue
   408  		}
   409  		if info.MACAddress == "" || info.InterfaceName == "" {
   410  			logger.Warningf("ignoring network %q: missing network interface information", info.ProviderId)
   411  			continue
   412  		}
   413  		networkInfo = append(networkInfo, info)
   414  	}
   415  	logger.Debugf("node %q network information: %#v", inst.Id(), networkInfo)
   416  	return networkInfo, nil
   417  }
   418  
   419  // StartInstance is specified in the InstanceBroker interface.
   420  func (environ *maasEnviron) StartInstance(args environs.StartInstanceParams) (
   421  	instance.Instance, *instance.HardwareCharacteristics, []network.Info, error,
   422  ) {
   423  	var inst *maasInstance
   424  	var err error
   425  	nodeName := args.Placement
   426  	requestedNetworks := args.MachineConfig.Networks
   427  	includeNetworks := append(args.Constraints.IncludeNetworks(), requestedNetworks...)
   428  	excludeNetworks := args.Constraints.ExcludeNetworks()
   429  	node, tools, err := environ.acquireNode(
   430  		nodeName,
   431  		args.Constraints,
   432  		includeNetworks,
   433  		excludeNetworks,
   434  		args.Tools)
   435  	if err != nil {
   436  		return nil, nil, nil, fmt.Errorf("cannot run instances: %v", err)
   437  	} else {
   438  		inst = &maasInstance{maasObject: &node, environ: environ}
   439  		args.MachineConfig.Tools = tools
   440  	}
   441  	defer func() {
   442  		if err != nil {
   443  			if err := environ.StopInstances(inst.Id()); err != nil {
   444  				logger.Errorf("error releasing failed instance: %v", err)
   445  			}
   446  		}
   447  	}()
   448  	var networkInfo []network.Info
   449  	if args.MachineConfig.HasNetworks() {
   450  		networkInfo, err = environ.setupNetworks(inst, set.NewStrings(requestedNetworks...))
   451  		if err != nil {
   452  			return nil, nil, nil, err
   453  		}
   454  	}
   455  
   456  	hostname, err := inst.hostname()
   457  	if err != nil {
   458  		return nil, nil, nil, err
   459  	}
   460  	if err := environs.FinishMachineConfig(args.MachineConfig, environ.Config(), args.Constraints); err != nil {
   461  		return nil, nil, nil, err
   462  	}
   463  	// TODO(thumper): 2013-08-28 bug 1217614
   464  	// The machine envronment config values are being moved to the agent config.
   465  	// Explicitly specify that the lxc containers use the network bridge defined above.
   466  	args.MachineConfig.AgentEnvironment[agent.LxcBridge] = "br0"
   467  	cloudcfg, err := newCloudinitConfig(hostname, networkInfo)
   468  	if err != nil {
   469  		return nil, nil, nil, err
   470  	}
   471  	userdata, err := environs.ComposeUserData(args.MachineConfig, cloudcfg)
   472  	if err != nil {
   473  		msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err)
   474  		return nil, nil, nil, msg
   475  	}
   476  	logger.Debugf("maas user data; %d bytes", len(userdata))
   477  
   478  	series := args.Tools.OneSeries()
   479  	if err := environ.startNode(*inst.maasObject, series, userdata); err != nil {
   480  		return nil, nil, nil, err
   481  	}
   482  	logger.Debugf("started instance %q", inst.Id())
   483  	// TODO(bug 1193998) - return instance hardware characteristics as well
   484  	return inst, nil, networkInfo, nil
   485  }
   486  
   487  // newCloudinitConfig creates a cloudinit.Config structure
   488  // suitable as a base for initialising a MAAS node.
   489  func newCloudinitConfig(hostname string, networkInfo []network.Info) (*cloudinit.Config, error) {
   490  	info := machineInfo{hostname}
   491  	runCmd, err := info.cloudinitRunCmd()
   492  	if err != nil {
   493  		return nil, err
   494  	}
   495  	cloudcfg := cloudinit.New()
   496  	cloudcfg.SetAptUpdate(true)
   497  	cloudcfg.AddPackage("bridge-utils")
   498  	cloudcfg.AddScripts(
   499  		"set -xe",
   500  		runCmd,
   501  		"ifdown eth0",
   502  		createBridgeNetwork(),
   503  		linkBridgeInInterfaces(),
   504  		"ifup br0",
   505  	)
   506  	setupNetworksOnBoot(cloudcfg, networkInfo)
   507  	return cloudcfg, nil
   508  }
   509  
   510  // setupNetworksOnBoot prepares a script to enable and start all
   511  // enabled network interfaces on boot.
   512  func setupNetworksOnBoot(cloudcfg *cloudinit.Config, networkInfo []network.Info) {
   513  	const ifaceConfig = `cat >> /etc/network/interfaces << EOF
   514  
   515  auto %s
   516  iface %s inet dhcp
   517  EOF
   518  `
   519  	// We need the vlan package for the vconfig command.
   520  	cloudcfg.AddPackage("vlan")
   521  
   522  	script := func(line string, args ...interface{}) {
   523  		cloudcfg.AddScripts(fmt.Sprintf(line, args...))
   524  	}
   525  	// Because eth0 is already configured in the br0 bridge, we
   526  	// don't want to break that.
   527  	configured := set.NewStrings("eth0")
   528  
   529  	// In order to support VLANs, we need to include 8021q module
   530  	// configure vconfig's set_name_type, but due to bug #1316762,
   531  	// we need to first check if it's already loaded.
   532  	script("sh -c 'lsmod | grep -q 8021q || modprobe 8021q'")
   533  	script("sh -c 'grep -q 8021q /etc/modules || echo 8021q >> /etc/modules'")
   534  	script("vconfig set_name_type DEV_PLUS_VID_NO_PAD")
   535  	// Now prepare each interface configuration
   536  	for _, info := range networkInfo {
   537  		if !configured.Contains(info.InterfaceName) {
   538  			// TODO(dimitern): We should respect user's choice
   539  			// and skip interfaces marked as Disabled, but we
   540  			// are postponing this until we have the networker
   541  			// in place.
   542  
   543  			// Register and bring up the physical interface.
   544  			script(ifaceConfig, info.InterfaceName, info.InterfaceName)
   545  			script("ifup %s", info.InterfaceName)
   546  			configured.Add(info.InterfaceName)
   547  		}
   548  		if info.VLANTag > 0 {
   549  			// We have a VLAN and need to create and register it after
   550  			// its parent interface was brought up.
   551  			script("vconfig add %s %d", info.InterfaceName, info.VLANTag)
   552  			vlan := info.ActualInterfaceName()
   553  			script(ifaceConfig, vlan, vlan)
   554  			script("ifup %s", vlan)
   555  		}
   556  	}
   557  }
   558  
   559  // StopInstances is specified in the InstanceBroker interface.
   560  func (environ *maasEnviron) StopInstances(ids ...instance.Id) error {
   561  	// Shortcut to exit quickly if 'instances' is an empty slice or nil.
   562  	if len(ids) == 0 {
   563  		return nil
   564  	}
   565  	// TODO(axw) 2014-05-13 #1319016
   566  	// Nodes that have been removed out of band will cause
   567  	// the release call to fail. We should parse the error
   568  	// returned from MAAS and retry, or otherwise request
   569  	// an enhancement to MAAS to ignore unknown node IDs.
   570  	nodes := environ.getMAASClient().GetSubObject("nodes")
   571  	_, err := nodes.CallPost("release", getSystemIdValues("nodes", ids))
   572  	return err
   573  }
   574  
   575  // instances calls the MAAS API to list nodes.  The "ids" slice is a filter for
   576  // specific instance IDs.  Due to how this works in the HTTP API, an empty
   577  // "ids" matches all instances (not none as you might expect).
   578  func (environ *maasEnviron) instances(ids []instance.Id) ([]instance.Instance, error) {
   579  	nodeListing := environ.getMAASClient().GetSubObject("nodes")
   580  	filter := getSystemIdValues("id", ids)
   581  	filter.Add("agent_name", environ.ecfg().maasAgentName())
   582  	listNodeObjects, err := nodeListing.CallGet("list", filter)
   583  	if err != nil {
   584  		return nil, err
   585  	}
   586  	listNodes, err := listNodeObjects.GetArray()
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  	instances := make([]instance.Instance, len(listNodes))
   591  	for index, nodeObj := range listNodes {
   592  		node, err := nodeObj.GetMAASObject()
   593  		if err != nil {
   594  			return nil, err
   595  		}
   596  		instances[index] = &maasInstance{
   597  			maasObject: &node,
   598  			environ:    environ,
   599  		}
   600  	}
   601  	return instances, nil
   602  }
   603  
   604  // Instances returns the instance.Instance objects corresponding to the given
   605  // slice of instance.Id.  The error is ErrNoInstances if no instances
   606  // were found.
   607  func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
   608  	if len(ids) == 0 {
   609  		// This would be treated as "return all instances" below, so
   610  		// treat it as a special case.
   611  		// The interface requires us to return this particular error
   612  		// if no instances were found.
   613  		return nil, environs.ErrNoInstances
   614  	}
   615  	instances, err := environ.instances(ids)
   616  	if err != nil {
   617  		return nil, err
   618  	}
   619  	if len(instances) == 0 {
   620  		return nil, environs.ErrNoInstances
   621  	}
   622  
   623  	idMap := make(map[instance.Id]instance.Instance)
   624  	for _, instance := range instances {
   625  		idMap[instance.Id()] = instance
   626  	}
   627  
   628  	result := make([]instance.Instance, len(ids))
   629  	for index, id := range ids {
   630  		result[index] = idMap[id]
   631  	}
   632  
   633  	if len(instances) < len(ids) {
   634  		return result, environs.ErrPartialInstances
   635  	}
   636  	return result, nil
   637  }
   638  
   639  // AllocateAddress requests a new address to be allocated for the
   640  // given instance on the given network. This is not implemented on the
   641  // MAAS provider yet.
   642  func (*maasEnviron) AllocateAddress(_ instance.Id, _ network.Id) (network.Address, error) {
   643  	// TODO(dimitern) 2014-05-06 bug #1316627
   644  	// Once MAAS API allows allocating an address,
   645  	// implement this using the API.
   646  	return network.Address{}, errors.NotImplementedf("AllocateAddress")
   647  }
   648  
   649  // ListNetworks returns basic information about all networks known
   650  // by the provider for the environment. They may be unknown to juju
   651  // yet (i.e. when called initially or when a new network was created).
   652  // This is not implemented by the MAAS provider yet.
   653  func (*maasEnviron) ListNetworks() ([]network.BasicInfo, error) {
   654  	return nil, errors.NotImplementedf("ListNetworks")
   655  }
   656  
   657  // AllInstances returns all the instance.Instance in this provider.
   658  func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) {
   659  	return environ.instances(nil)
   660  }
   661  
   662  // Storage is defined by the Environ interface.
   663  func (env *maasEnviron) Storage() storage.Storage {
   664  	env.ecfgMutex.Lock()
   665  	defer env.ecfgMutex.Unlock()
   666  	return env.storageUnlocked
   667  }
   668  
   669  func (environ *maasEnviron) Destroy() error {
   670  	return common.Destroy(environ)
   671  }
   672  
   673  // MAAS does not do firewalling so these port methods do nothing.
   674  func (*maasEnviron) OpenPorts([]network.Port) error {
   675  	logger.Debugf("unimplemented OpenPorts() called")
   676  	return nil
   677  }
   678  
   679  func (*maasEnviron) ClosePorts([]network.Port) error {
   680  	logger.Debugf("unimplemented ClosePorts() called")
   681  	return nil
   682  }
   683  
   684  func (*maasEnviron) Ports() ([]network.Port, error) {
   685  	logger.Debugf("unimplemented Ports() called")
   686  	return []network.Port{}, nil
   687  }
   688  
   689  func (*maasEnviron) Provider() environs.EnvironProvider {
   690  	return &providerInstance
   691  }
   692  
   693  // GetImageSources returns a list of sources which are used to search for simplestreams image metadata.
   694  func (e *maasEnviron) GetImageSources() ([]simplestreams.DataSource, error) {
   695  	// Add the simplestreams source off the control bucket.
   696  	return []simplestreams.DataSource{
   697  		storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseImagesPath)}, nil
   698  }
   699  
   700  // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata.
   701  func (e *maasEnviron) GetToolsSources() ([]simplestreams.DataSource, error) {
   702  	// Add the simplestreams source off the control bucket.
   703  	return []simplestreams.DataSource{
   704  		storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)}, nil
   705  }
   706  
   707  // networkDetails holds information about a MAAS network.
   708  type networkDetails struct {
   709  	Name        string
   710  	IP          string
   711  	Mask        string
   712  	VLANTag     int
   713  	Description string
   714  }
   715  
   716  // getInstanceNetworks returns a list of all MAAS networks for a given node.
   717  func (environ *maasEnviron) getInstanceNetworks(inst instance.Instance) ([]networkDetails, error) {
   718  	maasInst := inst.(*maasInstance)
   719  	maasObj := maasInst.maasObject
   720  	client := environ.getMAASClient().GetSubObject("networks")
   721  	nodeId, err := maasObj.GetField("system_id")
   722  	if err != nil {
   723  		return nil, err
   724  	}
   725  	params := url.Values{"node": {nodeId}}
   726  	json, err := client.CallGet("", params)
   727  	if err != nil {
   728  		return nil, err
   729  	}
   730  	jsonNets, err := json.GetArray()
   731  	if err != nil {
   732  		return nil, err
   733  	}
   734  
   735  	networks := make([]networkDetails, len(jsonNets))
   736  	for i, jsonNet := range jsonNets {
   737  		fields, err := jsonNet.GetMap()
   738  		if err != nil {
   739  			return nil, err
   740  		}
   741  		name, err := fields["name"].GetString()
   742  		if err != nil {
   743  			return nil, fmt.Errorf("cannot get name: %v", err)
   744  		}
   745  		ip, err := fields["ip"].GetString()
   746  		if err != nil {
   747  			return nil, fmt.Errorf("cannot get ip: %v", err)
   748  		}
   749  		netmask, err := fields["netmask"].GetString()
   750  		if err != nil {
   751  			return nil, fmt.Errorf("cannot get netmask: %v", err)
   752  		}
   753  		vlanTag := 0
   754  		vlanTagField, ok := fields["vlan_tag"]
   755  		if ok && !vlanTagField.IsNil() {
   756  			// vlan_tag is optional, so assume it's 0 when missing or nil.
   757  			vlanTagFloat, err := vlanTagField.GetFloat64()
   758  			if err != nil {
   759  				return nil, fmt.Errorf("cannot get vlan_tag: %v", err)
   760  			}
   761  			vlanTag = int(vlanTagFloat)
   762  		}
   763  		description, err := fields["description"].GetString()
   764  		if err != nil {
   765  			return nil, fmt.Errorf("cannot get description: %v", err)
   766  		}
   767  
   768  		networks[i] = networkDetails{
   769  			Name:        name,
   770  			IP:          ip,
   771  			Mask:        netmask,
   772  			VLANTag:     vlanTag,
   773  			Description: description,
   774  		}
   775  	}
   776  	return networks, nil
   777  }
   778  
   779  // getNetworkMACs returns all MAC addresses connected to the given
   780  // network.
   781  func (environ *maasEnviron) getNetworkMACs(networkName string) ([]string, error) {
   782  	client := environ.getMAASClient().GetSubObject("networks").GetSubObject(networkName)
   783  	json, err := client.CallGet("list_connected_macs", nil)
   784  	if err != nil {
   785  		return nil, err
   786  	}
   787  	jsonMACs, err := json.GetArray()
   788  	if err != nil {
   789  		return nil, err
   790  	}
   791  
   792  	macs := make([]string, len(jsonMACs))
   793  	for i, jsonMAC := range jsonMACs {
   794  		fields, err := jsonMAC.GetMap()
   795  		if err != nil {
   796  			return nil, err
   797  		}
   798  		macAddress, err := fields["mac_address"].GetString()
   799  		if err != nil {
   800  			return nil, fmt.Errorf("cannot get mac_address: %v", err)
   801  		}
   802  		macs[i] = macAddress
   803  	}
   804  	return macs, nil
   805  }
   806  
   807  // getInstanceNetworkInterfaces returns a map of interface MAC address
   808  // to name for each network interface of the given instance, as
   809  // discovered during the commissioning phase.
   810  func (environ *maasEnviron) getInstanceNetworkInterfaces(inst instance.Instance) (map[string]string, error) {
   811  	maasInst := inst.(*maasInstance)
   812  	maasObj := maasInst.maasObject
   813  	result, err := maasObj.CallGet("details", nil)
   814  	if err != nil {
   815  		return nil, err
   816  	}
   817  	// Get the node's lldp / lshw details discovered at commissioning.
   818  	data, err := result.GetBytes()
   819  	if err != nil {
   820  		return nil, err
   821  	}
   822  	var parsed map[string]interface{}
   823  	if err := bson.Unmarshal(data, &parsed); err != nil {
   824  		return nil, err
   825  	}
   826  	lshwData, ok := parsed["lshw"]
   827  	if !ok {
   828  		return nil, fmt.Errorf("no hardware information available for node %q", inst.Id())
   829  	}
   830  	lshwXML, ok := lshwData.([]byte)
   831  	if !ok {
   832  		return nil, fmt.Errorf("invalid hardware information for node %q", inst.Id())
   833  	}
   834  	// Now we have the lshw XML data, parse it to extract and return NICs.
   835  	return extractInterfaces(inst, lshwXML)
   836  }
   837  
   838  // extractInterfaces parses the XML output of lswh and extracts all
   839  // network interfaces, returing a map MAC address to interface name.
   840  func extractInterfaces(inst instance.Instance, lshwXML []byte) (map[string]string, error) {
   841  	type Node struct {
   842  		Id          string `xml:"id,attr"`
   843  		Description string `xml:"description"`
   844  		Serial      string `xml:"serial"`
   845  		LogicalName string `xml:"logicalname"`
   846  		Children    []Node `xml:"node"`
   847  	}
   848  	type List struct {
   849  		Nodes []Node `xml:"node"`
   850  	}
   851  	var lshw List
   852  	if err := xml.Unmarshal(lshwXML, &lshw); err != nil {
   853  		return nil, fmt.Errorf("cannot parse lshw XML details for node %q: %v", inst.Id(), err)
   854  	}
   855  	interfaces := make(map[string]string)
   856  	var processNodes func(nodes []Node)
   857  	processNodes = func(nodes []Node) {
   858  		for _, node := range nodes {
   859  			if strings.HasPrefix(node.Id, "network") {
   860  				interfaces[node.Serial] = node.LogicalName
   861  			}
   862  			processNodes(node.Children)
   863  		}
   864  	}
   865  	processNodes(lshw.Nodes)
   866  	return interfaces, nil
   867  }