Simplify QA commands
[ganeti-local] / qa / qa_utils.py
1 #
2 #
3
4 # Copyright (C) 2007 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 """Utilities for QA tests.
23
24 """
25
26 import os
27 import re
28 import sys
29 import subprocess
30
31 from ganeti import utils
32
33 import qa_config
34 import qa_error
35
36
37 _INFO_SEQ = None
38 _WARNING_SEQ = None
39 _ERROR_SEQ = None
40 _RESET_SEQ = None
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 AssertMatch(string, pattern):
97   """Raises an error when string doesn't match regexp pattern.
98
99   """
100   if not re.match(pattern, string):
101     raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
102
103
104 def AssertCommand(cmd, fail=False, node=None):
105   """Checks that a remote command succeeds.
106
107   @param cmd: either a string (the command to execute) or a list (to
108       be converted using L{utils.ShellQuoteArgs} into a string)
109   @type fail: boolean
110   @param fail: if the command is expected to fail instead of succeeding
111   @param node: if passed, it should be the node on which the command
112       should be executed, instead of the master node (can be either a
113       dict or a string)
114
115   """
116   if node is None:
117     node = qa_config.GetMasterNode()
118
119   if isinstance(node, basestring):
120     nodename = node
121   else:
122     nodename = node["primary"]
123
124   if isinstance(cmd, basestring):
125     cmdstr = cmd
126   else:
127     cmdstr = utils.ShellQuoteArgs(cmd)
128
129   rcode = StartSSH(nodename, cmdstr).wait()
130
131   if fail:
132     if rcode == 0:
133       raise qa_error.Error("Command '%s' on node %s was expected to fail but"
134                            " didn't" % (cmdstr, nodename))
135   else:
136     if rcode != 0:
137       raise qa_error.Error("Command '%s' on node %s failed, exit code %s" %
138                            (cmdstr, nodename, rcode))
139
140
141 def GetSSHCommand(node, cmd, strict=True):
142   """Builds SSH command to be executed.
143
144   Args:
145   - node: Node the command should run on
146   - cmd: Command to be executed as a list with all parameters
147   - strict: Whether to enable strict host key checking
148
149   """
150   args = [ 'ssh', '-oEscapeChar=none', '-oBatchMode=yes', '-l', 'root', '-t' ]
151
152   if strict:
153     tmp = 'yes'
154   else:
155     tmp = 'no'
156   args.append('-oStrictHostKeyChecking=%s' % tmp)
157   args.append('-oClearAllForwardings=yes')
158   args.append('-oForwardAgent=yes')
159   args.append(node)
160   args.append(cmd)
161
162   return args
163
164
165 def StartLocalCommand(cmd, **kwargs):
166   """Starts a local command.
167
168   """
169   print "Command: %s" % utils.ShellQuoteArgs(cmd)
170   return subprocess.Popen(cmd, shell=False, **kwargs)
171
172
173 def StartSSH(node, cmd, strict=True):
174   """Starts SSH.
175
176   """
177   return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
178
179
180 def GetCommandOutput(node, cmd):
181   """Returns the output of a command executed on the given node.
182
183   """
184   p = StartLocalCommand(GetSSHCommand(node, cmd), stdout=subprocess.PIPE)
185   AssertEqual(p.wait(), 0)
186   return p.stdout.read()
187
188
189 def UploadFile(node, src):
190   """Uploads a file to a node and returns the filename.
191
192   Caller needs to remove the returned file on the node when it's not needed
193   anymore.
194
195   """
196   # Make sure nobody else has access to it while preserving local permissions
197   mode = os.stat(src).st_mode & 0700
198
199   cmd = ('tmp=$(tempfile --mode %o --prefix gnt) && '
200          '[[ -f "${tmp}" ]] && '
201          'cat > "${tmp}" && '
202          'echo "${tmp}"') % mode
203
204   f = open(src, 'r')
205   try:
206     p = subprocess.Popen(GetSSHCommand(node, cmd), shell=False, stdin=f,
207                          stdout=subprocess.PIPE)
208     AssertEqual(p.wait(), 0)
209
210     # Return temporary filename
211     return p.stdout.read().strip()
212   finally:
213     f.close()
214
215
216 def BackupFile(node, path):
217   """Creates a backup of a file on the node and returns the filename.
218
219   Caller needs to remove the returned file on the node when it's not needed
220   anymore.
221
222   """
223   cmd = ("tmp=$(tempfile --prefix .gnt --directory=$(dirname %s)) && "
224          "[[ -f \"$tmp\" ]] && "
225          "cp %s $tmp && "
226          "echo $tmp") % (utils.ShellQuote(path), utils.ShellQuote(path))
227
228   # Return temporary filename
229   return GetCommandOutput(node, cmd).strip()
230
231
232 def _ResolveName(cmd, key):
233   """Helper function.
234
235   """
236   master = qa_config.GetMasterNode()
237
238   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
239   for line in output.splitlines():
240     (lkey, lvalue) = line.split(':', 1)
241     if lkey == key:
242       return lvalue.lstrip()
243   raise KeyError("Key not found")
244
245
246 def ResolveInstanceName(instance):
247   """Gets the full name of an instance.
248
249   @type instance: string
250   @param instance: Instance name
251
252   """
253   return _ResolveName(['gnt-instance', 'info', instance],
254                       'Instance name')
255
256
257 def ResolveNodeName(node):
258   """Gets the full name of a node.
259
260   """
261   return _ResolveName(['gnt-node', 'info', node['primary']],
262                       'Node name')
263
264
265 def GetNodeInstances(node, secondaries=False):
266   """Gets a list of instances on a node.
267
268   """
269   master = qa_config.GetMasterNode()
270   node_name = ResolveNodeName(node)
271
272   # Get list of all instances
273   cmd = ['gnt-instance', 'list', '--separator=:', '--no-headers',
274          '--output=name,pnode,snodes']
275   output = GetCommandOutput(master['primary'], utils.ShellQuoteArgs(cmd))
276
277   instances = []
278   for line in output.splitlines():
279     (name, pnode, snodes) = line.split(':', 2)
280     if ((not secondaries and pnode == node_name) or
281         (secondaries and node_name in snodes.split(','))):
282       instances.append(name)
283
284   return instances
285
286
287 def _FormatWithColor(text, seq):
288   if not seq:
289     return text
290   return "%s%s%s" % (seq, text, _RESET_SEQ)
291
292
293 FormatWarning = lambda text: _FormatWithColor(text, _WARNING_SEQ)
294 FormatError = lambda text: _FormatWithColor(text, _ERROR_SEQ)
295 FormatInfo = lambda text: _FormatWithColor(text, _INFO_SEQ)