1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """Utility functions for manipulating or working with text.
22
23 """
24
25
26 import re
27 import os
28 import time
29 import collections
30
31 from ganeti import errors
32
33
34
35 _PARSEUNIT_REGEX = re.compile(r"^([.\d]+)\s*([a-zA-Z]+)?$")
36
37
38 _SHELL_UNQUOTED_RE = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
39
40
41 _MAC_CHECK_RE = re.compile("^([0-9a-f]{2}:){5}[0-9a-f]{2}$", re.I)
42
43
44 _SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
45
46
48 """Try to match a name against a list.
49
50 This function will try to match a name like test1 against a list
51 like C{['test1.example.com', 'test2.example.com', ...]}. Against
52 this list, I{'test1'} as well as I{'test1.example'} will match, but
53 not I{'test1.ex'}. A multiple match will be considered as no match
54 at all (e.g. I{'test1'} against C{['test1.example.com',
55 'test1.example.org']}), except when the key fully matches an entry
56 (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
57
58 @type key: str
59 @param key: the name to be searched
60 @type name_list: list
61 @param name_list: the list of strings against which to search the key
62 @type case_sensitive: boolean
63 @param case_sensitive: whether to provide a case-sensitive match
64
65 @rtype: None or str
66 @return: None if there is no match I{or} if there are multiple matches,
67 otherwise the element from the list which matches
68
69 """
70 if key in name_list:
71 return key
72
73 re_flags = 0
74 if not case_sensitive:
75 re_flags |= re.IGNORECASE
76 key = key.upper()
77 mo = re.compile("^%s(\..*)?$" % re.escape(key), re_flags)
78 names_filtered = []
79 string_matches = []
80 for name in name_list:
81 if mo.match(name) is not None:
82 names_filtered.append(name)
83 if not case_sensitive and key == name.upper():
84 string_matches.append(name)
85
86 if len(string_matches) == 1:
87 return string_matches[0]
88 if len(names_filtered) == 1:
89 return names_filtered[0]
90 return None
91
92
127
128
130 """Tries to extract number and scale from the given string.
131
132 Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
133 [UNIT]}. If no unit is specified, it defaults to MiB. Return value
134 is always an int in MiB.
135
136 """
137 m = _PARSEUNIT_REGEX.match(str(input_string))
138 if not m:
139 raise errors.UnitParseError("Invalid format")
140
141 value = float(m.groups()[0])
142
143 unit = m.groups()[1]
144 if unit:
145 lcunit = unit.lower()
146 else:
147 lcunit = 'm'
148
149 if lcunit in ('m', 'mb', 'mib'):
150
151 pass
152
153 elif lcunit in ('g', 'gb', 'gib'):
154 value *= 1024
155
156 elif lcunit in ('t', 'tb', 'tib'):
157 value *= 1024 * 1024
158
159 else:
160 raise errors.UnitParseError("Unknown unit: %s" % unit)
161
162
163 if int(value) < value:
164 value += 1
165
166
167 value = int(value)
168 if value % 4:
169 value += 4 - value % 4
170
171 return value
172
173
175 """Quotes shell argument according to POSIX.
176
177 @type value: str
178 @param value: the argument to be quoted
179 @rtype: str
180 @return: the quoted value
181
182 """
183 if _SHELL_UNQUOTED_RE.match(value):
184 return value
185 else:
186 return "'%s'" % value.replace("'", "'\\''")
187
188
190 """Quotes a list of shell arguments.
191
192 @type args: list
193 @param args: list of arguments to be quoted
194 @rtype: str
195 @return: the quoted arguments concatenated with spaces
196
197 """
198 return " ".join([ShellQuote(i) for i in args])
199
200
202 """Helper class to write scripts with indentation.
203
204 """
205 INDENT_STR = " "
206
208 """Initializes this class.
209
210 """
211 self._fh = fh
212 self._indent = 0
213
215 """Increase indentation level by 1.
216
217 """
218 self._indent += 1
219
221 """Decrease indentation level by 1.
222
223 """
224 assert self._indent > 0
225 self._indent -= 1
226
227 - def Write(self, txt, *args):
228 """Write line to output file.
229
230 """
231 assert self._indent >= 0
232
233 self._fh.write(self._indent * self.INDENT_STR)
234
235 if args:
236 self._fh.write(txt % args)
237 else:
238 self._fh.write(txt)
239
240 self._fh.write("\n")
241
242
244 """Generates a random secret.
245
246 This will generate a pseudo-random secret returning an hex string
247 (so that it can be used where an ASCII string is needed).
248
249 @param numbytes: the number of bytes which will be represented by the returned
250 string (defaulting to 20, the length of a SHA1 hash)
251 @rtype: str
252 @return: an hex representation of the pseudo-random sequence
253
254 """
255 return os.urandom(numbytes).encode("hex")
256
257
259 """Normalizes and check if a MAC address is valid.
260
261 Checks whether the supplied MAC address is formally correct, only
262 accepts colon separated format. Normalize it to all lower.
263
264 @type mac: str
265 @param mac: the MAC to be validated
266 @rtype: str
267 @return: returns the normalized and validated MAC.
268
269 @raise errors.OpPrereqError: If the MAC isn't valid
270
271 """
272 if not _MAC_CHECK_RE.match(mac):
273 raise errors.OpPrereqError("Invalid MAC address '%s'" % mac,
274 errors.ECODE_INVAL)
275
276 return mac.lower()
277
278
280 """Return a 'safe' version of a source string.
281
282 This function mangles the input string and returns a version that
283 should be safe to display/encode as ASCII. To this end, we first
284 convert it to ASCII using the 'backslashreplace' encoding which
285 should get rid of any non-ASCII chars, and then we process it
286 through a loop copied from the string repr sources in the python; we
287 don't use string_escape anymore since that escape single quotes and
288 backslashes too, and that is too much; and that escaping is not
289 stable, i.e. string_escape(string_escape(x)) != string_escape(x).
290
291 @type text: str or unicode
292 @param text: input data
293 @rtype: str
294 @return: a safe version of text
295
296 """
297 if isinstance(text, unicode):
298
299 text = text.encode('ascii', 'backslashreplace')
300 resu = ""
301 for char in text:
302 c = ord(char)
303 if char == '\t':
304 resu += r'\t'
305 elif char == '\n':
306 resu += r'\n'
307 elif char == '\r':
308 resu += r'\'r'
309 elif c < 32 or c >= 127:
310 resu += "\\x%02x" % (c & 0xff)
311 else:
312 resu += char
313 return resu
314
315
317 """Split and unescape a string based on a given separator.
318
319 This function splits a string based on a separator where the
320 separator itself can be escape in order to be an element of the
321 elements. The escaping rules are (assuming coma being the
322 separator):
323 - a plain , separates the elements
324 - a sequence \\\\, (double backslash plus comma) is handled as a
325 backslash plus a separator comma
326 - a sequence \, (backslash plus comma) is handled as a
327 non-separator comma
328
329 @type text: string
330 @param text: the string to split
331 @type sep: string
332 @param text: the separator
333 @rtype: string
334 @return: a list of strings
335
336 """
337
338 slist = text.split(sep)
339
340
341 rlist = []
342 while slist:
343 e1 = slist.pop(0)
344 if e1.endswith("\\"):
345 num_b = len(e1) - len(e1.rstrip("\\"))
346 if num_b % 2 == 1 and slist:
347 e2 = slist.pop(0)
348
349
350 rlist.append(e1 + sep + e2)
351 continue
352 rlist.append(e1)
353
354 rlist = [re.sub(r"\\(.)", r"\1", v) for v in rlist]
355 return rlist
356
357
359 """Nicely join a set of identifiers.
360
361 @param names: set, list or tuple
362 @return: a string with the formatted results
363
364 """
365 return ", ".join([str(val) for val in names])
366
367
382
383
407
408
410 """Splits data chunks into lines separated by newline.
411
412 Instances provide a file-like interface.
413
414 """
416 """Initializes this class.
417
418 @type line_fn: callable
419 @param line_fn: Function called for each line, first parameter is line
420 @param args: Extra arguments for L{line_fn}
421
422 """
423 assert callable(line_fn)
424
425 if args:
426
427 self._line_fn = \
428 lambda line: line_fn(line, *args)
429 else:
430 self._line_fn = line_fn
431
432 self._lines = collections.deque()
433 self._buffer = ""
434
436 parts = (self._buffer + data).split("\n")
437 self._buffer = parts.pop()
438 self._lines.extend(parts)
439
441 while self._lines:
442 self._line_fn(self._lines.popleft().rstrip("\r\n"))
443
445 self.flush()
446 if self._buffer:
447 self._line_fn(self._buffer)
448
449
451 """Verifies is the given word is safe from the shell's p.o.v.
452
453 This means that we can pass this to a command via the shell and be
454 sure that it doesn't alter the command line and is passed as such to
455 the actual command.
456
457 Note that we are overly restrictive here, in order to be on the safe
458 side.
459
460 @type word: str
461 @param word: the word to check
462 @rtype: boolean
463 @return: True if the word is 'safe'
464
465 """
466 return bool(_SHELLPARAM_REGEX.match(word))
467
468
470 """Build a safe shell command line from the given arguments.
471
472 This function will check all arguments in the args list so that they
473 are valid shell parameters (i.e. they don't contain shell
474 metacharacters). If everything is ok, it will return the result of
475 template % args.
476
477 @type template: str
478 @param template: the string holding the template for the
479 string formatting
480 @rtype: str
481 @return: the expanded command line
482
483 """
484 for word in args:
485 if not IsValidShellParam(word):
486 raise errors.ProgrammerError("Shell argument '%s' contains"
487 " invalid characters" % word)
488 return template % args
489