1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
10
"github.com/juju/errors"
11
jc "github.com/juju/testing/checkers"
12
"github.com/juju/utils/set"
13
gc "gopkg.in/check.v1"
14
"gopkg.in/juju/charm.v6-unstable/hooks"
15
"gopkg.in/juju/names.v2"
17
"github.com/juju/juju/apiserver/params"
18
corestorage "github.com/juju/juju/storage"
19
"github.com/juju/juju/testing"
20
"github.com/juju/juju/worker/uniter/hook"
21
"github.com/juju/juju/worker/uniter/operation"
22
"github.com/juju/juju/worker/uniter/remotestate"
23
"github.com/juju/juju/worker/uniter/resolver"
24
"github.com/juju/juju/worker/uniter/storage"
27
type attachmentsSuite struct {
31
var _ = gc.Suite(&attachmentsSuite{})
33
func assertStorageTags(c *gc.C, a *storage.Attachments, tags ...names.StorageTag) {
34
sTags, err := a.StorageTags()
35
c.Assert(err, jc.ErrorIsNil)
36
c.Assert(sTags, jc.SameContents, tags)
39
func (s *attachmentsSuite) TestNewAttachments(c *gc.C) {
40
stateDir := filepath.Join(c.MkDir(), "nonexistent")
41
unitTag := names.NewUnitTag("mysql/0")
42
abort := make(chan struct{})
43
st := &mockStorageAccessor{
44
unitStorageAttachments: func(u names.UnitTag) ([]params.StorageAttachmentId, error) {
45
c.Assert(u, gc.Equals, unitTag)
50
_, err := storage.NewAttachments(st, unitTag, stateDir, abort)
51
c.Assert(err, jc.ErrorIsNil)
52
// state dir should have been created.
53
c.Assert(stateDir, jc.IsDirectory)
56
func (s *attachmentsSuite) TestNewAttachmentsInit(c *gc.C) {
58
unitTag := names.NewUnitTag("mysql/0")
59
abort := make(chan struct{})
61
// Simulate remote state returning a single Alive storage attachment.
62
storageTag := names.NewStorageTag("data/0")
63
attachmentIds := []params.StorageAttachmentId{{
64
StorageTag: storageTag.String(),
65
UnitTag: unitTag.String(),
67
attachment := params.StorageAttachment{
68
StorageTag: storageTag.String(),
69
UnitTag: unitTag.String(),
71
Kind: params.StorageKindBlock,
75
st := &mockStorageAccessor{
76
unitStorageAttachments: func(u names.UnitTag) ([]params.StorageAttachmentId, error) {
77
c.Assert(u, gc.Equals, unitTag)
78
return attachmentIds, nil
80
storageAttachment: func(s names.StorageTag, u names.UnitTag) (params.StorageAttachment, error) {
81
c.Assert(s, gc.Equals, storageTag)
82
return attachment, nil
86
withAttachments := func(f func(*storage.Attachments)) {
87
att, err := storage.NewAttachments(st, unitTag, stateDir, abort)
88
c.Assert(err, jc.ErrorIsNil)
92
// No state files, so no storagers will be started.
94
withAttachments(func(att *storage.Attachments) {
96
c.Assert(att.Pending(), gc.Equals, 1)
97
err := att.ValidateHook(hook.Info{
98
Kind: hooks.StorageAttached,
99
StorageId: storageTag.Id(),
101
c.Assert(err, gc.ErrorMatches, `unknown storage "data/0"`)
102
assertStorageTags(c, att) // no active attachment
104
c.Assert(called, gc.Equals, 1)
106
// Commit a storage-attached to local state and try again.
107
state0, err := storage.ReadStateFile(stateDir, storageTag)
108
c.Assert(err, jc.ErrorIsNil)
109
err = state0.CommitHook(hook.Info{Kind: hooks.StorageAttached, StorageId: "data/0"})
110
c.Assert(err, jc.ErrorIsNil)
111
// Create an extra one so we can make sure it gets removed.
112
state1, err := storage.ReadStateFile(stateDir, names.NewStorageTag("data/1"))
113
c.Assert(err, jc.ErrorIsNil)
114
err = state1.CommitHook(hook.Info{Kind: hooks.StorageAttached, StorageId: "data/1"})
115
c.Assert(err, jc.ErrorIsNil)
117
withAttachments(func(att *storage.Attachments) {
118
// We should be able to get the initial storage context
119
// for existing storage immediately, without having to
120
// wait for any hooks to fire.
121
ctx, err := att.Storage(storageTag)
122
c.Assert(err, jc.ErrorIsNil)
123
c.Assert(ctx, gc.NotNil)
124
c.Assert(ctx.Tag(), gc.Equals, storageTag)
125
c.Assert(ctx.Tag(), gc.Equals, storageTag)
126
c.Assert(ctx.Kind(), gc.Equals, corestorage.StorageKindBlock)
127
c.Assert(ctx.Location(), gc.Equals, "/dev/sdb")
130
c.Assert(att.Pending(), gc.Equals, 0)
131
err = att.ValidateHook(hook.Info{
132
Kind: hooks.StorageDetaching,
133
StorageId: storageTag.Id(),
135
c.Assert(err, jc.ErrorIsNil)
136
err = att.ValidateHook(hook.Info{
137
Kind: hooks.StorageAttached,
140
c.Assert(err, gc.ErrorMatches, `unknown storage "data/1"`)
141
assertStorageTags(c, att, storageTag)
143
c.Assert(called, gc.Equals, 2)
144
c.Assert(filepath.Join(stateDir, "data-0"), jc.IsNonEmptyFile)
145
c.Assert(filepath.Join(stateDir, "data-1"), jc.DoesNotExist)
148
func (s *attachmentsSuite) TestAttachmentsUpdateShortCircuitDeath(c *gc.C) {
149
stateDir := c.MkDir()
150
unitTag := names.NewUnitTag("mysql/0")
151
abort := make(chan struct{})
153
storageTag0 := names.NewStorageTag("data/0")
154
storageTag1 := names.NewStorageTag("data/1")
156
removed := set.NewTags()
157
st := &mockStorageAccessor{
158
unitStorageAttachments: func(u names.UnitTag) ([]params.StorageAttachmentId, error) {
161
remove: func(s names.StorageTag, u names.UnitTag) error {
162
c.Assert(u, gc.Equals, unitTag)
168
att, err := storage.NewAttachments(st, unitTag, stateDir, abort)
169
c.Assert(err, jc.ErrorIsNil)
170
r := storage.NewResolver(att)
172
// First make sure we create a storage-attached hook operation for
173
// data/0. We do this to show that until the hook is *committed*,
174
// we will still short-circuit removal.
175
localState := resolver.LocalState{State: operation.State{
176
Kind: operation.Continue,
178
_, err = r.NextOp(localState, remotestate.Snapshot{
180
Storage: map[names.StorageTag]remotestate.StorageSnapshot{
183
Kind: params.StorageKindBlock,
184
Location: "/dev/sdb",
188
}, &mockOperations{})
189
c.Assert(err, jc.ErrorIsNil)
191
for _, storageTag := range []names.StorageTag{storageTag0, storageTag1} {
192
_, err = r.NextOp(localState, remotestate.Snapshot{
194
Storage: map[names.StorageTag]remotestate.StorageSnapshot{
195
storageTag: {Life: params.Dying},
198
c.Assert(err, gc.Equals, resolver.ErrNoOperation)
200
c.Assert(removed.SortedValues(), jc.DeepEquals, []names.Tag{
201
storageTag0, storageTag1,
205
func (s *attachmentsSuite) TestAttachmentsStorage(c *gc.C) {
206
stateDir := c.MkDir()
207
unitTag := names.NewUnitTag("mysql/0")
208
abort := make(chan struct{})
210
st := &mockStorageAccessor{
211
unitStorageAttachments: func(u names.UnitTag) ([]params.StorageAttachmentId, error) {
216
att, err := storage.NewAttachments(st, unitTag, stateDir, abort)
217
c.Assert(err, jc.ErrorIsNil)
218
r := storage.NewResolver(att)
220
storageTag := names.NewStorageTag("data/0")
221
_, err = att.Storage(storageTag)
222
c.Assert(err, jc.Satisfies, errors.IsNotFound)
223
assertStorageTags(c, att)
225
// Inform the resolver of an attachment.
226
localState := resolver.LocalState{State: operation.State{
227
Kind: operation.Continue,
229
op, err := r.NextOp(localState, remotestate.Snapshot{
231
Storage: map[names.StorageTag]remotestate.StorageSnapshot{
233
Kind: params.StorageKindBlock,
235
Location: "/dev/sdb",
239
}, &mockOperations{})
240
c.Assert(err, jc.ErrorIsNil)
241
c.Assert(op.String(), gc.Equals, "run hook storage-attached")
242
assertStorageTags(c, att, storageTag)
244
ctx, err := att.Storage(storageTag)
245
c.Assert(err, jc.ErrorIsNil)
246
c.Assert(ctx, gc.NotNil)
247
c.Assert(ctx.Tag(), gc.Equals, storageTag)
248
c.Assert(ctx.Kind(), gc.Equals, corestorage.StorageKindBlock)
249
c.Assert(ctx.Location(), gc.Equals, "/dev/sdb")
252
func (s *attachmentsSuite) TestAttachmentsCommitHook(c *gc.C) {
253
stateDir := c.MkDir()
254
unitTag := names.NewUnitTag("mysql/0")
255
abort := make(chan struct{})
258
storageTag := names.NewStorageTag("data/0")
259
st := &mockStorageAccessor{
260
unitStorageAttachments: func(u names.UnitTag) ([]params.StorageAttachmentId, error) {
263
remove: func(s names.StorageTag, u names.UnitTag) error {
265
c.Assert(s, gc.Equals, storageTag)
270
att, err := storage.NewAttachments(st, unitTag, stateDir, abort)
271
c.Assert(err, jc.ErrorIsNil)
272
r := storage.NewResolver(att)
274
// Inform the resolver of an attachment.
275
localState := resolver.LocalState{State: operation.State{
276
Kind: operation.Continue,
278
_, err = r.NextOp(localState, remotestate.Snapshot{
280
Storage: map[names.StorageTag]remotestate.StorageSnapshot{
282
Kind: params.StorageKindBlock,
284
Location: "/dev/sdb",
288
}, &mockOperations{})
289
c.Assert(err, jc.ErrorIsNil)
290
c.Assert(att.Pending(), gc.Equals, 1)
292
// No file exists until storage-attached is committed.
293
stateFile := filepath.Join(stateDir, "data-0")
294
c.Assert(stateFile, jc.DoesNotExist)
296
err = att.CommitHook(hook.Info{
297
Kind: hooks.StorageAttached,
298
StorageId: storageTag.Id(),
300
c.Assert(err, jc.ErrorIsNil)
301
data, err := ioutil.ReadFile(stateFile)
302
c.Assert(err, jc.ErrorIsNil)
303
c.Assert(string(data), gc.Equals, "attached: true\n")
304
c.Assert(att.Pending(), gc.Equals, 0)
306
c.Assert(removed, jc.IsFalse)
307
err = att.CommitHook(hook.Info{
308
Kind: hooks.StorageDetaching,
309
StorageId: storageTag.Id(),
311
c.Assert(err, jc.ErrorIsNil)
312
c.Assert(stateFile, jc.DoesNotExist)
313
c.Assert(removed, jc.IsTrue)
316
func (s *attachmentsSuite) TestAttachmentsSetDying(c *gc.C) {
317
stateDir := c.MkDir()
318
unitTag := names.NewUnitTag("mysql/0")
319
abort := make(chan struct{})
321
storageTag := names.NewStorageTag("data/0")
322
var destroyed, removed bool
323
st := &mockStorageAccessor{
324
unitStorageAttachments: func(u names.UnitTag) ([]params.StorageAttachmentId, error) {
325
c.Assert(u, gc.Equals, unitTag)
326
return []params.StorageAttachmentId{{
327
StorageTag: storageTag.String(),
328
UnitTag: unitTag.String(),
331
storageAttachment: func(s names.StorageTag, u names.UnitTag) (params.StorageAttachment, error) {
332
c.Assert(u, gc.Equals, unitTag)
333
c.Assert(s, gc.Equals, storageTag)
334
return params.StorageAttachment{}, ¶ms.Error{
335
Message: "not provisioned",
336
Code: params.CodeNotProvisioned,
339
destroyUnitStorageAttachments: func(u names.UnitTag) error {
340
c.Assert(u, gc.Equals, unitTag)
344
remove: func(s names.StorageTag, u names.UnitTag) error {
345
c.Assert(removed, jc.IsFalse)
346
c.Assert(s, gc.Equals, storageTag)
347
c.Assert(u, gc.Equals, unitTag)
353
att, err := storage.NewAttachments(st, unitTag, stateDir, abort)
354
c.Assert(err, jc.ErrorIsNil)
355
c.Assert(att.Pending(), gc.Equals, 1)
356
r := storage.NewResolver(att)
358
// Inform the resolver that the unit is Dying. The storage is still
359
// Alive, and is now provisioned, but will be destroyed and removed
361
localState := resolver.LocalState{State: operation.State{
362
Kind: operation.Continue,
364
_, err = r.NextOp(localState, remotestate.Snapshot{
366
Storage: map[names.StorageTag]remotestate.StorageSnapshot{
368
Kind: params.StorageKindBlock,
370
Location: "/dev/sdb",
374
}, &mockOperations{})
375
c.Assert(err, gc.Equals, resolver.ErrNoOperation)
376
c.Assert(destroyed, jc.IsTrue)
377
c.Assert(att.Pending(), gc.Equals, 0)
378
c.Assert(removed, jc.IsTrue)
381
func (s *attachmentsSuite) TestAttachmentsWaitPending(c *gc.C) {
382
stateDir := c.MkDir()
383
unitTag := names.NewUnitTag("mysql/0")
384
abort := make(chan struct{})
386
storageTag := names.NewStorageTag("data/0")
387
st := &mockStorageAccessor{
388
unitStorageAttachments: func(u names.UnitTag) ([]params.StorageAttachmentId, error) {
393
att, err := storage.NewAttachments(st, unitTag, stateDir, abort)
394
c.Assert(err, jc.ErrorIsNil)
395
r := storage.NewResolver(att)
397
nextOp := func(installed bool) error {
398
localState := resolver.LocalState{State: operation.State{
399
Installed: installed,
400
Kind: operation.Continue,
402
_, err := r.NextOp(localState, remotestate.Snapshot{
404
Storage: map[names.StorageTag]remotestate.StorageSnapshot{
410
}, &mockOperations{})
414
// Inform the resolver of a new, unprovisioned storage attachment.
415
// Before install, we should wait for its completion; after install,
417
err = nextOp(false /* workload not installed */)
418
c.Assert(att.Pending(), gc.Equals, 1)
419
c.Assert(err, gc.Equals, resolver.ErrWaiting)
421
err = nextOp(true /* workload installed */)
422
c.Assert(err, gc.Equals, resolver.ErrNoOperation)