root / lib / hooksmaster.py @ 237a833c
History | View | Annotate | Download (10.2 kB)
1 |
#
|
---|---|
2 |
#
|
3 |
|
4 |
# Copyright (C) 2006, 2007, 2011, 2012 Google Inc.
|
5 |
#
|
6 |
# This program is free software; you can redistribute it and/or modify
|
7 |
# it under the terms of the GNU General Public License as published by
|
8 |
# the Free Software Foundation; either version 2 of the License, or
|
9 |
# (at your option) any later version.
|
10 |
#
|
11 |
# This program is distributed in the hope that it will be useful, but
|
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14 |
# General Public License for more details.
|
15 |
#
|
16 |
# You should have received a copy of the GNU General Public License
|
17 |
# along with this program; if not, write to the Free Software
|
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
19 |
# 02110-1301, USA.
|
20 |
|
21 |
|
22 |
"""Module implementing the logic for running hooks.
|
23 |
|
24 |
"""
|
25 |
|
26 |
from ganeti import constants |
27 |
from ganeti import errors |
28 |
from ganeti import utils |
29 |
from ganeti import compat |
30 |
from ganeti import pathutils |
31 |
|
32 |
|
33 |
def _RpcResultsToHooksResults(rpc_results): |
34 |
"""Function to convert RPC results to the format expected by HooksMaster.
|
35 |
|
36 |
@type rpc_results: dict(node: L{rpc.RpcResult})
|
37 |
@param rpc_results: RPC results
|
38 |
@rtype: dict(node: (fail_msg, offline, hooks_results))
|
39 |
@return: RPC results unpacked according to the format expected by
|
40 |
L({hooksmaster.HooksMaster}
|
41 |
|
42 |
"""
|
43 |
return dict((node, (rpc_res.fail_msg, rpc_res.offline, rpc_res.payload)) |
44 |
for (node, rpc_res) in rpc_results.items()) |
45 |
|
46 |
|
47 |
class HooksMaster(object): |
48 |
def __init__(self, opcode, hooks_path, nodes, hooks_execution_fn, |
49 |
hooks_results_adapt_fn, build_env_fn, prepare_post_nodes_fn, |
50 |
log_fn, htype=None, cluster_name=None, master_name=None): |
51 |
"""Base class for hooks masters.
|
52 |
|
53 |
This class invokes the execution of hooks according to the behaviour
|
54 |
specified by its parameters.
|
55 |
|
56 |
@type opcode: string
|
57 |
@param opcode: opcode of the operation to which the hooks are tied
|
58 |
@type hooks_path: string
|
59 |
@param hooks_path: prefix of the hooks directories
|
60 |
@type nodes: 2-tuple of lists
|
61 |
@param nodes: 2-tuple of lists containing nodes on which pre-hooks must be
|
62 |
run and nodes on which post-hooks must be run
|
63 |
@type hooks_execution_fn: function that accepts the following parameters:
|
64 |
(node_list, hooks_path, phase, environment)
|
65 |
@param hooks_execution_fn: function that will execute the hooks; can be
|
66 |
None, indicating that no conversion is necessary.
|
67 |
@type hooks_results_adapt_fn: function
|
68 |
@param hooks_results_adapt_fn: function that will adapt the return value of
|
69 |
hooks_execution_fn to the format expected by RunPhase
|
70 |
@type build_env_fn: function that returns a dictionary having strings as
|
71 |
keys
|
72 |
@param build_env_fn: function that builds the environment for the hooks
|
73 |
@type prepare_post_nodes_fn: function that take a list of node UUIDs and
|
74 |
returns a list of node UUIDs
|
75 |
@param prepare_post_nodes_fn: function that is invoked right before
|
76 |
executing post hooks and can change the list of node UUIDs to run the post
|
77 |
hooks on
|
78 |
@type log_fn: function that accepts a string
|
79 |
@param log_fn: logging function
|
80 |
@type htype: string or None
|
81 |
@param htype: None or one of L{constants.HTYPE_CLUSTER},
|
82 |
L{constants.HTYPE_NODE}, L{constants.HTYPE_INSTANCE}
|
83 |
@type cluster_name: string
|
84 |
@param cluster_name: name of the cluster
|
85 |
@type master_name: string
|
86 |
@param master_name: name of the master
|
87 |
|
88 |
"""
|
89 |
self.opcode = opcode
|
90 |
self.hooks_path = hooks_path
|
91 |
self.hooks_execution_fn = hooks_execution_fn
|
92 |
self.hooks_results_adapt_fn = hooks_results_adapt_fn
|
93 |
self.build_env_fn = build_env_fn
|
94 |
self.prepare_post_nodes_fn = prepare_post_nodes_fn
|
95 |
self.log_fn = log_fn
|
96 |
self.htype = htype
|
97 |
self.cluster_name = cluster_name
|
98 |
self.master_name = master_name
|
99 |
|
100 |
self.pre_env = self._BuildEnv(constants.HOOKS_PHASE_PRE) |
101 |
(self.pre_nodes, self.post_nodes) = nodes |
102 |
|
103 |
def _BuildEnv(self, phase): |
104 |
"""Compute the environment and the target nodes.
|
105 |
|
106 |
Based on the opcode and the current node list, this builds the
|
107 |
environment for the hooks and the target node list for the run.
|
108 |
|
109 |
"""
|
110 |
if phase == constants.HOOKS_PHASE_PRE:
|
111 |
prefix = "GANETI_"
|
112 |
elif phase == constants.HOOKS_PHASE_POST:
|
113 |
prefix = "GANETI_POST_"
|
114 |
else:
|
115 |
raise AssertionError("Unknown phase '%s'" % phase) |
116 |
|
117 |
env = {} |
118 |
|
119 |
if self.hooks_path is not None: |
120 |
phase_env = self.build_env_fn()
|
121 |
if phase_env:
|
122 |
assert not compat.any(key.upper().startswith(prefix) |
123 |
for key in phase_env) |
124 |
env.update(("%s%s" % (prefix, key), value)
|
125 |
for (key, value) in phase_env.items()) |
126 |
|
127 |
if phase == constants.HOOKS_PHASE_PRE:
|
128 |
assert compat.all((key.startswith("GANETI_") and |
129 |
not key.startswith("GANETI_POST_")) |
130 |
for key in env) |
131 |
|
132 |
elif phase == constants.HOOKS_PHASE_POST:
|
133 |
assert compat.all(key.startswith("GANETI_POST_") for key in env) |
134 |
assert isinstance(self.pre_env, dict) |
135 |
|
136 |
# Merge with pre-phase environment
|
137 |
assert not compat.any(key.startswith("GANETI_POST_") |
138 |
for key in self.pre_env) |
139 |
env.update(self.pre_env)
|
140 |
else:
|
141 |
raise AssertionError("Unknown phase '%s'" % phase) |
142 |
|
143 |
return env
|
144 |
|
145 |
def _RunWrapper(self, node_list, hpath, phase, phase_env): |
146 |
"""Simple wrapper over self.callfn.
|
147 |
|
148 |
This method fixes the environment before executing the hooks.
|
149 |
|
150 |
"""
|
151 |
env = { |
152 |
"PATH": constants.HOOKS_PATH,
|
153 |
"GANETI_HOOKS_VERSION": constants.HOOKS_VERSION,
|
154 |
"GANETI_OP_CODE": self.opcode, |
155 |
"GANETI_DATA_DIR": pathutils.DATA_DIR,
|
156 |
"GANETI_HOOKS_PHASE": phase,
|
157 |
"GANETI_HOOKS_PATH": hpath,
|
158 |
} |
159 |
|
160 |
if self.htype: |
161 |
env["GANETI_OBJECT_TYPE"] = self.htype |
162 |
|
163 |
if self.cluster_name is not None: |
164 |
env["GANETI_CLUSTER"] = self.cluster_name |
165 |
|
166 |
if self.master_name is not None: |
167 |
env["GANETI_MASTER"] = self.master_name |
168 |
|
169 |
if phase_env:
|
170 |
env = utils.algo.JoinDisjointDicts(env, phase_env) |
171 |
|
172 |
# Convert everything to strings
|
173 |
env = dict([(str(key), str(val)) for key, val in env.iteritems()]) |
174 |
|
175 |
assert compat.all(key == "PATH" or key.startswith("GANETI_") |
176 |
for key in env) |
177 |
|
178 |
return self.hooks_execution_fn(node_list, hpath, phase, env) |
179 |
|
180 |
def RunPhase(self, phase, node_names=None): |
181 |
"""Run all the scripts for a phase.
|
182 |
|
183 |
This is the main function of the HookMaster.
|
184 |
It executes self.hooks_execution_fn, and after running
|
185 |
self.hooks_results_adapt_fn on its results it expects them to be in the
|
186 |
form {node_name: (fail_msg, [(script, result, output), ...]}).
|
187 |
|
188 |
@param phase: one of L{constants.HOOKS_PHASE_POST} or
|
189 |
L{constants.HOOKS_PHASE_PRE}; it denotes the hooks phase
|
190 |
@param node_names: overrides the predefined list of nodes for the given
|
191 |
phase
|
192 |
@return: the processed results of the hooks multi-node rpc call
|
193 |
@raise errors.HooksFailure: on communication failure to the nodes
|
194 |
@raise errors.HooksAbort: on failure of one of the hooks
|
195 |
|
196 |
"""
|
197 |
if phase == constants.HOOKS_PHASE_PRE:
|
198 |
if node_names is None: |
199 |
node_names = self.pre_nodes
|
200 |
env = self.pre_env
|
201 |
elif phase == constants.HOOKS_PHASE_POST:
|
202 |
if node_names is None: |
203 |
node_names = self.post_nodes
|
204 |
if node_names is not None and self.prepare_post_nodes_fn is not None: |
205 |
node_names = frozenset(self.prepare_post_nodes_fn(list(node_names))) |
206 |
env = self._BuildEnv(phase)
|
207 |
else:
|
208 |
raise AssertionError("Unknown phase '%s'" % phase) |
209 |
|
210 |
if not node_names: |
211 |
# empty node list, we should not attempt to run this as either
|
212 |
# we're in the cluster init phase and the rpc client part can't
|
213 |
# even attempt to run, or this LU doesn't do hooks at all
|
214 |
return
|
215 |
|
216 |
results = self._RunWrapper(node_names, self.hooks_path, phase, env) |
217 |
if not results: |
218 |
msg = "Communication Failure"
|
219 |
if phase == constants.HOOKS_PHASE_PRE:
|
220 |
raise errors.HooksFailure(msg)
|
221 |
else:
|
222 |
self.log_fn(msg)
|
223 |
return results
|
224 |
|
225 |
converted_res = results |
226 |
if self.hooks_results_adapt_fn: |
227 |
converted_res = self.hooks_results_adapt_fn(results)
|
228 |
|
229 |
errs = [] |
230 |
for node_name, (fail_msg, offline, hooks_results) in converted_res.items(): |
231 |
if offline:
|
232 |
continue
|
233 |
|
234 |
if fail_msg:
|
235 |
self.log_fn("Communication failure to node %s: %s", node_name, fail_msg) |
236 |
continue
|
237 |
|
238 |
for script, hkr, output in hooks_results: |
239 |
if hkr == constants.HKR_FAIL:
|
240 |
if phase == constants.HOOKS_PHASE_PRE:
|
241 |
errs.append((node_name, script, output)) |
242 |
else:
|
243 |
if not output: |
244 |
output = "(no output)"
|
245 |
self.log_fn("On %s script %s failed, output: %s" % |
246 |
(node_name, script, output)) |
247 |
|
248 |
if errs and phase == constants.HOOKS_PHASE_PRE: |
249 |
raise errors.HooksAbort(errs)
|
250 |
|
251 |
return results
|
252 |
|
253 |
def RunConfigUpdate(self): |
254 |
"""Run the special configuration update hook
|
255 |
|
256 |
This is a special hook that runs only on the master after each
|
257 |
top-level LI if the configuration has been updated.
|
258 |
|
259 |
"""
|
260 |
phase = constants.HOOKS_PHASE_POST |
261 |
hpath = constants.HOOKS_NAME_CFGUPDATE |
262 |
nodes = [self.master_name]
|
263 |
self._RunWrapper(nodes, hpath, phase, self.pre_env) |
264 |
|
265 |
@staticmethod
|
266 |
def BuildFromLu(hooks_execution_fn, lu): |
267 |
if lu.HPATH is None: |
268 |
nodes = (None, None) |
269 |
else:
|
270 |
hooks_nodes = lu.BuildHooksNodes() |
271 |
to_name = lambda node_uuids: frozenset(lu.cfg.GetNodeNames(node_uuids)) |
272 |
if len(hooks_nodes) == 2: |
273 |
nodes = (to_name(hooks_nodes[0]), to_name(hooks_nodes[1])) |
274 |
elif len(hooks_nodes) == 3: |
275 |
nodes = (to_name(hooks_nodes[0]),
|
276 |
to_name(hooks_nodes[1]) | frozenset(hooks_nodes[2])) |
277 |
else:
|
278 |
raise errors.ProgrammerError(
|
279 |
"LogicalUnit.BuildHooksNodes must return a 2- or 3-tuple")
|
280 |
|
281 |
master_name = cluster_name = None
|
282 |
if lu.cfg:
|
283 |
master_name = lu.cfg.GetMasterNodeName() |
284 |
cluster_name = lu.cfg.GetClusterName() |
285 |
|
286 |
return HooksMaster(lu.op.OP_ID, lu.HPATH, nodes, hooks_execution_fn,
|
287 |
_RpcResultsToHooksResults, lu.BuildHooksEnv, |
288 |
lu.PreparePostHookNodes, lu.LogWarning, lu.HTYPE, |
289 |
cluster_name, master_name) |