summaryrefslogtreecommitdiffstats
path: root/state.py
blob: f78fa0afb8b3036e44b8f984bb264b6d83a5367a (plain)
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
__docformat__ = 'restructuredtext'

class Category:
    """
    A Category is a "set" of states. The category specifies a name, certain
    arguments that must be provided, and constraints for the values of those
    arguments. Any possible state which meets these restrictions is in the
    category.
    """

    def __init__(self, name, **args):
        """
        Categories are constructed with a `name` and a series of keyword
        arguments. `name` is a string. Each keyword argument that is specified
        is the name of a state argument a state must posess to be considered to
        be a member of the category. If the value of the keyword argument is
        `None` then the state argument can have any value, otherwise the value is a string
        and specifies a mandatory value for the state argument.
        """
        self.args = args
        self.name = name

    def equiv(self, other):
        """
        Returns True if any state in the Category `other` is in this Category. Note that
        this is *intransitive*; ``a.equiv(b)`` is not the same as ``b.equiv(a)``.
        """
        if self.name != other.name: return False
        for key, value in self.args.iteritems():
            if not other.args.has_key(key): return False
            if value == None: continue
            if other.args[key] == None: continue
            if not other.args[key] == value: return False
        return True
    
    def intersect(self, other):
        """
        Returns a new category consisting only of states that are in this
        category, and the category `other`.
        """
        if self.name != other.name: return None
        args = {}
        for key in list(set(self.args.keys() + other.args.keys())):
            if not self.args.has_key(key):
                args[key] = other.args[key]
            elif not other.args.has_key(key):
                args[key] = self.args[key]
            elif self.args[key] == None:
                args[key] = other.args[key]
            elif other.args[key] == None:
                args[key] = self.args[key]
            elif self.args[key] != other.args[key]:
                return None
            else: # self.args[key] == other.args[key]
                args[key] = self.args[key]
        return Category(self.name, **args)

    def __str__(self):
        string = self.name + "("
        had = False
        for key, val in self.args.iteritems():
            if had:
                string += ", "
            had = True
            if val == None:
                string += "%%%s" % key
            else:
                string += "%s: %s" % (key, val)
        return string + ")"

    def __repr__(self):
        return self.__str__()

    def __hash__(self):
        return hash((self.argstup(), self.name))

    def __eq__(self, other):
        return self.name == other.name and self.argstup() == other.argstup()

    def argstup(self):
        """
        Returns arguments as a sorted tuple.
        """
        retval = []
        keys = self.args.keys()
        keys.sort()
        for key in keys:
            retval.append((key, self.args[key]))
        return tuple(retval)

    def fill(self, info):
        """
        Set any arguments matched by this category that don't have explicitly
        required values to require the values in `info`.
        """
        args = {}
        for key, value in self.args.iteritems():
            if value != None or not info.has_key(key):
                args[key] = value
            else:
                args[key] = info[key]
        return Category(self.name, **args)

class StateMachine:
    """
    The state machine contains a list of dependencies between states, and a list
    of states which are "up."
    """

    def __init__(self):
        """
        Create a new state machine
        """
        self.holds = {}
        self.deps = []

    def assert_state(self, cat):
        """
        Move states in the given Category `cat` from down to up.
        """
        found = None
        for dependency in self.get_applicable_deps(cat):
            res = self.get_satisfied_states(cat, dependency)
            if len(res) == 0:
                return False
            if found == None:
                found = res
            else:
                found = self.intersect_list(found, res)
        if found == None:
            self.add_hold(cat)
            return True
        if len(found) == 0:
            return False
        for x in found:
            self.add_hold(x)
        return True

    def intersect_list(self, cats1, cats2):
        """
        Given two lists of categories, return a list of categories such that a
        state appearing in at least one category in each list will appear in at
        least one category in the returned list.
        """
        retval = set()
        found = set()
        for x in cats1:
            for y in cats2:
                if x == y: continue
                inter = x.intersect(y)
                if inter != None:
                    retval.add(inter)
                    found.add(x)
                    found.add(y)
        if len(found) == 0:
            return cats1 & cats2
        return (retval | ((cats1 & cats2) - found))

    def add_hold(self, cat):
        """
        Add a hold to a state. Does not check dependencies.
        """
        if self.holds.has_key(cat):
            self.holds[cat] = self.holds[cat] + 1
        else:
            self.holds[cat] = 1

    def get_satisfied_states(self, dependents, dependencies):
        """
        Given that states in `dependents` depend on states in `dependencies`,
        return a new Category that contains only the states in `dependents` that
        could match states in `dependencies`.
        """
        retval = []
        for key, val in self.holds.iteritems():
            if dependencies.equiv(key) and val > 0:
                retval.append(dependents.fill(key.args))
        return set(retval)

    def get_applicable_deps(self, cat):
        """
        Find dependencies that might apply to members of `cat`
        """
        retval = []
        for (x, y) in self.deps:
            if x.equiv(cat):
                retval.append(y.fill(cat.intersect(x).args))
        return retval

if __name__ == "__main__":
    sm = StateMachine()
    sm.deps.append((Category("mounted", type="nfs"), Category("network_up")))
    sm.deps.append((Category("mounted", uuid=None, devname=None, label=None), Category("found_disk", uuid=None, devname=None, label=None)))
    sm.deps.append((Category("mounted", uuid=None, devname=None, label=None), Category("vol_conf", uuid=None, devname=None, label=None)))
    sm.assert_state(Category("vol_conf", uuid=None, devname=None, label="myroot", type="ext3", mountpoint="/"))
    sm.assert_state(Category("found_disk", uuid="d3adb3ef", devname="/dev/sda", label="myroot"))
    sm.assert_state(Category("mounted", uuid=None, type="ext3", devname=None, label=None, mountpoint=None))
    print sm.holds