Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
2463 lines (2245 sloc) 73.1 KB
// Copyright 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package maas
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/juju/errors"
"github.com/juju/gomaasapi"
"github.com/juju/utils"
"github.com/juju/utils/os"
"github.com/juju/utils/series"
"github.com/juju/utils/set"
"github.com/juju/version"
"gopkg.in/juju/names.v2"
"github.com/juju/juju/cloudconfig/cloudinit"
"github.com/juju/juju/cloudconfig/instancecfg"
"github.com/juju/juju/cloudconfig/providerinit"
"github.com/juju/juju/constraints"
"github.com/juju/juju/environs"
"github.com/juju/juju/environs/config"
"github.com/juju/juju/environs/storage"
"github.com/juju/juju/environs/tags"
"github.com/juju/juju/instance"
"github.com/juju/juju/network"
"github.com/juju/juju/provider/common"
"github.com/juju/juju/state/multiwatcher"
"github.com/juju/juju/status"
"github.com/juju/juju/tools"
)
const (
// The version strings indicating the MAAS API version.
apiVersion1 = "1.0"
apiVersion2 = "2.0"
)
// A request may fail to due "eventual consistency" semantics, which
// should resolve fairly quickly. A request may also fail due to a slow
// state transition (for instance an instance taking a while to release
// a security group after termination). The former failure mode is
// dealt with by shortAttempt, the latter by LongAttempt.
var shortAttempt = utils.AttemptStrategy{
Total: 5 * time.Second,
Delay: 200 * time.Millisecond,
}
const statusPollInterval = 5 * time.Second
var (
ReleaseNodes = releaseNodes
DeploymentStatusCall = deploymentStatusCall
GetMAAS2Controller = getMAAS2Controller
)
func getMAAS2Controller(maasServer, apiKey string) (gomaasapi.Controller, error) {
return gomaasapi.NewController(gomaasapi.ControllerArgs{
BaseURL: maasServer,
APIKey: apiKey,
})
}
func releaseNodes(nodes gomaasapi.MAASObject, ids url.Values) error {
_, err := nodes.CallPost("release", ids)
return err
}
type maasEnviron struct {
name string
cloud environs.CloudSpec
uuid string
// archMutex gates access to supportedArchitectures
archMutex sync.Mutex
// ecfgMutex protects the *Unlocked fields below.
ecfgMutex sync.Mutex
ecfgUnlocked *maasModelConfig
maasClientUnlocked *gomaasapi.MAASObject
storageUnlocked storage.Storage
// maasController provides access to the MAAS 2.0 API.
maasController gomaasapi.Controller
// namespace is used to create the machine and device hostnames.
namespace instance.Namespace
availabilityZonesMutex sync.Mutex
availabilityZones []common.AvailabilityZone
// apiVersion tells us if we are using the MAAS 1.0 or 2.0 api.
apiVersion string
// GetCapabilities is a function that connects to MAAS to return its set of
// capabilities.
GetCapabilities MaasCapabilities
}
var _ environs.Environ = (*maasEnviron)(nil)
// MaasCapabilities represents a function that gets the capabilities of a MAAS
// installation.
type MaasCapabilities func(client *gomaasapi.MAASObject, serverURL string) (set.Strings, error)
func NewEnviron(cloud environs.CloudSpec, cfg *config.Config, getCaps MaasCapabilities) (*maasEnviron, error) {
if getCaps == nil {
getCaps = getCapabilities
}
env := &maasEnviron{
name: cfg.Name(),
uuid: cfg.UUID(),
cloud: cloud,
GetCapabilities: getCaps,
}
err := env.SetConfig(cfg)
if err != nil {
return nil, err
}
env.storageUnlocked = NewStorage(env)
env.namespace, err = instance.NewNamespace(cfg.UUID())
if err != nil {
return nil, errors.Trace(err)
}
return env, nil
}
func (env *maasEnviron) usingMAAS2() bool {
return env.apiVersion == apiVersion2
}
// PrepareForBootstrap is part of the Environ interface.
func (env *maasEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error {
if ctx.ShouldVerifyCredentials() {
if err := verifyCredentials(env); err != nil {
return err
}
}
return nil
}
// Create is part of the Environ interface.
func (env *maasEnviron) Create(environs.CreateParams) error {
if err := verifyCredentials(env); err != nil {
return err
}
return nil
}
// Bootstrap is part of the Environ interface.
func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) {
result, series, finalizer, err := common.BootstrapInstance(ctx, env, args)
if err != nil {
return nil, err
}
// We want to destroy the started instance if it doesn't transition to Deployed.
defer func() {
if err != nil {
if err := env.StopInstances(result.Instance.Id()); err != nil {
logger.Errorf("error releasing bootstrap instance: %v", err)
}
}
}()
waitingFinalizer := func(
ctx environs.BootstrapContext,
icfg *instancecfg.InstanceConfig,
dialOpts environs.BootstrapDialOpts,
) error {
// Wait for bootstrap instance to change to deployed state.
if err := env.waitForNodeDeployment(result.Instance.Id(), dialOpts.Timeout); err != nil {
return errors.Annotate(err, "bootstrap instance started but did not change to Deployed state")
}
return finalizer(ctx, icfg, dialOpts)
}
bsResult := &environs.BootstrapResult{
Arch: *result.Hardware.Arch,
Series: series,
Finalize: waitingFinalizer,
}
return bsResult, nil
}
// ControllerInstances is specified in the Environ interface.
func (env *maasEnviron) ControllerInstances(controllerUUID string) ([]instance.Id, error) {
if !env.usingMAAS2() {
return env.controllerInstances1(controllerUUID)
}
return env.controllerInstances2(controllerUUID)
}
func (env *maasEnviron) controllerInstances1(controllerUUID string) ([]instance.Id, error) {
return common.ProviderStateInstances(env.Storage())
}
func (env *maasEnviron) controllerInstances2(controllerUUID string) ([]instance.Id, error) {
instances, err := env.instances2(gomaasapi.MachinesArgs{
OwnerData: map[string]string{
tags.JujuIsController: "true",
tags.JujuController: controllerUUID,
},
})
if err != nil {
return nil, errors.Trace(err)
}
if len(instances) == 0 {
return nil, environs.ErrNotBootstrapped
}
ids := make([]instance.Id, len(instances))
for i := range instances {
ids[i] = instances[i].Id()
}
return ids, nil
}
// ecfg returns the environment's maasModelConfig, and protects it with a
// mutex.
func (env *maasEnviron) ecfg() *maasModelConfig {
env.ecfgMutex.Lock()
cfg := *env.ecfgUnlocked
env.ecfgMutex.Unlock()
return &cfg
}
// Config is specified in the Environ interface.
func (env *maasEnviron) Config() *config.Config {
return env.ecfg().Config
}
// SetConfig is specified in the Environ interface.
func (env *maasEnviron) SetConfig(cfg *config.Config) error {
env.ecfgMutex.Lock()
defer env.ecfgMutex.Unlock()
// The new config has already been validated by itself, but now we
// validate the transition from the old config to the new.
var oldCfg *config.Config
if env.ecfgUnlocked != nil {
oldCfg = env.ecfgUnlocked.Config
}
cfg, err := env.Provider().Validate(cfg, oldCfg)
if err != nil {
return errors.Trace(err)
}
ecfg, err := providerInstance.newConfig(cfg)
if err != nil {
return errors.Trace(err)
}
env.ecfgUnlocked = ecfg
maasServer, err := parseCloudEndpoint(env.cloud.Endpoint)
if err != nil {
return errors.Trace(err)
}
maasOAuth, err := parseOAuthToken(*env.cloud.Credential)
if err != nil {
return errors.Trace(err)
}
// We need to know the version of the server we're on. We support 1.9
// and 2.0. MAAS 1.9 uses the 1.0 api version and 2.0 uses the 2.0 api
// version.
apiVersion := apiVersion2
controller, err := GetMAAS2Controller(maasServer, maasOAuth)
switch {
case gomaasapi.IsUnsupportedVersionError(err):
apiVersion = apiVersion1
authClient, err := gomaasapi.NewAuthenticatedClient(maasServer, maasOAuth, apiVersion1)
if err != nil {
return errors.Trace(err)
}
env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient)
caps, err := env.GetCapabilities(env.maasClientUnlocked, maasServer)
if err != nil {
return errors.Trace(err)
}
if !caps.Contains(capNetworkDeploymentUbuntu) {
return errors.NewNotSupported(nil, "MAAS 1.9 or more recent is required")
}
case err != nil:
return errors.Trace(err)
default:
env.maasController = controller
}
env.apiVersion = apiVersion
return nil
}
func (env *maasEnviron) getSupportedArchitectures() ([]string, error) {
env.archMutex.Lock()
defer env.archMutex.Unlock()
fetchArchitectures := env.allArchitecturesWithFallback
if env.usingMAAS2() {
fetchArchitectures = env.allArchitectures2
}
return fetchArchitectures()
}
// SupportsSpaces is specified on environs.Networking.
func (env *maasEnviron) SupportsSpaces() (bool, error) {
return true, nil
}
// SupportsSpaceDiscovery is specified on environs.Networking.
func (env *maasEnviron) SupportsSpaceDiscovery() (bool, error) {
return true, nil
}
// SupportsContainerAddresses is specified on environs.Networking.
func (env *maasEnviron) SupportsContainerAddresses() (bool, error) {
return true, nil
}
// allArchitectures2 uses the MAAS2 controller to get architectures from boot
// resources.
func (env *maasEnviron) allArchitectures2() ([]string, error) {
resources, err := env.maasController.BootResources()
if err != nil {
return nil, errors.Trace(err)
}
architectures := set.NewStrings()
for _, resource := range resources {
architectures.Add(strings.Split(resource.Architecture(), "/")[0])
}
return architectures.SortedValues(), nil
}
// allArchitectureWithFallback queries MAAS for all of the boot-images
// across all registered nodegroups and collapses them down to unique
// architectures.
func (env *maasEnviron) allArchitecturesWithFallback() ([]string, error) {
architectures, err := env.allArchitectures()
if err != nil || len(architectures) == 0 {
logger.Debugf("error querying boot-images: %v", err)
logger.Debugf("falling back to listing nodes")
architectures, err := env.nodeArchitectures()
if err != nil {
return nil, errors.Trace(err)
}
return architectures, nil
} else {
return architectures, nil
}
}
func (env *maasEnviron) allArchitectures() ([]string, error) {
nodegroups, err := env.getNodegroups()
if err != nil {
return nil, err
}
architectures := set.NewStrings()
for _, nodegroup := range nodegroups {
bootImages, err := env.nodegroupBootImages(nodegroup)
if err != nil {
return nil, errors.Annotatef(err, "cannot get boot images for nodegroup %v", nodegroup)
}
for _, image := range bootImages {
architectures.Add(image.architecture)
}
}
return architectures.SortedValues(), nil
}
// getNodegroups returns the UUID corresponding to each nodegroup
// in the MAAS installation.
func (env *maasEnviron) getNodegroups() ([]string, error) {
nodegroupsListing := env.getMAASClient().GetSubObject("nodegroups")
nodegroupsResult, err := nodegroupsListing.CallGet("list", nil)
if err != nil {
return nil, err
}
list, err := nodegroupsResult.GetArray()
if err != nil {
return nil, err
}
nodegroups := make([]string, len(list))
for i, obj := range list {
nodegroup, err := obj.GetMap()
if err != nil {
return nil, err
}
uuid, err := nodegroup["uuid"].GetString()
if err != nil {
return nil, err
}
nodegroups[i] = uuid
}
return nodegroups, nil
}
type bootImage struct {
architecture string
release string
}
// nodegroupBootImages returns the set of boot-images for the specified nodegroup.
func (env *maasEnviron) nodegroupBootImages(nodegroupUUID string) ([]bootImage, error) {
nodegroupObject := env.getMAASClient().GetSubObject("nodegroups").GetSubObject(nodegroupUUID)
bootImagesObject := nodegroupObject.GetSubObject("boot-images/")
result, err := bootImagesObject.CallGet("", nil)
if err != nil {
return nil, err
}
list, err := result.GetArray()
if err != nil {
return nil, err
}
var bootImages []bootImage
for _, obj := range list {
bootimage, err := obj.GetMap()
if err != nil {
return nil, err
}
arch, err := bootimage["architecture"].GetString()
if err != nil {
return nil, err
}
release, err := bootimage["release"].GetString()
if err != nil {
return nil, err
}
bootImages = append(bootImages, bootImage{
architecture: arch,
release: release,
})
}
return bootImages, nil
}
// nodeArchitectures returns the architectures of all
// available nodes in the system.
//
// Note: this should only be used if we cannot query
// boot-images.
func (env *maasEnviron) nodeArchitectures() ([]string, error) {
filter := make(url.Values)
filter.Add("status", gomaasapi.NodeStatusDeclared)
filter.Add("status", gomaasapi.NodeStatusCommissioning)
filter.Add("status", gomaasapi.NodeStatusReady)
filter.Add("status", gomaasapi.NodeStatusReserved)
filter.Add("status", gomaasapi.NodeStatusAllocated)
// This is fine - nodeArchitectures is only used in MAAS 1 cases.
allInstances, err := env.instances1(filter)
if err != nil {
return nil, err
}
architectures := make(set.Strings)
for _, inst := range allInstances {
inst := inst.(*maas1Instance)
arch, _, err := inst.architecture()
if err != nil {
return nil, err
}
architectures.Add(arch)
}
// TODO(dfc) why is this sorted
return architectures.SortedValues(), nil
}
type maasAvailabilityZone struct {
name string
}
func (z maasAvailabilityZone) Name() string {
return z.name
}
func (z maasAvailabilityZone) Available() bool {
// MAAS' physical zone attributes only include name and description;
// there is no concept of availability.
return true
}
// AvailabilityZones returns a slice of availability zones
// for the configured region.
func (e *maasEnviron) AvailabilityZones() ([]common.AvailabilityZone, error) {
e.availabilityZonesMutex.Lock()
defer e.availabilityZonesMutex.Unlock()
if e.availabilityZones == nil {
var availabilityZones []common.AvailabilityZone
var err error
if e.usingMAAS2() {
availabilityZones, err = e.availabilityZones2()
if err != nil {
return nil, errors.Trace(err)
}
} else {
availabilityZones, err = e.availabilityZones1()
if err != nil {
return nil, errors.Trace(err)
}
}
e.availabilityZones = availabilityZones
}
return e.availabilityZones, nil
}
func (e *maasEnviron) availabilityZones1() ([]common.AvailabilityZone, error) {
zonesObject := e.getMAASClient().GetSubObject("zones")
result, err := zonesObject.CallGet("", nil)
if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == http.StatusNotFound {
return nil, errors.NewNotImplemented(nil, "the MAAS server does not support zones")
}
if err != nil {
return nil, errors.Annotate(err, "cannot query ")
}
list, err := result.GetArray()
if err != nil {
return nil, err
}
logger.Debugf("availability zones: %+v", list)
availabilityZones := make([]common.AvailabilityZone, len(list))
for i, obj := range list {
zone, err := obj.GetMap()
if err != nil {
return nil, err
}
name, err := zone["name"].GetString()
if err != nil {
return nil, err
}
availabilityZones[i] = maasAvailabilityZone{name}
}
return availabilityZones, nil
}
func (e *maasEnviron) availabilityZones2() ([]common.AvailabilityZone, error) {
zones, err := e.maasController.Zones()
if err != nil {
return nil, errors.Trace(err)
}
availabilityZones := make([]common.AvailabilityZone, len(zones))
for i, zone := range zones {
availabilityZones[i] = maasAvailabilityZone{zone.Name()}
}
return availabilityZones, nil
}
// InstanceAvailabilityZoneNames returns the availability zone names for each
// of the specified instances.
func (e *maasEnviron) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) {
instances, err := e.Instances(ids)
if err != nil && err != environs.ErrPartialInstances {
return nil, err
}
zones := make([]string, len(instances))
for i, inst := range instances {
if inst == nil {
continue
}
z, err := inst.(maasInstance).zone()
if err != nil {
logger.Errorf("could not get availability zone %v", err)
continue
}
zones[i] = z
}
return zones, nil
}
type maasPlacement struct {
nodeName string
zoneName string
}
func (e *maasEnviron) parsePlacement(placement string) (*maasPlacement, error) {
pos := strings.IndexRune(placement, '=')
if pos == -1 {
// If there's no '=' delimiter, assume it's a node name.
return &maasPlacement{nodeName: placement}, nil
}
switch key, value := placement[:pos], placement[pos+1:]; key {
case "zone":
availabilityZone := value
zones, err := e.AvailabilityZones()
if err != nil {
return nil, err
}
for _, z := range zones {
if z.Name() == availabilityZone {
return &maasPlacement{zoneName: availabilityZone}, nil
}
}
return nil, errors.Errorf("invalid availability zone %q", availabilityZone)
}
return nil, errors.Errorf("unknown placement directive: %v", placement)
}
func (env *maasEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error {
if placement == "" {
return nil
}
_, err := env.parsePlacement(placement)
return err
}
const (
capNetworkDeploymentUbuntu = "network-deployment-ubuntu"
)
// getCapabilities asks the MAAS server for its capabilities, if
// supported by the server.
func getCapabilities(client *gomaasapi.MAASObject, serverURL string) (set.Strings, error) {
caps := make(set.Strings)
var result gomaasapi.JSONObject
var err error
for a := shortAttempt.Start(); a.Next(); {
version := client.GetSubObject("version/")
result, err = version.CallGet("", nil)
if err == nil {
break
}
if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == 404 {
logger.Debugf("Failed attempting to get capabilities from maas endpoint %q: %v", serverURL, err)
message := "could not connect to MAAS controller - check the endpoint is correct"
trimmedURL := strings.TrimRight(serverURL, "/")
if !strings.HasSuffix(trimmedURL, "/MAAS") {
message += " (it normally ends with /MAAS)"
}
return caps, errors.NewNotSupported(nil, message)
}
}
if err != nil {
logger.Debugf("Can't connect to maas server at endpoint %q: %v", serverURL, err)
return caps, err
}
info, err := result.GetMap()
if err != nil {
logger.Debugf("Invalid data returned from maas endpoint %q: %v", serverURL, err)
// invalid data of some sort, probably not a MAAS server.
return caps, errors.New("failed to get expected data from server")
}
capsObj, ok := info["capabilities"]
if !ok {
return caps, fmt.Errorf("MAAS does not report capabilities")
}
items, err := capsObj.GetArray()
if err != nil {
logger.Debugf("Invalid data returned from maas endpoint %q: %v", serverURL, err)
return caps, errors.New("failed to get expected data from server")
}
for _, item := range items {
val, err := item.GetString()
if err != nil {
logger.Debugf("Invalid data returned from maas endpoint %q: %v", serverURL, err)
return set.NewStrings(), errors.New("failed to get expected data from server")
}
caps.Add(val)
}
return caps, nil
}
// getMAASClient returns a MAAS client object to use for a request, in a
// lock-protected fashion.
func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject {
env.ecfgMutex.Lock()
defer env.ecfgMutex.Unlock()
return env.maasClientUnlocked
}
var dashSuffix = regexp.MustCompile("^(.*)-\\d+$")
func spaceNamesToSpaceInfo(spaces []string, spaceMap map[string]network.SpaceInfo) ([]network.SpaceInfo, error) {
spaceInfos := []network.SpaceInfo{}
for _, name := range spaces {
info, ok := spaceMap[name]
if !ok {
matches := dashSuffix.FindAllStringSubmatch(name, 1)
if matches == nil {
return nil, errors.Errorf("unrecognised space in constraint %q", name)
}
// A -number was added to the space name when we
// converted to a juju name, we found
info, ok = spaceMap[matches[0][1]]
if !ok {
return nil, errors.Errorf("unrecognised space in constraint %q", name)
}
}
spaceInfos = append(spaceInfos, info)
}
return spaceInfos, nil
}
func (environ *maasEnviron) buildSpaceMap() (map[string]network.SpaceInfo, error) {
spaces, err := environ.Spaces()
if err != nil {
return nil, errors.Trace(err)
}
spaceMap := make(map[string]network.SpaceInfo)
empty := set.Strings{}
for _, space := range spaces {
jujuName := network.ConvertSpaceName(space.Name, empty)
spaceMap[jujuName] = space
}
return spaceMap, nil
}
func (environ *maasEnviron) spaceNamesToSpaceInfo(positiveSpaces, negativeSpaces []string) ([]network.SpaceInfo, []network.SpaceInfo, error) {
spaceMap, err := environ.buildSpaceMap()
if err != nil {
return nil, nil, errors.Trace(err)
}
positiveSpaceIds, err := spaceNamesToSpaceInfo(positiveSpaces, spaceMap)
if err != nil {
return nil, nil, errors.Trace(err)
}
negativeSpaceIds, err := spaceNamesToSpaceInfo(negativeSpaces, spaceMap)
if err != nil {
return nil, nil, errors.Trace(err)
}
return positiveSpaceIds, negativeSpaceIds, nil
}
// acquireNode2 allocates a machine from MAAS2.
func (environ *maasEnviron) acquireNode2(
nodeName, zoneName string,
cons constraints.Value,
interfaces []interfaceBinding,
volumes []volumeInfo,
) (maasInstance, error) {
acquireParams := convertConstraints2(cons)
positiveSpaceNames, negativeSpaceNames := convertSpacesFromConstraints(cons.Spaces)
positiveSpaces, negativeSpaces, err := environ.spaceNamesToSpaceInfo(positiveSpaceNames, negativeSpaceNames)
// If spaces aren't supported the constraints should be empty anyway.
if err != nil && !errors.IsNotSupported(err) {
return nil, errors.Trace(err)
}
err = addInterfaces2(&acquireParams, interfaces, positiveSpaces, negativeSpaces)
if err != nil {
return nil, errors.Trace(err)
}
addStorage2(&acquireParams, volumes)
acquireParams.AgentName = environ.uuid
if zoneName != "" {
acquireParams.Zone = zoneName
}
if nodeName != "" {
acquireParams.Hostname = nodeName
}
machine, constraintMatches, err := environ.maasController.AllocateMachine(acquireParams)
if err != nil {
return nil, errors.Trace(err)
}
return &maas2Instance{machine, constraintMatches}, nil
}
// acquireNode allocates a node from the MAAS.
func (environ *maasEnviron) acquireNode(
nodeName, zoneName string,
cons constraints.Value,
interfaces []interfaceBinding,
volumes []volumeInfo,
) (gomaasapi.MAASObject, error) {
// TODO(axw) 2014-08-18 #1358219
// We should be requesting preferred architectures if unspecified,
// like in the other providers.
//
// This is slightly complicated in MAAS as there are a finite
// number of each architecture; preference may also conflict with
// other constraints, such as tags. Thus, a preference becomes a
// demand (which may fail) if not handled properly.
acquireParams := convertConstraints(cons)
positiveSpaceNames, negativeSpaceNames := convertSpacesFromConstraints(cons.Spaces)
positiveSpaces, negativeSpaces, err := environ.spaceNamesToSpaceInfo(positiveSpaceNames, negativeSpaceNames)
// If spaces aren't supported the constraints should be empty anyway.
if err != nil && !errors.IsNotSupported(err) {
return gomaasapi.MAASObject{}, errors.Trace(err)
}
err = addInterfaces(acquireParams, interfaces, positiveSpaces, negativeSpaces)
if err != nil {
return gomaasapi.MAASObject{}, errors.Trace(err)
}
addStorage(acquireParams, volumes)
acquireParams.Add("agent_name", environ.uuid)
if zoneName != "" {
acquireParams.Add("zone", zoneName)
}
if nodeName != "" {
acquireParams.Add("name", nodeName)
}
var result gomaasapi.JSONObject
for a := shortAttempt.Start(); a.Next(); {
client := environ.getMAASClient().GetSubObject("nodes/")
logger.Tracef("calling acquire with params: %+v", acquireParams)
result, err = client.CallPost("acquire", acquireParams)
if err == nil {
break
}
}
if err != nil {
return gomaasapi.MAASObject{}, err
}
node, err := result.GetMAASObject()
if err != nil {
err := errors.Annotate(err, "unexpected result from 'acquire' on MAAS API")
return gomaasapi.MAASObject{}, err
}
return node, nil
}
// startNode installs and boots a node.
func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) (*gomaasapi.MAASObject, error) {
params := url.Values{
"distro_series": {series},
"user_data": {string(userdata)},
}
// Initialize err to a non-nil value as a sentinel for the following
// loop.
err := fmt.Errorf("(no error)")
var result gomaasapi.JSONObject
for a := shortAttempt.Start(); a.Next() && err != nil; {
result, err = node.CallPost("start", params)
if err == nil {
break
}
}
if err == nil {
var startedNode gomaasapi.MAASObject
startedNode, err = result.GetMAASObject()
if err != nil {
logger.Errorf("cannot process API response after successfully starting node: %v", err)
return nil, err
}
return &startedNode, nil
}
return nil, err
}
func (environ *maasEnviron) startNode2(node maas2Instance, series string, userdata []byte) (*maas2Instance, error) {
err := node.machine.Start(gomaasapi.StartArgs{DistroSeries: series, UserData: string(userdata)})
if err != nil {
return nil, errors.Trace(err)
}
// Machine.Start updates the machine in-place when it succeeds.
return &maas2Instance{machine: node.machine}, nil
}
// DistributeInstances implements the state.InstanceDistributor policy.
func (e *maasEnviron) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) {
return common.DistributeInstances(e, candidates, distributionGroup)
}
var availabilityZoneAllocations = common.AvailabilityZoneAllocations
// MaintainInstance is specified in the InstanceBroker interface.
func (*maasEnviron) MaintainInstance(args environs.StartInstanceParams) error {
return nil
}
// StartInstance is specified in the InstanceBroker interface.
func (environ *maasEnviron) StartInstance(args environs.StartInstanceParams) (
*environs.StartInstanceResult, error,
) {
var availabilityZones []string
var nodeName string
if args.Placement != "" {
placement, err := environ.parsePlacement(args.Placement)
if err != nil {
return nil, err
}
switch {
case placement.zoneName != "":
availabilityZones = append(availabilityZones, placement.zoneName)
default:
nodeName = placement.nodeName
}
}
// If no placement is specified, then automatically spread across
// the known zones for optimal spread across the instance distribution
// group.
if args.Placement == "" {
var group []instance.Id
var err error
if args.DistributionGroup != nil {
group, err = args.DistributionGroup()
if err != nil {
return nil, errors.Annotate(err, "cannot get distribution group")
}
}
zoneInstances, err := availabilityZoneAllocations(environ, group)
// TODO (mfoord): this branch is for old versions of MAAS and
// can be removed, but this means fixing tests.
if errors.IsNotImplemented(err) {
// Availability zones are an extension, so we may get a
// not implemented error; ignore these.
} else if err != nil {
return nil, errors.Annotate(err, "cannot get availability zone allocations")
} else if len(zoneInstances) > 0 {
for _, z := range zoneInstances {
availabilityZones = append(availabilityZones, z.ZoneName)
}
}
}
if len(availabilityZones) == 0 {
availabilityZones = []string{""}
}
// Storage.
volumes, err := buildMAASVolumeParameters(args.Volumes, args.Constraints)
if err != nil {
return nil, errors.Annotate(err, "invalid volume parameters")
}
var interfaceBindings []interfaceBinding
if len(args.EndpointBindings) != 0 {
for endpoint, spaceProviderID := range args.EndpointBindings {
interfaceBindings = append(interfaceBindings, interfaceBinding{
Name: endpoint,
SpaceProviderId: string(spaceProviderID),
})
}
}
snArgs := selectNodeArgs{
Constraints: args.Constraints,
AvailabilityZones: availabilityZones,
NodeName: nodeName,
Interfaces: interfaceBindings,
Volumes: volumes,
}
var inst maasInstance
if !environ.usingMAAS2() {
selectedNode, err := environ.selectNode(snArgs)
if err != nil {
return nil, errors.Errorf("cannot run instances: %v", err)
}
inst = &maas1Instance{
maasObject: selectedNode,
environ: environ,
statusGetter: environ.deploymentStatusOne,
}
} else {
inst, err = environ.selectNode2(snArgs)
if err != nil {
return nil, errors.Annotatef(err, "cannot run instances")
}
}
defer func() {
if err != nil {
if err := environ.StopInstances(inst.Id()); err != nil {
logger.Errorf("error releasing failed instance: %v", err)
}
}
}()
hc, err := inst.hardwareCharacteristics()
if err != nil {
return nil, err
}
series := args.Tools.OneSeries()
selectedTools, err := args.Tools.Match(tools.Filter{
Arch: *hc.Arch,
})
if err != nil {
return nil, errors.Trace(err)
}
if err := args.InstanceConfig.SetTools(selectedTools); err != nil {
return nil, errors.Trace(err)
}
hostname, err := inst.hostname()
if err != nil {
return nil, err
}
if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, environ.Config()); err != nil {
return nil, errors.Trace(err)
}
subnetsMap, err := environ.subnetToSpaceIds()
if err != nil {
return nil, errors.Trace(err)
}
cloudcfg, err := environ.newCloudinitConfig(hostname, series)
if err != nil {
return nil, errors.Trace(err)
}
userdata, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, MAASRenderer{})
if err != nil {
return nil, errors.Annotatef(err, "could not compose userdata for bootstrap node")
}
logger.Debugf("maas user data; %d bytes", len(userdata))
var interfaces []network.InterfaceInfo
if !environ.usingMAAS2() {
inst1 := inst.(*maas1Instance)
startedNode, err := environ.startNode(*inst1.maasObject, series, userdata)
if err != nil {
return nil, errors.Trace(err)
}
// Once the instance has started the response should contain the
// assigned IP addresses, even when NICs are set to "auto" instead of
// "static". So instead of selectedNode, which only contains the
// acquire-time details (no IP addresses for NICs set to "auto" vs
// "static"),e we use the up-to-date startedNode response to get the
// interfaces.
interfaces, err = maasObjectNetworkInterfaces(startedNode, subnetsMap)
if err != nil {
return nil, errors.Trace(err)
}
environ.tagInstance1(inst1, args.InstanceConfig)
} else {
inst2 := inst.(*maas2Instance)
startedInst, err := environ.startNode2(*inst2, series, userdata)
if err != nil {
return nil, errors.Trace(err)
}
interfaces, err = maas2NetworkInterfaces(startedInst, subnetsMap)
if err != nil {
return nil, errors.Trace(err)
}
environ.tagInstance2(inst2, args.InstanceConfig)
}
logger.Debugf("started instance %q", inst.Id())
requestedVolumes := make([]names.VolumeTag, len(args.Volumes))
for i, v := range args.Volumes {
requestedVolumes[i] = v.Tag
}
resultVolumes, resultAttachments, err := inst.volumes(
names.NewMachineTag(args.InstanceConfig.MachineId),
requestedVolumes,
)
if err != nil {
return nil, err
}
if len(resultVolumes) != len(requestedVolumes) {
err = errors.Errorf("requested %v storage volumes. %v returned.", len(requestedVolumes), len(resultVolumes))
return nil, err
}
return &environs.StartInstanceResult{
Instance: inst,
Hardware: hc,
NetworkInfo: interfaces,
Volumes: resultVolumes,
VolumeAttachments: resultAttachments,
}, nil
}
func instanceConfiguredInterfaceNames(usingMAAS2 bool, inst instance.Instance, subnetsMap map[string]network.Id) ([]string, error) {
var (
interfaces []network.InterfaceInfo
err error
)
if !usingMAAS2 {
inst1 := inst.(*maas1Instance)
interfaces, err = maasObjectNetworkInterfaces(inst1.maasObject, subnetsMap)
if err != nil {
return nil, errors.Trace(err)
}
} else {
inst2 := inst.(*maas2Instance)
interfaces, err = maas2NetworkInterfaces(inst2, subnetsMap)
if err != nil {
return nil, errors.Trace(err)
}
}
nameToNumAliases := make(map[string]int)
var linkedNames []string
for _, iface := range interfaces {
if iface.CIDR == "" { // CIDR comes from a linked subnet.
continue
}
switch iface.ConfigType {
case network.ConfigUnknown, network.ConfigManual:
continue // link is unconfigured
}
finalName := iface.InterfaceName
numAliases, seen := nameToNumAliases[iface.InterfaceName]
if !seen {
nameToNumAliases[iface.InterfaceName] = 0
} else {
numAliases++ // aliases start from 1
finalName += fmt.Sprintf(":%d", numAliases)
nameToNumAliases[iface.InterfaceName] = numAliases
}
linkedNames = append(linkedNames, finalName)
}
systemID := extractSystemId(inst.Id())
logger.Infof("interface names to bridge for node %q: %v", systemID, linkedNames)
return linkedNames, nil
}
func (environ *maasEnviron) tagInstance1(inst *maas1Instance, instanceConfig *instancecfg.InstanceConfig) {
if !multiwatcher.AnyJobNeedsState(instanceConfig.Jobs...) {
return
}
err := common.AddStateInstance(environ.Storage(), inst.Id())
if err != nil {
logger.Errorf("could not record instance in provider-state: %v", err)
}
}
func (environ *maasEnviron) tagInstance2(inst *maas2Instance, instanceConfig *instancecfg.InstanceConfig) {
err := inst.machine.SetOwnerData(instanceConfig.Tags)
if err != nil {
logger.Errorf("could not set owner data for instance: %v", err)
}
}
func (environ *maasEnviron) waitForNodeDeployment(id instance.Id, timeout time.Duration) error {
if environ.usingMAAS2() {
return environ.waitForNodeDeployment2(id, timeout)
}
systemId := extractSystemId(id)
longAttempt := utils.AttemptStrategy{
Delay: 10 * time.Second,
Total: timeout,
}
for a := longAttempt.Start(); a.Next(); {
statusValues, err := environ.deploymentStatus(id)
if errors.IsNotImplemented(err) {
return nil
}
if err != nil {
return errors.Trace(err)
}
if statusValues[systemId] == "Deployed" {
return nil
}
if statusValues[systemId] == "Failed deployment" {
return errors.Errorf("instance %q failed to deploy", id)
}
}
return errors.Errorf("instance %q is started but not deployed", id)
}
func (environ *maasEnviron) waitForNodeDeployment2(id instance.Id, timeout time.Duration) error {
// TODO(katco): 2016-08-09: lp:1611427
longAttempt := utils.AttemptStrategy{
Delay: 10 * time.Second,
Total: timeout,
}
for a := longAttempt.Start(); a.Next(); {
machine, err := environ.getInstance(id)
if err != nil {
return errors.Trace(err)
}
stat := machine.Status()
if stat.Status == status.Running {
return nil
}
if stat.Status == status.ProvisioningError {
return errors.Errorf("instance %q failed to deploy", id)
}
}
return errors.Errorf("instance %q is started but not deployed", id)
}
func (environ *maasEnviron) deploymentStatusOne(id instance.Id) (string, string) {
results, err := environ.deploymentStatus(id)
if err != nil {
return "", ""
}
systemId := extractSystemId(id)
substatus := environ.getDeploymentSubstatus(systemId)
return results[systemId], substatus
}
func (environ *maasEnviron) getDeploymentSubstatus(systemId string) string {
nodesAPI := environ.getMAASClient().GetSubObject("nodes")
result, err := nodesAPI.CallGet("list", nil)
if err != nil {
return ""
}
slices, err := result.GetArray()
if err != nil {
return ""
}
for _, slice := range slices {
resultMap, err := slice.GetMap()
if err != nil {
continue
}
sysId, err := resultMap["system_id"].GetString()
if err != nil {
continue
}
if sysId == systemId {
message, err := resultMap["substatus_message"].GetString()
if err != nil {
logger.Warningf("could not get string for substatus_message: %v", resultMap["substatus_message"])
return ""
}
return message
}
}
return ""
}
// deploymentStatus returns the deployment state of MAAS instances with
// the specified Juju instance ids.
// Note: the result is a map of MAAS systemId to state.
func (environ *maasEnviron) deploymentStatus(ids ...instance.Id) (map[string]string, error) {
nodesAPI := environ.getMAASClient().GetSubObject("nodes")
result, err := DeploymentStatusCall(nodesAPI, ids...)
if err != nil {
if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == http.StatusBadRequest {
return nil, errors.NewNotImplemented(err, "deployment status")
}
return nil, errors.Trace(err)
}
resultMap, err := result.GetMap()
if err != nil {
return nil, errors.Trace(err)
}
statusValues := make(map[string]string)
for systemId, jsonValue := range resultMap {
status, err := jsonValue.GetString()
if err != nil {
return nil, errors.Trace(err)
}
statusValues[systemId] = status
}
return statusValues, nil
}
func deploymentStatusCall(nodes gomaasapi.MAASObject, ids ...instance.Id) (gomaasapi.JSONObject, error) {
filter := getSystemIdValues("nodes", ids)
return nodes.CallGet("deployment_status", filter)
}
type selectNodeArgs struct {
AvailabilityZones []string
NodeName string
Constraints constraints.Value
Interfaces []interfaceBinding
Volumes []volumeInfo
}
func (environ *maasEnviron) selectNode(args selectNodeArgs) (*gomaasapi.MAASObject, error) {
var err error
var node gomaasapi.MAASObject
for i, zoneName := range args.AvailabilityZones {
node, err = environ.acquireNode(
args.NodeName,
zoneName,
args.Constraints,
args.Interfaces,
args.Volumes,
)
if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == http.StatusConflict {
if i+1 < len(args.AvailabilityZones) {
logger.Infof("could not acquire a node in zone %q, trying another zone", zoneName)
continue
}
}
if err != nil {
return nil, errors.Errorf("cannot run instances: %v", err)
}
// Since a return at the end of the function is required
// just break here.
break
}
return &node, nil
}
func (environ *maasEnviron) selectNode2(args selectNodeArgs) (maasInstance, error) {
var err error
var inst maasInstance
for i, zoneName := range args.AvailabilityZones {
inst, err = environ.acquireNode2(
args.NodeName,
zoneName,
args.Constraints,
args.Interfaces,
args.Volumes,
)
if gomaasapi.IsNoMatchError(err) {
if i+1 < len(args.AvailabilityZones) {
logger.Infof("could not acquire a node in zone %q, trying another zone", zoneName)
continue
}
}
if err != nil {
return nil, errors.Annotatef(err, "cannot run instance")
}
// Since a return at the end of the function is required
// just break here.
break
}
return inst, nil
}
// newCloudinitConfig creates a cloudinit.Config structure suitable as a base
// for initialising a MAAS node.
func (environ *maasEnviron) newCloudinitConfig(hostname, forSeries string) (cloudinit.CloudConfig, error) {
cloudcfg, err := cloudinit.New(forSeries)
if err != nil {
return nil, err
}
info := machineInfo{hostname}
runCmd, err := info.cloudinitRunCmd(cloudcfg)
if err != nil {
return nil, errors.Trace(err)
}
operatingSystem, err := series.GetOSFromSeries(forSeries)
if err != nil {
return nil, errors.Trace(err)
}
switch operatingSystem {
case os.Windows:
cloudcfg.AddScripts(runCmd)
case os.Ubuntu:
cloudcfg.SetSystemUpdate(true)
cloudcfg.AddScripts("set -xe", runCmd)
// DisableNetworkManagement can still disable the bridge(s) creation.
if on, set := environ.Config().DisableNetworkManagement(); on && set {
logger.Infof(
"network management disabled - not using %q bridge for containers",
instancecfg.DefaultBridgeName,
)
break
}
cloudcfg.AddPackage("bridge-utils")
}
return cloudcfg, nil
}
func (environ *maasEnviron) releaseNodes1(nodes gomaasapi.MAASObject, ids url.Values, recurse bool) error {
err := ReleaseNodes(nodes, ids)
if err == nil {
return nil
}
maasErr, ok := gomaasapi.GetServerError(err)
if !ok {
return errors.Annotate(err, "cannot release nodes")
}
// StatusCode 409 means a node couldn't be released due to
// a state conflict. Likely it's already released or disk
// erasing. We're assuming an error of 409 *only* means it's
// safe to assume the instance is already released.
// MaaS also releases (or attempts) all nodes, and raises
// a single error on failure. So even with an error 409, all
// nodes have been released.
if maasErr.StatusCode == 409 {
logger.Infof("ignoring error while releasing nodes (%v); all nodes released OK", err)
return nil
}
// a status code of 400, 403 or 404 means one of the nodes
// couldn't be found and none have been released. We have
// to release all the ones we can individually.
if maasErr.StatusCode != 400 && maasErr.StatusCode != 403 && maasErr.StatusCode != 404 {
return errors.Annotate(err, "cannot release nodes")
}
if !recurse {
// this node has already been released and we're golden
return nil
}
var lastErr error
for _, id := range ids["nodes"] {
idFilter := url.Values{}
idFilter.Add("nodes", id)
err := environ.releaseNodes1(nodes, idFilter, false)
if err != nil {
lastErr = err
logger.Errorf("error while releasing node %v (%v)", id, err)
}
}
return errors.Trace(lastErr)
}
func (environ *maasEnviron) releaseNodes2(ids []instance.Id, recurse bool) error {
args := gomaasapi.ReleaseMachinesArgs{
SystemIDs: instanceIdsToSystemIDs(ids),
Comment: "Released by Juju MAAS provider",
}
err := environ.maasController.ReleaseMachines(args)
switch {
case err == nil:
return nil
case gomaasapi.IsCannotCompleteError(err):
// CannotCompleteError means a node couldn't be released due to
// a state conflict. Likely it's already released or disk
// erasing. We're assuming this error *only* means it's
// safe to assume the instance is already released.
// MaaS also releases (or attempts) all nodes, and raises
// a single error on failure. So even with an error 409, all
// nodes have been released.
logger.Infof("ignoring error while releasing nodes (%v); all nodes released OK", err)
return nil
case gomaasapi.IsBadRequestError(err), gomaasapi.IsPermissionError(err):
// a status code of 400 or 403 means one of the nodes
// couldn't be found and none have been released. We have to
// release all the ones we can individually.
if !recurse {
// this node has already been released and we're golden
return nil
}
return environ.releaseNodesIndividually(ids)
default:
return errors.Annotatef(err, "cannot release nodes")
}
}
func (environ *maasEnviron) releaseNodesIndividually(ids []instance.Id) error {
var lastErr error
for _, id := range ids {
err := environ.releaseNodes2([]instance.Id{id}, false)
if err != nil {
lastErr = err
logger.Errorf("error while releasing node %v (%v)", id, err)
}
}
return errors.Trace(lastErr)
}
func instanceIdsToSystemIDs(ids []instance.Id) []string {
systemIDs := make([]string, len(ids))
for index, id := range ids {
systemIDs[index] = string(id)
}
return systemIDs
}
// StopInstances is specified in the InstanceBroker interface.
func (environ *maasEnviron) StopInstances(ids ...instance.Id) error {
// Shortcut to exit quickly if 'instances' is an empty slice or nil.
if len(ids) == 0 {
return nil
}
if environ.usingMAAS2() {
err := environ.releaseNodes2(ids, true)
if err != nil {
return errors.Trace(err)
}
} else {
nodes := environ.getMAASClient().GetSubObject("nodes")
err := environ.releaseNodes1(nodes, getSystemIdValues("nodes", ids), true)
if err != nil {
return errors.Trace(err)
}
}
return common.RemoveStateInstances(environ.Storage(), ids...)
}
// acquireInstances calls the MAAS API to list acquired nodes.
//
// The "ids" slice is a filter for specific instance IDs.
// Due to how this works in the HTTP API, an empty "ids"
// matches all instances (not none as you might expect).
func (environ *maasEnviron) acquiredInstances(ids []instance.Id) ([]instance.Instance, error) {
if !environ.usingMAAS2() {
filter := getSystemIdValues("id", ids)
filter.Add("agent_name", environ.uuid)
return environ.instances1(filter)
}
args := gomaasapi.MachinesArgs{
AgentName: environ.uuid,
SystemIDs: instanceIdsToSystemIDs(ids),
}
return environ.instances2(args)
}
// instances calls the MAAS API to list nodes matching the given filter.
func (environ *maasEnviron) instances1(filter url.Values) ([]instance.Instance, error) {
nodeListing := environ.getMAASClient().GetSubObject("nodes")
listNodeObjects, err := nodeListing.CallGet("list", filter)
if err != nil {
return nil, err
}
listNodes, err := listNodeObjects.GetArray()
if err != nil {
return nil, err
}
instances := make([]instance.Instance, len(listNodes))
for index, nodeObj := range listNodes {
node, err := nodeObj.GetMAASObject()
if err != nil {
return nil, err
}
instances[index] = &maas1Instance{
maasObject: &node,
environ: environ,
statusGetter: environ.deploymentStatusOne,
}
}
return instances, nil
}
func (environ *maasEnviron) instances2(args gomaasapi.MachinesArgs) ([]instance.Instance, error) {
machines, err := environ.maasController.Machines(args)
if err != nil {
return nil, errors.Trace(err)
}
instances := make([]instance.Instance, len(machines))
for index, machine := range machines {
instances[index] = &maas2Instance{machine: machine}
}
return instances, nil
}
// Instances returns the instance.Instance objects corresponding to the given
// slice of instance.Id. The error is ErrNoInstances if no instances
// were found.
func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
if len(ids) == 0 {
// This would be treated as "return all instances" below, so
// treat it as a special case.
// The interface requires us to return this particular error
// if no instances were found.
return nil, environs.ErrNoInstances
}
instances, err := environ.acquiredInstances(ids)
if err != nil {
return nil, errors.Trace(err)
}
if len(instances) == 0 {
return nil, environs.ErrNoInstances
}
idMap := make(map[instance.Id]instance.Instance)
for _, instance := range instances {
idMap[instance.Id()] = instance
}
missing := false
result := make([]instance.Instance, len(ids))
for index, id := range ids {
val, ok := idMap[id]
if !ok {
missing = true
continue
}
result[index] = val
}
if missing {
return result, environs.ErrPartialInstances
}
return result, nil
}
// subnetsFromNode fetches all the subnets for a specific node.
func (environ *maasEnviron) subnetsFromNode(nodeId string) ([]gomaasapi.JSONObject, error) {
client := environ.getMAASClient().GetSubObject("nodes").GetSubObject(nodeId)
json, err := client.CallGet("", nil)
if err != nil {
if maasErr, ok := errors.Cause(err).(gomaasapi.ServerError); ok && maasErr.StatusCode == http.StatusNotFound {
return nil, errors.NotFoundf("intance %q", nodeId)
}
return nil, errors.Trace(err)
}
nodeMap, err := json.GetMap()
if err != nil {
return nil, errors.Trace(err)
}
interfacesArray, err := nodeMap["interface_set"].GetArray()
if err != nil {
return nil, errors.Trace(err)
}
var subnets []gomaasapi.JSONObject
for _, iface := range interfacesArray {
ifaceMap, err := iface.GetMap()
if err != nil {
return nil, errors.Trace(err)
}
linksArray, err := ifaceMap["links"].GetArray()
if err != nil {
return nil, errors.Trace(err)
}
for _, link := range linksArray {
linkMap, err := link.GetMap()
if err != nil {
return nil, errors.Trace(err)
}
subnet, ok := linkMap["subnet"]
if !ok {
return nil, errors.New("subnet not found")
}
subnets = append(subnets, subnet)
}
}
return subnets, nil
}
// subnetFromJson populates a network.SubnetInfo from a gomaasapi.JSONObject
// representing a single subnet. This can come from either the subnets api
// endpoint or the node endpoint.
func (environ *maasEnviron) subnetFromJson(subnet gomaasapi.JSONObject, spaceId network.Id) (network.SubnetInfo, error) {
var subnetInfo network.SubnetInfo
fields, err := subnet.GetMap()
if err != nil {
return subnetInfo, errors.Trace(err)
}
subnetIdFloat, err := fields["id"].GetFloat64()
if err != nil {
return subnetInfo, errors.Annotatef(err, "cannot get subnet Id")
}
subnetId := strconv.Itoa(int(subnetIdFloat))
cidr, err := fields["cidr"].GetString()
if err != nil {
return subnetInfo, errors.Annotatef(err, "cannot get cidr")
}
vid := 0
vidField, ok := fields["vid"]
if ok && !vidField.IsNil() {
// vid is optional, so assume it's 0 when missing or nil.
vidFloat, err := vidField.GetFloat64()
if err != nil {
return subnetInfo, errors.Errorf("cannot get vlan tag: %v", err)
}
vid = int(vidFloat)
}
subnetInfo = network.SubnetInfo{
ProviderId: network.Id(subnetId),
VLANTag: vid,
CIDR: cidr,
SpaceProviderId: spaceId,
}
return subnetInfo, nil
}
// filteredSubnets fetches subnets, filtering optionally by nodeId and/or a
// slice of subnetIds. If subnetIds is empty then all subnets for that node are
// fetched. If nodeId is empty, all subnets are returned (filtering by subnetIds
// first, if set).
func (environ *maasEnviron) filteredSubnets(nodeId string, subnetIds []network.Id) ([]network.SubnetInfo, error) {
var jsonNets []gomaasapi.JSONObject
var err error
if nodeId != "" {
jsonNets, err = environ.subnetsFromNode(nodeId)
if err != nil {
return nil, errors.Trace(err)
}
} else {
jsonNets, err = environ.fetchAllSubnets()
if err != nil {
return nil, errors.Trace(err)
}
}
subnetIdSet := make(map[string]bool)
for _, netId := range subnetIds {
subnetIdSet[string(netId)] = false
}
subnetsMap, err := environ.subnetToSpaceIds()
if err != nil {
return nil, errors.Trace(err)
}
subnets := []network.SubnetInfo{}
for _, jsonNet := range jsonNets {
fields, err := jsonNet.GetMap()
if err != nil {
return nil, err
}
subnetIdFloat, err := fields["id"].GetFloat64()
if err != nil {
return nil, errors.Annotatef(err, "cannot get subnet Id: %v")
}
subnetId := strconv.Itoa(int(subnetIdFloat))
// If we're filtering by subnet id check if this subnet is one
// we're looking for.
if len(subnetIds) != 0 {
_, ok := subnetIdSet[subnetId]
if !ok {
// This id is not what we're looking for.
continue
}
subnetIdSet[subnetId] = true
}
cidr, err := fields["cidr"].GetString()
if err != nil {
return nil, errors.Annotatef(err, "cannot get subnet Id")
}
spaceId, ok := subnetsMap[cidr]
if !ok {
logger.Warningf("unrecognised subnet: %q, setting empty space id", cidr)
spaceId = network.UnknownId
}
subnetInfo, err := environ.subnetFromJson(jsonNet, spaceId)
if err != nil {
return nil, errors.Trace(err)
}
subnets = append(subnets, subnetInfo)
logger.Tracef("found subnet with info %#v", subnetInfo)
}
return subnets, checkNotFound(subnetIdSet)
}
func (environ *maasEnviron) getInstance(instId instance.Id) (instance.Instance, error) {
instances, err := environ.acquiredInstances([]instance.Id{instId})
if err != nil {
// This path can never trigger on MAAS 2, but MAAS 2 doesn't
// return an error for a machine not found, it just returns
// empty results. The clause below catches that.
if maasErr, ok := errors.Cause(err).(gomaasapi.ServerError); ok && maasErr.StatusCode == http.StatusNotFound {
return nil, errors.NotFoundf("instance %q", instId)
}
return nil, errors.Annotatef(err, "getting instance %q", instId)
}
if len(instances) == 0 {
return nil, errors.NotFoundf("instance %q", instId)
}
inst := instances[0]
return inst, nil
}
// fetchAllSubnets calls the MAAS subnets API to get all subnets and returns the
// JSON response or an error. If capNetworkDeploymentUbuntu is not available, an
// error satisfying errors.IsNotSupported will be returned.
func (environ *maasEnviron) fetchAllSubnets() ([]gomaasapi.JSONObject, error) {
client := environ.getMAASClient().GetSubObject("subnets")
json, err := client.CallGet("", nil)
if err != nil {
return nil, errors.Trace(err)
}
return json.GetArray()
}
// subnetToSpaceIds fetches the spaces from MAAS and builds a map of subnets to
// space ids.
func (environ *maasEnviron) subnetToSpaceIds() (map[string]network.Id, error) {
subnetsMap := make(map[string]network.Id)
spaces, err := environ.Spaces()
if err != nil {
return subnetsMap, errors.Trace(err)
}
for _, space := range spaces {
for _, subnet := range space.Subnets {
subnetsMap[subnet.CIDR] = space.ProviderId
}
}
return subnetsMap, nil
}
// Spaces returns all the spaces, that have subnets, known to the provider.
// Space name is not filled in as the provider doesn't know the juju name for
// the space.
func (environ *maasEnviron) Spaces() ([]network.SpaceInfo, error) {
if !environ.usingMAAS2() {
return environ.spaces1()
}
return environ.spaces2()
}
func (environ *maasEnviron) spaces1() ([]network.SpaceInfo, error) {
spacesClient := environ.getMAASClient().GetSubObject("spaces")
spacesJson, err := spacesClient.CallGet("", nil)
if err != nil {
return nil, errors.Trace(err)
}
spacesArray, err := spacesJson.GetArray()
if err != nil {
return nil, errors.Trace(err)
}
spaces := []network.SpaceInfo{}
for _, spaceJson := range spacesArray {
spaceMap, err := spaceJson.GetMap()
if err != nil {
return nil, errors.Trace(err)
}
providerIdRaw, err := spaceMap["id"].GetFloat64()
if err != nil {
return nil, errors.Trace(err)
}
providerId := network.Id(fmt.Sprintf("%.0f", providerIdRaw))
name, err := spaceMap["name"].GetString()
if err != nil {
return nil, errors.Trace(err)
}
space := network.SpaceInfo{Name: name, ProviderId: providerId}
subnetsArray, err := spaceMap["subnets"].GetArray()
if err != nil {
return nil, errors.Trace(err)
}
for _, subnetJson := range subnetsArray {
subnet, err := environ.subnetFromJson(subnetJson, providerId)
if err != nil {
return nil, errors.Trace(err)
}
space.Subnets = append(space.Subnets, subnet)
}
// Skip spaces with no subnets.
if len(space.Subnets) > 0 {
spaces = append(spaces, space)
}
}
return spaces, nil
}
func (environ *maasEnviron) spaces2() ([]network.SpaceInfo, error) {
spaces, err := environ.maasController.Spaces()
if err != nil {
return nil, errors.Trace(err)
}
var result []network.SpaceInfo
for _, space := range spaces {
if len(space.Subnets()) == 0 {
continue
}
outSpace := network.SpaceInfo{
Name: space.Name(),
ProviderId: network.Id(strconv.Itoa(space.ID())),
Subnets: make([]network.SubnetInfo, len(space.Subnets())),
}
for i, subnet := range space.Subnets() {
subnetInfo := network.SubnetInfo{
ProviderId: network.Id(strconv.Itoa(subnet.ID())),
VLANTag: subnet.VLAN().VID(),
CIDR: subnet.CIDR(),
SpaceProviderId: network.Id(strconv.Itoa(space.ID())),
}
outSpace.Subnets[i] = subnetInfo
}
result = append(result, outSpace)
}
return result, nil
}
// Subnets returns basic information about the specified subnets known
// by the provider for the specified instance. subnetIds must not be
// empty. Implements NetworkingEnviron.Subnets.
func (environ *maasEnviron) Subnets(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) {
if environ.usingMAAS2() {
return environ.subnets2(instId, subnetIds)
}
return environ.subnets1(instId, subnetIds)
}
func (environ *maasEnviron) subnets1(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) {
var nodeId string
if instId != instance.UnknownId {
inst, err := environ.getInstance(instId)
if err != nil {
return nil, errors.Trace(err)
}
nodeId, err = environ.nodeIdFromInstance(inst)
if err != nil {
return nil, errors.Trace(err)
}
}
subnets, err := environ.filteredSubnets(nodeId, subnetIds)
if err != nil {
return nil, errors.Trace(err)
}
if instId != instance.UnknownId {
logger.Debugf("instance %q has subnets %v", instId, subnets)
} else {
logger.Debugf("found subnets %v", subnets)
}
return subnets, nil
}
func (environ *maasEnviron) subnets2(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) {
subnets := []network.SubnetInfo{}
if instId == instance.UnknownId {
spaces, err := environ.Spaces()
if err != nil {
return nil, errors.Trace(err)
}
for _, space := range spaces {
subnets = append(subnets, space.Subnets...)
}
} else {
var err error
subnets, err = environ.filteredSubnets2(instId)
if err != nil {
return nil, errors.Trace(err)
}
}
if len(subnetIds) == 0 {
return subnets, nil
}
result := []network.SubnetInfo{}
subnetMap := make(map[string]bool)
for _, subnetId := range subnetIds {
subnetMap[string(subnetId)] = false
}
for _, subnet := range subnets {
_, ok := subnetMap[string(subnet.ProviderId)]
if !ok {
// This id is not what we're looking for.
continue
}
subnetMap[string(subnet.ProviderId)] = true
result = append(result, subnet)
}
return result, checkNotFound(subnetMap)
}
func (environ *maasEnviron) filteredSubnets2(instId instance.Id) ([]network.SubnetInfo, error) {
args := gomaasapi.MachinesArgs{
AgentName: environ.uuid,
SystemIDs: []string{string(instId)},
}
machines, err := environ.maasController.Machines(args)
if err != nil {
return nil, errors.Trace(err)
}
if len(machines) == 0 {
return nil, errors.NotFoundf("machine %v", instId)
} else if len(machines) > 1 {
return nil, errors.Errorf("unexpected response getting machine details %v: %v", instId, machines)
}
machine := machines[0]
spaceMap, err := environ.buildSpaceMap()
if err != nil {
return nil, errors.Trace(err)
}
result := []network.SubnetInfo{}
for _, iface := range machine.InterfaceSet() {
for _, link := range iface.Links() {
subnet := link.Subnet()
space, ok := spaceMap[subnet.Space()]
if !ok {
return nil, errors.Errorf("missing space %v on subnet %v", subnet.Space(), subnet.CIDR())
}
subnetInfo := network.SubnetInfo{
ProviderId: network.Id(strconv.Itoa(subnet.ID())),
VLANTag: subnet.VLAN().VID(),
CIDR: subnet.CIDR(),
SpaceProviderId: space.ProviderId,
}
result = append(result, subnetInfo)
}
}
return result, nil
}
func checkNotFound(subnetIdSet map[string]bool) error {
notFound := []string{}
for subnetId, found := range subnetIdSet {
if !found {
notFound = append(notFound, string(subnetId))
}
}
if len(notFound) != 0 {
return errors.Errorf("failed to find the following subnets: %v", strings.Join(notFound, ", "))
}
return nil
}
// AllInstances returns all the instance.Instance in this provider.
func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) {
return environ.acquiredInstances(nil)
}
// Storage is defined by the Environ interface.
func (env *maasEnviron) Storage() storage.Storage {
env.ecfgMutex.Lock()
defer env.ecfgMutex.Unlock()
return env.storageUnlocked
}
func (environ *maasEnviron) Destroy() error {
if err := common.Destroy(environ); err != nil {
return errors.Trace(err)
}
return environ.Storage().RemoveAll()
}
// DestroyController implements the Environ interface.
func (environ *maasEnviron) DestroyController(controllerUUID string) error {
// TODO(wallyworld): destroy hosted model resources
return environ.Destroy()
}
// MAAS does not do firewalling so these port methods do nothing.
func (*maasEnviron) OpenPorts([]network.IngressRule) error {
logger.Debugf("unimplemented OpenPorts() called")
return nil
}
func (*maasEnviron) ClosePorts([]network.IngressRule) error {
logger.Debugf("unimplemented ClosePorts() called")
return nil
}
func (*maasEnviron) IngressRules() ([]network.IngressRule, error) {
logger.Debugf("unimplemented Rules() called")
return nil, nil
}
func (*maasEnviron) Provider() environs.EnvironProvider {
return &providerInstance
}
func (environ *maasEnviron) nodeIdFromInstance(inst instance.Instance) (string, error) {
maasInst := inst.(*maas1Instance)
maasObj := maasInst.maasObject
nodeId, err := maasObj.GetField("system_id")
if err != nil {
return "", err
}
return nodeId, err
}
func (env *maasEnviron) AllocateContainerAddresses(hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) {
if len(preparedInfo) == 0 {
return nil, errors.Errorf("no prepared info to allocate")
}
logger.Debugf("using prepared container info: %+v", preparedInfo)
if !env.usingMAAS2() {
return env.allocateContainerAddresses1(hostInstanceID, containerTag, preparedInfo)
}
return env.allocateContainerAddresses2(hostInstanceID, containerTag, preparedInfo)
}
func (env *maasEnviron) allocateContainerAddresses1(hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) {
subnetCIDRToVLANID := make(map[string]string)
subnetsAPI := env.getMAASClient().GetSubObject("subnets")
result, err := subnetsAPI.CallGet("", nil)
if err != nil {
return nil, errors.Annotate(err, "cannot get subnets")
}
subnetsJSON, err := getJSONBytes(result)
if err != nil {
return nil, errors.Annotate(err, "cannot get subnets JSON")
}
var subnets []maasSubnet
if err := json.Unmarshal(subnetsJSON, &subnets); err != nil {
return nil, errors.Annotate(err, "cannot parse subnets JSON")
}
for _, subnet := range subnets {
subnetCIDRToVLANID[subnet.CIDR] = strconv.Itoa(subnet.VLAN.ID)
}
var primaryNICInfo network.InterfaceInfo
for _, nic := range preparedInfo {
if nic.InterfaceName == "eth0" {
primaryNICInfo = nic
break
}
}
if primaryNICInfo.InterfaceName == "" {
return nil, errors.Errorf("cannot find primary interface for container")
}
logger.Debugf("primary device NIC prepared info: %+v", primaryNICInfo)
deviceName, err := env.namespace.Hostname(containerTag.Id())
if err != nil {
return nil, errors.Trace(err)
}
primaryMACAddress := primaryNICInfo.MACAddress
containerDevice, err := env.createDevice(hostInstanceID, deviceName, primaryMACAddress)
if err != nil {
return nil, errors.Annotate(err, "cannot create device for container")
}
deviceID := instance.Id(containerDevice.ResourceURI)
logger.Debugf("created device %q with primary MAC address %q", deviceID, primaryMACAddress)
interfaces, err := env.deviceInterfaces(deviceID)
if err != nil {
return nil, errors.Annotate(err, "cannot get device interfaces")
}
if len(interfaces) != 1 {
return nil, errors.Errorf("expected 1 device interface, got %d", len(interfaces))
}
primaryNICName := interfaces[0].Name
primaryNICID := strconv.Itoa(interfaces[0].ID)
primaryNICSubnetCIDR := primaryNICInfo.CIDR
primaryNICVLANID, hasSubnet := subnetCIDRToVLANID[primaryNICSubnetCIDR]
if hasSubnet {
updatedPrimaryNIC, err := env.updateDeviceInterface(deviceID, primaryNICID, primaryNICName, primaryMACAddress, primaryNICVLANID)
if err != nil {
return nil, errors.Annotatef(err, "cannot update device interface %q", interfaces[0].Name)
}
logger.Debugf("device %q primary interface %q updated: %+v", containerDevice.SystemID, primaryNICName, updatedPrimaryNIC)
}
deviceNICIDs := make([]string, len(preparedInfo))
nameToParentName := make(map[string]string)
for i, nic := range preparedInfo {
maasNICID := ""
nameToParentName[nic.InterfaceName] = nic.ParentInterfaceName
nicVLANID, knownSubnet := subnetCIDRToVLANID[nic.CIDR]
if nic.InterfaceName != primaryNICName {
if !knownSubnet {
logger.Warningf("NIC %v has no subnet - setting to manual and using untagged VLAN", nic.InterfaceName)
nicVLANID = primaryNICVLANID
} else {
logger.Infof("linking NIC %v to subnet %v - using static IP", nic.InterfaceName, nic.CIDR)
}
createdNIC, err := env.createDeviceInterface(deviceID, nic.InterfaceName, nic.MACAddress, nicVLANID)
if err != nil {
return nil, errors.Annotate(err, "creating device interface")
}
maasNICID = strconv.Itoa(createdNIC.ID)
logger.Debugf("created device interface: %+v", createdNIC)
} else {
maasNICID = primaryNICID
}
deviceNICIDs[i] = maasNICID
subnetID := string(nic.ProviderSubnetId)
if !knownSubnet {
continue
}
linkedInterface, err := env.linkDeviceInterfaceToSubnet(deviceID, maasNICID, subnetID, modeStatic)
if err != nil {
logger.Warningf("linking NIC %v to subnet %v failed: %v", nic.InterfaceName, nic.CIDR, err)
} else {
logger.Debugf("linked device interface to subnet: %+v", linkedInterface)
}
}
finalInterfaces, err := env.deviceInterfaceInfo(deviceID, nameToParentName)
if err != nil {
return nil, errors.Annotate(err, "cannot get device interfaces")
}
logger.Debugf("allocated device interfaces: %+v", finalInterfaces)
return finalInterfaces, nil
}
func (env *maasEnviron) allocateContainerAddresses2(hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) {
subnetCIDRToSubnet := make(map[string]gomaasapi.Subnet)
spaces, err := env.maasController.Spaces()
if err != nil {
return nil, errors.Trace(err)
}
for _, space := range spaces {
for _, subnet := range space.Subnets() {
subnetCIDRToSubnet[subnet.CIDR()] = subnet
}
}
// map from the source subnet (what subnet is the device in), to what
// static routes should be used.
subnetToStaticRoutes := make(map[string][]gomaasapi.StaticRoute)
staticRoutes, err := env.maasController.StaticRoutes()
if err != nil {
// MAAS 2.0 does not support static-routes, and will return a 404. MAAS
// does not report support for static-routes in its capabilities, nor
// does it have a different API version between 2.1 and 2.0. So we make
// the attempt, and treat a 404 as not having any configured static
// routes.
// gomaaasapi wraps a ServerError in an UnexpectedError, so we need to
// dig to make sure we have the right cause:
handled := false
if gomaasapi.IsUnexpectedError(err) {
msg := err.Error()
if strings.Contains(msg, "404") &&
strings.Contains(msg, "Unknown API endpoint:") &&
strings.Contains(msg, "/static-routes/") {
logger.Debugf("static-routes not supported: %v", err)
handled = true
staticRoutes = nil
} else {
logger.Warningf("IsUnexpectedError, but didn't match: %q %#v", msg, err)
}
} else {
logger.Warningf("not IsUnexpectedError: %#v", err)
}
if !handled {
logger.Warningf("error looking up static-routes: %v", err)
return nil, errors.Annotate(err, "unable to look up static-routes")
}
}
for _, route := range staticRoutes {
source := route.Source()
sourceCIDR := source.CIDR()
subnetToStaticRoutes[sourceCIDR] = append(subnetToStaticRoutes[sourceCIDR], route)
}
logger.Debugf("found static routes: %# v", subnetToStaticRoutes)
// Containers always use 'eth0' as their primary NIC
var primaryNICInfo network.InterfaceInfo
primaryNICName := "eth0"
for _, nic := range preparedInfo {
if nic.InterfaceName == primaryNICName {
primaryNICInfo = nic
break
}
}
if primaryNICInfo.InterfaceName == "" {
return nil, errors.Errorf("cannot find primary interface for container")
}
logger.Debugf("primary device NIC prepared info: %+v", primaryNICInfo)
primaryNICSubnetCIDR := primaryNICInfo.CIDR
subnet, hasSubnet := subnetCIDRToSubnet[primaryNICSubnetCIDR]
if !hasSubnet {
logger.Debugf("primary device NIC %q has no linked subnet - leaving unconfigured", primaryNICInfo.InterfaceName)
}
primaryMACAddress := primaryNICInfo.MACAddress
args := gomaasapi.MachinesArgs{
AgentName: env.uuid,
SystemIDs: []string{string(hostInstanceID)},
}
machines, err := env.maasController.Machines(args)
if err != nil {
return nil, errors.Trace(err)
}
if len(machines) != 1 {
return nil, errors.Errorf("unexpected response fetching machine %v: %v", hostInstanceID, machines)
}
machine := machines[0]
deviceName, err := env.namespace.Hostname(containerTag.Id())
if err != nil {
return nil, errors.Trace(err)
}
// Check to see if we've already tried to allocate information for this device:
devicesArgs := gomaasapi.DevicesArgs{
Hostname: []string{deviceName},
}
var device gomaasapi.Device
maybeDevices, err := machine.Devices(devicesArgs)
if err != nil {
logger.Warningf("error while trying to lookup %q: %v", deviceName, err)
} else {
if len(maybeDevices) == 1 {
logger.Debugf("found MAAS device for container %q, using existing device", deviceName)
device = maybeDevices[0]
} else if len(maybeDevices) > 1 {
logger.Warningf("found more than 1 MAAS devices (%d) for container %q", len(maybeDevices), deviceName)
return nil, errors.Errorf("found more than 1 MAAS device (%d) for container %q", len(maybeDevices), deviceName)
} else {
logger.Debugf("no existing MAAS devices for container %q, creating", deviceName)
}
}
if device == nil {
// The device didn't already exist, so we Create it.
createDeviceArgs := gomaasapi.CreateMachineDeviceArgs{
Hostname: deviceName,
MACAddress: primaryMACAddress,
Subnet: subnet, // can be nil
InterfaceName: primaryNICName,
}
device, err = machine.CreateDevice(createDeviceArgs)
if err != nil {
return nil, errors.Trace(err)
}
}
interface_set := device.InterfaceSet()
if len(interface_set) != 1 {
// Shouldn't be possible as machine.CreateDevice always returns us
// one interface.
return nil, errors.Errorf("unexpected number of interfaces in response from creating device: %v", interface_set)
}
primaryNICVLAN := interface_set[0].VLAN()
nameToParentName := make(map[string]string)
for _, nic := range preparedInfo {
nameToParentName[nic.InterfaceName] = nic.ParentInterfaceName
if nic.InterfaceName != primaryNICName {
createArgs := gomaasapi.CreateInterfaceArgs{
Name: nic.InterfaceName,
MTU: nic.MTU,
MACAddress: nic.MACAddress,
}
subnet, knownSubnet := subnetCIDRToSubnet[nic.CIDR]
if !knownSubnet {
logger.Warningf("NIC %v has no subnet - setting to manual and using 'primaryNIC' VLAN %d", nic.InterfaceName, primaryNICVLAN.ID())
createArgs.VLAN = primaryNICVLAN
} else {
createArgs.VLAN = subnet.VLAN()
logger.Infof("linking NIC %v to subnet %v - using static IP", nic.InterfaceName, subnet.CIDR())
}
createdNIC, err := device.CreateInterface(createArgs)
if err != nil {
return nil, errors.Annotate(err, "creating device interface")
}
logger.Debugf("created device interface: %+v", createdNIC)
if !knownSubnet {
continue
}
linkArgs := gomaasapi.LinkSubnetArgs{
Mode: gomaasapi.LinkModeStatic,
Subnet: subnet,
}
if err := createdNIC.LinkSubnet(linkArgs); err != nil {
logger.Warningf("linking NIC %v to subnet %v failed: %v", nic.InterfaceName, subnet.CIDR(), err)
} else {
logger.Debugf("linked device interface to subnet: %+v", createdNIC)
}
}
}
finalInterfaces, err := env.deviceInterfaceInfo2(device.SystemID(), nameToParentName, subnetToStaticRoutes)
if err != nil {
return nil, errors.Annotate(err, "cannot get device interfaces")
}
logger.Debugf("allocated device interfaces: %+v", finalInterfaces)
return finalInterfaces, nil
}
func (env *maasEnviron) ReleaseContainerAddresses(interfaces []network.ProviderInterfaceInfo) error {
macAddresses := make([]string, len(interfaces))
for i, info := range interfaces {
macAddresses[i] = info.MACAddress
}
if !env.usingMAAS2() {
return env.releaseContainerAddresses1(macAddresses)
}
return env.releaseContainerAddresses2(macAddresses)
}
func (env *maasEnviron) releaseContainerAddresses1(macAddresses []string) error {
devicesAPI := env.getMAASClient().GetSubObject("devices")
values := url.Values{}
for _, address := range macAddresses {
values.Add("mac_address", address)
}
result, err := devicesAPI.CallGet("list", values)
if err != nil {
return errors.Trace(err)
}
devicesArray, err := result.GetArray()
if err != nil {
return errors.Trace(err)
}
deviceIds := make([]string, len(devicesArray))
for i, deviceItem := range devicesArray {
deviceMap, err := deviceItem.GetMap()
if err != nil {
return errors.Trace(err)
}
id, err := deviceMap["system_id"].GetString()
if err != nil {
return errors.Trace(err)
}
deviceIds[i] = id
}
// If one device matched on multiple MAC addresses (like for
// multi-nic containers) it will be in the slice multiple
// times. Skip devices we've seen already.
deviceIdSet := set.NewStrings(deviceIds...)
deviceIds = deviceIdSet.SortedValues()
for _, id := range deviceIds {
err := devicesAPI.GetSubObject(id).Delete()
if err != nil {
return errors.Annotatef(err, "deleting device %s", id)
}
}
return nil
}
func (env *maasEnviron) releaseContainerAddresses2(macAddresses []string) error {
devices, err := env.maasController.Devices(gomaasapi.DevicesArgs{MACAddresses: macAddresses})
if err != nil {
return errors.Trace(err)
}
// If one device matched on multiple MAC addresses (like for
// multi-nic containers) it will be in the slice multiple
// times. Skip devices we've seen already.
seen := set.NewStrings()
for _, device := range devices {
if seen.Contains(device.SystemID()) {
continue
}
seen.Add(device.SystemID())
err = device.Delete()
if err != nil {
return errors.Annotatef(err, "deleting device %s", device.SystemID())
}
}
return nil
}
// AdoptResources updates all the instances to indicate they
// are now associated with the specified controller.
func (env *maasEnviron) AdoptResources(controllerUUID string, fromVersion version.Number) error {
if !env.usingMAAS2() {
// We don't track instance -> controller for MAAS1.
return nil
}
instances, err := env.AllInstances()
if err != nil {
return errors.Trace(err)
}
var failed []instance.Id
for _, instance := range instances {
maas2Instance, ok := instance.(*maas2Instance)
if !ok {
// This should never happen.
return errors.Errorf("instance %q wasn't a maas2Instance", instance.Id())
}
// From the MAAS docs: "[SetOwnerData] will not remove any
// previous keys unless explicitly passed with an empty
// string." So not passing all of the keys here is fine.
// https://maas.ubuntu.com/docs2.0/api.html#machine
err := maas2Instance.machine.SetOwnerData(map[string]string{tags.JujuController: controllerUUID})
if err != nil {
logger.Errorf("error setting controller uuid tag for %q: %v", instance.Id(), err)
failed = append(failed, instance.Id())
}
}
if failed != nil {
return errors.Errorf("failed to update controller for some instances: %v", failed)
}
return nil
}