Statistics
| Branch: | Tag: | Revision:

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)