Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ 364e1664

History | View | Annotate | Download (30.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
"""Utility functions for I/O.
22

23
"""
24

    
25
import os
26
import re
27
import logging
28
import shutil
29
import tempfile
30
import errno
31
import time
32
import stat
33
import grp
34
import pwd
35

    
36
from ganeti import errors
37
from ganeti import constants
38
from ganeti import pathutils
39
from ganeti.utils import filelock
40

    
41
#: Directory used by fsck(8) to store recovered data, usually at a file
42
#: system's root directory
43
_LOST_AND_FOUND = "lost+found"
44

    
45
# Possible values for keep_perms in WriteFile()
46
KP_NEVER = 0
47
KP_ALWAYS = 1
48
KP_IF_EXISTS = 2
49

    
50
KEEP_PERMS_VALUES = [
51
  KP_NEVER,
52
  KP_ALWAYS,
53
  KP_IF_EXISTS,
54
  ]
55

    
56

    
57
def ErrnoOrStr(err):
58
  """Format an EnvironmentError exception.
59

60
  If the L{err} argument has an errno attribute, it will be looked up
61
  and converted into a textual C{E...} description. Otherwise the
62
  string representation of the error will be returned.
63

64
  @type err: L{EnvironmentError}
65
  @param err: the exception to format
66

67
  """
68
  if hasattr(err, "errno"):
69
    detail = errno.errorcode[err.errno]
70
  else:
71
    detail = str(err)
72
  return detail
73

    
74

    
75
class FileStatHelper:
76
  """Helper to store file handle's C{fstat}.
77

78
  Useful in combination with L{ReadFile}'s C{preread} parameter.
79

80
  """
81
  def __init__(self):
82
    """Initializes this class.
83

84
    """
85
    self.st = None
86

    
87
  def __call__(self, fh):
88
    """Calls C{fstat} on file handle.
89

90
    """
91
    self.st = os.fstat(fh.fileno())
92

    
93

    
94
def ReadFile(file_name, size=-1, preread=None):
95
  """Reads a file.
96

97
  @type size: int
98
  @param size: Read at most size bytes (if negative, entire file)
99
  @type preread: callable receiving file handle as single parameter
100
  @param preread: Function called before file is read
101
  @rtype: str
102
  @return: the (possibly partial) content of the file
103

104
  """
105
  f = open(file_name, "r")
106
  try:
107
    if preread:
108
      preread(f)
109

    
110
    return f.read(size)
111
  finally:
112
    f.close()
113

    
114

    
115
def WriteFile(file_name, fn=None, data=None,
116
              mode=None, uid=-1, gid=-1,
117
              atime=None, mtime=None, close=True,
118
              dry_run=False, backup=False,
119
              prewrite=None, postwrite=None, keep_perms=KP_NEVER):
120
  """(Over)write a file atomically.
121

122
  The file_name and either fn (a function taking one argument, the
123
  file descriptor, and which should write the data to it) or data (the
124
  contents of the file) must be passed. The other arguments are
125
  optional and allow setting the file mode, owner and group, and the
126
  mtime/atime of the file.
127

128
  If the function doesn't raise an exception, it has succeeded and the
129
  target file has the new contents. If the function has raised an
130
  exception, an existing target file should be unmodified and the
131
  temporary file should be removed.
132

133
  @type file_name: str
134
  @param file_name: the target filename
135
  @type fn: callable
136
  @param fn: content writing function, called with
137
      file descriptor as parameter
138
  @type data: str
139
  @param data: contents of the file
140
  @type mode: int
141
  @param mode: file mode
142
  @type uid: int
143
  @param uid: the owner of the file
144
  @type gid: int
145
  @param gid: the group of the file
146
  @type atime: int
147
  @param atime: a custom access time to be set on the file
148
  @type mtime: int
149
  @param mtime: a custom modification time to be set on the file
150
  @type close: boolean
151
  @param close: whether to close file after writing it
152
  @type prewrite: callable
153
  @param prewrite: function to be called before writing content
154
  @type postwrite: callable
155
  @param postwrite: function to be called after writing content
156
  @type keep_perms: members of L{KEEP_PERMS_VALUES}
157
  @param keep_perms: if L{KP_NEVER} (default), owner, group, and mode are
158
      taken from the other parameters; if L{KP_ALWAYS}, owner, group, and
159
      mode are copied from the existing file; if L{KP_IF_EXISTS}, owner,
160
      group, and mode are taken from the file, and if the file doesn't
161
      exist, they are taken from the other parameters. It is an error to
162
      pass L{KP_ALWAYS} when the file doesn't exist or when C{uid}, C{gid},
163
      or C{mode} are set to non-default values.
164

165
  @rtype: None or int
166
  @return: None if the 'close' parameter evaluates to True,
167
      otherwise the file descriptor
168

169
  @raise errors.ProgrammerError: if any of the arguments are not valid
170

171
  """
172
  if not os.path.isabs(file_name):
173
    raise errors.ProgrammerError("Path passed to WriteFile is not"
174
                                 " absolute: '%s'" % file_name)
175

    
176
  if [fn, data].count(None) != 1:
177
    raise errors.ProgrammerError("fn or data required")
178

    
179
  if [atime, mtime].count(None) == 1:
180
    raise errors.ProgrammerError("Both atime and mtime must be either"
181
                                 " set or None")
182

    
183
  if not keep_perms in KEEP_PERMS_VALUES:
184
    raise errors.ProgrammerError("Invalid value for keep_perms: %s" %
185
                                 keep_perms)
186
  if keep_perms == KP_ALWAYS and (uid != -1 or gid != -1 or mode is not None):
187
    raise errors.ProgrammerError("When keep_perms==KP_ALWAYS, 'uid', 'gid',"
188
                                 " and 'mode' cannot be set")
189

    
190
  if backup and not dry_run and os.path.isfile(file_name):
191
    CreateBackup(file_name)
192

    
193
  if keep_perms == KP_ALWAYS or keep_perms == KP_IF_EXISTS:
194
    # os.stat() raises an exception if the file doesn't exist
195
    try:
196
      file_stat = os.stat(file_name)
197
      mode = stat.S_IMODE(file_stat.st_mode)
198
      uid = file_stat.st_uid
199
      gid = file_stat.st_gid
200
    except OSError:
201
      if keep_perms == KP_ALWAYS:
202
        raise
203
      # else: if keeep_perms == KP_IF_EXISTS it's ok if the file doesn't exist
204

    
205
  # Whether temporary file needs to be removed (e.g. if any error occurs)
206
  do_remove = True
207

    
208
  # Function result
209
  result = None
210

    
211
  (dir_name, base_name) = os.path.split(file_name)
212
  (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
213
                                    dir=dir_name)
214
  try:
215
    try:
216
      if uid != -1 or gid != -1:
217
        os.chown(new_name, uid, gid)
218
      if mode:
219
        os.chmod(new_name, mode)
220
      if callable(prewrite):
221
        prewrite(fd)
222
      if data is not None:
223
        if isinstance(data, unicode):
224
          data = data.encode()
225
        assert isinstance(data, str)
226
        to_write = len(data)
227
        offset = 0
228
        while offset < to_write:
229
          written = os.write(fd, buffer(data, offset))
230
          assert written >= 0
231
          assert written <= to_write - offset
232
          offset += written
233
        assert offset == to_write
234
      else:
235
        fn(fd)
236
      if callable(postwrite):
237
        postwrite(fd)
238
      os.fsync(fd)
239
      if atime is not None and mtime is not None:
240
        os.utime(new_name, (atime, mtime))
241
    finally:
242
      # Close file unless the file descriptor should be returned
243
      if close:
244
        os.close(fd)
245
      else:
246
        result = fd
247

    
248
    # Rename file to destination name
249
    if not dry_run:
250
      os.rename(new_name, file_name)
251
      # Successful, no need to remove anymore
252
      do_remove = False
253
  finally:
254
    if do_remove:
255
      RemoveFile(new_name)
256

    
257
  return result
258

    
259

    
260
def GetFileID(path=None, fd=None):
261
  """Returns the file 'id', i.e. the dev/inode and mtime information.
262

263
  Either the path to the file or the fd must be given.
264

265
  @param path: the file path
266
  @param fd: a file descriptor
267
  @return: a tuple of (device number, inode number, mtime)
268

269
  """
270
  if [path, fd].count(None) != 1:
271
    raise errors.ProgrammerError("One and only one of fd/path must be given")
272

    
273
  if fd is None:
274
    st = os.stat(path)
275
  else:
276
    st = os.fstat(fd)
277

    
278
  return (st.st_dev, st.st_ino, st.st_mtime)
279

    
280

    
281
def VerifyFileID(fi_disk, fi_ours):
282
  """Verifies that two file IDs are matching.
283

284
  Differences in the inode/device are not accepted, but and older
285
  timestamp for fi_disk is accepted.
286

287
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
288
      file data
289
  @param fi_ours: tuple (dev, inode, mtime) representing the last
290
      written file data
291
  @rtype: boolean
292

293
  """
294
  (d1, i1, m1) = fi_disk
295
  (d2, i2, m2) = fi_ours
296

    
297
  return (d1, i1) == (d2, i2) and m1 <= m2
298

    
299

    
300
def SafeWriteFile(file_name, file_id, **kwargs):
301
  """Wraper over L{WriteFile} that locks the target file.
302

303
  By keeping the target file locked during WriteFile, we ensure that
304
  cooperating writers will safely serialise access to the file.
305

306
  @type file_name: str
307
  @param file_name: the target filename
308
  @type file_id: tuple
309
  @param file_id: a result from L{GetFileID}
310

311
  """
312
  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
313
  try:
314
    filelock.LockFile(fd)
315
    if file_id is not None:
316
      disk_id = GetFileID(fd=fd)
317
      if not VerifyFileID(disk_id, file_id):
318
        raise errors.LockError("Cannot overwrite file %s, it has been modified"
319
                               " since last written" % file_name)
320
    return WriteFile(file_name, **kwargs)
321
  finally:
322
    os.close(fd)
323

    
324

    
325
def ReadOneLineFile(file_name, strict=False):
326
  """Return the first non-empty line from a file.
327

328
  @type strict: boolean
329
  @param strict: if True, abort if the file has more than one
330
      non-empty line
331

332
  """
333
  file_lines = ReadFile(file_name).splitlines()
334
  full_lines = filter(bool, file_lines)
335
  if not file_lines or not full_lines:
336
    raise errors.GenericError("No data in one-liner file %s" % file_name)
337
  elif strict and len(full_lines) > 1:
338
    raise errors.GenericError("Too many lines in one-liner file %s" %
339
                              file_name)
340
  return full_lines[0]
341

    
342

    
343
def RemoveFile(filename):
344
  """Remove a file ignoring some errors.
345

346
  Remove a file, ignoring non-existing ones or directories. Other
347
  errors are passed.
348

349
  @type filename: str
350
  @param filename: the file to be removed
351

352
  """
353
  try:
354
    os.unlink(filename)
355
  except OSError, err:
356
    if err.errno not in (errno.ENOENT, errno.EISDIR):
357
      raise
358

    
359

    
360
def RemoveDir(dirname):
361
  """Remove an empty directory.
362

363
  Remove a directory, ignoring non-existing ones.
364
  Other errors are passed. This includes the case,
365
  where the directory is not empty, so it can't be removed.
366

367
  @type dirname: str
368
  @param dirname: the empty directory to be removed
369

370
  """
371
  try:
372
    os.rmdir(dirname)
373
  except OSError, err:
374
    if err.errno != errno.ENOENT:
375
      raise
376

    
377

    
378
def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
379
               dir_gid=None):
380
  """Renames a file.
381

382
  This just creates the very least directory if it does not exist and C{mkdir}
383
  is set to true.
384

385
  @type old: string
386
  @param old: Original path
387
  @type new: string
388
  @param new: New path
389
  @type mkdir: bool
390
  @param mkdir: Whether to create target directory if it doesn't exist
391
  @type mkdir_mode: int
392
  @param mkdir_mode: Mode for newly created directories
393
  @type dir_uid: int
394
  @param dir_uid: The uid for the (if fresh created) dir
395
  @type dir_gid: int
396
  @param dir_gid: The gid for the (if fresh created) dir
397

398
  """
399
  try:
400
    return os.rename(old, new)
401
  except OSError, err:
402
    # In at least one use case of this function, the job queue, directory
403
    # creation is very rare. Checking for the directory before renaming is not
404
    # as efficient.
405
    if mkdir and err.errno == errno.ENOENT:
406
      # Create directory and try again
407
      dir_path = os.path.dirname(new)
408
      MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
409

    
410
      return os.rename(old, new)
411

    
412
    raise
413

    
414

    
415
def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
416
                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
417
  """Enforces that given path has given permissions.
418

419
  @param path: The path to the file
420
  @param mode: The mode of the file
421
  @param uid: The uid of the owner of this file
422
  @param gid: The gid of the owner of this file
423
  @param must_exist: Specifies if non-existance of path will be an error
424
  @param _chmod_fn: chmod function to use (unittest only)
425
  @param _chown_fn: chown function to use (unittest only)
426

427
  """
428
  logging.debug("Checking %s", path)
429

    
430
  # chown takes -1 if you want to keep one part of the ownership, however
431
  # None is Python standard for that. So we remap them here.
432
  if uid is None:
433
    uid = -1
434
  if gid is None:
435
    gid = -1
436

    
437
  try:
438
    st = _stat_fn(path)
439

    
440
    fmode = stat.S_IMODE(st[stat.ST_MODE])
441
    if fmode != mode:
442
      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
443
      _chmod_fn(path, mode)
444

    
445
    if max(uid, gid) > -1:
446
      fuid = st[stat.ST_UID]
447
      fgid = st[stat.ST_GID]
448
      if fuid != uid or fgid != gid:
449
        logging.debug("Changing owner of %s from UID %s/GID %s to"
450
                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
451
        _chown_fn(path, uid, gid)
452
  except EnvironmentError, err:
453
    if err.errno == errno.ENOENT:
454
      if must_exist:
455
        raise errors.GenericError("Path %s should exist, but does not" % path)
456
    else:
457
      raise errors.GenericError("Error while changing permissions on %s: %s" %
458
                                (path, err))
459

    
460

    
461
def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
462
                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
463
  """Enforces that given path is a dir and has given mode, uid and gid set.
464

465
  @param path: The path to the file
466
  @param mode: The mode of the file
467
  @param uid: The uid of the owner of this file
468
  @param gid: The gid of the owner of this file
469
  @param _lstat_fn: Stat function to use (unittest only)
470
  @param _mkdir_fn: mkdir function to use (unittest only)
471
  @param _perm_fn: permission setter function to use (unittest only)
472

473
  """
474
  logging.debug("Checking directory %s", path)
475
  try:
476
    # We don't want to follow symlinks
477
    st = _lstat_fn(path)
478
  except EnvironmentError, err:
479
    if err.errno != errno.ENOENT:
480
      raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
481
    _mkdir_fn(path)
482
  else:
483
    if not stat.S_ISDIR(st[stat.ST_MODE]):
484
      raise errors.GenericError(("Path %s is expected to be a directory, but "
485
                                 "isn't") % path)
486

    
487
  _perm_fn(path, mode, uid=uid, gid=gid)
488

    
489

    
490
def Makedirs(path, mode=0750):
491
  """Super-mkdir; create a leaf directory and all intermediate ones.
492

493
  This is a wrapper around C{os.makedirs} adding error handling not implemented
494
  before Python 2.5.
495

496
  """
497
  try:
498
    os.makedirs(path, mode)
499
  except OSError, err:
500
    # Ignore EEXIST. This is only handled in os.makedirs as included in
501
    # Python 2.5 and above.
502
    if err.errno != errno.EEXIST or not os.path.exists(path):
503
      raise
504

    
505

    
506
def TimestampForFilename():
507
  """Returns the current time formatted for filenames.
508

509
  The format doesn't contain colons as some shells and applications treat them
510
  as separators. Uses the local timezone.
511

512
  """
513
  return time.strftime("%Y-%m-%d_%H_%M_%S")
514

    
515

    
516
def CreateBackup(file_name):
517
  """Creates a backup of a file.
518

519
  @type file_name: str
520
  @param file_name: file to be backed up
521
  @rtype: str
522
  @return: the path to the newly created backup
523
  @raise errors.ProgrammerError: for invalid file names
524

525
  """
526
  if not os.path.isfile(file_name):
527
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
528
                                 file_name)
529

    
530
  prefix = ("%s.backup-%s." %
531
            (os.path.basename(file_name), TimestampForFilename()))
532
  dir_name = os.path.dirname(file_name)
533

    
534
  fsrc = open(file_name, "rb")
535
  try:
536
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
537
    fdst = os.fdopen(fd, "wb")
538
    try:
539
      logging.debug("Backing up %s at %s", file_name, backup_name)
540
      shutil.copyfileobj(fsrc, fdst)
541
    finally:
542
      fdst.close()
543
  finally:
544
    fsrc.close()
545

    
546
  return backup_name
547

    
548

    
549
def ListVisibleFiles(path, _is_mountpoint=os.path.ismount):
550
  """Returns a list of visible files in a directory.
551

552
  @type path: str
553
  @param path: the directory to enumerate
554
  @rtype: list
555
  @return: the list of all files not starting with a dot
556
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
557

558
  """
559
  if not IsNormAbsPath(path):
560
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
561
                                 " absolute/normalized: '%s'" % path)
562

    
563
  mountpoint = _is_mountpoint(path)
564

    
565
  def fn(name):
566
    """File name filter.
567

568
    Ignores files starting with a dot (".") as by Unix convention they're
569
    considered hidden. The "lost+found" directory found at the root of some
570
    filesystems is also hidden.
571

572
    """
573
    return not (name.startswith(".") or
574
                (mountpoint and name == _LOST_AND_FOUND and
575
                 os.path.isdir(os.path.join(path, name))))
576

    
577
  return filter(fn, os.listdir(path))
578

    
579

    
580
def EnsureDirs(dirs):
581
  """Make required directories, if they don't exist.
582

583
  @param dirs: list of tuples (dir_name, dir_mode)
584
  @type dirs: list of (string, integer)
585

586
  """
587
  for dir_name, dir_mode in dirs:
588
    try:
589
      os.mkdir(dir_name, dir_mode)
590
    except EnvironmentError, err:
591
      if err.errno != errno.EEXIST:
592
        raise errors.GenericError("Cannot create needed directory"
593
                                  " '%s': %s" % (dir_name, err))
594
    try:
595
      os.chmod(dir_name, dir_mode)
596
    except EnvironmentError, err:
597
      raise errors.GenericError("Cannot change directory permissions on"
598
                                " '%s': %s" % (dir_name, err))
599
    if not os.path.isdir(dir_name):
600
      raise errors.GenericError("%s is not a directory" % dir_name)
601

    
602

    
603
def FindFile(name, search_path, test=os.path.exists):
604
  """Look for a filesystem object in a given path.
605

606
  This is an abstract method to search for filesystem object (files,
607
  dirs) under a given search path.
608

609
  @type name: str
610
  @param name: the name to look for
611
  @type search_path: str
612
  @param search_path: location to start at
613
  @type test: callable
614
  @param test: a function taking one argument that should return True
615
      if the a given object is valid; the default value is
616
      os.path.exists, causing only existing files to be returned
617
  @rtype: str or None
618
  @return: full path to the object if found, None otherwise
619

620
  """
621
  # validate the filename mask
622
  if constants.EXT_PLUGIN_MASK.match(name) is None:
623
    logging.critical("Invalid value passed for external script name: '%s'",
624
                     name)
625
    return None
626

    
627
  for dir_name in search_path:
628
    # FIXME: investigate switch to PathJoin
629
    item_name = os.path.sep.join([dir_name, name])
630
    # check the user test and that we're indeed resolving to the given
631
    # basename
632
    if test(item_name) and os.path.basename(item_name) == name:
633
      return item_name
634
  return None
635

    
636

    
637
def IsNormAbsPath(path):
638
  """Check whether a path is absolute and also normalized
639

640
  This avoids things like /dir/../../other/path to be valid.
641

642
  """
643
  return os.path.normpath(path) == path and os.path.isabs(path)
644

    
645

    
646
def IsBelowDir(root, other_path):
647
  """Check whether a path is below a root dir.
648

649
  This works around the nasty byte-byte comparison of commonprefix.
650

651
  """
652
  if not (os.path.isabs(root) and os.path.isabs(other_path)):
653
    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
654
                     (root, other_path))
655

    
656
  norm_other = os.path.normpath(other_path)
657

    
658
  if norm_other == os.sep:
659
    # The root directory can never be below another path
660
    return False
661

    
662
  norm_root = os.path.normpath(root)
663

    
664
  if norm_root == os.sep:
665
    # This is the root directory, no need to add another slash
666
    prepared_root = norm_root
667
  else:
668
    prepared_root = "%s%s" % (norm_root, os.sep)
669

    
670
  return os.path.commonprefix([prepared_root, norm_other]) == prepared_root
671

    
672

    
673
URL_RE = re.compile(r'(https?|ftps?)://')
674

    
675

    
676
def IsUrl(path):
677
  """Check whether a path is a HTTP URL.
678

679
  """
680
  return URL_RE.match(path)
681

    
682

    
683
def PathJoin(*args):
684
  """Safe-join a list of path components.
685

686
  Requirements:
687
      - the first argument must be an absolute path
688
      - no component in the path must have backtracking (e.g. /../),
689
        since we check for normalization at the end
690

691
  @param args: the path components to be joined
692
  @raise ValueError: for invalid paths
693

694
  """
695
  # ensure we're having at least two paths passed in
696
  if len(args) <= 1:
697
    raise errors.ProgrammerError("PathJoin requires two arguments")
698
  # ensure the first component is an absolute and normalized path name
699
  root = args[0]
700
  if not IsNormAbsPath(root):
701
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
702
  result = os.path.join(*args)
703
  # ensure that the whole path is normalized
704
  if not IsNormAbsPath(result):
705
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
706
  # check that we're still under the original prefix
707
  if not IsBelowDir(root, result):
708
    raise ValueError("Error: path joining resulted in different prefix"
709
                     " (%s != %s)" % (result, root))
710
  return result
711

    
712

    
713
def TailFile(fname, lines=20):
714
  """Return the last lines from a file.
715

716
  @note: this function will only read and parse the last 4KB of
717
      the file; if the lines are very long, it could be that less
718
      than the requested number of lines are returned
719

720
  @param fname: the file name
721
  @type lines: int
722
  @param lines: the (maximum) number of lines to return
723

724
  """
725
  fd = open(fname, "r")
726
  try:
727
    fd.seek(0, 2)
728
    pos = fd.tell()
729
    pos = max(0, pos - 4096)
730
    fd.seek(pos, 0)
731
    raw_data = fd.read()
732
  finally:
733
    fd.close()
734

    
735
  rows = raw_data.splitlines()
736
  return rows[-lines:]
737

    
738

    
739
def BytesToMebibyte(value):
740
  """Converts bytes to mebibytes.
741

742
  @type value: int
743
  @param value: Value in bytes
744
  @rtype: int
745
  @return: Value in mebibytes
746

747
  """
748
  return int(round(value / (1024.0 * 1024.0), 0))
749

    
750

    
751
def CalculateDirectorySize(path):
752
  """Calculates the size of a directory recursively.
753

754
  @type path: string
755
  @param path: Path to directory
756
  @rtype: int
757
  @return: Size in mebibytes
758

759
  """
760
  size = 0
761

    
762
  for (curpath, _, files) in os.walk(path):
763
    for filename in files:
764
      st = os.lstat(PathJoin(curpath, filename))
765
      size += st.st_size
766

    
767
  return BytesToMebibyte(size)
768

    
769

    
770
def GetFilesystemStats(path):
771
  """Returns the total and free space on a filesystem.
772

773
  @type path: string
774
  @param path: Path on filesystem to be examined
775
  @rtype: int
776
  @return: tuple of (Total space, Free space) in mebibytes
777

778
  """
779
  st = os.statvfs(path)
780

    
781
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
782
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
783
  return (tsize, fsize)
784

    
785

    
786
def ReadPidFile(pidfile):
787
  """Read a pid from a file.
788

789
  @type  pidfile: string
790
  @param pidfile: path to the file containing the pid
791
  @rtype: int
792
  @return: The process id, if the file exists and contains a valid PID,
793
           otherwise 0
794

795
  """
796
  try:
797
    raw_data = ReadOneLineFile(pidfile)
798
  except EnvironmentError, err:
799
    if err.errno != errno.ENOENT:
800
      logging.exception("Can't read pid file")
801
    return 0
802

    
803
  return _ParsePidFileContents(raw_data)
804

    
805

    
806
def _ParsePidFileContents(data):
807
  """Tries to extract a process ID from a PID file's content.
808

809
  @type data: string
810
  @rtype: int
811
  @return: Zero if nothing could be read, PID otherwise
812

813
  """
814
  try:
815
    pid = int(data)
816
  except (TypeError, ValueError):
817
    logging.info("Can't parse pid file contents", exc_info=True)
818
    return 0
819
  else:
820
    return pid
821

    
822

    
823
def ReadLockedPidFile(path):
824
  """Reads a locked PID file.
825

826
  This can be used together with L{utils.process.StartDaemon}.
827

828
  @type path: string
829
  @param path: Path to PID file
830
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
831

832
  """
833
  try:
834
    fd = os.open(path, os.O_RDONLY)
835
  except EnvironmentError, err:
836
    if err.errno == errno.ENOENT:
837
      # PID file doesn't exist
838
      return None
839
    raise
840

    
841
  try:
842
    try:
843
      # Try to acquire lock
844
      filelock.LockFile(fd)
845
    except errors.LockError:
846
      # Couldn't lock, daemon is running
847
      return int(os.read(fd, 100))
848
  finally:
849
    os.close(fd)
850

    
851
  return None
852

    
853

    
854
def _SplitSshKey(key):
855
  """Splits a line for SSH's C{authorized_keys} file.
856

857
  If the line has no options (e.g. no C{command="..."}), only the significant
858
  parts, the key type and its hash, are used. Otherwise the whole line is used
859
  (split at whitespace).
860

861
  @type key: string
862
  @param key: Key line
863
  @rtype: tuple
864

865
  """
866
  parts = key.split()
867

    
868
  if parts and parts[0] in constants.SSHAK_ALL:
869
    # If the key has no options in front of it, we only want the significant
870
    # fields
871
    return (False, parts[:2])
872
  else:
873
    # Can't properly split the line, so use everything
874
    return (True, parts)
875

    
876

    
877
def AddAuthorizedKey(file_obj, key):
878
  """Adds an SSH public key to an authorized_keys file.
879

880
  @type file_obj: str or file handle
881
  @param file_obj: path to authorized_keys file
882
  @type key: str
883
  @param key: string containing key
884

885
  """
886
  key_fields = _SplitSshKey(key)
887

    
888
  if isinstance(file_obj, basestring):
889
    f = open(file_obj, "a+")
890
  else:
891
    f = file_obj
892

    
893
  try:
894
    nl = True
895
    for line in f:
896
      # Ignore whitespace changes
897
      if _SplitSshKey(line) == key_fields:
898
        break
899
      nl = line.endswith("\n")
900
    else:
901
      if not nl:
902
        f.write("\n")
903
      f.write(key.rstrip("\r\n"))
904
      f.write("\n")
905
      f.flush()
906
  finally:
907
    f.close()
908

    
909

    
910
def RemoveAuthorizedKey(file_name, key):
911
  """Removes an SSH public key from an authorized_keys file.
912

913
  @type file_name: str
914
  @param file_name: path to authorized_keys file
915
  @type key: str
916
  @param key: string containing key
917

918
  """
919
  key_fields = _SplitSshKey(key)
920

    
921
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
922
  try:
923
    out = os.fdopen(fd, "w")
924
    try:
925
      f = open(file_name, "r")
926
      try:
927
        for line in f:
928
          # Ignore whitespace changes while comparing lines
929
          if _SplitSshKey(line) != key_fields:
930
            out.write(line)
931

    
932
        out.flush()
933
        os.rename(tmpname, file_name)
934
      finally:
935
        f.close()
936
    finally:
937
      out.close()
938
  except:
939
    RemoveFile(tmpname)
940
    raise
941

    
942

    
943
def DaemonPidFileName(name):
944
  """Compute a ganeti pid file absolute path
945

946
  @type name: str
947
  @param name: the daemon name
948
  @rtype: str
949
  @return: the full path to the pidfile corresponding to the given
950
      daemon name
951

952
  """
953
  return PathJoin(pathutils.RUN_DIR, "%s.pid" % name)
954

    
955

    
956
def WritePidFile(pidfile):
957
  """Write the current process pidfile.
958

959
  @type pidfile: string
960
  @param pidfile: the path to the file to be written
961
  @raise errors.LockError: if the pid file already exists and
962
      points to a live process
963
  @rtype: int
964
  @return: the file descriptor of the lock file; do not close this unless
965
      you want to unlock the pid file
966

967
  """
968
  # We don't rename nor truncate the file to not drop locks under
969
  # existing processes
970
  fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
971

    
972
  # Lock the PID file (and fail if not possible to do so). Any code
973
  # wanting to send a signal to the daemon should try to lock the PID
974
  # file before reading it. If acquiring the lock succeeds, the daemon is
975
  # no longer running and the signal should not be sent.
976
  try:
977
    filelock.LockFile(fd_pidfile)
978
  except errors.LockError:
979
    msg = ["PID file '%s' is already locked by another process" % pidfile]
980
    # Try to read PID file
981
    pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
982
    if pid > 0:
983
      msg.append(", PID read from file is %s" % pid)
984
    raise errors.PidFileLockError("".join(msg))
985

    
986
  os.write(fd_pidfile, "%d\n" % os.getpid())
987

    
988
  return fd_pidfile
989

    
990

    
991
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
992
  """Reads the watcher pause file.
993

994
  @type filename: string
995
  @param filename: Path to watcher pause file
996
  @type now: None, float or int
997
  @param now: Current time as Unix timestamp
998
  @type remove_after: int
999
  @param remove_after: Remove watcher pause file after specified amount of
1000
    seconds past the pause end time
1001

1002
  """
1003
  if now is None:
1004
    now = time.time()
1005

    
1006
  try:
1007
    value = ReadFile(filename)
1008
  except IOError, err:
1009
    if err.errno != errno.ENOENT:
1010
      raise
1011
    value = None
1012

    
1013
  if value is not None:
1014
    try:
1015
      value = int(value)
1016
    except ValueError:
1017
      logging.warning(("Watcher pause file (%s) contains invalid value,"
1018
                       " removing it"), filename)
1019
      RemoveFile(filename)
1020
      value = None
1021

    
1022
    if value is not None:
1023
      # Remove file if it's outdated
1024
      if now > (value + remove_after):
1025
        RemoveFile(filename)
1026
        value = None
1027

    
1028
      elif now > value:
1029
        value = None
1030

    
1031
  return value
1032

    
1033

    
1034
def NewUUID():
1035
  """Returns a random UUID.
1036

1037
  @note: This is a Linux-specific method as it uses the /proc
1038
      filesystem.
1039
  @rtype: str
1040

1041
  """
1042
  return ReadFile(constants.RANDOM_UUID_FILE, size=128).rstrip("\n")
1043

    
1044

    
1045
class TemporaryFileManager(object):
1046
  """Stores the list of files to be deleted and removes them on demand.
1047

1048
  """
1049

    
1050
  def __init__(self):
1051
    self._files = []
1052

    
1053
  def __del__(self):
1054
    self.Cleanup()
1055

    
1056
  def Add(self, filename):
1057
    """Add file to list of files to be deleted.
1058

1059
    @type filename: string
1060
    @param filename: path to filename to be added
1061

1062
    """
1063
    self._files.append(filename)
1064

    
1065
  def Remove(self, filename):
1066
    """Remove file from list of files to be deleted.
1067

1068
    @type filename: string
1069
    @param filename: path to filename to be deleted
1070

1071
    """
1072
    self._files.remove(filename)
1073

    
1074
  def Cleanup(self):
1075
    """Delete all files marked for deletion
1076

1077
    """
1078
    while self._files:
1079
      RemoveFile(self._files.pop())
1080

    
1081

    
1082
def IsUserInGroup(uid, gid):
1083
  """Returns True if the user belongs to the group.
1084

1085
  @type uid: int
1086
  @param uid: the user id
1087
  @type gid: int
1088
  @param gid: the group id
1089
  @rtype: bool
1090

1091
  """
1092
  user = pwd.getpwuid(uid)
1093
  group = grp.getgrgid(gid)
1094
  return user.pw_gid == gid or user.pw_name in group.gr_mem
1095

    
1096

    
1097
def CanRead(username, filename):
1098
  """Returns True if the user can access (read) the file.
1099

1100
  @type username: string
1101
  @param username: the name of the user
1102
  @type filename: string
1103
  @param filename: the name of the file
1104
  @rtype: bool
1105

1106
  """
1107
  filestats = os.stat(filename)
1108
  user = pwd.getpwnam(username)
1109
  uid = user.pw_uid
1110
  user_readable = filestats.st_mode & stat.S_IRUSR != 0
1111
  group_readable = filestats.st_mode & stat.S_IRGRP != 0
1112
  return ((filestats.st_uid == uid and user_readable)
1113
          or (filestats.st_uid != uid and
1114
              IsUserInGroup(uid, filestats.st_gid) and group_readable))