Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ 80a0546b

History | View | Annotate | Download (29 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 logging
27
import shutil
28
import tempfile
29
import errno
30
import time
31
import stat
32

    
33
from ganeti import errors
34
from ganeti import constants
35
from ganeti import pathutils
36
from ganeti.utils import filelock
37

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

    
42
# Possible values for keep_perms in WriteFile()
43
KP_NEVER = 0
44
KP_ALWAYS = 1
45
KP_IF_EXISTS = 2
46

    
47
KEEP_PERMS_VALUES = [
48
  KP_NEVER,
49
  KP_ALWAYS,
50
  KP_IF_EXISTS,
51
  ]
52

    
53

    
54
def ErrnoOrStr(err):
55
  """Format an EnvironmentError exception.
56

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

61
  @type err: L{EnvironmentError}
62
  @param err: the exception to format
63

64
  """
65
  if hasattr(err, "errno"):
66
    detail = errno.errorcode[err.errno]
67
  else:
68
    detail = str(err)
69
  return detail
70

    
71

    
72
class FileStatHelper:
73
  """Helper to store file handle's C{fstat}.
74

75
  Useful in combination with L{ReadFile}'s C{preread} parameter.
76

77
  """
78
  def __init__(self):
79
    """Initializes this class.
80

81
    """
82
    self.st = None
83

    
84
  def __call__(self, fh):
85
    """Calls C{fstat} on file handle.
86

87
    """
88
    self.st = os.fstat(fh.fileno())
89

    
90

    
91
def ReadFile(file_name, size=-1, preread=None):
92
  """Reads a file.
93

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

101
  """
102
  f = open(file_name, "r")
103
  try:
104
    if preread:
105
      preread(f)
106

    
107
    return f.read(size)
108
  finally:
109
    f.close()
110

    
111

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

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

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

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

162
  @rtype: None or int
163
  @return: None if the 'close' parameter evaluates to True,
164
      otherwise the file descriptor
165

166
  @raise errors.ProgrammerError: if any of the arguments are not valid
167

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

    
173
  if [fn, data].count(None) != 1:
174
    raise errors.ProgrammerError("fn or data required")
175

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

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

    
187
  if backup and not dry_run and os.path.isfile(file_name):
188
    CreateBackup(file_name)
189

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

    
202
  # Whether temporary file needs to be removed (e.g. if any error occurs)
203
  do_remove = True
204

    
205
  # Function result
206
  result = None
207

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

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

    
254
  return result
255

    
256

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

260
  Either the path to the file or the fd must be given.
261

262
  @param path: the file path
263
  @param fd: a file descriptor
264
  @return: a tuple of (device number, inode number, mtime)
265

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

    
270
  if fd is None:
271
    st = os.stat(path)
272
  else:
273
    st = os.fstat(fd)
274

    
275
  return (st.st_dev, st.st_ino, st.st_mtime)
276

    
277

    
278
def VerifyFileID(fi_disk, fi_ours):
279
  """Verifies that two file IDs are matching.
280

281
  Differences in the inode/device are not accepted, but and older
282
  timestamp for fi_disk is accepted.
283

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

290
  """
291
  (d1, i1, m1) = fi_disk
292
  (d2, i2, m2) = fi_ours
293

    
294
  return (d1, i1) == (d2, i2) and m1 <= m2
295

    
296

    
297
def SafeWriteFile(file_name, file_id, **kwargs):
298
  """Wraper over L{WriteFile} that locks the target file.
299

300
  By keeping the target file locked during WriteFile, we ensure that
301
  cooperating writers will safely serialise access to the file.
302

303
  @type file_name: str
304
  @param file_name: the target filename
305
  @type file_id: tuple
306
  @param file_id: a result from L{GetFileID}
307

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

    
321

    
322
def ReadOneLineFile(file_name, strict=False):
323
  """Return the first non-empty line from a file.
324

325
  @type strict: boolean
326
  @param strict: if True, abort if the file has more than one
327
      non-empty line
328

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

    
339

    
340
def RemoveFile(filename):
341
  """Remove a file ignoring some errors.
342

343
  Remove a file, ignoring non-existing ones or directories. Other
344
  errors are passed.
345

346
  @type filename: str
347
  @param filename: the file to be removed
348

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

    
356

    
357
def RemoveDir(dirname):
358
  """Remove an empty directory.
359

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

364
  @type dirname: str
365
  @param dirname: the empty directory to be removed
366

367
  """
368
  try:
369
    os.rmdir(dirname)
370
  except OSError, err:
371
    if err.errno != errno.ENOENT:
372
      raise
373

    
374

    
375
def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
376
               dir_gid=None):
377
  """Renames a file.
378

379
  This just creates the very least directory if it does not exist and C{mkdir}
380
  is set to true.
381

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

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

    
407
      return os.rename(old, new)
408

    
409
    raise
410

    
411

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

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

424
  """
425
  logging.debug("Checking %s", path)
426

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

    
434
  try:
435
    st = _stat_fn(path)
436

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

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

    
457

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

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

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

    
484
  _perm_fn(path, mode, uid=uid, gid=gid)
485

    
486

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

490
  This is a wrapper around C{os.makedirs} adding error handling not implemented
491
  before Python 2.5.
492

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

    
502

    
503
def TimestampForFilename():
504
  """Returns the current time formatted for filenames.
505

506
  The format doesn't contain colons as some shells and applications treat them
507
  as separators. Uses the local timezone.
508

509
  """
510
  return time.strftime("%Y-%m-%d_%H_%M_%S")
511

    
512

    
513
def CreateBackup(file_name):
514
  """Creates a backup of a file.
515

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

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

    
527
  prefix = ("%s.backup-%s." %
528
            (os.path.basename(file_name), TimestampForFilename()))
529
  dir_name = os.path.dirname(file_name)
530

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

    
543
  return backup_name
544

    
545

    
546
def ListVisibleFiles(path, _is_mountpoint=os.path.ismount):
547
  """Returns a list of visible files in a directory.
548

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

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

    
560
  mountpoint = _is_mountpoint(path)
561

    
562
  def fn(name):
563
    """File name filter.
564

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

569
    """
570
    return not (name.startswith(".") or
571
                (mountpoint and name == _LOST_AND_FOUND and
572
                 os.path.isdir(os.path.join(path, name))))
573

    
574
  return filter(fn, os.listdir(path))
575

    
576

    
577
def EnsureDirs(dirs):
578
  """Make required directories, if they don't exist.
579

580
  @param dirs: list of tuples (dir_name, dir_mode)
581
  @type dirs: list of (string, integer)
582

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

    
599

    
600
def FindFile(name, search_path, test=os.path.exists):
601
  """Look for a filesystem object in a given path.
602

603
  This is an abstract method to search for filesystem object (files,
604
  dirs) under a given search path.
605

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

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

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

    
633

    
634
def IsNormAbsPath(path):
635
  """Check whether a path is absolute and also normalized
636

637
  This avoids things like /dir/../../other/path to be valid.
638

639
  """
640
  return os.path.normpath(path) == path and os.path.isabs(path)
641

    
642

    
643
def IsBelowDir(root, other_path):
644
  """Check whether a path is below a root dir.
645

646
  This works around the nasty byte-byte comparison of commonprefix.
647

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

    
653
  norm_other = os.path.normpath(other_path)
654

    
655
  if norm_other == os.sep:
656
    # The root directory can never be below another path
657
    return False
658

    
659
  norm_root = os.path.normpath(root)
660

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

    
667
  return os.path.commonprefix([prepared_root, norm_other]) == prepared_root
668

    
669

    
670
def PathJoin(*args):
671
  """Safe-join a list of path components.
672

673
  Requirements:
674
      - the first argument must be an absolute path
675
      - no component in the path must have backtracking (e.g. /../),
676
        since we check for normalization at the end
677

678
  @param args: the path components to be joined
679
  @raise ValueError: for invalid paths
680

681
  """
682
  # ensure we're having at least one path passed in
683
  assert args
684
  # ensure the first component is an absolute and normalized path name
685
  root = args[0]
686
  if not IsNormAbsPath(root):
687
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
688
  result = os.path.join(*args)
689
  # ensure that the whole path is normalized
690
  if not IsNormAbsPath(result):
691
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
692
  # check that we're still under the original prefix
693
  if not IsBelowDir(root, result):
694
    raise ValueError("Error: path joining resulted in different prefix"
695
                     " (%s != %s)" % (result, root))
696
  return result
697

    
698

    
699
def TailFile(fname, lines=20):
700
  """Return the last lines from a file.
701

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

706
  @param fname: the file name
707
  @type lines: int
708
  @param lines: the (maximum) number of lines to return
709

710
  """
711
  fd = open(fname, "r")
712
  try:
713
    fd.seek(0, 2)
714
    pos = fd.tell()
715
    pos = max(0, pos - 4096)
716
    fd.seek(pos, 0)
717
    raw_data = fd.read()
718
  finally:
719
    fd.close()
720

    
721
  rows = raw_data.splitlines()
722
  return rows[-lines:]
723

    
724

    
725
def BytesToMebibyte(value):
726
  """Converts bytes to mebibytes.
727

728
  @type value: int
729
  @param value: Value in bytes
730
  @rtype: int
731
  @return: Value in mebibytes
732

733
  """
734
  return int(round(value / (1024.0 * 1024.0), 0))
735

    
736

    
737
def CalculateDirectorySize(path):
738
  """Calculates the size of a directory recursively.
739

740
  @type path: string
741
  @param path: Path to directory
742
  @rtype: int
743
  @return: Size in mebibytes
744

745
  """
746
  size = 0
747

    
748
  for (curpath, _, files) in os.walk(path):
749
    for filename in files:
750
      st = os.lstat(PathJoin(curpath, filename))
751
      size += st.st_size
752

    
753
  return BytesToMebibyte(size)
754

    
755

    
756
def GetFilesystemStats(path):
757
  """Returns the total and free space on a filesystem.
758

759
  @type path: string
760
  @param path: Path on filesystem to be examined
761
  @rtype: int
762
  @return: tuple of (Total space, Free space) in mebibytes
763

764
  """
765
  st = os.statvfs(path)
766

    
767
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
768
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
769
  return (tsize, fsize)
770

    
771

    
772
def ReadPidFile(pidfile):
773
  """Read a pid from a file.
774

775
  @type  pidfile: string
776
  @param pidfile: path to the file containing the pid
777
  @rtype: int
778
  @return: The process id, if the file exists and contains a valid PID,
779
           otherwise 0
780

781
  """
782
  try:
783
    raw_data = ReadOneLineFile(pidfile)
784
  except EnvironmentError, err:
785
    if err.errno != errno.ENOENT:
786
      logging.exception("Can't read pid file")
787
    return 0
788

    
789
  return _ParsePidFileContents(raw_data)
790

    
791

    
792
def _ParsePidFileContents(data):
793
  """Tries to extract a process ID from a PID file's content.
794

795
  @type data: string
796
  @rtype: int
797
  @return: Zero if nothing could be read, PID otherwise
798

799
  """
800
  try:
801
    pid = int(data)
802
  except (TypeError, ValueError):
803
    logging.info("Can't parse pid file contents", exc_info=True)
804
    return 0
805
  else:
806
    return pid
807

    
808

    
809
def ReadLockedPidFile(path):
810
  """Reads a locked PID file.
811

812
  This can be used together with L{utils.process.StartDaemon}.
813

814
  @type path: string
815
  @param path: Path to PID file
816
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
817

818
  """
819
  try:
820
    fd = os.open(path, os.O_RDONLY)
821
  except EnvironmentError, err:
822
    if err.errno == errno.ENOENT:
823
      # PID file doesn't exist
824
      return None
825
    raise
826

    
827
  try:
828
    try:
829
      # Try to acquire lock
830
      filelock.LockFile(fd)
831
    except errors.LockError:
832
      # Couldn't lock, daemon is running
833
      return int(os.read(fd, 100))
834
  finally:
835
    os.close(fd)
836

    
837
  return None
838

    
839

    
840
def _SplitSshKey(key):
841
  """Splits a line for SSH's C{authorized_keys} file.
842

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

847
  @type key: string
848
  @param key: Key line
849
  @rtype: tuple
850

851
  """
852
  parts = key.split()
853

    
854
  if parts and parts[0] in constants.SSHAK_ALL:
855
    # If the key has no options in front of it, we only want the significant
856
    # fields
857
    return (False, parts[:2])
858
  else:
859
    # Can't properly split the line, so use everything
860
    return (True, parts)
861

    
862

    
863
def AddAuthorizedKey(file_obj, key):
864
  """Adds an SSH public key to an authorized_keys file.
865

866
  @type file_obj: str or file handle
867
  @param file_obj: path to authorized_keys file
868
  @type key: str
869
  @param key: string containing key
870

871
  """
872
  key_fields = _SplitSshKey(key)
873

    
874
  if isinstance(file_obj, basestring):
875
    f = open(file_obj, "a+")
876
  else:
877
    f = file_obj
878

    
879
  try:
880
    nl = True
881
    for line in f:
882
      # Ignore whitespace changes
883
      if _SplitSshKey(line) == key_fields:
884
        break
885
      nl = line.endswith("\n")
886
    else:
887
      if not nl:
888
        f.write("\n")
889
      f.write(key.rstrip("\r\n"))
890
      f.write("\n")
891
      f.flush()
892
  finally:
893
    f.close()
894

    
895

    
896
def RemoveAuthorizedKey(file_name, key):
897
  """Removes an SSH public key from an authorized_keys file.
898

899
  @type file_name: str
900
  @param file_name: path to authorized_keys file
901
  @type key: str
902
  @param key: string containing key
903

904
  """
905
  key_fields = _SplitSshKey(key)
906

    
907
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
908
  try:
909
    out = os.fdopen(fd, "w")
910
    try:
911
      f = open(file_name, "r")
912
      try:
913
        for line in f:
914
          # Ignore whitespace changes while comparing lines
915
          if _SplitSshKey(line) != key_fields:
916
            out.write(line)
917

    
918
        out.flush()
919
        os.rename(tmpname, file_name)
920
      finally:
921
        f.close()
922
    finally:
923
      out.close()
924
  except:
925
    RemoveFile(tmpname)
926
    raise
927

    
928

    
929
def DaemonPidFileName(name):
930
  """Compute a ganeti pid file absolute path
931

932
  @type name: str
933
  @param name: the daemon name
934
  @rtype: str
935
  @return: the full path to the pidfile corresponding to the given
936
      daemon name
937

938
  """
939
  return PathJoin(pathutils.RUN_DIR, "%s.pid" % name)
940

    
941

    
942
def WritePidFile(pidfile):
943
  """Write the current process pidfile.
944

945
  @type pidfile: string
946
  @param pidfile: the path to the file to be written
947
  @raise errors.LockError: if the pid file already exists and
948
      points to a live process
949
  @rtype: int
950
  @return: the file descriptor of the lock file; do not close this unless
951
      you want to unlock the pid file
952

953
  """
954
  # We don't rename nor truncate the file to not drop locks under
955
  # existing processes
956
  fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
957

    
958
  # Lock the PID file (and fail if not possible to do so). Any code
959
  # wanting to send a signal to the daemon should try to lock the PID
960
  # file before reading it. If acquiring the lock succeeds, the daemon is
961
  # no longer running and the signal should not be sent.
962
  try:
963
    filelock.LockFile(fd_pidfile)
964
  except errors.LockError:
965
    msg = ["PID file '%s' is already locked by another process" % pidfile]
966
    # Try to read PID file
967
    pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
968
    if pid > 0:
969
      msg.append(", PID read from file is %s" % pid)
970
    raise errors.PidFileLockError("".join(msg))
971

    
972
  os.write(fd_pidfile, "%d\n" % os.getpid())
973

    
974
  return fd_pidfile
975

    
976

    
977
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
978
  """Reads the watcher pause file.
979

980
  @type filename: string
981
  @param filename: Path to watcher pause file
982
  @type now: None, float or int
983
  @param now: Current time as Unix timestamp
984
  @type remove_after: int
985
  @param remove_after: Remove watcher pause file after specified amount of
986
    seconds past the pause end time
987

988
  """
989
  if now is None:
990
    now = time.time()
991

    
992
  try:
993
    value = ReadFile(filename)
994
  except IOError, err:
995
    if err.errno != errno.ENOENT:
996
      raise
997
    value = None
998

    
999
  if value is not None:
1000
    try:
1001
      value = int(value)
1002
    except ValueError:
1003
      logging.warning(("Watcher pause file (%s) contains invalid value,"
1004
                       " removing it"), filename)
1005
      RemoveFile(filename)
1006
      value = None
1007

    
1008
    if value is not None:
1009
      # Remove file if it's outdated
1010
      if now > (value + remove_after):
1011
        RemoveFile(filename)
1012
        value = None
1013

    
1014
      elif now > value:
1015
        value = None
1016

    
1017
  return value
1018

    
1019

    
1020
def NewUUID():
1021
  """Returns a random UUID.
1022

1023
  @note: This is a Linux-specific method as it uses the /proc
1024
      filesystem.
1025
  @rtype: str
1026

1027
  """
1028
  return ReadFile(constants.RANDOM_UUID_FILE, size=128).rstrip("\n")
1029

    
1030

    
1031
class TemporaryFileManager(object):
1032
  """Stores the list of files to be deleted and removes them on demand.
1033

1034
  """
1035

    
1036
  def __init__(self):
1037
    self._files = []
1038

    
1039
  def __del__(self):
1040
    self.Cleanup()
1041

    
1042
  def Add(self, filename):
1043
    """Add file to list of files to be deleted.
1044

1045
    @type filename: string
1046
    @param filename: path to filename to be added
1047

1048
    """
1049
    self._files.append(filename)
1050

    
1051
  def Remove(self, filename):
1052
    """Remove file from list of files to be deleted.
1053

1054
    @type filename: string
1055
    @param filename: path to filename to be deleted
1056

1057
    """
1058
    self._files.remove(filename)
1059

    
1060
  def Cleanup(self):
1061
    """Delete all files marked for deletion
1062

1063
    """
1064
    while self._files:
1065
      RemoveFile(self._files.pop())