~juju-qa/ubuntu/yakkety/juju/2.0-rc3-again

« back to all changes in this revision

Viewing changes to src/launchpad.net/juju-core/environs/maas/environ.go

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2013-04-24 22:34:47 UTC
  • Revision ID: package-import@ubuntu.com-20130424223447-f0qdji7ubnyo0s71
Tags: upstream-1.10.0.1
ImportĀ upstreamĀ versionĀ 1.10.0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
package maas
 
2
 
 
3
import (
 
4
        "encoding/base64"
 
5
        "errors"
 
6
        "fmt"
 
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"
 
18
        "net/url"
 
19
        "sync"
 
20
        "time"
 
21
)
 
22
 
 
23
const (
 
24
        mgoPort     = 37017
 
25
        apiPort     = 17070
 
26
        jujuDataDir = "/var/lib/juju"
 
27
        // We're using v1.0 of the MAAS API.
 
28
        apiVersion = "1.0"
 
29
)
 
30
 
 
31
var mgoPortSuffix = fmt.Sprintf(":%d", mgoPort)
 
32
var apiPortSuffix = fmt.Sprintf(":%d", apiPort)
 
33
 
 
34
var longAttempt = utils.AttemptStrategy{
 
35
        Total: 3 * time.Minute,
 
36
        Delay: 1 * time.Second,
 
37
}
 
38
 
 
39
type maasEnviron struct {
 
40
        name string
 
41
 
 
42
        // ecfgMutex protects the *Unlocked fields below.
 
43
        ecfgMutex sync.Mutex
 
44
 
 
45
        ecfgUnlocked       *maasEnvironConfig
 
46
        maasClientUnlocked *gomaasapi.MAASObject
 
47
        storageUnlocked    environs.Storage
 
48
}
 
49
 
 
50
var _ environs.Environ = (*maasEnviron)(nil)
 
51
 
 
52
var couldNotAllocate = errors.New("Could not allocate MAAS environment object.")
 
53
 
 
54
func NewEnviron(cfg *config.Config) (*maasEnviron, error) {
 
55
        env := new(maasEnviron)
 
56
        if env == nil {
 
57
                return nil, couldNotAllocate
 
58
        }
 
59
        err := env.SetConfig(cfg)
 
60
        if err != nil {
 
61
                return nil, err
 
62
        }
 
63
        env.storageUnlocked = NewStorage(env)
 
64
        return env, nil
 
65
}
 
66
 
 
67
func (env *maasEnviron) Name() string {
 
68
        return env.name
 
69
}
 
70
 
 
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{
 
76
                // Fixed entries.
 
77
                MongoPort: mgoPort,
 
78
                APIPort:   apiPort,
 
79
                DataDir:   jujuDataDir,
 
80
 
 
81
                // Parameter entries.
 
82
                MachineId:    machineID,
 
83
                MachineNonce: machineNonce,
 
84
                StateInfo:    stateInfo,
 
85
                APIInfo:      apiInfo,
 
86
        }
 
87
}
 
88
 
 
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.
 
93
        const machineID = "0"
 
94
        mcfg := env.makeMachineConfig(machineID, state.BootstrapNonce, nil, nil)
 
95
        mcfg.StateServer = true
 
96
 
 
97
        log.Debugf("environs/maas: bootstrapping environment %q", env.Name())
 
98
        possibleTools, err := environs.FindBootstrapTools(env, cons)
 
99
        if err != nil {
 
100
                return nil, err
 
101
        }
 
102
        inst, err := env.obtainNode(machineID, cons, possibleTools, mcfg)
 
103
        if err != nil {
 
104
                return nil, fmt.Errorf("cannot start bootstrap instance: %v", err)
 
105
        }
 
106
        return inst, nil
 
107
}
 
108
 
 
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)
 
114
        if err != nil {
 
115
                return err
 
116
        }
 
117
        err = env.saveState(&bootstrapState{StateInstances: []state.InstanceId{inst.Id()}})
 
118
        if err != nil {
 
119
                if err := env.releaseInstance(inst); err != nil {
 
120
                        log.Errorf("environs/maas: cannot release failed bootstrap instance: %v", err)
 
121
                }
 
122
                return fmt.Errorf("cannot save state: %v", err)
 
123
        }
 
124
 
 
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.
 
128
        return nil
 
129
}
 
130
 
 
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
 
136
        // code.
 
137
        st, err := env.loadState()
 
138
        if err != nil {
 
139
                return nil, nil, err
 
140
        }
 
141
        cert, hasCert := env.Config().CACert()
 
142
        if !hasCert {
 
143
                return nil, nil, fmt.Errorf("no CA certificate in environment configuration")
 
144
        }
 
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())
 
154
                        return nil, nil, err
 
155
                }
 
156
                log.Debugf("environs/maas: started processing instances: %#v", insts)
 
157
                for _, inst := range insts {
 
158
                        if inst == nil {
 
159
                                continue
 
160
                        }
 
161
                        name, err := inst.DNSName()
 
162
                        if err != nil {
 
163
                                continue
 
164
                        }
 
165
                        if name != "" {
 
166
                                stateAddrs = append(stateAddrs, name+mgoPortSuffix)
 
167
                                apiAddrs = append(apiAddrs, name+apiPortSuffix)
 
168
                        }
 
169
                }
 
170
        }
 
171
        if len(stateAddrs) == 0 {
 
172
                return nil, nil, fmt.Errorf("timed out waiting for mgo address from %v", st.StateInstances)
 
173
        }
 
174
        return &state.Info{
 
175
                        Addrs:  stateAddrs,
 
176
                        CACert: cert,
 
177
                }, &api.Info{
 
178
                        Addrs:  apiAddrs,
 
179
                        CACert: cert,
 
180
                }, nil
 
181
}
 
182
 
 
183
// ecfg returns the environment's maasEnvironConfig, and protects it with a
 
184
// mutex.
 
185
func (env *maasEnviron) ecfg() *maasEnvironConfig {
 
186
        env.ecfgMutex.Lock()
 
187
        defer env.ecfgMutex.Unlock()
 
188
        return env.ecfgUnlocked
 
189
}
 
190
 
 
191
// Config is specified in the Environ interface.
 
192
func (env *maasEnviron) Config() *config.Config {
 
193
        return env.ecfg().Config
 
194
}
 
195
 
 
196
// SetConfig is specified in the Environ interface.
 
197
func (env *maasEnviron) SetConfig(cfg *config.Config) error {
 
198
        env.ecfgMutex.Lock()
 
199
        defer env.ecfgMutex.Unlock()
 
200
 
 
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
 
206
        }
 
207
        cfg, err := env.Provider().Validate(cfg, oldCfg)
 
208
        if err != nil {
 
209
                return err
 
210
        }
 
211
 
 
212
        ecfg, err := providerInstance.newConfig(cfg)
 
213
        if err != nil {
 
214
                return err
 
215
        }
 
216
 
 
217
        env.name = cfg.Name()
 
218
        env.ecfgUnlocked = ecfg
 
219
 
 
220
        authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.MAASServer(), ecfg.MAASOAuth(), apiVersion)
 
221
        if err != nil {
 
222
                return err
 
223
        }
 
224
        env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient)
 
225
 
 
226
        return nil
 
227
}
 
228
 
 
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 {
 
232
        env.ecfgMutex.Lock()
 
233
        defer env.ecfgMutex.Unlock()
 
234
 
 
235
        return env.maasClientUnlocked
 
236
}
 
237
 
 
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)
 
246
        }
 
247
        if cons.CpuCores != nil {
 
248
                params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores))
 
249
        }
 
250
        if cons.Mem != nil {
 
251
                params.Add("mem", fmt.Sprintf("%d", *cons.Mem))
 
252
        }
 
253
        if cons.CpuPower != nil {
 
254
                log.Warningf("environs/maas: ignoring unsupported constraint 'cpu-power'")
 
255
        }
 
256
        return params
 
257
}
 
258
 
 
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,
 
264
        }
 
265
        constraintsParams := convertConstraints(cons)
 
266
        var result gomaasapi.JSONObject
 
267
        var err error
 
268
        for a := retry.Start(); a.Next(); {
 
269
                client := environ.getMAASClient().GetSubObject("nodes/")
 
270
                result, err = client.CallPost("acquire", constraintsParams)
 
271
                if err == nil {
 
272
                        break
 
273
                }
 
274
        }
 
275
        if err != nil {
 
276
                return gomaasapi.MAASObject{}, nil, err
 
277
        }
 
278
        node, err := result.GetMAASObject()
 
279
        if err != nil {
 
280
                msg := fmt.Errorf("unexpected result from 'acquire' on MAAS API: %v", err)
 
281
                return gomaasapi.MAASObject{}, nil, msg
 
282
        }
 
283
        tools := possibleTools[0]
 
284
        log.Warningf("environs/maas: picked arbitrary tools %q", tools)
 
285
        return node, tools, nil
 
286
}
 
287
 
 
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,
 
293
        }
 
294
        userDataParam := base64.StdEncoding.EncodeToString(userdata)
 
295
        params := url.Values{
 
296
                "distro_series": {series},
 
297
                "user_data":     {userDataParam},
 
298
        }
 
299
        // Initialize err to a non-nil value as a sentinel for the following
 
300
        // loop.
 
301
        err := fmt.Errorf("(no error)")
 
302
        for a := retry.Start(); a.Next() && err != nil; {
 
303
                _, err = node.CallPost("start", params)
 
304
        }
 
305
        return err
 
306
}
 
307
 
 
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)
 
314
        }
 
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)
 
318
        } else {
 
319
                instance = &maasInstance{&node, environ}
 
320
                mcfg.Tools = tools
 
321
        }
 
322
        defer func() {
 
323
                if err != nil {
 
324
                        if err := environ.releaseInstance(instance); err != nil {
 
325
                                log.Errorf("environs/maas: error releasing failed instance: %v", err)
 
326
                        }
 
327
                }
 
328
        }()
 
329
 
 
330
        hostname, err := instance.DNSName()
 
331
        if err != nil {
 
332
                return nil, err
 
333
        }
 
334
        info := machineInfo{string(instance.Id()), hostname}
 
335
        runCmd, err := info.cloudinitRunCmd()
 
336
        if err != nil {
 
337
                return nil, err
 
338
        }
 
339
        if err := environs.FinishMachineConfig(mcfg, environ.Config(), cons); err != nil {
 
340
                return nil, err
 
341
        }
 
342
        userdata, err := userData(mcfg, runCmd)
 
343
        if err != nil {
 
344
                msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err)
 
345
                return nil, msg
 
346
        }
 
347
        if err := environ.startNode(*instance.maasObject, series[0], userdata); err != nil {
 
348
                return nil, err
 
349
        }
 
350
        log.Debugf("environs/maas: started instance %q", instance.Id())
 
351
        return instance, nil
 
352
}
 
353
 
 
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)
 
357
        if err != nil {
 
358
                return nil, err
 
359
        }
 
360
        mcfg := environ.makeMachineConfig(machineID, machineNonce, stateInfo, apiInfo)
 
361
        return environ.obtainNode(machineID, cons, possibleTools, mcfg)
 
362
}
 
363
 
 
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 {
 
368
                return nil
 
369
        }
 
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.
 
373
        var firstErr error
 
374
        for _, instance := range instances {
 
375
                err := environ.releaseInstance(instance)
 
376
                if firstErr == nil {
 
377
                        firstErr = err
 
378
                }
 
379
        }
 
380
        return firstErr
 
381
}
 
382
 
 
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)
 
388
        if err != nil {
 
389
                log.Debugf("environs/maas: error releasing instance %v", maasInst)
 
390
        }
 
391
        return err
 
392
}
 
393
 
 
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) {
 
398
        if len(ids) == 0 {
 
399
                return nil, nil
 
400
        }
 
401
        return environ.instances(ids)
 
402
}
 
403
 
 
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
 
408
// return.
 
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)
 
413
        if err != nil {
 
414
                return nil, err
 
415
        }
 
416
        listNodes, err := listNodeObjects.GetArray()
 
417
        if err != nil {
 
418
                return nil, err
 
419
        }
 
420
        instances := make([]environs.Instance, len(listNodes))
 
421
        for index, nodeObj := range listNodes {
 
422
                node, err := nodeObj.GetMAASObject()
 
423
                if err != nil {
 
424
                        return nil, err
 
425
                }
 
426
                instances[index] = &maasInstance{
 
427
                        maasObject: &node,
 
428
                        environ:    environ,
 
429
                }
 
430
        }
 
431
        if len(ids) != 0 && len(ids) != len(instances) {
 
432
                return instances, environs.ErrPartialInstances
 
433
        }
 
434
        return instances, nil
 
435
}
 
436
 
 
437
// AllInstances returns all the environs.Instance in this provider.
 
438
func (environ *maasEnviron) AllInstances() ([]environs.Instance, error) {
 
439
        return environ.instances(nil)
 
440
}
 
441
 
 
442
// Storage is defined by the Environ interface.
 
443
func (env *maasEnviron) Storage() environs.Storage {
 
444
        env.ecfgMutex.Lock()
 
445
        defer env.ecfgMutex.Unlock()
 
446
        return env.storageUnlocked
 
447
}
 
448
 
 
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
 
453
}
 
454
 
 
455
func (environ *maasEnviron) Destroy(ensureInsts []environs.Instance) error {
 
456
        log.Debugf("environs/maas: destroying environment %q", environ.name)
 
457
        insts, err := environ.AllInstances()
 
458
        if err != nil {
 
459
                return fmt.Errorf("cannot get instances: %v", err)
 
460
        }
 
461
        found := make(map[state.InstanceId]bool)
 
462
        for _, inst := range insts {
 
463
                found[inst.Id()] = true
 
464
        }
 
465
 
 
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 {
 
469
                id := inst.Id()
 
470
                if !found[id] {
 
471
                        insts = append(insts, inst)
 
472
                        found[id] = true
 
473
                }
 
474
        }
 
475
        err = environ.StopInstances(insts)
 
476
        if err != nil {
 
477
                return err
 
478
        }
 
479
 
 
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()
 
485
}
 
486
 
 
487
func (*maasEnviron) AssignmentPolicy() state.AssignmentPolicy {
 
488
        return state.AssignUnused
 
489
}
 
490
 
 
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")
 
494
        return nil
 
495
}
 
496
 
 
497
func (*maasEnviron) ClosePorts([]params.Port) error {
 
498
        log.Debugf("environs/maas: unimplemented ClosePorts() called")
 
499
        return nil
 
500
}
 
501
 
 
502
func (*maasEnviron) Ports() ([]params.Port, error) {
 
503
        log.Debugf("environs/maas: unimplemented Ports() called")
 
504
        return []params.Port{}, nil
 
505
}
 
506
 
 
507
func (*maasEnviron) Provider() environs.EnvironProvider {
 
508
        return &providerInstance
 
509
}