Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ 16abfbc2

History | View | Annotate | Download (20.3 kB)

1
#
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 subprocess
31
import re
32
import socket
33
import tempfile
34
import shutil
35
import errno
36

    
37
from ganeti import logger
38
from ganeti import errors
39

    
40

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

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

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

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

    
62

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

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

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

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

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

    
86

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

    
91

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

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

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

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

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

    
111
  errcount = 0
112

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

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

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

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

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

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

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

    
156
  _locksheld.append(name)
157

    
158

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

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

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

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

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

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

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

    
185

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

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

    
193

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

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

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

203
  Returns: `RunResult` instance
204

205
  """
206
  if isinstance(cmd, list):
207
    cmd = [str(val) for val in cmd]
208
    strcmd = " ".join(cmd)
209
    shell = False
210
  else:
211
    strcmd = cmd
212
    shell = True
213
  env = os.environ.copy()
214
  env["LC_ALL"] = "C"
215
  child = subprocess.Popen(cmd, shell=shell,
216
                           stderr=subprocess.PIPE,
217
                           stdout=subprocess.PIPE,
218
                           stdin=subprocess.PIPE,
219
                           close_fds=True, env=env)
220

    
221
  child.stdin.close()
222
  out = child.stdout.read()
223
  err = child.stderr.read()
224

    
225
  status = child.wait()
226
  if status >= 0:
227
    exitcode = status
228
    signal = None
229
  else:
230
    exitcode = None
231
    signal = -status
232

    
233
  return RunResult(exitcode, signal, out, err, strcmd)
234

    
235

    
236
def RunCmdUnlocked(cmd):
237
  """Execute a shell command without the 'cmd' lock.
238

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

243
  The argument and return values are the same as for the `RunCmd()`
244
  function.
245

246
  Args:
247
    cmd - command to run. (str)
248

249
  Returns:
250
    `RunResult`
251

252
  """
253
  Unlock('cmd')
254
  ret = RunCmd(cmd)
255
  Lock('cmd')
256

    
257
  return ret
258

    
259

    
260
def RemoveFile(filename):
261
  """Remove a file ignoring some errors.
262

263
  Remove a file, ignoring non-existing ones or directories. Other
264
  errors are passed.
265

266
  """
267
  try:
268
    os.unlink(filename)
269
  except OSError, err:
270
    if err.errno not in (errno.ENOENT, errno.EISDIR):
271
      raise
272

    
273

    
274
def _FingerprintFile(filename):
275
  """Compute the fingerprint of a file.
276

277
  If the file does not exist, a None will be returned
278
  instead.
279

280
  Args:
281
    filename - Filename (str)
282

283
  """
284
  if not (os.path.exists(filename) and os.path.isfile(filename)):
285
    return None
286

    
287
  f = open(filename)
288

    
289
  fp = sha.sha()
290
  while True:
291
    data = f.read(4096)
292
    if not data:
293
      break
294

    
295
    fp.update(data)
296

    
297
  return fp.hexdigest()
298

    
299

    
300
def FingerprintFiles(files):
301
  """Compute fingerprints for a list of files.
302

303
  Args:
304
    files - array of filenames.  ( [str, ...] )
305

306
  Return value:
307
    dictionary of filename: fingerprint for the files that exist
308

309
  """
310
  ret = {}
311

    
312
  for filename in files:
313
    cksum = _FingerprintFile(filename)
314
    if cksum:
315
      ret[filename] = cksum
316

    
317
  return ret
318

    
319

    
320
def CheckDict(target, template, logname=None):
321
  """Ensure a dictionary has a required set of keys.
322

323
  For the given dictionaries `target` and `template`, ensure target
324
  has all the keys from template. Missing keys are added with values
325
  from template.
326

327
  Args:
328
    target   - the dictionary to check
329
    template - template dictionary
330
    logname  - a caller-chosen string to identify the debug log
331
               entry; if None, no logging will be done
332

333
  Returns value:
334
    None
335

336
  """
337
  missing = []
338
  for k in template:
339
    if k not in target:
340
      missing.append(k)
341
      target[k] = template[k]
342

    
343
  if missing and logname:
344
    logger.Debug('%s missing keys %s' %
345
                 (logname, ', '.join(missing)))
346

    
347

    
348
def IsProcessAlive(pid):
349
  """Check if a given pid exists on the system.
350

351
  Returns: true or false, depending on if the pid exists or not
352

353
  Remarks: zombie processes treated as not alive
354

355
  """
356
  try:
357
    f = open("/proc/%d/status" % pid)
358
  except IOError, err:
359
    if err.errno in (errno.ENOENT, errno.ENOTDIR):
360
      return False
361

    
362
  alive = True
363
  try:
364
    data = f.readlines()
365
    if len(data) > 1:
366
      state = data[1].split()
367
      if len(state) > 1 and state[1] == "Z":
368
        alive = False
369
  finally:
370
    f.close()
371

    
372
  return alive
373

    
374

    
375
def MatchNameComponent(key, name_list):
376
  """Try to match a name against a list.
377

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

384
  Args:
385
    key: the name to be searched
386
    name_list: the list of strings against which to search the key
387

388
  Returns:
389
    None if there is no match *or* if there are multiple matches
390
    otherwise the element from the list which matches
391

392
  """
393
  mo = re.compile("^%s(\..*)?$" % re.escape(key))
394
  names_filtered = [name for name in name_list if mo.match(name) is not None]
395
  if len(names_filtered) != 1:
396
    return None
397
  return names_filtered[0]
398

    
399

    
400
class HostInfo:
401
  """Class implementing resolver and hostname functionality
402

403
  """
404
  def __init__(self, name=None):
405
    """Initialize the host name object.
406

407
    If the name argument is not passed, it will use this system's
408
    name.
409

410
    """
411
    if name is None:
412
      name = self.SysName()
413

    
414
    self.query = name
415
    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
416
    self.ip = self.ipaddrs[0]
417

    
418
  @staticmethod
419
  def SysName():
420
    """Return the current system's name.
421

422
    This is simply a wrapper over socket.gethostname()
423

424
    """
425
    return socket.gethostname()
426

    
427
  @staticmethod
428
  def LookupHostname(hostname):
429
    """Look up hostname
430

431
    Args:
432
      hostname: hostname to look up
433

434
    Returns:
435
      a tuple (name, aliases, ipaddrs) as returned by socket.gethostbyname_ex
436
      in case of errors in resolving, we raise a ResolverError
437

438
    """
439
    try:
440
      result = socket.gethostbyname_ex(hostname)
441
    except socket.gaierror, err:
442
      # hostname not found in DNS
443
      raise errors.ResolverError(hostname, err.args[0], err.args[1])
444

    
445
    return result
446

    
447

    
448
def ListVolumeGroups():
449
  """List volume groups and their size
450

451
  Returns:
452
     Dictionary with keys volume name and values the size of the volume
453

454
  """
455
  command = "vgs --noheadings --units m --nosuffix -o name,size"
456
  result = RunCmd(command)
457
  retval = {}
458
  if result.failed:
459
    return retval
460

    
461
  for line in result.stdout.splitlines():
462
    try:
463
      name, size = line.split()
464
      size = int(float(size))
465
    except (IndexError, ValueError), err:
466
      logger.Error("Invalid output from vgs (%s): %s" % (err, line))
467
      continue
468

    
469
    retval[name] = size
470

    
471
  return retval
472

    
473

    
474
def BridgeExists(bridge):
475
  """Check whether the given bridge exists in the system
476

477
  Returns:
478
     True if it does, false otherwise.
479

480
  """
481
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
482

    
483

    
484
def NiceSort(name_list):
485
  """Sort a list of strings based on digit and non-digit groupings.
486

487
  Given a list of names ['a1', 'a10', 'a11', 'a2'] this function will
488
  sort the list in the logical order ['a1', 'a2', 'a10', 'a11'].
489

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

494
  Return value
495
    - a copy of the list sorted according to our algorithm
496

497
  """
498
  _SORTER_BASE = "(\D+|\d+)"
499
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
500
                                                  _SORTER_BASE, _SORTER_BASE,
501
                                                  _SORTER_BASE, _SORTER_BASE,
502
                                                  _SORTER_BASE, _SORTER_BASE)
503
  _SORTER_RE = re.compile(_SORTER_FULL)
504
  _SORTER_NODIGIT = re.compile("^\D*$")
505
  def _TryInt(val):
506
    """Attempts to convert a variable to integer."""
507
    if val is None or _SORTER_NODIGIT.match(val):
508
      return val
509
    rval = int(val)
510
    return rval
511

    
512
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
513
             for name in name_list]
514
  to_sort.sort()
515
  return [tup[1] for tup in to_sort]
516

    
517

    
518
def CheckDaemonAlive(pid_file, process_string):
519
  """Check wether the specified daemon is alive.
520

521
  Args:
522
   - pid_file: file to read the daemon pid from, the file is
523
               expected to contain only a single line containing
524
               only the PID
525
   - process_string: a substring that we expect to find in
526
                     the command line of the daemon process
527

528
  Returns:
529
   - True if the daemon is judged to be alive (that is:
530
      - the PID file exists, is readable and contains a number
531
      - a process of the specified PID is running
532
      - that process contains the specified string in its
533
        command line
534
      - the process is not in state Z (zombie))
535
   - False otherwise
536

537
  """
538
  try:
539
    pid_file = file(pid_file, 'r')
540
    try:
541
      pid = int(pid_file.readline())
542
    finally:
543
      pid_file.close()
544

    
545
    cmdline_file_path = "/proc/%s/cmdline" % (pid)
546
    cmdline_file = open(cmdline_file_path, 'r')
547
    try:
548
      cmdline = cmdline_file.readline()
549
    finally:
550
      cmdline_file.close()
551

    
552
    if not process_string in cmdline:
553
      return False
554

    
555
    stat_file_path =  "/proc/%s/stat" % (pid)
556
    stat_file = open(stat_file_path, 'r')
557
    try:
558
      process_state = stat_file.readline().split()[2]
559
    finally:
560
      stat_file.close()
561

    
562
    if process_state == 'Z':
563
      return False
564

    
565
  except (IndexError, IOError, ValueError):
566
    return False
567

    
568
  return True
569

    
570

    
571
def TryConvert(fn, val):
572
  """Try to convert a value ignoring errors.
573

574
  This function tries to apply function `fn` to `val`. If no
575
  ValueError or TypeError exceptions are raised, it will return the
576
  result, else it will return the original value. Any other exceptions
577
  are propagated to the caller.
578

579
  """
580
  try:
581
    nv = fn(val)
582
  except (ValueError, TypeError), err:
583
    nv = val
584
  return nv
585

    
586

    
587
def IsValidIP(ip):
588
  """Verifies the syntax of an IP address.
589

590
  This function checks if the ip address passes is valid or not based
591
  on syntax (not ip range, class calculations or anything).
592

593
  """
594
  unit = "(0|[1-9]\d{0,2})"
595
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
596

    
597

    
598
def IsValidShellParam(word):
599
  """Verifies is the given word is safe from the shell's p.o.v.
600

601
  This means that we can pass this to a command via the shell and be
602
  sure that it doesn't alter the command line and is passed as such to
603
  the actual command.
604

605
  Note that we are overly restrictive here, in order to be on the safe
606
  side.
607

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

    
611

    
612
def BuildShellCmd(template, *args):
613
  """Build a safe shell command line from the given arguments.
614

615
  This function will check all arguments in the args list so that they
616
  are valid shell parameters (i.e. they don't contain shell
617
  metacharaters). If everything is ok, it will return the result of
618
  template % args.
619

620
  """
621
  for word in args:
622
    if not IsValidShellParam(word):
623
      raise errors.ProgrammerError("Shell argument '%s' contains"
624
                                   " invalid characters" % word)
625
  return template % args
626

    
627

    
628
def FormatUnit(value):
629
  """Formats an incoming number of MiB with the appropriate unit.
630

631
  Value needs to be passed as a numeric type. Return value is always a string.
632

633
  """
634
  if value < 1024:
635
    return "%dM" % round(value, 0)
636

    
637
  elif value < (1024 * 1024):
638
    return "%0.1fG" % round(float(value) / 1024, 1)
639

    
640
  else:
641
    return "%0.1fT" % round(float(value) / 1024 / 1024, 1)
642

    
643

    
644
def ParseUnit(input_string):
645
  """Tries to extract number and scale from the given string.
646

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

650
  """
651
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', input_string)
652
  if not m:
653
    raise errors.UnitParseError("Invalid format")
654

    
655
  value = float(m.groups()[0])
656

    
657
  unit = m.groups()[1]
658
  if unit:
659
    lcunit = unit.lower()
660
  else:
661
    lcunit = 'm'
662

    
663
  if lcunit in ('m', 'mb', 'mib'):
664
    # Value already in MiB
665
    pass
666

    
667
  elif lcunit in ('g', 'gb', 'gib'):
668
    value *= 1024
669

    
670
  elif lcunit in ('t', 'tb', 'tib'):
671
    value *= 1024 * 1024
672

    
673
  else:
674
    raise errors.UnitParseError("Unknown unit: %s" % unit)
675

    
676
  # Make sure we round up
677
  if int(value) < value:
678
    value += 1
679

    
680
  # Round up to the next multiple of 4
681
  value = int(value)
682
  if value % 4:
683
    value += 4 - value % 4
684

    
685
  return value
686

    
687

    
688
def AddAuthorizedKey(file_name, key):
689
  """Adds an SSH public key to an authorized_keys file.
690

691
  Args:
692
    file_name: Path to authorized_keys file
693
    key: String containing key
694
  """
695
  key_fields = key.split()
696

    
697
  f = open(file_name, 'a+')
698
  try:
699
    nl = True
700
    for line in f:
701
      # Ignore whitespace changes
702
      if line.split() == key_fields:
703
        break
704
      nl = line.endswith('\n')
705
    else:
706
      if not nl:
707
        f.write("\n")
708
      f.write(key.rstrip('\r\n'))
709
      f.write("\n")
710
      f.flush()
711
  finally:
712
    f.close()
713

    
714

    
715
def RemoveAuthorizedKey(file_name, key):
716
  """Removes an SSH public key from an authorized_keys file.
717

718
  Args:
719
    file_name: Path to authorized_keys file
720
    key: String containing key
721
  """
722
  key_fields = key.split()
723

    
724
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
725
  out = os.fdopen(fd, 'w')
726
  try:
727
    f = open(file_name, 'r')
728
    try:
729
      for line in f:
730
        # Ignore whitespace changes while comparing lines
731
        if line.split() != key_fields:
732
          out.write(line)
733

    
734
      out.flush()
735
      os.rename(tmpname, file_name)
736
    finally:
737
      f.close()
738
  finally:
739
    out.close()
740

    
741

    
742
def CreateBackup(file_name):
743
  """Creates a backup of a file.
744

745
  Returns: the path to the newly created backup file.
746

747
  """
748
  if not os.path.isfile(file_name):
749
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
750
                                file_name)
751

    
752
  prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
753
  dir = os.path.dirname(file_name)
754

    
755
  fsrc = open(file_name, 'rb')
756
  try:
757
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir)
758
    fdst = os.fdopen(fd, 'wb')
759
    try:
760
      shutil.copyfileobj(fsrc, fdst)
761
    finally:
762
      fdst.close()
763
  finally:
764
    fsrc.close()
765

    
766
  return backup_name
767

    
768

    
769
def ShellQuote(value):
770
  """Quotes shell argument according to POSIX.
771

772
  """
773
  if _re_shell_unquoted.match(value):
774
    return value
775
  else:
776
    return "'%s'" % value.replace("'", "'\\''")
777

    
778

    
779
def ShellQuoteArgs(args):
780
  """Quotes all given shell arguments and concatenates using spaces.
781

782
  """
783
  return ' '.join([ShellQuote(i) for i in args])
784

    
785

    
786

    
787
def TcpPing(source, target, port, timeout=10, live_port_needed=False):
788
  """Simple ping implementation using TCP connect(2).
789

790
  Try to do a TCP connect(2) from the specified source IP to the specified
791
  target IP and the specified target port. If live_port_needed is set to true,
792
  requires the remote end to accept the connection. The timeout is specified
793
  in seconds and defaults to 10 seconds
794

795
  """
796
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
797

    
798
  sucess = False
799

    
800
  try:
801
    sock.bind((source, 0))
802
  except socket.error, (errcode, errstring):
803
    if errcode == errno.EADDRNOTAVAIL:
804
      success = False
805

    
806
  sock.settimeout(timeout)
807

    
808
  try:
809
    sock.connect((target, port))
810
    sock.close()
811
    success = True
812
  except socket.timeout:
813
    success = False
814
  except socket.error, (errcode, errstring):
815
    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
816

    
817
  return success
818

    
819

    
820
def ListVisibleFiles(path):
821
  """Returns a list of all visible files in a directory.
822

823
  """
824
  return [i for i in os.listdir(path) if not i.startswith(".")]