Improve remote API QA tests
[ganeti-local] / qa / qa_utils.py
1 # Copyright (C) 2007 Google Inc.
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 # General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
16 # 02110-1301, USA.
17
18
19 """Utilities for QA tests.
20
21 """
22
23 import os
24 import sys
25 import subprocess
26
27 from ganeti import utils
28
29 import qa_config
30 import qa_error
31
32
33 _INFO_SEQ = None
34 _WARNING_SEQ = None
35 _ERROR_SEQ = None
36 _RESET_SEQ = None
37
38
39 # List of all hooks
40 _hooks = []
41
42
43 def _SetupColours():
44   """Initializes the colour constants.
45
46   """
47   global _INFO_SEQ, _WARNING_SEQ, _ERROR_SEQ, _RESET_SEQ
48
49   # Don't use colours if stdout isn't a terminal
50   if not sys.stdout.isatty():
51     return
52
53   try:
54     import curses
55   except ImportError:
56     # Don't use colours if curses module can't be imported
57     return
58
59   curses.setupterm()
60
61   _RESET_SEQ = curses.tigetstr("op")
62
63   setaf = curses.tigetstr("setaf")
64   _INFO_SEQ = curses.tparm(setaf, curses.COLOR_GREEN)
65   _WARNING_SEQ = curses.tparm(setaf, curses.COLOR_YELLOW)
66   _ERROR_SEQ = curses.tparm(setaf, curses.COLOR_RED)
67
68
69 _SetupColours()
70
71
72 def AssertIn(item, sequence):
73   """Raises an error when item is not in sequence.
74
75   """
76   if item not in sequence:
77     raise qa_error.Error('%r not in %r' % (item, sequence))
78
79
80 def AssertEqual(first, second):
81   """Raises an error when values aren't equal.
82
83   """
84   if not first == second:
85     raise qa_error.Error('%r == %r' % (first, second))
86
87
88 def AssertNotEqual(first, second):
89   """Raises an error when values are equal.
90
91   """
92   if not first != second:
93     raise qa_error.Error('%r != %r' % (first, second))
94
95
96 def GetSSHCommand(node, cmd, strict=True):
97   """Builds SSH command to be executed.
98
99   """
100   args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root' ]
101
102   if strict:
103     tmp = 'yes'
104   else:
105     tmp = 'no'
106   args.append('-oStrictHostKeyChecking=%s' % tmp)
107   args.append('-oClearAllForwardings=yes')
108   args.append('-oForwardAgent=yes')
109   args.append(node)
110
111   if qa_config.options.dry_run:
112     prefix = 'exit 0; '
113   else:
114     prefix = ''
115
116   args.append(prefix + cmd)
117
118   print 'SSH:', utils.ShellQuoteArgs(args)
119
120   return args
121
122
123 def StartSSH(node, cmd, strict=True):
124   """Starts SSH.
125
126   """
127   return subprocess.Popen(GetSSHCommand(node, cmd, strict=strict),
128                           shell=False)
129
130
131 def GetCommandOutput(node, cmd):
132   """Returns the output of a command executed on the given node.
133
134   """
135   p = subprocess.Popen(GetSSHCommand(node, cmd),
136                        shell=False, stdout=subprocess.PIPE)
137   AssertEqual(p.wait(), 0)
138   return p.stdout.read()
139
140
141 def UploadFile(node, src):
142   """Uploads a file to a node and returns the filename.
143
144   Caller needs to remove the returned file on the node when it's not needed
145   anymore.
146   """
147   # Make sure nobody else has access to it while preserving local permissions
148   mode = os.stat(src).st_mode & 0700
149
150   cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
151          '[[ -f "${tmp}" ]] && '
152          'cat > "${tmp}" && '
153          'echo "${tmp}"') % mode
154
155   f = open(src, 'r')
156   try:
157     p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
158                          stdout=subprocess.PIPE)
159     AssertEqual(p.wait(), 0)
160
161     # Return temporary filename
162     return p.stdout.read().strip()
163   finally:
164     f.close()
165
166
167 def _ResolveName(cmd, key):
168   """Helper function.
169
170   """
171   master = qa_config.GetMasterNode()
172
173   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
174   for line in output.splitlines():
175     (lkey, lvalue) = line.split(':', 1)
176     if lkey == key:
177       return lvalue.lstrip()
178   raise KeyError("Key not found")
179
180
181 def ResolveInstanceName(instance):
182   """Gets the full name of an instance.
183
184   """
185   return _ResolveName(['gnt-instance', 'info', instance['name']],
186                       'Instance name')
187
188
189 def ResolveNodeName(node):
190   """Gets the full name of a node.
191
192   """
193   return _ResolveName(['gnt-node', 'info', node['primary']],
194                       'Node name')
195
196
197 def GetNodeInstances(node, secondaries=False):
198   """Gets a list of instances on a node.
199
200   """
201   master = qa_config.GetMasterNode()
202
203   node_name = ResolveNodeName(node)
204
205   # Get list of all instances
206   cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
207          '--output=name,pnode,snodes']
208   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
209
210   instances = []
211   for line in output.splitlines():
212     (name, pnode, snodes) = line.split(':', 2)
213     if ((not secondaries and pnode == node_name) or
214         (secondaries and node_name in snodes.split(','))):
215       instances.append(name)
216
217   return instances
218
219
220 def _FormatWithColor(text, seq):
221   if not seq:
222     return text
223   return "%s%s%s" % (seq, text, _RESET_SEQ)
224
225
226 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
227 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
228 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)
229
230
231 def LoadHooks():
232   """Load all QA hooks.
233
234   """
235   hooks_dir = qa_config.get('options', {}).get('hooks-dir', None)
236   if not hooks_dir:
237     return
238   if hooks_dir not in sys.path:
239     sys.path.insert(0, hooks_dir)
240   for name in utils.ListVisibleFiles(hooks_dir):
241     if name.endswith('.py'):
242       # Load and instanciate hook
243       print "Loading hook %s" % name
244       _hooks.append(__import__(name[:-3], None, None, ['']).hook())
245
246
247 class QaHookContext:
248   name = None
249   phase = None
250   success = None
251   args = None
252   kwargs = None
253
254
255 def _CallHooks(ctx):
256   """Calls all hooks with the given context.
257
258   """
259   if not _hooks:
260     return
261
262   name = "%s-%s" % (ctx.phase, ctx.name)
263   if ctx.success is not None:
264     msg = "%s (success=%s)" % (name, ctx.success)
265   else:
266     msg = name
267   print FormatInfo("Begin %s" % msg)
268   for hook in _hooks:
269     hook.run(ctx)
270   print FormatInfo("End %s" % name)
271
272
273 def DefineHook(name):
274   """Wraps a function with calls to hooks.
275
276   Usage: prefix function with @qa_utils.DefineHook(...)
277
278   This based on PEP 318, "Decorators for Functions and Methods".
279
280   """
281   def wrapper(fn):
282     def new_f(*args, **kwargs):
283       # Create context
284       ctx = QaHookContext()
285       ctx.name = name
286       ctx.phase = 'pre'
287       ctx.args = args
288       ctx.kwargs = kwargs
289
290       _CallHooks(ctx)
291       try:
292         ctx.phase = 'post'
293         ctx.success = True
294         try:
295           # Call real function
296           return fn(*args, **kwargs)
297         except:
298           ctx.success = False
299           raise
300       finally:
301         _CallHooks(ctx)
302
303     # Override function metadata
304     new_f.func_name = fn.func_name
305     new_f.func_doc = fn.func_doc
306
307     return new_f
308
309   return wrapper