1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 """Utility functions for manipulating or working with text.
31
32 """
33
34
35 import re
36 import os
37 import time
38 import collections
39
40 from ganeti import errors
41
42
43
44 _PARSEUNIT_REGEX = re.compile(r"^([.\d]+)\s*([a-zA-Z]+)?$")
45
46
47 _SHELL_UNQUOTED_RE = re.compile("^[-.,=:/_+@A-Za-z0-9]+$")
48
49
50 _SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
51
52
53 _ASCII_ELLIPSIS = "..."
54
55
56 _MAC_ADDR_OCTET_RE = r"[0-9a-f]{2}"
57
58
60 """Try to match a name against a list.
61
62 This function will try to match a name like test1 against a list
63 like C{['test1.example.com', 'test2.example.com', ...]}. Against
64 this list, I{'test1'} as well as I{'test1.example'} will match, but
65 not I{'test1.ex'}. A multiple match will be considered as no match
66 at all (e.g. I{'test1'} against C{['test1.example.com',
67 'test1.example.org']}), except when the key fully matches an entry
68 (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
69
70 @type key: str
71 @param key: the name to be searched
72 @type name_list: list
73 @param name_list: the list of strings against which to search the key
74 @type case_sensitive: boolean
75 @param case_sensitive: whether to provide a case-sensitive match
76
77 @rtype: None or str
78 @return: None if there is no match I{or} if there are multiple matches,
79 otherwise the element from the list which matches
80
81 """
82 if key in name_list:
83 return key
84
85 re_flags = 0
86 if not case_sensitive:
87 re_flags |= re.IGNORECASE
88 key = key.upper()
89
90 name_re = re.compile(r"^%s(\..*)?$" % re.escape(key), re_flags)
91
92 names_filtered = []
93 string_matches = []
94 for name in name_list:
95 if name_re.match(name) is not None:
96 names_filtered.append(name)
97 if not case_sensitive and key == name.upper():
98 string_matches.append(name)
99
100 if len(string_matches) == 1:
101 return string_matches[0]
102 if len(names_filtered) == 1:
103 return names_filtered[0]
104
105 return None
106
107
109 """Helper function for L{DnsNameGlobPattern}.
110
111 Returns regular expression pattern for parts of the pattern.
112
113 """
114 text = match.group(0)
115
116 if text == "*":
117 return "[^.]*"
118 elif text == "?":
119 return "[^.]"
120 else:
121 return re.escape(text)
122
123
125 """Generates regular expression from DNS name globbing pattern.
126
127 A DNS name globbing pattern (e.g. C{*.site}) is converted to a regular
128 expression. Escape sequences or ranges (e.g. [a-z]) are not supported.
129
130 Matching always starts at the leftmost part. An asterisk (*) matches all
131 characters except the dot (.) separating DNS name parts. A question mark (?)
132 matches a single character except the dot (.).
133
134 @type pattern: string
135 @param pattern: DNS name globbing pattern
136 @rtype: string
137 @return: Regular expression
138
139 """
140 return r"^%s(\..*)?$" % re.sub(r"\*|\?|[^*?]*", _DnsNameGlobHelper, pattern)
141
142
177
178
180 """Tries to extract number and scale from the given string.
181
182 Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
183 [UNIT]}. If no unit is specified, it defaults to MiB. Return value
184 is always an int in MiB.
185
186 """
187 m = _PARSEUNIT_REGEX.match(str(input_string))
188 if not m:
189 raise errors.UnitParseError("Invalid format")
190
191 value = float(m.groups()[0])
192
193 unit = m.groups()[1]
194 if unit:
195 lcunit = unit.lower()
196 else:
197 lcunit = "m"
198
199 if lcunit in ("m", "mb", "mib"):
200
201 pass
202
203 elif lcunit in ("g", "gb", "gib"):
204 value *= 1024
205
206 elif lcunit in ("t", "tb", "tib"):
207 value *= 1024 * 1024
208
209 else:
210 raise errors.UnitParseError("Unknown unit: %s" % unit)
211
212
213 if int(value) < value:
214 value += 1
215
216
217 value = int(value)
218 if value % 4:
219 value += 4 - value % 4
220
221 return value
222
223
225 """Quotes shell argument according to POSIX.
226
227 @type value: str
228 @param value: the argument to be quoted
229 @rtype: str
230 @return: the quoted value
231
232 """
233 if _SHELL_UNQUOTED_RE.match(value):
234 return value
235 else:
236 return "'%s'" % value.replace("'", "'\\''")
237
238
240 """Quotes a list of shell arguments.
241
242 @type args: list
243 @param args: list of arguments to be quoted
244 @rtype: str
245 @return: the quoted arguments concatenated with spaces
246
247 """
248 return " ".join([ShellQuote(i) for i in args])
249
250
252 """Out of a list of shell comands construct a single one.
253
254 """
255 return ["/bin/sh", "-c", " && ".join(ShellQuoteArgs(c) for c in cmdlist)]
256
257
259 """Helper class to write scripts with indentation.
260
261 """
262 INDENT_STR = " "
263
265 """Initializes this class.
266
267 """
268 self._fh = fh
269 self._indent_enabled = indent
270 self._indent = 0
271
273 """Increase indentation level by 1.
274
275 """
276 self._indent += 1
277
279 """Decrease indentation level by 1.
280
281 """
282 assert self._indent > 0
283 self._indent -= 1
284
285 - def Write(self, txt, *args):
286 """Write line to output file.
287
288 """
289 assert self._indent >= 0
290
291 if args:
292 line = txt % args
293 else:
294 line = txt
295
296 if line and self._indent_enabled:
297
298 self._fh.write(self._indent * self.INDENT_STR)
299
300 self._fh.write(line)
301
302 self._fh.write("\n")
303
304
306 """Generates a random secret.
307
308 This will generate a pseudo-random secret returning an hex string
309 (so that it can be used where an ASCII string is needed).
310
311 @param numbytes: the number of bytes which will be represented by the returned
312 string (defaulting to 20, the length of a SHA1 hash)
313 @rtype: str
314 @return: an hex representation of the pseudo-random sequence
315
316 """
317 return os.urandom(numbytes).encode("hex")
318
319
321 """Builds a regular expression for verifying MAC addresses.
322
323 @type octets: integer
324 @param octets: How many octets to expect (1-6)
325 @return: Compiled regular expression
326
327 """
328 assert octets > 0
329 assert octets <= 6
330
331 return re.compile("^%s$" % ":".join([_MAC_ADDR_OCTET_RE] * octets),
332 re.I)
333
334
335
336 _MAC_CHECK_RE = _MakeMacAddrRegexp(6)
337
338
339 _MAC_PREFIX_CHECK_RE = _MakeMacAddrRegexp(3)
340
341
343 """Checks a MAC address using a regular expression.
344
345 @param check_re: Compiled regular expression as returned by C{re.compile}
346 @type mac: string
347 @param mac: MAC address to be validated
348 @type msg: string
349 @param msg: Error message (%s will be replaced with MAC address)
350
351 """
352 if check_re.match(mac):
353 return mac.lower()
354
355 raise errors.OpPrereqError(msg % mac, errors.ECODE_INVAL)
356
357
359 """Normalizes and check if a MAC address is valid and contains six octets.
360
361 Checks whether the supplied MAC address is formally correct. Accepts
362 colon-separated format only. Normalize it to all lower case.
363
364 @type mac: string
365 @param mac: MAC address to be validated
366 @rtype: string
367 @return: Normalized and validated MAC address
368 @raise errors.OpPrereqError: If the MAC address isn't valid
369
370 """
371 return _MacAddressCheck(_MAC_CHECK_RE, mac, "Invalid MAC address '%s'")
372
373
375 """Normalizes a potential MAC address prefix (three octets).
376
377 Checks whether the supplied string is a valid MAC address prefix consisting
378 of three colon-separated octets. The result is normalized to all lower case.
379
380 @type mac: string
381 @param mac: Prefix to be validated
382 @rtype: string
383 @return: Normalized and validated prefix
384 @raise errors.OpPrereqError: If the MAC address prefix isn't valid
385
386 """
387 return _MacAddressCheck(_MAC_PREFIX_CHECK_RE, mac,
388 "Invalid MAC address prefix '%s'")
389
390
392 """Return a 'safe' version of a source string.
393
394 This function mangles the input string and returns a version that
395 should be safe to display/encode as ASCII. To this end, we first
396 convert it to ASCII using the 'backslashreplace' encoding which
397 should get rid of any non-ASCII chars, and then we process it
398 through a loop copied from the string repr sources in the python; we
399 don't use string_escape anymore since that escape single quotes and
400 backslashes too, and that is too much; and that escaping is not
401 stable, i.e. string_escape(string_escape(x)) != string_escape(x).
402
403 @type text: str or unicode
404 @param text: input data
405 @rtype: str
406 @return: a safe version of text
407
408 """
409 if isinstance(text, unicode):
410
411 text = text.encode("ascii", "backslashreplace")
412 resu = ""
413 for char in text:
414 c = ord(char)
415 if char == "\t":
416 resu += r"\t"
417 elif char == "\n":
418 resu += r"\n"
419 elif char == "\r":
420 resu += r'\'r'
421 elif c < 32 or c >= 127:
422 resu += "\\x%02x" % (c & 0xff)
423 else:
424 resu += char
425 return resu
426
427
429 r"""Split and unescape a string based on a given separator.
430
431 This function splits a string based on a separator where the
432 separator itself can be escape in order to be an element of the
433 elements. The escaping rules are (assuming coma being the
434 separator):
435 - a plain , separates the elements
436 - a sequence \\\\, (double backslash plus comma) is handled as a
437 backslash plus a separator comma
438 - a sequence \, (backslash plus comma) is handled as a
439 non-separator comma
440
441 @type text: string
442 @param text: the string to split
443 @type sep: string
444 @param text: the separator
445 @rtype: string
446 @return: a list of strings
447
448 """
449
450 slist = text.split(sep)
451
452
453 rlist = []
454 while slist:
455 e1 = slist.pop(0)
456 if e1.endswith("\\"):
457 num_b = len(e1) - len(e1.rstrip("\\"))
458 if num_b % 2 == 1 and slist:
459 e2 = slist.pop(0)
460
461
462
463 slist.insert(0, e1 + sep + e2)
464 continue
465
466 rlist.append(e1)
467
468 rlist = [re.sub(r"\\(.)", r"\1", v) for v in rlist]
469 return rlist
470
471
473 """Encode a list in a way parsable by UnescapeAndSplit.
474
475 @type slist: list of strings
476 @param slist: the strings to be encoded
477 @rtype: string
478 @return: the encoding of the list oas a string
479
480 """
481 return sep.join([re.sub("\\" + sep, "\\\\" + sep,
482 re.sub(r"\\", r"\\\\", v)) for v in slist])
483
484
486 """Nicely join a set of identifiers.
487
488 @param names: set, list or tuple
489 @return: a string with the formatted results
490
491 """
492 return ", ".join([str(val) for val in names])
493
494
515
516
540
541
543 """Splits data chunks into lines separated by newline.
544
545 Instances provide a file-like interface.
546
547 """
549 """Initializes this class.
550
551 @type line_fn: callable
552 @param line_fn: Function called for each line, first parameter is line
553 @param args: Extra arguments for L{line_fn}
554
555 """
556 assert callable(line_fn)
557
558 if args:
559
560 self._line_fn = \
561 lambda line: line_fn(line, *args)
562 else:
563 self._line_fn = line_fn
564
565 self._lines = collections.deque()
566 self._buffer = ""
567
569 parts = (self._buffer + data).split("\n")
570 self._buffer = parts.pop()
571 self._lines.extend(parts)
572
574 while self._lines:
575 self._line_fn(self._lines.popleft().rstrip("\r\n"))
576
578 self.flush()
579 if self._buffer:
580 self._line_fn(self._buffer)
581
582
584 """Verifies is the given word is safe from the shell's p.o.v.
585
586 This means that we can pass this to a command via the shell and be
587 sure that it doesn't alter the command line and is passed as such to
588 the actual command.
589
590 Note that we are overly restrictive here, in order to be on the safe
591 side.
592
593 @type word: str
594 @param word: the word to check
595 @rtype: boolean
596 @return: True if the word is 'safe'
597
598 """
599 return bool(_SHELLPARAM_REGEX.match(word))
600
601
603 """Build a safe shell command line from the given arguments.
604
605 This function will check all arguments in the args list so that they
606 are valid shell parameters (i.e. they don't contain shell
607 metacharacters). If everything is ok, it will return the result of
608 template % args.
609
610 @type template: str
611 @param template: the string holding the template for the
612 string formatting
613 @rtype: str
614 @return: the expanded command line
615
616 """
617 for word in args:
618 if not IsValidShellParam(word):
619 raise errors.ProgrammerError("Shell argument '%s' contains"
620 " invalid characters" % word)
621 return template % args
622
623
648
649
651 """Truncate string and add ellipsis if needed.
652
653 @type text: string
654 @param text: Text
655 @type length: integer
656 @param length: Desired length
657 @rtype: string
658 @return: Truncated text
659
660 """
661 assert length > len(_ASCII_ELLIPSIS)
662
663
664 if not isinstance(text, basestring):
665 text = str(text)
666
667 if len(text) <= length:
668 return text
669 else:
670 return text[:length - len(_ASCII_ELLIPSIS)] + _ASCII_ELLIPSIS
671
672
686
687
698