7
"launchpad.net/gomaasapi"
8
"launchpad.net/juju-core/constraints"
9
"launchpad.net/juju-core/environs"
10
"launchpad.net/juju-core/environs/cloudinit"
11
"launchpad.net/juju-core/environs/config"
12
"launchpad.net/juju-core/environs/tools"
13
"launchpad.net/juju-core/log"
14
"launchpad.net/juju-core/state"
15
"launchpad.net/juju-core/state/api"
16
"launchpad.net/juju-core/state/api/params"
17
"launchpad.net/juju-core/utils"
26
jujuDataDir = "/var/lib/juju"
27
// We're using v1.0 of the MAAS API.
31
var mgoPortSuffix = fmt.Sprintf(":%d", mgoPort)
32
var apiPortSuffix = fmt.Sprintf(":%d", apiPort)
34
var longAttempt = utils.AttemptStrategy{
35
Total: 3 * time.Minute,
36
Delay: 1 * time.Second,
39
type maasEnviron struct {
42
// ecfgMutex protects the *Unlocked fields below.
45
ecfgUnlocked *maasEnvironConfig
46
maasClientUnlocked *gomaasapi.MAASObject
47
storageUnlocked environs.Storage
50
var _ environs.Environ = (*maasEnviron)(nil)
52
var couldNotAllocate = errors.New("Could not allocate MAAS environment object.")
54
func NewEnviron(cfg *config.Config) (*maasEnviron, error) {
55
env := new(maasEnviron)
57
return nil, couldNotAllocate
59
err := env.SetConfig(cfg)
63
env.storageUnlocked = NewStorage(env)
67
func (env *maasEnviron) Name() string {
71
// makeMachineConfig sets up a basic machine configuration for use with
72
// userData(). You may still need to supply more information, but this takes
73
// care of the fixed entries and the ones that are always needed.
74
func (env *maasEnviron) makeMachineConfig(machineID, machineNonce string, stateInfo *state.Info, apiInfo *api.Info) *cloudinit.MachineConfig {
75
return &cloudinit.MachineConfig{
83
MachineNonce: machineNonce,
89
// startBootstrapNode starts the juju bootstrap node for this environment.
90
func (env *maasEnviron) startBootstrapNode(cons constraints.Value) (environs.Instance, error) {
91
// The bootstrap instance gets machine id "0". This is not related to
92
// instance ids or MAAS system ids. Juju assigns the machine ID.
94
mcfg := env.makeMachineConfig(machineID, state.BootstrapNonce, nil, nil)
95
mcfg.StateServer = true
97
log.Debugf("environs/maas: bootstrapping environment %q", env.Name())
98
possibleTools, err := environs.FindBootstrapTools(env, cons)
102
inst, err := env.obtainNode(machineID, cons, possibleTools, mcfg)
104
return nil, fmt.Errorf("cannot start bootstrap instance: %v", err)
109
// Bootstrap is specified in the Environ interface.
110
func (env *maasEnviron) Bootstrap(cons constraints.Value) error {
111
// TODO(fwereade): this should check for an existing environment before
112
// starting a new one -- even given raciness, it's better than nothing.
113
inst, err := env.startBootstrapNode(cons)
117
err = env.saveState(&bootstrapState{StateInstances: []state.InstanceId{inst.Id()}})
119
if err := env.releaseInstance(inst); err != nil {
120
log.Errorf("environs/maas: cannot release failed bootstrap instance: %v", err)
122
return fmt.Errorf("cannot save state: %v", err)
125
// TODO make safe in the case of racing Bootstraps
126
// If two Bootstraps are called concurrently, there's
127
// no way to make sure that only one succeeds.
131
// StateInfo is specified in the Environ interface.
132
func (env *maasEnviron) StateInfo() (*state.Info, *api.Info, error) {
133
// This code is cargo-culted from the openstack/ec2 providers.
134
// It's a bit unclear what the "longAttempt" loop is actually for
135
// but this should probably be refactored outside of the provider
137
st, err := env.loadState()
141
cert, hasCert := env.Config().CACert()
143
return nil, nil, fmt.Errorf("no CA certificate in environment configuration")
145
var stateAddrs []string
146
var apiAddrs []string
147
// Wait for the DNS names of any of the instances
148
// to become available.
149
log.Debugf("environs/maas: waiting for DNS name(s) of state server instances %v", st.StateInstances)
150
for a := longAttempt.Start(); len(stateAddrs) == 0 && a.Next(); {
151
insts, err := env.Instances(st.StateInstances)
152
if err != nil && err != environs.ErrPartialInstances {
153
log.Debugf("environs/maas: error getting state instance: %v", err.Error())
156
log.Debugf("environs/maas: started processing instances: %#v", insts)
157
for _, inst := range insts {
161
name, err := inst.DNSName()
166
stateAddrs = append(stateAddrs, name+mgoPortSuffix)
167
apiAddrs = append(apiAddrs, name+apiPortSuffix)
171
if len(stateAddrs) == 0 {
172
return nil, nil, fmt.Errorf("timed out waiting for mgo address from %v", st.StateInstances)
183
// ecfg returns the environment's maasEnvironConfig, and protects it with a
185
func (env *maasEnviron) ecfg() *maasEnvironConfig {
187
defer env.ecfgMutex.Unlock()
188
return env.ecfgUnlocked
191
// Config is specified in the Environ interface.
192
func (env *maasEnviron) Config() *config.Config {
193
return env.ecfg().Config
196
// SetConfig is specified in the Environ interface.
197
func (env *maasEnviron) SetConfig(cfg *config.Config) error {
199
defer env.ecfgMutex.Unlock()
201
// The new config has already been validated by itself, but now we
202
// validate the transition from the old config to the new.
203
var oldCfg *config.Config
204
if env.ecfgUnlocked != nil {
205
oldCfg = env.ecfgUnlocked.Config
207
cfg, err := env.Provider().Validate(cfg, oldCfg)
212
ecfg, err := providerInstance.newConfig(cfg)
217
env.name = cfg.Name()
218
env.ecfgUnlocked = ecfg
220
authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.MAASServer(), ecfg.MAASOAuth(), apiVersion)
224
env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient)
229
// getMAASClient returns a MAAS client object to use for a request, in a
230
// lock-protected fashion.
231
func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject {
233
defer env.ecfgMutex.Unlock()
235
return env.maasClientUnlocked
238
// convertConstraints converts the given constraints into an url.Values
239
// object suitable to pass to MAAS when acquiring a node.
240
// CpuPower is ignored because it cannot translated into something
241
// meaningful for MAAS right now.
242
func convertConstraints(cons constraints.Value) url.Values {
243
params := url.Values{}
244
if cons.Arch != nil {
245
params.Add("arch", *cons.Arch)
247
if cons.CpuCores != nil {
248
params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores))
251
params.Add("mem", fmt.Sprintf("%d", *cons.Mem))
253
if cons.CpuPower != nil {
254
log.Warningf("environs/maas: ignoring unsupported constraint 'cpu-power'")
259
// acquireNode allocates a node from the MAAS.
260
func (environ *maasEnviron) acquireNode(cons constraints.Value, possibleTools tools.List) (gomaasapi.MAASObject, *state.Tools, error) {
261
retry := utils.AttemptStrategy{
262
Total: 5 * time.Second,
263
Delay: 200 * time.Millisecond,
265
constraintsParams := convertConstraints(cons)
266
var result gomaasapi.JSONObject
268
for a := retry.Start(); a.Next(); {
269
client := environ.getMAASClient().GetSubObject("nodes/")
270
result, err = client.CallPost("acquire", constraintsParams)
276
return gomaasapi.MAASObject{}, nil, err
278
node, err := result.GetMAASObject()
280
msg := fmt.Errorf("unexpected result from 'acquire' on MAAS API: %v", err)
281
return gomaasapi.MAASObject{}, nil, msg
283
tools := possibleTools[0]
284
log.Warningf("environs/maas: picked arbitrary tools %q", tools)
285
return node, tools, nil
288
// startNode installs and boots a node.
289
func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) error {
290
retry := utils.AttemptStrategy{
291
Total: 5 * time.Second,
292
Delay: 200 * time.Millisecond,
294
userDataParam := base64.StdEncoding.EncodeToString(userdata)
295
params := url.Values{
296
"distro_series": {series},
297
"user_data": {userDataParam},
299
// Initialize err to a non-nil value as a sentinel for the following
301
err := fmt.Errorf("(no error)")
302
for a := retry.Start(); a.Next() && err != nil; {
303
_, err = node.CallPost("start", params)
308
// obtainNode allocates and starts a MAAS node. It is used both for the
309
// implementation of StartInstance, and to initialize the bootstrap node.
310
func (environ *maasEnviron) obtainNode(machineId string, cons constraints.Value, possibleTools tools.List, mcfg *cloudinit.MachineConfig) (_ *maasInstance, err error) {
311
series := possibleTools.Series()
312
if len(series) != 1 {
313
return nil, fmt.Errorf("expected single series, got %v", series)
315
var instance *maasInstance
316
if node, tools, err := environ.acquireNode(cons, possibleTools); err != nil {
317
return nil, fmt.Errorf("cannot run instances: %v", err)
319
instance = &maasInstance{&node, environ}
324
if err := environ.releaseInstance(instance); err != nil {
325
log.Errorf("environs/maas: error releasing failed instance: %v", err)
330
hostname, err := instance.DNSName()
334
info := machineInfo{string(instance.Id()), hostname}
335
runCmd, err := info.cloudinitRunCmd()
339
if err := environs.FinishMachineConfig(mcfg, environ.Config(), cons); err != nil {
342
userdata, err := userData(mcfg, runCmd)
344
msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err)
347
if err := environ.startNode(*instance.maasObject, series[0], userdata); err != nil {
350
log.Debugf("environs/maas: started instance %q", instance.Id())
354
// StartInstance is specified in the Environ interface.
355
func (environ *maasEnviron) StartInstance(machineID, machineNonce string, series string, cons constraints.Value, stateInfo *state.Info, apiInfo *api.Info) (environs.Instance, error) {
356
possibleTools, err := environs.FindInstanceTools(environ, series, cons)
360
mcfg := environ.makeMachineConfig(machineID, machineNonce, stateInfo, apiInfo)
361
return environ.obtainNode(machineID, cons, possibleTools, mcfg)
364
// StopInstances is specified in the Environ interface.
365
func (environ *maasEnviron) StopInstances(instances []environs.Instance) error {
366
// Shortcut to exit quickly if 'instances' is an empty slice or nil.
367
if len(instances) == 0 {
370
// Tell MAAS to release each of the instances. If there are errors,
371
// return only the first one (but release all instances regardless).
372
// Note that releasing instances also turns them off.
374
for _, instance := range instances {
375
err := environ.releaseInstance(instance)
383
// releaseInstance releases a single instance.
384
func (environ *maasEnviron) releaseInstance(inst environs.Instance) error {
385
maasInst := inst.(*maasInstance)
386
maasObj := maasInst.maasObject
387
_, err := maasObj.CallPost("release", nil)
389
log.Debugf("environs/maas: error releasing instance %v", maasInst)
394
// Instances returns the environs.Instance objects corresponding to the given
395
// slice of state.InstanceId. Similar to what the ec2 provider does,
396
// Instances returns nil if the given slice is empty or nil.
397
func (environ *maasEnviron) Instances(ids []state.InstanceId) ([]environs.Instance, error) {
401
return environ.instances(ids)
404
// instances is an internal method which returns the instances matching the
405
// given instance ids or all the instances if 'ids' is empty.
406
// If the some of the intances could not be found, it returns the instance
407
// that could be found plus the error environs.ErrPartialInstances in the error
409
func (environ *maasEnviron) instances(ids []state.InstanceId) ([]environs.Instance, error) {
410
nodeListing := environ.getMAASClient().GetSubObject("nodes")
411
filter := getSystemIdValues(ids)
412
listNodeObjects, err := nodeListing.CallGet("list", filter)
416
listNodes, err := listNodeObjects.GetArray()
420
instances := make([]environs.Instance, len(listNodes))
421
for index, nodeObj := range listNodes {
422
node, err := nodeObj.GetMAASObject()
426
instances[index] = &maasInstance{
431
if len(ids) != 0 && len(ids) != len(instances) {
432
return instances, environs.ErrPartialInstances
434
return instances, nil
437
// AllInstances returns all the environs.Instance in this provider.
438
func (environ *maasEnviron) AllInstances() ([]environs.Instance, error) {
439
return environ.instances(nil)
442
// Storage is defined by the Environ interface.
443
func (env *maasEnviron) Storage() environs.Storage {
445
defer env.ecfgMutex.Unlock()
446
return env.storageUnlocked
449
// PublicStorage is defined by the Environ interface.
450
func (env *maasEnviron) PublicStorage() environs.StorageReader {
451
// MAAS does not have a shared storage.
452
return environs.EmptyStorage
455
func (environ *maasEnviron) Destroy(ensureInsts []environs.Instance) error {
456
log.Debugf("environs/maas: destroying environment %q", environ.name)
457
insts, err := environ.AllInstances()
459
return fmt.Errorf("cannot get instances: %v", err)
461
found := make(map[state.InstanceId]bool)
462
for _, inst := range insts {
463
found[inst.Id()] = true
466
// Add any instances we've been told about but haven't yet shown
467
// up in the instance list.
468
for _, inst := range ensureInsts {
471
insts = append(insts, inst)
475
err = environ.StopInstances(insts)
480
// To properly observe e.storageUnlocked we need to get its value while
481
// holding e.ecfgMutex. e.Storage() does this for us, then we convert
482
// back to the (*storage) to access the private deleteAll() method.
483
st := environ.Storage().(*maasStorage)
484
return st.deleteAll()
487
func (*maasEnviron) AssignmentPolicy() state.AssignmentPolicy {
488
return state.AssignUnused
491
// MAAS does not do firewalling so these port methods do nothing.
492
func (*maasEnviron) OpenPorts([]params.Port) error {
493
log.Debugf("environs/maas: unimplemented OpenPorts() called")
497
func (*maasEnviron) ClosePorts([]params.Port) error {
498
log.Debugf("environs/maas: unimplemented ClosePorts() called")
502
func (*maasEnviron) Ports() ([]params.Port, error) {
503
log.Debugf("environs/maas: unimplemented Ports() called")
504
return []params.Port{}, nil
507
func (*maasEnviron) Provider() environs.EnvironProvider {
508
return &providerInstance