/
diff.py
208 lines (185 loc) · 6.98 KB
/
diff.py
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
199
200
201
202
203
204
205
206
207
208
from __future__ import annotations
import itertools
from typing import Any, AnyStr, Callable, Dict, List, Optional, Tuple
from nixops.logger import MachineLogger
from nixops.state import StateDict
class Handler:
def __init__(
self,
keys: List[str],
after: Optional[List] = None,
handle: Optional[Callable] = None,
) -> None:
if after is None:
after = []
if handle is None:
self.handle = self._default_handle
else:
self.handle = handle
self._keys = keys
self._dependencies = after
def _default_handle(self):
"""
Method that should be implemented to handle the changes
of keys returned by get_keys()
This should be done currently by monkey-patching this method
by passing a resource state method that realizes the change.
"""
raise NotImplementedError
def get_deps(self) -> List[Handler]:
return self._dependencies
def get_keys(self, *_: AnyStr) -> List[str]:
return self._keys
class Diff:
"""
Diff engine main class which implements methods for doing diffs between
the state/config and generating a plan: sequence of handlers to be executed.
"""
SET = 0
UPDATE = 1
UNSET = 2
def __init__(
self,
# FIXME: type should be 'nixops.deployment.Deployment'
# however we have to upgrade to python3 in order
# to solve the import cycle by forward declaration
depl: Any,
logger: MachineLogger,
config: Dict[str, Any],
state: StateDict,
res_type: str,
):
# type: (...) -> None
self.handlers: List[Handler] = []
self._definition = config
self._state = state
self._depl = depl
self._type = res_type
self.logger = logger
self._diff = {} # type: Dict[str, int]
self._reserved = [
"index",
"state",
"_type",
"deployment",
"_name",
"name",
"creationTime",
]
def set_reserved_keys(self, keys: List[str]) -> None:
"""
Reserved keys are nix options or internal state keys that we don't
want them to trigger the diff engine so we simply ignore the diff
of the reserved keys.
"""
self._reserved.extend(keys)
def get_keys(self) -> List[str]:
diff = [k for k in self._diff if k not in self._reserved]
return diff
def plan(self, show: bool = False) -> List[Handler]:
"""
This will go through the attributes of the resource and evaluate
the diff between definition and state then return a sorted list
of the handlers to be called to realize the diff.
"""
keys = list(self._state.keys()) + list(self._definition.keys())
for k in keys:
self.eval_resource_attr_diff(k)
for k in self.get_keys():
definition = self.get_resource_definition(k)
if show:
if self._diff[k] == self.SET:
self.logger.log(
"will set attribute {0} to {1}".format(k, definition)
)
elif self._diff[k] == self.UPDATE:
self.logger.log(
"{0} will be updated from {1} to {2}".format(
k, self._state[k], definition
)
)
else:
self.logger.log(
"will unset attribute {0} with previous value {1} ".format(
k, self._state[k]
)
)
return self.get_handlers_sequence()
def set_handlers(self, handlers: List[Handler]) -> None:
self.handlers = handlers
def topological_sort(self, handlers: List[Handler]) -> List[Handler]:
"""
Implements a topological sort of a direct acyclic graph of
handlers using the depth first search algorithm.
The output is a sorted sequence of handlers based on their
dependencies.
"""
# TODO implement cycle detection
parent = {} # type: Dict[Handler, Optional[Handler]]
sequence = [] # type: List[Handler]
def visit(handler: Handler) -> None:
for v in handler.get_deps():
if v not in parent:
parent[v] = handler
visit(v)
sequence.append(handler)
for h in handlers:
if h not in parent:
parent[h] = None
visit(h)
return [h for h in sequence if h in handlers]
def get_handlers_sequence(self, combinations: int = 1) -> List[Handler]:
if len(self.get_keys()) == 0:
return []
h_tuple: Tuple[Handler, ...]
for h_tuple in itertools.combinations(self.handlers, combinations):
keys: List[str] = []
for item in h_tuple:
keys.extend(item.get_keys())
if combinations == len(self.handlers):
keys_not_found = set(self.get_keys()) - set(keys)
if len(keys_not_found) > 0:
raise Exception(
"Couldn't find any combination of handlers"
" that realize the change of {0} for resource type {1}".format(
str(keys_not_found), self._type
)
)
if set(self.get_keys()) <= set(keys):
handlers_seq = self.topological_sort(list(h_tuple))
return handlers_seq
return self.get_handlers_sequence(combinations + 1)
def eval_resource_attr_diff(self, key: str) -> None:
s = self._state.get(key, None)
d = self.get_resource_definition(key)
if s is None and d is not None:
self._diff[key] = self.SET
elif s is not None and d is None:
self._diff[key] = self.UNSET
elif s is not None and d is None:
if s != d:
self._diff[key] = self.UPDATE
def get_resource_definition(self, key: str) -> Any:
def retrieve_def(d):
# type: (Any) -> Any
if isinstance(d, str) and d.startswith("res-"):
name = d[4:].split(".")[0]
res_type = d.split(".")[1]
k = d.split(".")[2] if len(d.split(".")) > 2 else key
res = self._depl.get_generic_resource(name, res_type)
if res.state != res.UP:
return "computed"
try:
d = getattr(res, k)
except AttributeError:
d = res._state[k]
return d
d = self._definition.get(key, None)
if isinstance(d, list):
options = []
for option in d:
item = retrieve_def(option)
options.append(item)
return options
d = retrieve_def(d)
return d