forked from juju/juju
/
state.go
161 lines (147 loc) · 4.79 KB
/
state.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package storage
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/juju/errors"
"github.com/juju/utils"
"gopkg.in/juju/charm.v6/hooks"
"gopkg.in/juju/names.v2"
"github.com/juju/juju/worker/uniter/hook"
)
// state describes the state of a storage attachment.
type state struct {
// storage is the tag of the storage attachment.
storage names.StorageTag
// attached records the uniter's knowledge of the
// storage attachment state.
attached bool
}
// ValidateHook returns an error if the supplied hook.Info does not represent
// a valid change to the storage state. Hooks must always be validated
// against the current state before they are run, to ensure that the system
// meets its guarantees about hook execution order.
func (s *state) ValidateHook(hi hook.Info) (err error) {
defer errors.DeferredAnnotatef(&err, "inappropriate %q hook for storage %q", hi.Kind, s.storage.Id())
if hi.StorageId != s.storage.Id() {
return errors.Errorf("expected storage %q, got storage %q", s.storage.Id(), hi.StorageId)
}
switch hi.Kind {
case hooks.StorageAttached:
if s.attached {
return errors.New("storage already attached")
}
case hooks.StorageDetaching:
if !s.attached {
return errors.New("storage not attached")
}
}
return nil
}
// stateFile is a filesystem-backed representation of the state of a
// storage attachment. Concurrent modifications to the underlying state
// file will have undefined consequences.
type stateFile struct {
// path identifies the directory holding persistent state.
path string
// state is the cached state of the directory, which is guaranteed
// to be synchronized with the true state so long as no concurrent
// changes are made to the directory.
state
}
// readStateFile loads a stateFile from the subdirectory of dirPath named
// for the supplied storage tag. If the directory does not exist, no error
// is returned.
func readStateFile(dirPath string, tag names.StorageTag) (d *stateFile, err error) {
filename := strings.Replace(tag.Id(), "/", "-", -1)
d = &stateFile{
filepath.Join(dirPath, filename),
state{storage: tag},
}
defer errors.DeferredAnnotatef(&err, "cannot load storage %q state from %q", tag.Id(), d.path)
if _, err := os.Stat(d.path); os.IsNotExist(err) {
return d, nil
} else if err != nil {
return nil, err
}
var info diskInfo
if err := utils.ReadYaml(d.path, &info); err != nil {
return nil, errors.Errorf("invalid storage state file %q: %v", d.path, err)
}
if info.Attached == nil {
return nil, errors.Errorf("invalid storage state file %q: missing 'attached'", d.path)
}
d.state.attached = *info.Attached
return d, nil
}
// readAllStateFiles loads and returns every stateFile persisted inside
// the supplied dirPath. If dirPath does not exist, no error is returned.
func readAllStateFiles(dirPath string) (files map[names.StorageTag]*stateFile, err error) {
defer errors.DeferredAnnotatef(&err, "cannot load storage state from %q", dirPath)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
return nil, nil
} else if err != nil {
return nil, err
}
fis, err := ioutil.ReadDir(dirPath)
if err != nil {
return nil, err
}
files = make(map[names.StorageTag]*stateFile)
for _, fi := range fis {
if fi.IsDir() {
continue
}
storageId := fi.Name()
if i := strings.LastIndex(storageId, "-"); i > 0 {
storageId = storageId[:i] + "/" + storageId[i+1:]
if !names.IsValidStorage(storageId) {
continue
}
} else {
// Lack of "-" means it's not a valid storage ID.
continue
}
tag := names.NewStorageTag(storageId)
f, err := readStateFile(dirPath, tag)
if err != nil {
return nil, err
}
files[tag] = f
}
return files, nil
}
// CommitHook atomically writes to disk the storage state change in hi.
// It must be called after the respective hook was executed successfully.
// CommitHook doesn't validate hi but guarantees that successive writes
// of the same hi are idempotent.
func (d *stateFile) CommitHook(hi hook.Info) (err error) {
defer errors.DeferredAnnotatef(&err, "failed to write %q hook info for %q on state directory", hi.Kind, hi.StorageId)
if hi.Kind == hooks.StorageDetaching {
return d.Remove()
}
attached := true
di := diskInfo{&attached}
if err := utils.WriteYaml(d.path, &di); err != nil {
return err
}
// If write was successful, update own state.
d.state.attached = true
return nil
}
// Remove removes the directory if it exists and is empty.
func (d *stateFile) Remove() error {
if err := os.Remove(d.path); err != nil && !os.IsNotExist(err) {
return err
}
// If atomic delete succeeded, update own state.
d.state.attached = false
return nil
}
// diskInfo defines the storage attachment data serialization.
type diskInfo struct {
Attached *bool `yaml:"attached,omitempty"`
}