github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/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  	"bytes"
     8  	"encoding/base64"
     9  	"encoding/xml"
    10  	"fmt"
    11  	"net"
    12  	"net/http"
    13  	"net/url"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"text/template"
    18  	"time"
    19  
    20  	"github.com/juju/errors"
    21  	"github.com/juju/utils"
    22  	"github.com/juju/utils/set"
    23  	"gopkg.in/mgo.v2/bson"
    24  	"launchpad.net/gomaasapi"
    25  
    26  	"github.com/juju/juju/agent"
    27  	"github.com/juju/juju/cloudconfig/cloudinit"
    28  	"github.com/juju/juju/cloudconfig/instancecfg"
    29  	"github.com/juju/juju/cloudconfig/providerinit"
    30  	"github.com/juju/juju/constraints"
    31  	"github.com/juju/juju/environs"
    32  	"github.com/juju/juju/environs/config"
    33  	"github.com/juju/juju/environs/storage"
    34  	"github.com/juju/juju/instance"
    35  	"github.com/juju/juju/network"
    36  	"github.com/juju/juju/provider/common"
    37  	"github.com/juju/juju/state/multiwatcher"
    38  	"github.com/juju/juju/tools"
    39  	"github.com/juju/juju/version"
    40  	"github.com/juju/names"
    41  )
    42  
    43  const (
    44  	// We're using v1.0 of the MAAS API.
    45  	apiVersion = "1.0"
    46  )
    47  
    48  // A request may fail to due "eventual consistency" semantics, which
    49  // should resolve fairly quickly.  A request may also fail due to a slow
    50  // state transition (for instance an instance taking a while to release
    51  // a security group after termination).  The former failure mode is
    52  // dealt with by shortAttempt, the latter by LongAttempt.
    53  var shortAttempt = utils.AttemptStrategy{
    54  	Total: 5 * time.Second,
    55  	Delay: 200 * time.Millisecond,
    56  }
    57  
    58  var (
    59  	ReleaseNodes         = releaseNodes
    60  	ReserveIPAddress     = reserveIPAddress
    61  	ReleaseIPAddress     = releaseIPAddress
    62  	DeploymentStatusCall = deploymentStatusCall
    63  )
    64  
    65  func releaseNodes(nodes gomaasapi.MAASObject, ids url.Values) error {
    66  	_, err := nodes.CallPost("release", ids)
    67  	return err
    68  }
    69  
    70  func reserveIPAddress(ipaddresses gomaasapi.MAASObject, cidr string, addr network.Address) error {
    71  	params := url.Values{}
    72  	params.Add("network", cidr)
    73  	params.Add("requested_address", addr.Value)
    74  	_, err := ipaddresses.CallPost("reserve", params)
    75  	return err
    76  }
    77  
    78  func releaseIPAddress(ipaddresses gomaasapi.MAASObject, addr network.Address) error {
    79  	params := url.Values{}
    80  	params.Add("ip", addr.Value)
    81  	_, err := ipaddresses.CallPost("release", params)
    82  	return err
    83  }
    84  
    85  type maasEnviron struct {
    86  	common.SupportsUnitPlacementPolicy
    87  
    88  	name string
    89  
    90  	// archMutex gates access to supportedArchitectures
    91  	archMutex sync.Mutex
    92  	// supportedArchitectures caches the architectures
    93  	// for which images can be instantiated.
    94  	supportedArchitectures []string
    95  
    96  	// ecfgMutex protects the *Unlocked fields below.
    97  	ecfgMutex sync.Mutex
    98  
    99  	ecfgUnlocked       *maasEnvironConfig
   100  	maasClientUnlocked *gomaasapi.MAASObject
   101  	storageUnlocked    storage.Storage
   102  
   103  	availabilityZonesMutex sync.Mutex
   104  	availabilityZones      []common.AvailabilityZone
   105  }
   106  
   107  var _ environs.Environ = (*maasEnviron)(nil)
   108  
   109  func NewEnviron(cfg *config.Config) (*maasEnviron, error) {
   110  	env := new(maasEnviron)
   111  	err := env.SetConfig(cfg)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	env.name = cfg.Name()
   116  	env.storageUnlocked = NewStorage(env)
   117  	return env, nil
   118  }
   119  
   120  // Bootstrap is specified in the Environ interface.
   121  func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (arch, series string, _ environs.BootstrapFinalizer, _ error) {
   122  	if !environs.AddressAllocationEnabled() {
   123  		// When address allocation is not enabled, we should use the
   124  		// default bridge for both LXC and KVM containers. The bridge
   125  		// is created as part of the userdata for every node during
   126  		// StartInstance.
   127  		logger.Infof(
   128  			"address allocation feature disabled; using %q bridge for all containers",
   129  			instancecfg.DefaultBridgeName,
   130  		)
   131  		args.ContainerBridgeName = instancecfg.DefaultBridgeName
   132  	} else {
   133  		logger.Debugf(
   134  			"address allocation feature enabled; using static IPs for containers: %q",
   135  			instancecfg.DefaultBridgeName,
   136  		)
   137  	}
   138  
   139  	result, series, finalizer, err := common.BootstrapInstance(ctx, env, args)
   140  	if err != nil {
   141  		return "", "", nil, err
   142  	}
   143  
   144  	// We want to destroy the started instance if it doesn't transition to Deployed.
   145  	defer func() {
   146  		if err != nil {
   147  			if err := env.StopInstances(result.Instance.Id()); err != nil {
   148  				logger.Errorf("error releasing bootstrap instance: %v", err)
   149  			}
   150  		}
   151  	}()
   152  	// Wait for bootstrap instance to change to deployed state.
   153  	if err := env.waitForNodeDeployment(result.Instance.Id()); err != nil {
   154  		return "", "", nil, errors.Annotate(err, "bootstrap instance started but did not change to Deployed state")
   155  	}
   156  	return *result.Hardware.Arch, series, finalizer, nil
   157  }
   158  
   159  // StateServerInstances is specified in the Environ interface.
   160  func (env *maasEnviron) StateServerInstances() ([]instance.Id, error) {
   161  	return common.ProviderStateInstances(env, env.Storage())
   162  }
   163  
   164  // ecfg returns the environment's maasEnvironConfig, and protects it with a
   165  // mutex.
   166  func (env *maasEnviron) ecfg() *maasEnvironConfig {
   167  	env.ecfgMutex.Lock()
   168  	defer env.ecfgMutex.Unlock()
   169  	return env.ecfgUnlocked
   170  }
   171  
   172  // Config is specified in the Environ interface.
   173  func (env *maasEnviron) Config() *config.Config {
   174  	return env.ecfg().Config
   175  }
   176  
   177  // SetConfig is specified in the Environ interface.
   178  func (env *maasEnviron) SetConfig(cfg *config.Config) error {
   179  	env.ecfgMutex.Lock()
   180  	defer env.ecfgMutex.Unlock()
   181  
   182  	// The new config has already been validated by itself, but now we
   183  	// validate the transition from the old config to the new.
   184  	var oldCfg *config.Config
   185  	if env.ecfgUnlocked != nil {
   186  		oldCfg = env.ecfgUnlocked.Config
   187  	}
   188  	cfg, err := env.Provider().Validate(cfg, oldCfg)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	ecfg, err := providerInstance.newConfig(cfg)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	env.ecfgUnlocked = ecfg
   199  
   200  	authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.maasServer(), ecfg.maasOAuth(), apiVersion)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient)
   205  
   206  	return nil
   207  }
   208  
   209  // SupportedArchitectures is specified on the EnvironCapability interface.
   210  func (env *maasEnviron) SupportedArchitectures() ([]string, error) {
   211  	env.archMutex.Lock()
   212  	defer env.archMutex.Unlock()
   213  	if env.supportedArchitectures != nil {
   214  		return env.supportedArchitectures, nil
   215  	}
   216  	bootImages, err := env.allBootImages()
   217  	if err != nil || len(bootImages) == 0 {
   218  		logger.Debugf("error querying boot-images: %v", err)
   219  		logger.Debugf("falling back to listing nodes")
   220  		supportedArchitectures, err := env.nodeArchitectures()
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		env.supportedArchitectures = supportedArchitectures
   225  	} else {
   226  		architectures := make(set.Strings)
   227  		for _, image := range bootImages {
   228  			architectures.Add(image.architecture)
   229  		}
   230  		env.supportedArchitectures = architectures.SortedValues()
   231  	}
   232  	return env.supportedArchitectures, nil
   233  }
   234  
   235  // SupportsAddressAllocation is specified on environs.Networking.
   236  func (env *maasEnviron) SupportsAddressAllocation(_ network.Id) (bool, error) {
   237  	if !environs.AddressAllocationEnabled() {
   238  		return false, errors.NotSupportedf("address allocation")
   239  	}
   240  
   241  	caps, err := env.getCapabilities()
   242  	if err != nil {
   243  		return false, errors.Annotatef(err, "getCapabilities failed")
   244  	}
   245  	return caps.Contains(capStaticIPAddresses), nil
   246  }
   247  
   248  // allBootImages queries MAAS for all of the boot-images across
   249  // all registered nodegroups.
   250  func (env *maasEnviron) allBootImages() ([]bootImage, error) {
   251  	nodegroups, err := env.getNodegroups()
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	var allBootImages []bootImage
   256  	seen := make(set.Strings)
   257  	for _, nodegroup := range nodegroups {
   258  		bootImages, err := env.nodegroupBootImages(nodegroup)
   259  		if err != nil {
   260  			return nil, errors.Annotatef(err, "cannot get boot images for nodegroup %v", nodegroup)
   261  		}
   262  		for _, image := range bootImages {
   263  			str := fmt.Sprint(image)
   264  			if seen.Contains(str) {
   265  				continue
   266  			}
   267  			seen.Add(str)
   268  			allBootImages = append(allBootImages, image)
   269  		}
   270  	}
   271  	return allBootImages, nil
   272  }
   273  
   274  // getNodegroups returns the UUID corresponding to each nodegroup
   275  // in the MAAS installation.
   276  func (env *maasEnviron) getNodegroups() ([]string, error) {
   277  	nodegroupsListing := env.getMAASClient().GetSubObject("nodegroups")
   278  	nodegroupsResult, err := nodegroupsListing.CallGet("list", nil)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	list, err := nodegroupsResult.GetArray()
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	nodegroups := make([]string, len(list))
   287  	for i, obj := range list {
   288  		nodegroup, err := obj.GetMap()
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  		uuid, err := nodegroup["uuid"].GetString()
   293  		if err != nil {
   294  			return nil, err
   295  		}
   296  		nodegroups[i] = uuid
   297  	}
   298  	return nodegroups, nil
   299  }
   300  
   301  func (env *maasEnviron) getNodegroupInterfaces(nodegroups []string) map[string][]net.IP {
   302  	nodegroupsObject := env.getMAASClient().GetSubObject("nodegroups")
   303  
   304  	nodegroupsInterfacesMap := make(map[string][]net.IP)
   305  	for _, uuid := range nodegroups {
   306  		interfacesObject := nodegroupsObject.GetSubObject(uuid).GetSubObject("interfaces")
   307  		interfacesResult, err := interfacesObject.CallGet("list", nil)
   308  		if err != nil {
   309  			logger.Debugf("cannot list interfaces for nodegroup %v: %v", uuid, err)
   310  			continue
   311  		}
   312  		interfaces, err := interfacesResult.GetArray()
   313  		if err != nil {
   314  			logger.Debugf("cannot get interfaces for nodegroup %v: %v", uuid, err)
   315  			continue
   316  		}
   317  		for _, interfaceResult := range interfaces {
   318  			nic, err := interfaceResult.GetMap()
   319  			if err != nil {
   320  				logger.Debugf("cannot get interface %v for nodegroup %v: %v", nic, uuid, err)
   321  				continue
   322  			}
   323  			ip, err := nic["ip"].GetString()
   324  			if err != nil {
   325  				logger.Debugf("cannot get interface IP %v for nodegroup %v: %v", nic, uuid, err)
   326  				continue
   327  			}
   328  			static_low, err := nic["static_ip_range_low"].GetString()
   329  			if err != nil {
   330  				logger.Debugf("cannot get static IP range lower bound for interface %v on nodegroup %v: %v", nic, uuid, err)
   331  				continue
   332  			}
   333  			static_high, err := nic["static_ip_range_high"].GetString()
   334  			if err != nil {
   335  				logger.Infof("cannot get static IP range higher bound for interface %v on nodegroup %v: %v", nic, uuid, err)
   336  				continue
   337  			}
   338  			static_low_ip := net.ParseIP(static_low)
   339  			static_high_ip := net.ParseIP(static_high)
   340  			if static_low_ip == nil || static_high_ip == nil {
   341  				logger.Debugf("invalid IP in static range for interface %v on nodegroup %v: %q %q", nic, uuid, static_low_ip, static_high_ip)
   342  				continue
   343  			}
   344  			nodegroupsInterfacesMap[ip] = []net.IP{static_low_ip, static_high_ip}
   345  		}
   346  	}
   347  	return nodegroupsInterfacesMap
   348  }
   349  
   350  type bootImage struct {
   351  	architecture string
   352  	release      string
   353  }
   354  
   355  // nodegroupBootImages returns the set of boot-images for the specified nodegroup.
   356  func (env *maasEnviron) nodegroupBootImages(nodegroupUUID string) ([]bootImage, error) {
   357  	nodegroupObject := env.getMAASClient().GetSubObject("nodegroups").GetSubObject(nodegroupUUID)
   358  	bootImagesObject := nodegroupObject.GetSubObject("boot-images/")
   359  	result, err := bootImagesObject.CallGet("", nil)
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	list, err := result.GetArray()
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  	var bootImages []bootImage
   368  	for _, obj := range list {
   369  		bootimage, err := obj.GetMap()
   370  		if err != nil {
   371  			return nil, err
   372  		}
   373  		arch, err := bootimage["architecture"].GetString()
   374  		if err != nil {
   375  			return nil, err
   376  		}
   377  		release, err := bootimage["release"].GetString()
   378  		if err != nil {
   379  			return nil, err
   380  		}
   381  		bootImages = append(bootImages, bootImage{
   382  			architecture: arch,
   383  			release:      release,
   384  		})
   385  	}
   386  	return bootImages, nil
   387  }
   388  
   389  // nodeArchitectures returns the architectures of all
   390  // available nodes in the system.
   391  //
   392  // Note: this should only be used if we cannot query
   393  // boot-images.
   394  func (env *maasEnviron) nodeArchitectures() ([]string, error) {
   395  	filter := make(url.Values)
   396  	filter.Add("status", gomaasapi.NodeStatusDeclared)
   397  	filter.Add("status", gomaasapi.NodeStatusCommissioning)
   398  	filter.Add("status", gomaasapi.NodeStatusReady)
   399  	filter.Add("status", gomaasapi.NodeStatusReserved)
   400  	filter.Add("status", gomaasapi.NodeStatusAllocated)
   401  	allInstances, err := env.instances(filter)
   402  	if err != nil {
   403  		return nil, err
   404  	}
   405  	architectures := make(set.Strings)
   406  	for _, inst := range allInstances {
   407  		inst := inst.(*maasInstance)
   408  		arch, _, err := inst.architecture()
   409  		if err != nil {
   410  			return nil, err
   411  		}
   412  		architectures.Add(arch)
   413  	}
   414  	// TODO(dfc) why is this sorted
   415  	return architectures.SortedValues(), nil
   416  }
   417  
   418  type maasAvailabilityZone struct {
   419  	name string
   420  }
   421  
   422  func (z maasAvailabilityZone) Name() string {
   423  	return z.name
   424  }
   425  
   426  func (z maasAvailabilityZone) Available() bool {
   427  	// MAAS' physical zone attributes only include name and description;
   428  	// there is no concept of availability.
   429  	return true
   430  }
   431  
   432  // AvailabilityZones returns a slice of availability zones
   433  // for the configured region.
   434  func (e *maasEnviron) AvailabilityZones() ([]common.AvailabilityZone, error) {
   435  	e.availabilityZonesMutex.Lock()
   436  	defer e.availabilityZonesMutex.Unlock()
   437  	if e.availabilityZones == nil {
   438  		zonesObject := e.getMAASClient().GetSubObject("zones")
   439  		result, err := zonesObject.CallGet("", nil)
   440  		if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == http.StatusNotFound {
   441  			return nil, errors.NewNotImplemented(nil, "the MAAS server does not support zones")
   442  		}
   443  		if err != nil {
   444  			return nil, errors.Annotate(err, "cannot query ")
   445  		}
   446  		list, err := result.GetArray()
   447  		if err != nil {
   448  			return nil, err
   449  		}
   450  		logger.Debugf("availability zones: %+v", list)
   451  		availabilityZones := make([]common.AvailabilityZone, len(list))
   452  		for i, obj := range list {
   453  			zone, err := obj.GetMap()
   454  			if err != nil {
   455  				return nil, err
   456  			}
   457  			name, err := zone["name"].GetString()
   458  			if err != nil {
   459  				return nil, err
   460  			}
   461  			availabilityZones[i] = maasAvailabilityZone{name}
   462  		}
   463  		e.availabilityZones = availabilityZones
   464  	}
   465  	return e.availabilityZones, nil
   466  }
   467  
   468  // InstanceAvailabilityZoneNames returns the availability zone names for each
   469  // of the specified instances.
   470  func (e *maasEnviron) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) {
   471  	instances, err := e.Instances(ids)
   472  	if err != nil && err != environs.ErrPartialInstances {
   473  		return nil, err
   474  	}
   475  	zones := make([]string, len(instances))
   476  	for i, inst := range instances {
   477  		if inst == nil {
   478  			continue
   479  		}
   480  		zones[i] = inst.(*maasInstance).zone()
   481  	}
   482  	return zones, nil
   483  }
   484  
   485  type maasPlacement struct {
   486  	nodeName string
   487  	zoneName string
   488  }
   489  
   490  func (e *maasEnviron) parsePlacement(placement string) (*maasPlacement, error) {
   491  	pos := strings.IndexRune(placement, '=')
   492  	if pos == -1 {
   493  		// If there's no '=' delimiter, assume it's a node name.
   494  		return &maasPlacement{nodeName: placement}, nil
   495  	}
   496  	switch key, value := placement[:pos], placement[pos+1:]; key {
   497  	case "zone":
   498  		availabilityZone := value
   499  		zones, err := e.AvailabilityZones()
   500  		if err != nil {
   501  			return nil, err
   502  		}
   503  		for _, z := range zones {
   504  			if z.Name() == availabilityZone {
   505  				return &maasPlacement{zoneName: availabilityZone}, nil
   506  			}
   507  		}
   508  		return nil, errors.Errorf("invalid availability zone %q", availabilityZone)
   509  	}
   510  	return nil, errors.Errorf("unknown placement directive: %v", placement)
   511  }
   512  
   513  func (env *maasEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error {
   514  	if placement == "" {
   515  		return nil
   516  	}
   517  	_, err := env.parsePlacement(placement)
   518  	return err
   519  }
   520  
   521  const (
   522  	capNetworksManagement = "networks-management"
   523  	capStaticIPAddresses  = "static-ipaddresses"
   524  )
   525  
   526  // getCapabilities asks the MAAS server for its capabilities, if
   527  // supported by the server.
   528  func (env *maasEnviron) getCapabilities() (set.Strings, error) {
   529  	caps := make(set.Strings)
   530  	var result gomaasapi.JSONObject
   531  	var err error
   532  
   533  	for a := shortAttempt.Start(); a.Next(); {
   534  		client := env.getMAASClient().GetSubObject("version/")
   535  		result, err = client.CallGet("", nil)
   536  		if err != nil {
   537  			if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == 404 {
   538  				return caps, fmt.Errorf("MAAS does not support version info")
   539  			}
   540  		} else {
   541  			break
   542  		}
   543  	}
   544  	if err != nil {
   545  		return caps, err
   546  	}
   547  	info, err := result.GetMap()
   548  	if err != nil {
   549  		return caps, err
   550  	}
   551  	capsObj, ok := info["capabilities"]
   552  	if !ok {
   553  		return caps, fmt.Errorf("MAAS does not report capabilities")
   554  	}
   555  	items, err := capsObj.GetArray()
   556  	if err != nil {
   557  		return caps, err
   558  	}
   559  	for _, item := range items {
   560  		val, err := item.GetString()
   561  		if err != nil {
   562  			return set.NewStrings(), err
   563  		}
   564  		caps.Add(val)
   565  	}
   566  	return caps, nil
   567  }
   568  
   569  // getMAASClient returns a MAAS client object to use for a request, in a
   570  // lock-protected fashion.
   571  func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject {
   572  	env.ecfgMutex.Lock()
   573  	defer env.ecfgMutex.Unlock()
   574  
   575  	return env.maasClientUnlocked
   576  }
   577  
   578  // convertConstraints converts the given constraints into an url.Values
   579  // object suitable to pass to MAAS when acquiring a node.
   580  // CpuPower is ignored because it cannot translated into something
   581  // meaningful for MAAS right now.
   582  func convertConstraints(cons constraints.Value) url.Values {
   583  	params := url.Values{}
   584  	if cons.Arch != nil {
   585  		// Note: Juju and MAAS use the same architecture names.
   586  		// MAAS also accepts a subarchitecture (e.g. "highbank"
   587  		// for ARM), which defaults to "generic" if unspecified.
   588  		params.Add("arch", *cons.Arch)
   589  	}
   590  	if cons.CpuCores != nil {
   591  		params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores))
   592  	}
   593  	if cons.Mem != nil {
   594  		params.Add("mem", fmt.Sprintf("%d", *cons.Mem))
   595  	}
   596  	if cons.Tags != nil && len(*cons.Tags) > 0 {
   597  		tags, notTags := parseTags(*cons.Tags)
   598  		if len(tags) > 0 {
   599  			params.Add("tags", strings.Join(tags, ","))
   600  		}
   601  		if len(notTags) > 0 {
   602  			params.Add("not_tags", strings.Join(notTags, ","))
   603  		}
   604  	}
   605  	if cons.CpuPower != nil {
   606  		logger.Warningf("ignoring unsupported constraint 'cpu-power'")
   607  	}
   608  	return params
   609  }
   610  
   611  // parseTags parses a tags constraints, splitting it into a positive
   612  // and negative tags to pass to MAAS. Positive tags have no prefix,
   613  // negative tags have a "^" prefix. All spaces inside the rawTags are
   614  // stripped before parsing.
   615  func parseTags(rawTags []string) (tags, notTags []string) {
   616  	for _, tag := range rawTags {
   617  		tag = strings.Replace(tag, " ", "", -1)
   618  		if len(tag) == 0 {
   619  			continue
   620  		}
   621  		if strings.HasPrefix(tag, "^") {
   622  			notTags = append(notTags, strings.TrimPrefix(tag, "^"))
   623  		} else {
   624  			tags = append(tags, tag)
   625  		}
   626  	}
   627  	return tags, notTags
   628  }
   629  
   630  // addNetworks converts networks include/exclude information into
   631  // url.Values object suitable to pass to MAAS when acquiring a node.
   632  func addNetworks(params url.Values, includeNetworks, excludeNetworks []string) {
   633  	// Network Inclusion/Exclusion setup
   634  	if len(includeNetworks) > 0 {
   635  		for _, name := range includeNetworks {
   636  			params.Add("networks", name)
   637  		}
   638  	}
   639  	if len(excludeNetworks) > 0 {
   640  		for _, name := range excludeNetworks {
   641  			params.Add("not_networks", name)
   642  		}
   643  	}
   644  }
   645  
   646  // addVolumes converts volume information into
   647  // url.Values object suitable to pass to MAAS when acquiring a node.
   648  func addVolumes(params url.Values, volumes []volumeInfo) {
   649  	if len(volumes) == 0 {
   650  		return
   651  	}
   652  	// Requests for specific values are passed to the acquire URL
   653  	// as a storage URL parameter of the form:
   654  	// [volume-name:]sizeinGB[tag,...]
   655  	// See http://maas.ubuntu.com/docs/api.html#nodes
   656  
   657  	// eg storage=root:0(ssd),data:20(magnetic,5400rpm),45
   658  	makeVolumeParams := func(v volumeInfo) string {
   659  		var params string
   660  		if v.name != "" {
   661  			params = v.name + ":"
   662  		}
   663  		params += fmt.Sprintf("%d", v.sizeInGB)
   664  		if len(v.tags) > 0 {
   665  			params += fmt.Sprintf("(%s)", strings.Join(v.tags, ","))
   666  		}
   667  		return params
   668  	}
   669  	var volParms []string
   670  	for _, v := range volumes {
   671  		params := makeVolumeParams(v)
   672  		volParms = append(volParms, params)
   673  	}
   674  	params.Add("storage", strings.Join(volParms, ","))
   675  }
   676  
   677  // acquireNode allocates a node from the MAAS.
   678  func (environ *maasEnviron) acquireNode(
   679  	nodeName, zoneName string, cons constraints.Value, includeNetworks, excludeNetworks []string, volumes []volumeInfo,
   680  ) (gomaasapi.MAASObject, error) {
   681  
   682  	acquireParams := convertConstraints(cons)
   683  	addNetworks(acquireParams, includeNetworks, excludeNetworks)
   684  	addVolumes(acquireParams, volumes)
   685  	acquireParams.Add("agent_name", environ.ecfg().maasAgentName())
   686  	if zoneName != "" {
   687  		acquireParams.Add("zone", zoneName)
   688  	}
   689  	if nodeName != "" {
   690  		acquireParams.Add("name", nodeName)
   691  	} else if cons.Arch == nil {
   692  		// TODO(axw) 2014-08-18 #1358219
   693  		// We should be requesting preferred
   694  		// architectures if unspecified, like
   695  		// in the other providers.
   696  		//
   697  		// This is slightly complicated in MAAS
   698  		// as there are a finite number of each
   699  		// architecture; preference may also
   700  		// conflict with other constraints, such
   701  		// as tags. Thus, a preference becomes a
   702  		// demand (which may fail) if not handled
   703  		// properly.
   704  		logger.Warningf(
   705  			"no architecture was specified, acquiring an arbitrary node",
   706  		)
   707  	}
   708  
   709  	var result gomaasapi.JSONObject
   710  	var err error
   711  	for a := shortAttempt.Start(); a.Next(); {
   712  		client := environ.getMAASClient().GetSubObject("nodes/")
   713  		result, err = client.CallPost("acquire", acquireParams)
   714  		if err == nil {
   715  			break
   716  		}
   717  	}
   718  	if err != nil {
   719  		return gomaasapi.MAASObject{}, err
   720  	}
   721  	node, err := result.GetMAASObject()
   722  	if err != nil {
   723  		err := errors.Annotate(err, "unexpected result from 'acquire' on MAAS API")
   724  		return gomaasapi.MAASObject{}, err
   725  	}
   726  	return node, nil
   727  }
   728  
   729  // startNode installs and boots a node.
   730  func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) error {
   731  	userDataParam := base64.StdEncoding.EncodeToString(userdata)
   732  	params := url.Values{
   733  		"distro_series": {series},
   734  		"user_data":     {userDataParam},
   735  	}
   736  	// Initialize err to a non-nil value as a sentinel for the following
   737  	// loop.
   738  	err := fmt.Errorf("(no error)")
   739  	for a := shortAttempt.Start(); a.Next() && err != nil; {
   740  		_, err = node.CallPost("start", params)
   741  	}
   742  	return err
   743  }
   744  
   745  var unsupportedConstraints = []string{
   746  	constraints.CpuPower,
   747  	constraints.InstanceType,
   748  }
   749  
   750  // ConstraintsValidator is defined on the Environs interface.
   751  func (environ *maasEnviron) ConstraintsValidator() (constraints.Validator, error) {
   752  	validator := constraints.NewValidator()
   753  	validator.RegisterUnsupported(unsupportedConstraints)
   754  	supportedArches, err := environ.SupportedArchitectures()
   755  	if err != nil {
   756  		return nil, err
   757  	}
   758  	validator.RegisterVocabulary(constraints.Arch, supportedArches)
   759  	return validator, nil
   760  }
   761  
   762  // setupNetworks prepares a []network.InterfaceInfo for the given
   763  // instance. Any networks in networksToDisable will be configured as
   764  // disabled on the machine. Any disabled network interfaces (as
   765  // discovered from the lshw output for the node) will stay disabled.
   766  // The interface name discovered as primary is also returned.
   767  func (environ *maasEnviron) setupNetworks(inst instance.Instance, networksToDisable set.Strings) ([]network.InterfaceInfo, string, error) {
   768  	// Get the instance network interfaces first.
   769  	interfaces, primaryIface, err := environ.getInstanceNetworkInterfaces(inst)
   770  	if err != nil {
   771  		return nil, "", errors.Annotatef(err, "getInstanceNetworkInterfaces failed")
   772  	}
   773  	logger.Debugf("node %q has network interfaces %v", inst.Id(), interfaces)
   774  	networks, err := environ.getInstanceNetworks(inst)
   775  	if err != nil {
   776  		return nil, "", errors.Annotatef(err, "getInstanceNetworks failed")
   777  	}
   778  	logger.Debugf("node %q has networks %v", inst.Id(), networks)
   779  	var tempInterfaceInfo []network.InterfaceInfo
   780  	for _, netw := range networks {
   781  		disabled := networksToDisable.Contains(netw.Name)
   782  		netCIDR := &net.IPNet{
   783  			IP:   net.ParseIP(netw.IP),
   784  			Mask: net.IPMask(net.ParseIP(netw.Mask)),
   785  		}
   786  		macs, err := environ.getNetworkMACs(netw.Name)
   787  		if err != nil {
   788  			return nil, "", errors.Annotatef(err, "getNetworkMACs failed")
   789  		}
   790  		logger.Debugf("network %q has MACs: %v", netw.Name, macs)
   791  		for _, mac := range macs {
   792  			if ifinfo, ok := interfaces[mac]; ok {
   793  				tempInterfaceInfo = append(tempInterfaceInfo, network.InterfaceInfo{
   794  					MACAddress:    mac,
   795  					InterfaceName: ifinfo.InterfaceName,
   796  					DeviceIndex:   ifinfo.DeviceIndex,
   797  					CIDR:          netCIDR.String(),
   798  					VLANTag:       netw.VLANTag,
   799  					ProviderId:    network.Id(netw.Name),
   800  					NetworkName:   netw.Name,
   801  					Disabled:      disabled || ifinfo.Disabled,
   802  				})
   803  			}
   804  		}
   805  	}
   806  	// Verify we filled-in everything for all networks/interfaces
   807  	// and drop incomplete records.
   808  	var interfaceInfo []network.InterfaceInfo
   809  	for _, info := range tempInterfaceInfo {
   810  		if info.ProviderId == "" || info.NetworkName == "" || info.CIDR == "" {
   811  			logger.Infof("ignoring interface %q: missing subnet info", info.InterfaceName)
   812  			continue
   813  		}
   814  		if info.MACAddress == "" || info.InterfaceName == "" {
   815  			logger.Infof("ignoring subnet %q: missing interface info", info.ProviderId)
   816  			continue
   817  		}
   818  		interfaceInfo = append(interfaceInfo, info)
   819  	}
   820  	logger.Debugf("node %q network information: %#v", inst.Id(), interfaceInfo)
   821  	return interfaceInfo, primaryIface, nil
   822  }
   823  
   824  // DistributeInstances implements the state.InstanceDistributor policy.
   825  func (e *maasEnviron) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) {
   826  	return common.DistributeInstances(e, candidates, distributionGroup)
   827  }
   828  
   829  var availabilityZoneAllocations = common.AvailabilityZoneAllocations
   830  
   831  // MaintainInstance is specified in the InstanceBroker interface.
   832  func (*maasEnviron) MaintainInstance(args environs.StartInstanceParams) error {
   833  	return nil
   834  }
   835  
   836  // StartInstance is specified in the InstanceBroker interface.
   837  func (environ *maasEnviron) StartInstance(args environs.StartInstanceParams) (
   838  	*environs.StartInstanceResult, error,
   839  ) {
   840  	var availabilityZones []string
   841  	var nodeName string
   842  	if args.Placement != "" {
   843  		placement, err := environ.parsePlacement(args.Placement)
   844  		if err != nil {
   845  			return nil, err
   846  		}
   847  		switch {
   848  		case placement.zoneName != "":
   849  			availabilityZones = append(availabilityZones, placement.zoneName)
   850  		default:
   851  			nodeName = placement.nodeName
   852  		}
   853  	}
   854  
   855  	// If no placement is specified, then automatically spread across
   856  	// the known zones for optimal spread across the instance distribution
   857  	// group.
   858  	if args.Placement == "" {
   859  		var group []instance.Id
   860  		var err error
   861  		if args.DistributionGroup != nil {
   862  			group, err = args.DistributionGroup()
   863  			if err != nil {
   864  				return nil, errors.Annotate(err, "cannot get distribution group")
   865  			}
   866  		}
   867  		zoneInstances, err := availabilityZoneAllocations(environ, group)
   868  		if errors.IsNotImplemented(err) {
   869  			// Availability zones are an extension, so we may get a
   870  			// not implemented error; ignore these.
   871  		} else if err != nil {
   872  			return nil, errors.Annotate(err, "cannot get availability zone allocations")
   873  		} else if len(zoneInstances) > 0 {
   874  			for _, z := range zoneInstances {
   875  				availabilityZones = append(availabilityZones, z.ZoneName)
   876  			}
   877  		}
   878  	}
   879  	if len(availabilityZones) == 0 {
   880  		availabilityZones = []string{""}
   881  	}
   882  
   883  	// Networking.
   884  	requestedNetworks := args.InstanceConfig.Networks
   885  	includeNetworks := append(args.Constraints.IncludeNetworks(), requestedNetworks...)
   886  	excludeNetworks := args.Constraints.ExcludeNetworks()
   887  
   888  	// Storage.
   889  	volumes, err := buildMAASVolumeParameters(args.Volumes, args.Constraints)
   890  	if err != nil {
   891  		return nil, errors.Annotate(err, "invalid volume parameters")
   892  	}
   893  
   894  	snArgs := selectNodeArgs{
   895  		Constraints:       args.Constraints,
   896  		AvailabilityZones: availabilityZones,
   897  		NodeName:          nodeName,
   898  		IncludeNetworks:   includeNetworks,
   899  		ExcludeNetworks:   excludeNetworks,
   900  		Volumes:           volumes,
   901  	}
   902  	node, err := environ.selectNode(snArgs)
   903  	if err != nil {
   904  		return nil, errors.Errorf("cannot run instances: %v", err)
   905  	}
   906  
   907  	inst := &maasInstance{maasObject: node, environ: environ}
   908  	defer func() {
   909  		if err != nil {
   910  			if err := environ.StopInstances(inst.Id()); err != nil {
   911  				logger.Errorf("error releasing failed instance: %v", err)
   912  			}
   913  		}
   914  	}()
   915  
   916  	hc, err := inst.hardwareCharacteristics()
   917  	if err != nil {
   918  		return nil, err
   919  	}
   920  
   921  	selectedTools, err := args.Tools.Match(tools.Filter{
   922  		Arch: *hc.Arch,
   923  	})
   924  	if err != nil {
   925  		return nil, err
   926  	}
   927  	args.InstanceConfig.Tools = selectedTools[0]
   928  
   929  	var networkInfo []network.InterfaceInfo
   930  	networkInfo, primaryIface, err := environ.setupNetworks(inst, set.NewStrings(excludeNetworks...))
   931  	if err != nil {
   932  		return nil, err
   933  	}
   934  
   935  	hostname, err := inst.hostname()
   936  	if err != nil {
   937  		return nil, err
   938  	}
   939  	// Override the network bridge to use for both LXC and KVM
   940  	// containers on the new instance, if address allocation feature
   941  	// flag is not enabled.
   942  	if !environs.AddressAllocationEnabled() {
   943  		if args.InstanceConfig.AgentEnvironment == nil {
   944  			args.InstanceConfig.AgentEnvironment = make(map[string]string)
   945  		}
   946  		args.InstanceConfig.AgentEnvironment[agent.LxcBridge] = instancecfg.DefaultBridgeName
   947  	}
   948  	if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, environ.Config()); err != nil {
   949  		return nil, err
   950  	}
   951  	series := args.InstanceConfig.Tools.Version.Series
   952  
   953  	cloudcfg, err := environ.newCloudinitConfig(hostname, primaryIface, series)
   954  	if err != nil {
   955  		return nil, err
   956  	}
   957  	userdata, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg)
   958  	if err != nil {
   959  		msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err)
   960  		return nil, msg
   961  	}
   962  	logger.Debugf("maas user data; %d bytes", len(userdata))
   963  
   964  	if err := environ.startNode(*inst.maasObject, series, userdata); err != nil {
   965  		return nil, err
   966  	}
   967  	logger.Debugf("started instance %q", inst.Id())
   968  
   969  	if multiwatcher.AnyJobNeedsState(args.InstanceConfig.Jobs...) {
   970  		if err := common.AddStateInstance(environ.Storage(), inst.Id()); err != nil {
   971  			logger.Errorf("could not record instance in provider-state: %v", err)
   972  		}
   973  	}
   974  
   975  	requestedVolumes := make([]names.VolumeTag, len(args.Volumes))
   976  	for i, v := range args.Volumes {
   977  		requestedVolumes[i] = v.Tag
   978  	}
   979  	resultVolumes, resultAttachments, err := inst.volumes(
   980  		names.NewMachineTag(args.InstanceConfig.MachineId),
   981  		requestedVolumes,
   982  	)
   983  	if err != nil {
   984  		return nil, err
   985  	}
   986  	if len(resultVolumes) != len(requestedVolumes) {
   987  		err = errors.New("the version of MAAS being used does not support Juju storage")
   988  		return nil, err
   989  	}
   990  
   991  	return &environs.StartInstanceResult{
   992  		Instance:          inst,
   993  		Hardware:          hc,
   994  		NetworkInfo:       networkInfo,
   995  		Volumes:           resultVolumes,
   996  		VolumeAttachments: resultAttachments,
   997  	}, nil
   998  }
   999  
  1000  // Override for testing.
  1001  var nodeDeploymentTimeout = func(environ *maasEnviron) time.Duration {
  1002  	sshTimeouts := environ.Config().BootstrapSSHOpts()
  1003  	return sshTimeouts.Timeout
  1004  }
  1005  
  1006  func (environ *maasEnviron) waitForNodeDeployment(id instance.Id) error {
  1007  	systemId := extractSystemId(id)
  1008  	longAttempt := utils.AttemptStrategy{
  1009  		Delay: 10 * time.Second,
  1010  		Total: nodeDeploymentTimeout(environ),
  1011  	}
  1012  
  1013  	for a := longAttempt.Start(); a.Next(); {
  1014  		statusValues, err := environ.deploymentStatus(id)
  1015  		if errors.IsNotImplemented(err) {
  1016  			return nil
  1017  		}
  1018  		if err != nil {
  1019  			return errors.Trace(err)
  1020  		}
  1021  		if statusValues[systemId] == "Deployed" {
  1022  			return nil
  1023  		}
  1024  		if statusValues[systemId] == "Failed deployment" {
  1025  			return errors.Errorf("instance %q failed to deploy", id)
  1026  		}
  1027  	}
  1028  	return errors.Errorf("instance %q is started but not deployed", id)
  1029  }
  1030  
  1031  // deploymentStatus returns the deployment state of MAAS instances with
  1032  // the specified Juju instance ids.
  1033  // Note: the result is a map of MAAS systemId to state.
  1034  func (environ *maasEnviron) deploymentStatus(ids ...instance.Id) (map[string]string, error) {
  1035  	nodesAPI := environ.getMAASClient().GetSubObject("nodes")
  1036  	result, err := DeploymentStatusCall(nodesAPI, ids...)
  1037  	if err != nil {
  1038  		if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == http.StatusBadRequest {
  1039  			return nil, errors.NewNotImplemented(err, "deployment status")
  1040  		}
  1041  		return nil, errors.Trace(err)
  1042  	}
  1043  	resultMap, err := result.GetMap()
  1044  	if err != nil {
  1045  		return nil, errors.Trace(err)
  1046  	}
  1047  	statusValues := make(map[string]string)
  1048  	for systemId, jsonValue := range resultMap {
  1049  		status, err := jsonValue.GetString()
  1050  		if err != nil {
  1051  			return nil, errors.Trace(err)
  1052  		}
  1053  		statusValues[systemId] = status
  1054  	}
  1055  	return statusValues, nil
  1056  }
  1057  
  1058  func deploymentStatusCall(nodes gomaasapi.MAASObject, ids ...instance.Id) (gomaasapi.JSONObject, error) {
  1059  	filter := getSystemIdValues("nodes", ids)
  1060  	return nodes.CallGet("deployment_status", filter)
  1061  }
  1062  
  1063  type selectNodeArgs struct {
  1064  	AvailabilityZones []string
  1065  	NodeName          string
  1066  	Constraints       constraints.Value
  1067  	IncludeNetworks   []string
  1068  	ExcludeNetworks   []string
  1069  	Volumes           []volumeInfo
  1070  }
  1071  
  1072  func (environ *maasEnviron) selectNode(args selectNodeArgs) (*gomaasapi.MAASObject, error) {
  1073  	var err error
  1074  	var node gomaasapi.MAASObject
  1075  
  1076  	for i, zoneName := range args.AvailabilityZones {
  1077  		node, err = environ.acquireNode(
  1078  			args.NodeName,
  1079  			zoneName,
  1080  			args.Constraints,
  1081  			args.IncludeNetworks,
  1082  			args.ExcludeNetworks,
  1083  			args.Volumes,
  1084  		)
  1085  
  1086  		if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == http.StatusConflict {
  1087  			if i+1 < len(args.AvailabilityZones) {
  1088  				logger.Infof("could not acquire a node in zone %q, trying another zone", zoneName)
  1089  				continue
  1090  			}
  1091  		}
  1092  		if err != nil {
  1093  			return nil, errors.Errorf("cannot run instances: %v", err)
  1094  		}
  1095  		// Since a return at the end of the function is required
  1096  		// just break here.
  1097  		break
  1098  	}
  1099  	return &node, nil
  1100  }
  1101  
  1102  const bridgeConfigTemplate = `
  1103  # In case we already created the bridge, don't do it again.
  1104  grep -q "iface {{.Bridge}} inet dhcp" && exit 0
  1105  
  1106  # Discover primary interface at run-time using the default route (if set)
  1107  PRIMARY_IFACE=$(ip route list exact 0/0 | egrep -o 'dev [^ ]+' | cut -b5-)
  1108  
  1109  # If $PRIMARY_IFACE is empty, there's nothing to do.
  1110  [ -z "$PRIMARY_IFACE" ] && exit 0
  1111  
  1112  # Change the config to make $PRIMARY_IFACE manual instead of DHCP,
  1113  # then create the bridge and enslave $PRIMARY_IFACE into it.
  1114  grep -q "iface ${PRIMARY_IFACE} inet dhcp" {{.Config}} && \
  1115  sed -i "s/iface ${PRIMARY_IFACE} inet dhcp//" {{.Config}} && \
  1116  cat >> {{.Config}} << EOF
  1117  
  1118  # Primary interface (defining the default route)
  1119  iface ${PRIMARY_IFACE} inet manual
  1120  
  1121  # Bridge to use for LXC/KVM containers
  1122  auto {{.Bridge}}
  1123  iface {{.Bridge}} inet dhcp
  1124      bridge_ports ${PRIMARY_IFACE}
  1125  EOF
  1126  
  1127  # Make the primary interface not auto-starting.
  1128  grep -q "auto ${PRIMARY_IFACE}" {{.Config}} && \
  1129  sed -i "s/auto ${PRIMARY_IFACE}//" {{.Config}}
  1130  
  1131  # Stop $PRIMARY_IFACE and start the bridge instead.
  1132  ifdown -v ${PRIMARY_IFACE} ; ifup -v {{.Bridge}}
  1133  
  1134  # Finally, remove the route using $PRIMARY_IFACE (if any) so it won't
  1135  # clash with the same automatically added route for juju-br0 (except
  1136  # for the device name).
  1137  ip route flush dev $PRIMARY_IFACE scope link proto kernel || true
  1138  `
  1139  
  1140  // setupJujuNetworking returns a string representing the script to run
  1141  // in order to prepare the Juju-specific networking config on a node.
  1142  func setupJujuNetworking() (string, error) {
  1143  	parsedTemplate := template.Must(
  1144  		template.New("BridgeConfig").Parse(bridgeConfigTemplate),
  1145  	)
  1146  	var buf bytes.Buffer
  1147  	err := parsedTemplate.Execute(&buf, map[string]interface{}{
  1148  		"Config": "/etc/network/interfaces",
  1149  		"Bridge": instancecfg.DefaultBridgeName,
  1150  	})
  1151  	if err != nil {
  1152  		return "", errors.Annotate(err, "bridge config template error")
  1153  	}
  1154  	return buf.String(), nil
  1155  }
  1156  
  1157  // newCloudinitConfig creates a cloudinit.Config structure
  1158  // suitable as a base for initialising a MAAS node.
  1159  func (environ *maasEnviron) newCloudinitConfig(hostname, primaryIface, series string) (cloudinit.CloudConfig, error) {
  1160  	cloudcfg, err := cloudinit.New(series)
  1161  	if err != nil {
  1162  		return nil, err
  1163  	}
  1164  
  1165  	info := machineInfo{hostname}
  1166  	runCmd, err := info.cloudinitRunCmd(cloudcfg)
  1167  	if err != nil {
  1168  		return nil, errors.Trace(err)
  1169  	}
  1170  
  1171  	operatingSystem, err := version.GetOSFromSeries(series)
  1172  	if err != nil {
  1173  		return nil, errors.Trace(err)
  1174  	}
  1175  	switch operatingSystem {
  1176  	case version.Windows:
  1177  		cloudcfg.AddScripts(runCmd)
  1178  	case version.Ubuntu:
  1179  		cloudcfg.SetSystemUpdate(true)
  1180  		cloudcfg.AddScripts("set -xe", runCmd)
  1181  		// Only create the default bridge if we're not using static
  1182  		// address allocation for containers.
  1183  		if !environs.AddressAllocationEnabled() {
  1184  			// Address allocated feature flag might be disabled, but
  1185  			// DisableNetworkManagement can still disable the bridge
  1186  			// creation.
  1187  			if on, set := environ.Config().DisableNetworkManagement(); on && set {
  1188  				logger.Infof(
  1189  					"network management disabled - not using %q bridge for containers",
  1190  					instancecfg.DefaultBridgeName,
  1191  				)
  1192  				break
  1193  			}
  1194  			bridgeScript, err := setupJujuNetworking()
  1195  			if err != nil {
  1196  				return nil, errors.Trace(err)
  1197  			}
  1198  			cloudcfg.AddPackage("bridge-utils")
  1199  			cloudcfg.AddRunCmd(bridgeScript)
  1200  		}
  1201  	}
  1202  	return cloudcfg, nil
  1203  }
  1204  
  1205  func (environ *maasEnviron) releaseNodes(nodes gomaasapi.MAASObject, ids url.Values, recurse bool) error {
  1206  	err := ReleaseNodes(nodes, ids)
  1207  	if err == nil {
  1208  		return nil
  1209  	}
  1210  	maasErr, ok := err.(gomaasapi.ServerError)
  1211  	if !ok {
  1212  		return errors.Annotate(err, "cannot release nodes")
  1213  	}
  1214  
  1215  	// StatusCode 409 means a node couldn't be released due to
  1216  	// a state conflict. Likely it's already released or disk
  1217  	// erasing. We're assuming an error of 409 *only* means it's
  1218  	// safe to assume the instance is already released.
  1219  	// MaaS also releases (or attempts) all nodes, and raises
  1220  	// a single error on failure. So even with an error 409, all
  1221  	// nodes have been released.
  1222  	if maasErr.StatusCode == 409 {
  1223  		logger.Infof("ignoring error while releasing nodes (%v); all nodes released OK", err)
  1224  		return nil
  1225  	}
  1226  
  1227  	// a status code of 400, 403 or 404 means one of the nodes
  1228  	// couldn't be found and none have been released. We have
  1229  	// to release all the ones we can individually.
  1230  	if maasErr.StatusCode != 400 && maasErr.StatusCode != 403 && maasErr.StatusCode != 404 {
  1231  		return errors.Annotate(err, "cannot release nodes")
  1232  	}
  1233  	if !recurse {
  1234  		// this node has already been released and we're golden
  1235  		return nil
  1236  	}
  1237  
  1238  	var lastErr error
  1239  	for _, id := range ids["nodes"] {
  1240  		idFilter := url.Values{}
  1241  		idFilter.Add("nodes", id)
  1242  		err := environ.releaseNodes(nodes, idFilter, false)
  1243  		if err != nil {
  1244  			lastErr = err
  1245  			logger.Errorf("error while releasing node %v (%v)", id, err)
  1246  		}
  1247  	}
  1248  	return errors.Trace(lastErr)
  1249  
  1250  }
  1251  
  1252  // StopInstances is specified in the InstanceBroker interface.
  1253  func (environ *maasEnviron) StopInstances(ids ...instance.Id) error {
  1254  	// Shortcut to exit quickly if 'instances' is an empty slice or nil.
  1255  	if len(ids) == 0 {
  1256  		return nil
  1257  	}
  1258  	nodes := environ.getMAASClient().GetSubObject("nodes")
  1259  	err := environ.releaseNodes(nodes, getSystemIdValues("nodes", ids), true)
  1260  	if err != nil {
  1261  		// error will already have been wrapped
  1262  		return err
  1263  	}
  1264  	return common.RemoveStateInstances(environ.Storage(), ids...)
  1265  
  1266  }
  1267  
  1268  // acquireInstances calls the MAAS API to list acquired nodes.
  1269  //
  1270  // The "ids" slice is a filter for specific instance IDs.
  1271  // Due to how this works in the HTTP API, an empty "ids"
  1272  // matches all instances (not none as you might expect).
  1273  func (environ *maasEnviron) acquiredInstances(ids []instance.Id) ([]instance.Instance, error) {
  1274  	filter := getSystemIdValues("id", ids)
  1275  	filter.Add("agent_name", environ.ecfg().maasAgentName())
  1276  	return environ.instances(filter)
  1277  }
  1278  
  1279  // instances calls the MAAS API to list nodes matching the given filter.
  1280  func (environ *maasEnviron) instances(filter url.Values) ([]instance.Instance, error) {
  1281  	nodeListing := environ.getMAASClient().GetSubObject("nodes")
  1282  	listNodeObjects, err := nodeListing.CallGet("list", filter)
  1283  	if err != nil {
  1284  		return nil, err
  1285  	}
  1286  	listNodes, err := listNodeObjects.GetArray()
  1287  	if err != nil {
  1288  		return nil, err
  1289  	}
  1290  	instances := make([]instance.Instance, len(listNodes))
  1291  	for index, nodeObj := range listNodes {
  1292  		node, err := nodeObj.GetMAASObject()
  1293  		if err != nil {
  1294  			return nil, err
  1295  		}
  1296  		instances[index] = &maasInstance{
  1297  			maasObject: &node,
  1298  			environ:    environ,
  1299  		}
  1300  	}
  1301  	return instances, nil
  1302  }
  1303  
  1304  // Instances returns the instance.Instance objects corresponding to the given
  1305  // slice of instance.Id.  The error is ErrNoInstances if no instances
  1306  // were found.
  1307  func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
  1308  	if len(ids) == 0 {
  1309  		// This would be treated as "return all instances" below, so
  1310  		// treat it as a special case.
  1311  		// The interface requires us to return this particular error
  1312  		// if no instances were found.
  1313  		return nil, environs.ErrNoInstances
  1314  	}
  1315  	instances, err := environ.acquiredInstances(ids)
  1316  	if err != nil {
  1317  		return nil, err
  1318  	}
  1319  	if len(instances) == 0 {
  1320  		return nil, environs.ErrNoInstances
  1321  	}
  1322  
  1323  	idMap := make(map[instance.Id]instance.Instance)
  1324  	for _, instance := range instances {
  1325  		idMap[instance.Id()] = instance
  1326  	}
  1327  
  1328  	result := make([]instance.Instance, len(ids))
  1329  	for index, id := range ids {
  1330  		result[index] = idMap[id]
  1331  	}
  1332  
  1333  	if len(instances) < len(ids) {
  1334  		return result, environs.ErrPartialInstances
  1335  	}
  1336  	return result, nil
  1337  }
  1338  
  1339  // AllocateAddress requests an address to be allocated for the
  1340  // given instance on the given network.
  1341  func (environ *maasEnviron) AllocateAddress(instId instance.Id, subnetId network.Id, addr network.Address) (err error) {
  1342  	if !environs.AddressAllocationEnabled() {
  1343  		return errors.NotSupportedf("address allocation")
  1344  	}
  1345  
  1346  	defer errors.DeferredAnnotatef(&err, "failed to allocate address %q for instance %q", addr, instId)
  1347  	var subnets []network.SubnetInfo
  1348  
  1349  	subnets, err = environ.Subnets(instId, []network.Id{subnetId})
  1350  	logger.Tracef("Subnets(%q, %q, %q) returned: %v (%v)", instId, subnetId, addr, subnets, err)
  1351  	if err != nil {
  1352  		return errors.Trace(err)
  1353  	}
  1354  	if len(subnets) != 1 {
  1355  		return errors.Errorf("could not find subnet matching %q", subnetId)
  1356  	}
  1357  	foundSub := subnets[0]
  1358  	logger.Tracef("found subnet %#v", foundSub)
  1359  
  1360  	cidr := foundSub.CIDR
  1361  	ipaddresses := environ.getMAASClient().GetSubObject("ipaddresses")
  1362  	err = ReserveIPAddress(ipaddresses, cidr, addr)
  1363  	if err == nil {
  1364  		logger.Infof("allocated address %q for instance %q on subnet %q", addr, instId, cidr)
  1365  		return nil
  1366  	}
  1367  
  1368  	maasErr, ok := err.(gomaasapi.ServerError)
  1369  	if !ok {
  1370  		return errors.Trace(err)
  1371  	}
  1372  	// For an "out of range" IP address, maas raises
  1373  	// StaticIPAddressOutOfRange - an error 403
  1374  	// If there are no more addresses we get
  1375  	// StaticIPAddressExhaustion - an error 503
  1376  	// For an address already in use we get
  1377  	// StaticIPAddressUnavailable - an error 404
  1378  	if maasErr.StatusCode == 404 {
  1379  		logger.Tracef("address %q not available for allocation", addr)
  1380  		return environs.ErrIPAddressUnavailable
  1381  	} else if maasErr.StatusCode == 503 {
  1382  		logger.Tracef("no more addresses available on the subnet")
  1383  		return environs.ErrIPAddressesExhausted
  1384  	}
  1385  	// any error other than a 404 or 503 is "unexpected" and should
  1386  	// be returned directly.
  1387  	return errors.Trace(err)
  1388  }
  1389  
  1390  // ReleaseAddress releases a specific address previously allocated with
  1391  // AllocateAddress.
  1392  func (environ *maasEnviron) ReleaseAddress(instId instance.Id, _ network.Id, addr network.Address) (err error) {
  1393  	if !environs.AddressAllocationEnabled() {
  1394  		return errors.NotSupportedf("address allocation")
  1395  	}
  1396  
  1397  	defer errors.DeferredAnnotatef(&err, "failed to release IP address %q from instance %q", addr, instId)
  1398  	ipaddresses := environ.getMAASClient().GetSubObject("ipaddresses")
  1399  	// This can return a 404 error if the address has already been released
  1400  	// or is unknown by maas. However this, like any other error, would be
  1401  	// unexpected - so we don't treat it specially and just return it to
  1402  	// the caller.
  1403  	return ReleaseIPAddress(ipaddresses, addr)
  1404  }
  1405  
  1406  // NetworkInterfaces implements Environ.NetworkInterfaces.
  1407  func (environ *maasEnviron) NetworkInterfaces(instId instance.Id) ([]network.InterfaceInfo, error) {
  1408  	instances, err := environ.acquiredInstances([]instance.Id{instId})
  1409  	if err != nil {
  1410  		return nil, errors.Annotatef(err, "could not find instance %q", instId)
  1411  	}
  1412  	if len(instances) == 0 {
  1413  		return nil, errors.NotFoundf("instance %q", instId)
  1414  	}
  1415  	inst := instances[0]
  1416  	interfaces, _, err := environ.getInstanceNetworkInterfaces(inst)
  1417  	if err != nil {
  1418  		return nil, errors.Annotatef(err, "failed to get instance %q network interfaces", instId)
  1419  	}
  1420  
  1421  	networks, err := environ.getInstanceNetworks(inst)
  1422  	if err != nil {
  1423  		return nil, errors.Annotatef(err, "failed to get instance %q subnets", instId)
  1424  	}
  1425  
  1426  	macToNetworkMap := make(map[string]networkDetails)
  1427  	for _, network := range networks {
  1428  		macs, err := environ.listConnectedMacs(network)
  1429  		if err != nil {
  1430  			return nil, errors.Trace(err)
  1431  		}
  1432  		for _, mac := range macs {
  1433  			macToNetworkMap[mac] = network
  1434  		}
  1435  	}
  1436  
  1437  	result := []network.InterfaceInfo{}
  1438  	for serial, iface := range interfaces {
  1439  		deviceIndex := iface.DeviceIndex
  1440  		interfaceName := iface.InterfaceName
  1441  		disabled := iface.Disabled
  1442  
  1443  		ifaceInfo := network.InterfaceInfo{
  1444  			DeviceIndex:   deviceIndex,
  1445  			InterfaceName: interfaceName,
  1446  			Disabled:      disabled,
  1447  			NoAutoStart:   disabled,
  1448  			MACAddress:    serial,
  1449  			ConfigType:    network.ConfigDHCP,
  1450  		}
  1451  		details, ok := macToNetworkMap[serial]
  1452  		if ok {
  1453  			ifaceInfo.VLANTag = details.VLANTag
  1454  			ifaceInfo.ProviderSubnetId = network.Id(details.Name)
  1455  			mask := net.IPMask(net.ParseIP(details.Mask))
  1456  			cidr := net.IPNet{net.ParseIP(details.IP), mask}
  1457  			ifaceInfo.CIDR = cidr.String()
  1458  			ifaceInfo.Address = network.NewAddress(cidr.IP.String())
  1459  		} else {
  1460  			logger.Debugf("no subnet information for MAC address %q, instance %q", serial, instId)
  1461  		}
  1462  		result = append(result, ifaceInfo)
  1463  	}
  1464  	return result, nil
  1465  }
  1466  
  1467  // listConnectedMacs calls the MAAS list_connected_macs API to fetch all the
  1468  // the MAC addresses attached to a specific network.
  1469  func (environ *maasEnviron) listConnectedMacs(network networkDetails) ([]string, error) {
  1470  	client := environ.getMAASClient().GetSubObject("networks").GetSubObject(network.Name)
  1471  	json, err := client.CallGet("list_connected_macs", nil)
  1472  	if err != nil {
  1473  		return nil, err
  1474  	}
  1475  
  1476  	macs, err := json.GetArray()
  1477  	if err != nil {
  1478  		return nil, err
  1479  	}
  1480  	result := []string{}
  1481  	for _, macObj := range macs {
  1482  		macMap, err := macObj.GetMap()
  1483  		if err != nil {
  1484  			return nil, err
  1485  		}
  1486  		mac, err := macMap["mac_address"].GetString()
  1487  		if err != nil {
  1488  			return nil, err
  1489  		}
  1490  
  1491  		result = append(result, mac)
  1492  	}
  1493  	return result, nil
  1494  }
  1495  
  1496  // Subnets returns basic information about the specified subnets known
  1497  // by the provider for the specified instance. subnetIds must not be
  1498  // empty. Implements NetworkingEnviron.Subnets.
  1499  func (environ *maasEnviron) Subnets(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) {
  1500  	// At some point in the future an empty netIds may mean "fetch all subnets"
  1501  	// but until that functionality is needed it's an error.
  1502  	if len(subnetIds) == 0 {
  1503  		return nil, errors.Errorf("subnetIds must not be empty")
  1504  	}
  1505  	instances, err := environ.acquiredInstances([]instance.Id{instId})
  1506  	if err != nil {
  1507  		return nil, errors.Annotatef(err, "could not find instance %q", instId)
  1508  	}
  1509  	if len(instances) == 0 {
  1510  		return nil, errors.NotFoundf("instance %v", instId)
  1511  	}
  1512  	inst := instances[0]
  1513  	// The MAAS API get networks call returns named subnets, not physical networks,
  1514  	// so we save the data from this call into a variable called subnets.
  1515  	// http://maas.ubuntu.com/docs/api.html#networks
  1516  	subnets, err := environ.getInstanceNetworks(inst)
  1517  	if err != nil {
  1518  		return nil, errors.Annotatef(err, "cannot get instance %q subnets", instId)
  1519  	}
  1520  	logger.Debugf("instance %q has subnets %v", instId, subnets)
  1521  
  1522  	nodegroups, err := environ.getNodegroups()
  1523  	if err != nil {
  1524  		return nil, errors.Annotatef(err, "cannot get instance %q node groups", instId)
  1525  	}
  1526  	nodegroupInterfaces := environ.getNodegroupInterfaces(nodegroups)
  1527  
  1528  	subnetIdSet := make(map[network.Id]bool)
  1529  	for _, netId := range subnetIds {
  1530  		subnetIdSet[netId] = false
  1531  	}
  1532  	processedIds := make(map[network.Id]bool)
  1533  
  1534  	var networkInfo []network.SubnetInfo
  1535  	for _, subnet := range subnets {
  1536  		_, ok := subnetIdSet[network.Id(subnet.Name)]
  1537  		if !ok {
  1538  			// This id is not what we're looking for.
  1539  			continue
  1540  		}
  1541  		if _, ok := processedIds[network.Id(subnet.Name)]; ok {
  1542  			// Don't add the same subnet twice.
  1543  			continue
  1544  		}
  1545  		// mark that we've found this subnet
  1546  		processedIds[network.Id(subnet.Name)] = true
  1547  		subnetIdSet[network.Id(subnet.Name)] = true
  1548  		netCIDR := &net.IPNet{
  1549  			IP:   net.ParseIP(subnet.IP),
  1550  			Mask: net.IPMask(net.ParseIP(subnet.Mask)),
  1551  		}
  1552  		var allocatableHigh, allocatableLow net.IP
  1553  		for ip, bounds := range nodegroupInterfaces {
  1554  			contained := netCIDR.Contains(net.ParseIP(ip))
  1555  			if contained {
  1556  				allocatableLow = bounds[0]
  1557  				allocatableHigh = bounds[1]
  1558  				break
  1559  			}
  1560  		}
  1561  		subnetInfo := network.SubnetInfo{
  1562  			CIDR:              netCIDR.String(),
  1563  			VLANTag:           subnet.VLANTag,
  1564  			ProviderId:        network.Id(subnet.Name),
  1565  			AllocatableIPLow:  allocatableLow,
  1566  			AllocatableIPHigh: allocatableHigh,
  1567  		}
  1568  
  1569  		// Verify we filled-in everything for all networks
  1570  		// and drop incomplete records.
  1571  		if subnetInfo.ProviderId == "" || subnetInfo.CIDR == "" {
  1572  			logger.Infof("ignoring subnet  %q: missing information (%#v)", subnet.Name, subnetInfo)
  1573  			continue
  1574  		}
  1575  
  1576  		logger.Tracef("found subnet with info %#v", subnetInfo)
  1577  		networkInfo = append(networkInfo, subnetInfo)
  1578  	}
  1579  	logger.Debugf("available subnets for instance %v: %#v", inst.Id(), networkInfo)
  1580  
  1581  	notFound := []network.Id{}
  1582  	for subnetId, found := range subnetIdSet {
  1583  		if !found {
  1584  			notFound = append(notFound, subnetId)
  1585  		}
  1586  	}
  1587  	if len(notFound) != 0 {
  1588  		return nil, errors.Errorf("failed to find the following subnets: %v", notFound)
  1589  	}
  1590  
  1591  	return networkInfo, nil
  1592  }
  1593  
  1594  // AllInstances returns all the instance.Instance in this provider.
  1595  func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) {
  1596  	return environ.acquiredInstances(nil)
  1597  }
  1598  
  1599  // Storage is defined by the Environ interface.
  1600  func (env *maasEnviron) Storage() storage.Storage {
  1601  	env.ecfgMutex.Lock()
  1602  	defer env.ecfgMutex.Unlock()
  1603  	return env.storageUnlocked
  1604  }
  1605  
  1606  func (environ *maasEnviron) Destroy() error {
  1607  	if err := common.Destroy(environ); err != nil {
  1608  		return errors.Trace(err)
  1609  	}
  1610  	return environ.Storage().RemoveAll()
  1611  }
  1612  
  1613  // MAAS does not do firewalling so these port methods do nothing.
  1614  func (*maasEnviron) OpenPorts([]network.PortRange) error {
  1615  	logger.Debugf("unimplemented OpenPorts() called")
  1616  	return nil
  1617  }
  1618  
  1619  func (*maasEnviron) ClosePorts([]network.PortRange) error {
  1620  	logger.Debugf("unimplemented ClosePorts() called")
  1621  	return nil
  1622  }
  1623  
  1624  func (*maasEnviron) Ports() ([]network.PortRange, error) {
  1625  	logger.Debugf("unimplemented Ports() called")
  1626  	return nil, nil
  1627  }
  1628  
  1629  func (*maasEnviron) Provider() environs.EnvironProvider {
  1630  	return &providerInstance
  1631  }
  1632  
  1633  // networkDetails holds information about a MAAS network.
  1634  type networkDetails struct {
  1635  	Name        string
  1636  	IP          string
  1637  	Mask        string
  1638  	VLANTag     int
  1639  	Description string
  1640  }
  1641  
  1642  // getInstanceNetworks returns a list of all MAAS networks for a given node.
  1643  func (environ *maasEnviron) getInstanceNetworks(inst instance.Instance) ([]networkDetails, error) {
  1644  	maasInst := inst.(*maasInstance)
  1645  	maasObj := maasInst.maasObject
  1646  	client := environ.getMAASClient().GetSubObject("networks")
  1647  	nodeId, err := maasObj.GetField("system_id")
  1648  	if err != nil {
  1649  		return nil, err
  1650  	}
  1651  	params := url.Values{"node": {nodeId}}
  1652  	json, err := client.CallGet("", params)
  1653  	if err != nil {
  1654  		return nil, err
  1655  	}
  1656  	jsonNets, err := json.GetArray()
  1657  	if err != nil {
  1658  		return nil, err
  1659  	}
  1660  
  1661  	networks := make([]networkDetails, len(jsonNets))
  1662  	for i, jsonNet := range jsonNets {
  1663  		fields, err := jsonNet.GetMap()
  1664  		if err != nil {
  1665  			return nil, err
  1666  		}
  1667  		name, err := fields["name"].GetString()
  1668  		if err != nil {
  1669  			return nil, fmt.Errorf("cannot get name: %v", err)
  1670  		}
  1671  		ip, err := fields["ip"].GetString()
  1672  		if err != nil {
  1673  			return nil, fmt.Errorf("cannot get ip: %v", err)
  1674  		}
  1675  		netmask, err := fields["netmask"].GetString()
  1676  		if err != nil {
  1677  			return nil, fmt.Errorf("cannot get netmask: %v", err)
  1678  		}
  1679  		vlanTag := 0
  1680  		vlanTagField, ok := fields["vlan_tag"]
  1681  		if ok && !vlanTagField.IsNil() {
  1682  			// vlan_tag is optional, so assume it's 0 when missing or nil.
  1683  			vlanTagFloat, err := vlanTagField.GetFloat64()
  1684  			if err != nil {
  1685  				return nil, fmt.Errorf("cannot get vlan_tag: %v", err)
  1686  			}
  1687  			vlanTag = int(vlanTagFloat)
  1688  		}
  1689  		description, err := fields["description"].GetString()
  1690  		if err != nil {
  1691  			return nil, fmt.Errorf("cannot get description: %v", err)
  1692  		}
  1693  
  1694  		networks[i] = networkDetails{
  1695  			Name:        name,
  1696  			IP:          ip,
  1697  			Mask:        netmask,
  1698  			VLANTag:     vlanTag,
  1699  			Description: description,
  1700  		}
  1701  	}
  1702  	return networks, nil
  1703  }
  1704  
  1705  // getNetworkMACs returns all MAC addresses connected to the given
  1706  // network.
  1707  func (environ *maasEnviron) getNetworkMACs(networkName string) ([]string, error) {
  1708  	client := environ.getMAASClient().GetSubObject("networks").GetSubObject(networkName)
  1709  	json, err := client.CallGet("list_connected_macs", nil)
  1710  	if err != nil {
  1711  		return nil, err
  1712  	}
  1713  	jsonMACs, err := json.GetArray()
  1714  	if err != nil {
  1715  		return nil, err
  1716  	}
  1717  
  1718  	macs := make([]string, len(jsonMACs))
  1719  	for i, jsonMAC := range jsonMACs {
  1720  		fields, err := jsonMAC.GetMap()
  1721  		if err != nil {
  1722  			return nil, err
  1723  		}
  1724  		macAddress, err := fields["mac_address"].GetString()
  1725  		if err != nil {
  1726  			return nil, fmt.Errorf("cannot get mac_address: %v", err)
  1727  		}
  1728  		macs[i] = macAddress
  1729  	}
  1730  	return macs, nil
  1731  }
  1732  
  1733  // getInstanceNetworkInterfaces returns a map of interface MAC address
  1734  // to ifaceInfo for each network interface of the given instance, as
  1735  // discovered during the commissioning phase. In addition, it also
  1736  // returns the interface name discovered as primary.
  1737  func (environ *maasEnviron) getInstanceNetworkInterfaces(inst instance.Instance) (map[string]ifaceInfo, string, error) {
  1738  	maasInst := inst.(*maasInstance)
  1739  	maasObj := maasInst.maasObject
  1740  	result, err := maasObj.CallGet("details", nil)
  1741  	if err != nil {
  1742  		return nil, "", errors.Trace(err)
  1743  	}
  1744  	// Get the node's lldp / lshw details discovered at commissioning.
  1745  	data, err := result.GetBytes()
  1746  	if err != nil {
  1747  		return nil, "", errors.Trace(err)
  1748  	}
  1749  	var parsed map[string]interface{}
  1750  	if err := bson.Unmarshal(data, &parsed); err != nil {
  1751  		return nil, "", errors.Trace(err)
  1752  	}
  1753  	lshwData, ok := parsed["lshw"]
  1754  	if !ok {
  1755  		return nil, "", errors.Errorf("no hardware information available for node %q", inst.Id())
  1756  	}
  1757  	lshwXML, ok := lshwData.([]byte)
  1758  	if !ok {
  1759  		return nil, "", errors.Errorf("invalid hardware information for node %q", inst.Id())
  1760  	}
  1761  	// Now we have the lshw XML data, parse it to extract and return NICs.
  1762  	return extractInterfaces(inst, lshwXML)
  1763  }
  1764  
  1765  type ifaceInfo struct {
  1766  	DeviceIndex   int
  1767  	InterfaceName string
  1768  	Disabled      bool
  1769  }
  1770  
  1771  // extractInterfaces parses the XML output of lswh and extracts all
  1772  // network interfaces, returing a map MAC address to ifaceInfo, as
  1773  // well as the interface name discovered as primary.
  1774  func extractInterfaces(inst instance.Instance, lshwXML []byte) (map[string]ifaceInfo, string, error) {
  1775  	type Node struct {
  1776  		Id          string `xml:"id,attr"`
  1777  		Disabled    bool   `xml:"disabled,attr,omitempty"`
  1778  		Description string `xml:"description"`
  1779  		Serial      string `xml:"serial"`
  1780  		LogicalName string `xml:"logicalname"`
  1781  		Children    []Node `xml:"node"`
  1782  	}
  1783  	type List struct {
  1784  		Nodes []Node `xml:"node"`
  1785  	}
  1786  	var lshw List
  1787  	if err := xml.Unmarshal(lshwXML, &lshw); err != nil {
  1788  		return nil, "", errors.Annotatef(err, "cannot parse lshw XML details for node %q", inst.Id())
  1789  	}
  1790  	primaryIface := ""
  1791  	interfaces := make(map[string]ifaceInfo)
  1792  	var processNodes func(nodes []Node) error
  1793  	var baseIndex int
  1794  	processNodes = func(nodes []Node) error {
  1795  		for _, node := range nodes {
  1796  			if strings.HasPrefix(node.Id, "network") {
  1797  				index := baseIndex
  1798  				if strings.HasPrefix(node.Id, "network:") {
  1799  					// There is an index suffix, parse it.
  1800  					var err error
  1801  					index, err = strconv.Atoi(strings.TrimPrefix(node.Id, "network:"))
  1802  					if err != nil {
  1803  						return errors.Annotatef(err, "lshw output for node %q has invalid ID suffix for %q", inst.Id(), node.Id)
  1804  					}
  1805  				} else {
  1806  					baseIndex++
  1807  				}
  1808  
  1809  				if primaryIface == "" && !node.Disabled {
  1810  					primaryIface = node.LogicalName
  1811  					logger.Debugf("node %q primary network interface is %q", inst.Id(), primaryIface)
  1812  				}
  1813  				interfaces[node.Serial] = ifaceInfo{
  1814  					DeviceIndex:   index,
  1815  					InterfaceName: node.LogicalName,
  1816  					Disabled:      node.Disabled,
  1817  				}
  1818  				if node.Disabled {
  1819  					logger.Debugf("node %q skipping disabled network interface %q", inst.Id(), node.LogicalName)
  1820  				}
  1821  
  1822  			}
  1823  			if err := processNodes(node.Children); err != nil {
  1824  				return err
  1825  			}
  1826  		}
  1827  		return nil
  1828  	}
  1829  	err := processNodes(lshw.Nodes)
  1830  	return interfaces, primaryIface, err
  1831  }