root / lib / hooksmaster.py @ bfca72bc
History | View | Annotate | Download (9.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, log_fn, htype=None,
|
50 |
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 log_fn: function that accepts a string
|
74 |
@param log_fn: logging function
|
75 |
@type htype: string or None
|
76 |
@param htype: None or one of L{constants.HTYPE_CLUSTER},
|
77 |
L{constants.HTYPE_NODE}, L{constants.HTYPE_INSTANCE}
|
78 |
@type cluster_name: string
|
79 |
@param cluster_name: name of the cluster
|
80 |
@type master_name: string
|
81 |
@param master_name: name of the master
|
82 |
|
83 |
"""
|
84 |
self.opcode = opcode
|
85 |
self.hooks_path = hooks_path
|
86 |
self.hooks_execution_fn = hooks_execution_fn
|
87 |
self.hooks_results_adapt_fn = hooks_results_adapt_fn
|
88 |
self.build_env_fn = build_env_fn
|
89 |
self.log_fn = log_fn
|
90 |
self.htype = htype
|
91 |
self.cluster_name = cluster_name
|
92 |
self.master_name = master_name
|
93 |
|
94 |
self.pre_env = self._BuildEnv(constants.HOOKS_PHASE_PRE) |
95 |
(self.pre_nodes, self.post_nodes) = nodes |
96 |
|
97 |
def _BuildEnv(self, phase): |
98 |
"""Compute the environment and the target nodes.
|
99 |
|
100 |
Based on the opcode and the current node list, this builds the
|
101 |
environment for the hooks and the target node list for the run.
|
102 |
|
103 |
"""
|
104 |
if phase == constants.HOOKS_PHASE_PRE:
|
105 |
prefix = "GANETI_"
|
106 |
elif phase == constants.HOOKS_PHASE_POST:
|
107 |
prefix = "GANETI_POST_"
|
108 |
else:
|
109 |
raise AssertionError("Unknown phase '%s'" % phase) |
110 |
|
111 |
env = {} |
112 |
|
113 |
if self.hooks_path is not None: |
114 |
phase_env = self.build_env_fn()
|
115 |
if phase_env:
|
116 |
assert not compat.any(key.upper().startswith(prefix) |
117 |
for key in phase_env) |
118 |
env.update(("%s%s" % (prefix, key), value)
|
119 |
for (key, value) in phase_env.items()) |
120 |
|
121 |
if phase == constants.HOOKS_PHASE_PRE:
|
122 |
assert compat.all((key.startswith("GANETI_") and |
123 |
not key.startswith("GANETI_POST_")) |
124 |
for key in env) |
125 |
|
126 |
elif phase == constants.HOOKS_PHASE_POST:
|
127 |
assert compat.all(key.startswith("GANETI_POST_") for key in env) |
128 |
assert isinstance(self.pre_env, dict) |
129 |
|
130 |
# Merge with pre-phase environment
|
131 |
assert not compat.any(key.startswith("GANETI_POST_") |
132 |
for key in self.pre_env) |
133 |
env.update(self.pre_env)
|
134 |
else:
|
135 |
raise AssertionError("Unknown phase '%s'" % phase) |
136 |
|
137 |
return env
|
138 |
|
139 |
def _RunWrapper(self, node_list, hpath, phase, phase_env): |
140 |
"""Simple wrapper over self.callfn.
|
141 |
|
142 |
This method fixes the environment before executing the hooks.
|
143 |
|
144 |
"""
|
145 |
env = { |
146 |
"PATH": constants.HOOKS_PATH,
|
147 |
"GANETI_HOOKS_VERSION": constants.HOOKS_VERSION,
|
148 |
"GANETI_OP_CODE": self.opcode, |
149 |
"GANETI_DATA_DIR": pathutils.DATA_DIR,
|
150 |
"GANETI_HOOKS_PHASE": phase,
|
151 |
"GANETI_HOOKS_PATH": hpath,
|
152 |
} |
153 |
|
154 |
if self.htype: |
155 |
env["GANETI_OBJECT_TYPE"] = self.htype |
156 |
|
157 |
if self.cluster_name is not None: |
158 |
env["GANETI_CLUSTER"] = self.cluster_name |
159 |
|
160 |
if self.master_name is not None: |
161 |
env["GANETI_MASTER"] = self.master_name |
162 |
|
163 |
if phase_env:
|
164 |
env = utils.algo.JoinDisjointDicts(env, phase_env) |
165 |
|
166 |
# Convert everything to strings
|
167 |
env = dict([(str(key), str(val)) for key, val in env.iteritems()]) |
168 |
|
169 |
assert compat.all(key == "PATH" or key.startswith("GANETI_") |
170 |
for key in env) |
171 |
|
172 |
return self.hooks_execution_fn(node_list, hpath, phase, env) |
173 |
|
174 |
def RunPhase(self, phase, nodes=None): |
175 |
"""Run all the scripts for a phase.
|
176 |
|
177 |
This is the main function of the HookMaster.
|
178 |
It executes self.hooks_execution_fn, and after running
|
179 |
self.hooks_results_adapt_fn on its results it expects them to be in the form
|
180 |
{node_name: (fail_msg, [(script, result, output), ...]}).
|
181 |
|
182 |
@param phase: one of L{constants.HOOKS_PHASE_POST} or
|
183 |
L{constants.HOOKS_PHASE_PRE}; it denotes the hooks phase
|
184 |
@param nodes: overrides the predefined list of nodes for the given phase
|
185 |
@return: the processed results of the hooks multi-node rpc call
|
186 |
@raise errors.HooksFailure: on communication failure to the nodes
|
187 |
@raise errors.HooksAbort: on failure of one of the hooks
|
188 |
|
189 |
"""
|
190 |
if phase == constants.HOOKS_PHASE_PRE:
|
191 |
if nodes is None: |
192 |
nodes = self.pre_nodes
|
193 |
env = self.pre_env
|
194 |
elif phase == constants.HOOKS_PHASE_POST:
|
195 |
if nodes is None: |
196 |
nodes = self.post_nodes
|
197 |
env = self._BuildEnv(phase)
|
198 |
else:
|
199 |
raise AssertionError("Unknown phase '%s'" % phase) |
200 |
|
201 |
if not nodes: |
202 |
# empty node list, we should not attempt to run this as either
|
203 |
# we're in the cluster init phase and the rpc client part can't
|
204 |
# even attempt to run, or this LU doesn't do hooks at all
|
205 |
return
|
206 |
|
207 |
results = self._RunWrapper(nodes, self.hooks_path, phase, env) |
208 |
if not results: |
209 |
msg = "Communication Failure"
|
210 |
if phase == constants.HOOKS_PHASE_PRE:
|
211 |
raise errors.HooksFailure(msg)
|
212 |
else:
|
213 |
self.log_fn(msg)
|
214 |
return results
|
215 |
|
216 |
converted_res = results |
217 |
if self.hooks_results_adapt_fn: |
218 |
converted_res = self.hooks_results_adapt_fn(results)
|
219 |
|
220 |
errs = [] |
221 |
for node_name, (fail_msg, offline, hooks_results) in converted_res.items(): |
222 |
if offline:
|
223 |
continue
|
224 |
|
225 |
if fail_msg:
|
226 |
self.log_fn("Communication failure to node %s: %s", node_name, fail_msg) |
227 |
continue
|
228 |
|
229 |
for script, hkr, output in hooks_results: |
230 |
if hkr == constants.HKR_FAIL:
|
231 |
if phase == constants.HOOKS_PHASE_PRE:
|
232 |
errs.append((node_name, script, output)) |
233 |
else:
|
234 |
if not output: |
235 |
output = "(no output)"
|
236 |
self.log_fn("On %s script %s failed, output: %s" % |
237 |
(node_name, script, output)) |
238 |
|
239 |
if errs and phase == constants.HOOKS_PHASE_PRE: |
240 |
raise errors.HooksAbort(errs)
|
241 |
|
242 |
return results
|
243 |
|
244 |
def RunConfigUpdate(self): |
245 |
"""Run the special configuration update hook
|
246 |
|
247 |
This is a special hook that runs only on the master after each
|
248 |
top-level LI if the configuration has been updated.
|
249 |
|
250 |
"""
|
251 |
phase = constants.HOOKS_PHASE_POST |
252 |
hpath = constants.HOOKS_NAME_CFGUPDATE |
253 |
nodes = [self.master_name]
|
254 |
self._RunWrapper(nodes, hpath, phase, self.pre_env) |
255 |
|
256 |
@staticmethod
|
257 |
def BuildFromLu(hooks_execution_fn, lu): |
258 |
if lu.HPATH is None: |
259 |
nodes = (None, None) |
260 |
else:
|
261 |
nodes = map(frozenset, lu.BuildHooksNodes()) |
262 |
|
263 |
master_name = cluster_name = None
|
264 |
if lu.cfg:
|
265 |
master_name = lu.cfg.GetMasterNode() |
266 |
cluster_name = lu.cfg.GetClusterName() |
267 |
|
268 |
return HooksMaster(lu.op.OP_ID, lu.HPATH, nodes, hooks_execution_fn,
|
269 |
_RpcResultsToHooksResults, lu.BuildHooksEnv, |
270 |
lu.LogWarning, lu.HTYPE, cluster_name, master_name) |