Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ 3ecf6786

History | View | Annotate | Download (18.5 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2006, 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
"""Ganeti small utilities
23
"""
24

    
25

    
26
import sys
27
import os
28
import sha
29
import time
30
import popen2
31
import re
32
import socket
33
import tempfile
34
import shutil
35
from errno import ENOENT, ENOTDIR, EISDIR, EEXIST
36

    
37
from ganeti import logger
38
from ganeti import errors
39

    
40
_locksheld = []
41
_re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
42

    
43
class RunResult(object):
44
  """Simple class for holding the result of running external programs.
45

46
  Instance variables:
47
    exit_code: the exit code of the program, or None (if the program
48
               didn't exit())
49
    signal: numeric signal that caused the program to finish, or None
50
            (if the program wasn't terminated by a signal)
51
    stdout: the standard output of the program
52
    stderr: the standard error of the program
53
    failed: a Boolean value which is True in case the program was
54
            terminated by a signal or exited with a non-zero exit code
55
    fail_reason: a string detailing the termination reason
56

57
  """
58
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
59
               "failed", "fail_reason", "cmd"]
60

    
61

    
62
  def __init__(self, exit_code, signal, stdout, stderr, cmd):
63
    self.cmd = cmd
64
    self.exit_code = exit_code
65
    self.signal = signal
66
    self.stdout = stdout
67
    self.stderr = stderr
68
    self.failed = (signal is not None or exit_code != 0)
69

    
70
    if self.signal is not None:
71
      self.fail_reason = "terminated by signal %s" % self.signal
72
    elif self.exit_code is not None:
73
      self.fail_reason = "exited with exit code %s" % self.exit_code
74
    else:
75
      self.fail_reason = "unable to determine termination reason"
76

    
77
  def _GetOutput(self):
78
    """Returns the combined stdout and stderr for easier usage.
79

80
    """
81
    return self.stdout + self.stderr
82

    
83
  output = property(_GetOutput, None, None, "Return full output")
84

    
85

    
86
def _GetLockFile(subsystem):
87
  """Compute the file name for a given lock name."""
88
  return "/var/lock/ganeti_lock_%s" % subsystem
89

    
90

    
91
def Lock(name, max_retries=None, debug=False):
92
  """Lock a given subsystem.
93

94
  In case the lock is already held by an alive process, the function
95
  will sleep indefintely and poll with a one second interval.
96

97
  When the optional integer argument 'max_retries' is passed with a
98
  non-zero value, the function will sleep only for this number of
99
  times, and then it will will raise a LockError if the lock can't be
100
  acquired. Passing in a negative number will cause only one try to
101
  get the lock. Passing a positive number will make the function retry
102
  for approximately that number of seconds.
103

104
  """
105
  lockfile = _GetLockFile(name)
106

    
107
  if name in _locksheld:
108
    raise errors.LockError('Lock "%s" already held!' % (name,))
109

    
110
  errcount = 0
111

    
112
  retries = 0
113
  while True:
114
    try:
115
      fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC)
116
      break
117
    except OSError, creat_err:
118
      if creat_err.errno != EEXIST:
119
        raise errors.LockError("Can't create the lock file. Error '%s'." %
120
                               str(creat_err))
121

    
122
      try:
123
        pf = open(lockfile, 'r')
124
      except IOError, open_err:
125
        errcount += 1
126
        if errcount >= 5:
127
          raise errors.LockError("Lock file exists but cannot be opened."
128
                                 " Error: '%s'." % str(open_err))
129
        time.sleep(1)
130
        continue
131

    
132
      try:
133
        pid = int(pf.read())
134
      except ValueError:
135
        raise errors.LockError("Invalid pid string in %s" %
136
                               (lockfile,))
137

    
138
      if not IsProcessAlive(pid):
139
        raise errors.LockError("Stale lockfile %s for pid %d?" %
140
                               (lockfile, pid))
141

    
142
      if max_retries and max_retries <= retries:
143
        raise errors.LockError("Can't acquire lock during the specified"
144
                               " time, aborting.")
145
      if retries == 5 and (debug or sys.stdin.isatty()):
146
        logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid))
147

    
148
      time.sleep(1)
149
      retries += 1
150
      continue
151

    
152
  os.write(fd, '%d\n' % (os.getpid(),))
153
  os.close(fd)
154

    
155
  _locksheld.append(name)
156

    
157

    
158
def Unlock(name):
159
  """Unlock a given subsystem.
160

161
  """
162
  lockfile = _GetLockFile(name)
163

    
164
  try:
165
    fd = os.open(lockfile, os.O_RDONLY)
166
  except OSError:
167
    raise errors.LockError('Lock "%s" not held.' % (name,))
168

    
169
  f = os.fdopen(fd, 'r')
170
  pid_str = f.read()
171

    
172
  try:
173
    pid = int(pid_str)
174
  except ValueError:
175
    raise errors.LockError('Unable to determine PID of locking process.')
176

    
177
  if pid != os.getpid():
178
    raise errors.LockError('Lock not held by me (%d != %d)' %
179
                           (os.getpid(), pid,))
180

    
181
  os.unlink(lockfile)
182
  _locksheld.remove(name)
183

    
184

    
185
def LockCleanup():
186
  """Remove all locks.
187

188
  """
189
  for lock in _locksheld:
190
    Unlock(lock)
191

    
192

    
193
def RunCmd(cmd):
194
  """Execute a (shell) command.
195

196
  The command should not read from its standard input, as it will be
197
  closed.
198

199
  Args:
200
    cmd: command to run. (str)
201

202
  Returns: `RunResult` instance
203

204
  """
205
  if isinstance(cmd, list):
206
    cmd = [str(val) for val in cmd]
207
  child = popen2.Popen3(cmd, capturestderr=True)
208

    
209
  child.tochild.close()
210
  out = child.fromchild.read()
211
  err = child.childerr.read()
212

    
213
  status = child.wait()
214
  if os.WIFSIGNALED(status):
215
    signal = os.WTERMSIG(status)
216
  else:
217
    signal = None
218
  if os.WIFEXITED(status):
219
    exitcode = os.WEXITSTATUS(status)
220
  else:
221
    exitcode = None
222

    
223
  if isinstance(cmd, list):
224
    strcmd = " ".join(cmd)
225
  else:
226
    strcmd = str(cmd)
227

    
228
  return RunResult(exitcode, signal, out, err, strcmd)
229

    
230

    
231
def RunCmdUnlocked(cmd):
232
  """Execute a shell command without the 'cmd' lock.
233

234
  This variant of `RunCmd()` drops the 'cmd' lock before running the
235
  command and re-aquires it afterwards, thus it can be used to call
236
  other ganeti commands.
237

238
  The argument and return values are the same as for the `RunCmd()`
239
  function.
240

241
  Args:
242
    cmd - command to run. (str)
243

244
  Returns:
245
    `RunResult`
246

247
  """
248
  Unlock('cmd')
249
  ret = RunCmd(cmd)
250
  Lock('cmd')
251

    
252
  return ret
253

    
254

    
255
def RemoveFile(filename):
256
  """Remove a file ignoring some errors.
257

258
  Remove a file, ignoring non-existing ones or directories. Other
259
  errors are passed.
260

261
  """
262
  try:
263
    os.unlink(filename)
264
  except OSError, err:
265
    if err.errno not in (ENOENT, EISDIR):
266
      raise
267

    
268

    
269
def _FingerprintFile(filename):
270
  """Compute the fingerprint of a file.
271

272
  If the file does not exist, a None will be returned
273
  instead.
274

275
  Args:
276
    filename - Filename (str)
277

278
  """
279
  if not (os.path.exists(filename) and os.path.isfile(filename)):
280
    return None
281

    
282
  f = open(filename)
283

    
284
  fp = sha.sha()
285
  while True:
286
    data = f.read(4096)
287
    if not data:
288
      break
289

    
290
    fp.update(data)
291

    
292
  return fp.hexdigest()
293

    
294

    
295
def FingerprintFiles(files):
296
  """Compute fingerprints for a list of files.
297

298
  Args:
299
    files - array of filenames.  ( [str, ...] )
300

301
  Return value:
302
    dictionary of filename: fingerprint for the files that exist
303

304
  """
305
  ret = {}
306

    
307
  for filename in files:
308
    cksum = _FingerprintFile(filename)
309
    if cksum:
310
      ret[filename] = cksum
311

    
312
  return ret
313

    
314

    
315
def CheckDict(target, template, logname=None):
316
  """Ensure a dictionary has a required set of keys.
317

318
  For the given dictionaries `target` and `template`, ensure target
319
  has all the keys from template. Missing keys are added with values
320
  from template.
321

322
  Args:
323
    target   - the dictionary to check
324
    template - template dictionary
325
    logname  - a caller-chosen string to identify the debug log
326
               entry; if None, no logging will be done
327

328
  Returns value:
329
    None
330

331
  """
332
  missing = []
333
  for k in template:
334
    if k not in target:
335
      missing.append(k)
336
      target[k] = template[k]
337

    
338
  if missing and logname:
339
    logger.Debug('%s missing keys %s' %
340
                 (logname, ', '.join(missing)))
341

    
342

    
343
def IsProcessAlive(pid):
344
  """Check if a given pid exists on the system.
345

346
  Returns: true or false, depending on if the pid exists or not
347

348
  Remarks: zombie processes treated as not alive
349

350
  """
351
  try:
352
    f = open("/proc/%d/status" % pid)
353
  except IOError, err:
354
    if err.errno in (ENOENT, ENOTDIR):
355
      return False
356

    
357
  alive = True
358
  try:
359
    data = f.readlines()
360
    if len(data) > 1:
361
      state = data[1].split()
362
      if len(state) > 1 and state[1] == "Z":
363
        alive = False
364
  finally:
365
    f.close()
366

    
367
  return alive
368

    
369

    
370
def MatchNameComponent(key, name_list):
371
  """Try to match a name against a list.
372

373
  This function will try to match a name like test1 against a list
374
  like ['test1.example.com', 'test2.example.com', ...]. Against this
375
  list, 'test1' as well as 'test1.example' will match, but not
376
  'test1.ex'. A multiple match will be considered as no match at all
377
  (e.g. 'test1' against ['test1.example.com', 'test1.example.org']).
378

379
  Args:
380
    key: the name to be searched
381
    name_list: the list of strings against which to search the key
382

383
  Returns:
384
    None if there is no match *or* if there are multiple matches
385
    otherwise the element from the list which matches
386

387
  """
388
  mo = re.compile("^%s(\..*)?$" % re.escape(key))
389
  names_filtered = [name for name in name_list if mo.match(name) is not None]
390
  if len(names_filtered) != 1:
391
    return None
392
  return names_filtered[0]
393

    
394

    
395
def LookupHostname(hostname):
396
  """Look up hostname
397

398
  Args:
399
    hostname: hostname to look up, can be also be a non FQDN
400

401
  Returns:
402
    Dictionary with keys:
403
    - ip: IP addr
404
    - hostname_full: hostname fully qualified
405
    - hostname: hostname fully qualified (historic artifact)
406

407
  """
408
  try:
409
    (fqdn, dummy, ipaddrs) = socket.gethostbyname_ex(hostname)
410
    ipaddr = ipaddrs[0]
411
  except socket.gaierror:
412
    # hostname not found in DNS
413
    return None
414

    
415
  returnhostname = {
416
    "ip": ipaddr,
417
    "hostname_full": fqdn,
418
    "hostname": fqdn,
419
    }
420

    
421
  return returnhostname
422

    
423

    
424
def ListVolumeGroups():
425
  """List volume groups and their size
426

427
  Returns:
428
     Dictionary with keys volume name and values the size of the volume
429

430
  """
431
  command = "vgs --noheadings --units m --nosuffix -o name,size"
432
  result = RunCmd(command)
433
  retval = {}
434
  if result.failed:
435
    return retval
436

    
437
  for line in result.stdout.splitlines():
438
    try:
439
      name, size = line.split()
440
      size = int(float(size))
441
    except (IndexError, ValueError), err:
442
      logger.Error("Invalid output from vgs (%s): %s" % (err, line))
443
      continue
444

    
445
    retval[name] = size
446

    
447
  return retval
448

    
449

    
450
def BridgeExists(bridge):
451
  """Check whether the given bridge exists in the system
452

453
  Returns:
454
     True if it does, false otherwise.
455

456
  """
457
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
458

    
459

    
460
def NiceSort(name_list):
461
  """Sort a list of strings based on digit and non-digit groupings.
462

463
  Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
464
  sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
465

466
  The sort algorithm breaks each name in groups of either only-digits
467
  or no-digits. Only the first eight such groups are considered, and
468
  after that we just use what's left of the string.
469

470
  Return value
471
    - a copy of the list sorted according to our algorithm
472

473
  """
474
  _SORTER_BASE = "(\D+|\d+)"
475
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
476
                                                  _SORTER_BASE, _SORTER_BASE,
477
                                                  _SORTER_BASE, _SORTER_BASE,
478
                                                  _SORTER_BASE, _SORTER_BASE)
479
  _SORTER_RE = re.compile(_SORTER_FULL)
480
  _SORTER_NODIGIT = re.compile("^\D*$")
481
  def _TryInt(val):
482
    """Attempts to convert a variable to integer."""
483
    if val is None or _SORTER_NODIGIT.match(val):
484
      return val
485
    rval = int(val)
486
    return rval
487

    
488
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
489
             for name in name_list]
490
  to_sort.sort()
491
  return [tup[1] for tup in to_sort]
492

    
493

    
494
def CheckDaemonAlive(pid_file, process_string):
495
  """Check wether the specified daemon is alive.
496

497
  Args:
498
   - pid_file: file to read the daemon pid from, the file is
499
               expected to contain only a single line containing
500
               only the PID
501
   - process_string: a substring that we expect to find in
502
                     the command line of the daemon process
503

504
  Returns:
505
   - True if the daemon is judged to be alive (that is:
506
      - the PID file exists, is readable and contains a number
507
      - a process of the specified PID is running
508
      - that process contains the specified string in its
509
        command line
510
      - the process is not in state Z (zombie))
511
   - False otherwise
512

513
  """
514
  try:
515
    pid_file = file(pid_file, 'r')
516
    try:
517
      pid = int(pid_file.readline())
518
    finally:
519
      pid_file.close()
520

    
521
    cmdline_file_path = "/proc/%s/cmdline" % (pid)
522
    cmdline_file = open(cmdline_file_path, 'r')
523
    try:
524
      cmdline = cmdline_file.readline()
525
    finally:
526
      cmdline_file.close()
527

    
528
    if not process_string in cmdline:
529
      return False
530

    
531
    stat_file_path =  "/proc/%s/stat" % (pid)
532
    stat_file = open(stat_file_path, 'r')
533
    try:
534
      process_state = stat_file.readline().split()[2]
535
    finally:
536
      stat_file.close()
537

    
538
    if process_state == 'Z':
539
      return False
540

    
541
  except (IndexError, IOError, ValueError):
542
    return False
543

    
544
  return True
545

    
546

    
547
def TryConvert(fn, val):
548
  """Try to convert a value ignoring errors.
549

550
  This function tries to apply function `fn` to `val`. If no
551
  ValueError or TypeError exceptions are raised, it will return the
552
  result, else it will return the original value. Any other exceptions
553
  are propagated to the caller.
554

555
  """
556
  try:
557
    nv = fn(val)
558
  except (ValueError, TypeError), err:
559
    nv = val
560
  return nv
561

    
562

    
563
def IsValidIP(ip):
564
  """Verifies the syntax of an IP address.
565

566
  This function checks if the ip address passes is valid or not based
567
  on syntax (not ip range, class calculations or anything).
568

569
  """
570
  unit = "(0|[1-9]\d{0,2})"
571
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
572

    
573

    
574
def IsValidShellParam(word):
575
  """Verifies is the given word is safe from the shell's p.o.v.
576

577
  This means that we can pass this to a command via the shell and be
578
  sure that it doesn't alter the command line and is passed as such to
579
  the actual command.
580

581
  Note that we are overly restrictive here, in order to be on the safe
582
  side.
583

584
  """
585
  return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
586

    
587

    
588
def BuildShellCmd(template, *args):
589
  """Build a safe shell command line from the given arguments.
590

591
  This function will check all arguments in the args list so that they
592
  are valid shell parameters (i.e. they don't contain shell
593
  metacharaters). If everything is ok, it will return the result of
594
  template % args.
595

596
  """
597
  for word in args:
598
    if not IsValidShellParam(word):
599
      raise errors.ProgrammerError("Shell argument '%s' contains"
600
                                   " invalid characters" % word)
601
  return template % args
602

    
603

    
604
def FormatUnit(value):
605
  """Formats an incoming number of MiB with the appropriate unit.
606

607
  Value needs to be passed as a numeric type. Return value is always a string.
608

609
  """
610
  if value < 1024:
611
    return "%dM" % round(value, 0)
612

    
613
  elif value < (1024 * 1024):
614
    return "%0.1fG" % round(float(value) / 1024, 1)
615

    
616
  else:
617
    return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
618

    
619

    
620
def ParseUnit(input_string):
621
  """Tries to extract number and scale from the given string.
622

623
  Input must be in the format NUMBER+ [DOT NUMBER+] SPACE* [UNIT]. If no unit
624
  is specified, it defaults to MiB. Return value is always an int in MiB.
625

626
  """
627
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
628
  if not m:
629
    raise errors.UnitParseError("Invalid format")
630

    
631
  value = float(m.groups()[0])
632

    
633
  unit = m.groups()[1]
634
  if unit:
635
    lcunit = unit.lower()
636
  else:
637
    lcunit = 'm'
638

    
639
  if lcunit in ('m', 'mb', 'mib'):
640
    # Value already in MiB
641
    pass
642

    
643
  elif lcunit in ('g', 'gb', 'gib'):
644
    value *= 1024
645

    
646
  elif lcunit in ('t', 'tb', 'tib'):
647
    value *= 1024 * 1024
648

    
649
  else:
650
    raise errors.UnitParseError("Unknown unit: %s" % unit)
651

    
652
  # Make sure we round up
653
  if int(value) < value:
654
    value += 1
655

    
656
  # Round up to the next multiple of 4
657
  value = int(value)
658
  if value % 4:
659
    value += 4 - value % 4
660

    
661
  return value
662

    
663

    
664
def AddAuthorizedKey(file_name, key):
665
  """Adds an SSH public key to an authorized_keys file.
666

667
  Args:
668
    file_name: Path to authorized_keys file
669
    key: String containing key
670
  """
671
  key_fields = key.split()
672

    
673
  f = open(file_name, 'a+')
674
  try:
675
    nl = True
676
    for line in f:
677
      # Ignore whitespace changes
678
      if line.split() == key_fields:
679
        break
680
      nl = line.endswith('\n')
681
    else:
682
      if not nl:
683
        f.write("\n")
684
      f.write(key.rstrip('\r\n'))
685
      f.write("\n")
686
      f.flush()
687
  finally:
688
    f.close()
689

    
690

    
691
def RemoveAuthorizedKey(file_name, key):
692
  """Removes an SSH public key from an authorized_keys file.
693

694
  Args:
695
    file_name: Path to authorized_keys file
696
    key: String containing key
697
  """
698
  key_fields = key.split()
699

    
700
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
701
  out = os.fdopen(fd, 'w')
702
  try:
703
    f = open(file_name, 'r')
704
    try:
705
      for line in f:
706
        # Ignore whitespace changes while comparing lines
707
        if line.split() != key_fields:
708
          out.write(line)
709

    
710
      out.flush()
711
      os.rename(tmpname, file_name)
712
    finally:
713
      f.close()
714
  finally:
715
    out.close()
716

    
717

    
718
def CreateBackup(file_name):
719
  """Creates a backup of a file.
720

721
  Returns: the path to the newly created backup file.
722

723
  """
724
  if not os.path.isfile(file_name):
725
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
726
                                file_name)
727

    
728
  # Warning: the following code contains a race condition when we create more
729
  # than one backup of the same file in a second.
730
  backup_name = file_name + '.backup-%d' % int(time.time())
731
  shutil.copyfile(file_name, backup_name)
732
  return backup_name
733

    
734

    
735
def ShellQuote(value):
736
  """Quotes shell argument according to POSIX.
737

738
  """
739
  if _re_shell_unquoted.match(value):
740
    return value
741
  else:
742
    return "'%s'" % value.replace("'", "'\\''")
743

    
744

    
745
def ShellQuoteArgs(args):
746
  """Quotes all given shell arguments and concatenates using spaces.
747

748
  """
749
  return ' '.join([ShellQuote(i) for i in args])