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