Statistics
| Branch: | Tag: | Revision:

root / lib / utils / io.py @ a9d68e40

History | View | Annotate | Download (20.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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

    
32
from ganeti import errors
33
from ganeti import constants
34
from ganeti.utils import filelock
35

    
36

    
37
#: Path generating random UUID
38
_RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
39

    
40

    
41
def ReadFile(file_name, size=-1):
42
  """Reads a file.
43

44
  @type size: int
45
  @param size: Read at most size bytes (if negative, entire file)
46
  @rtype: str
47
  @return: the (possibly partial) content of the file
48

49
  """
50
  f = open(file_name, "r")
51
  try:
52
    return f.read(size)
53
  finally:
54
    f.close()
55

    
56

    
57
def WriteFile(file_name, fn=None, data=None,
58
              mode=None, uid=-1, gid=-1,
59
              atime=None, mtime=None, close=True,
60
              dry_run=False, backup=False,
61
              prewrite=None, postwrite=None):
62
  """(Over)write a file atomically.
63

64
  The file_name and either fn (a function taking one argument, the
65
  file descriptor, and which should write the data to it) or data (the
66
  contents of the file) must be passed. The other arguments are
67
  optional and allow setting the file mode, owner and group, and the
68
  mtime/atime of the file.
69

70
  If the function doesn't raise an exception, it has succeeded and the
71
  target file has the new contents. If the function has raised an
72
  exception, an existing target file should be unmodified and the
73
  temporary file should be removed.
74

75
  @type file_name: str
76
  @param file_name: the target filename
77
  @type fn: callable
78
  @param fn: content writing function, called with
79
      file descriptor as parameter
80
  @type data: str
81
  @param data: contents of the file
82
  @type mode: int
83
  @param mode: file mode
84
  @type uid: int
85
  @param uid: the owner of the file
86
  @type gid: int
87
  @param gid: the group of the file
88
  @type atime: int
89
  @param atime: a custom access time to be set on the file
90
  @type mtime: int
91
  @param mtime: a custom modification time to be set on the file
92
  @type close: boolean
93
  @param close: whether to close file after writing it
94
  @type prewrite: callable
95
  @param prewrite: function to be called before writing content
96
  @type postwrite: callable
97
  @param postwrite: function to be called after writing content
98

99
  @rtype: None or int
100
  @return: None if the 'close' parameter evaluates to True,
101
      otherwise the file descriptor
102

103
  @raise errors.ProgrammerError: if any of the arguments are not valid
104

105
  """
106
  if not os.path.isabs(file_name):
107
    raise errors.ProgrammerError("Path passed to WriteFile is not"
108
                                 " absolute: '%s'" % file_name)
109

    
110
  if [fn, data].count(None) != 1:
111
    raise errors.ProgrammerError("fn or data required")
112

    
113
  if [atime, mtime].count(None) == 1:
114
    raise errors.ProgrammerError("Both atime and mtime must be either"
115
                                 " set or None")
116

    
117
  if backup and not dry_run and os.path.isfile(file_name):
118
    CreateBackup(file_name)
119

    
120
  # Whether temporary file needs to be removed (e.g. if any error occurs)
121
  do_remove = True
122

    
123
  # Function result
124
  result = None
125

    
126
  (dir_name, base_name) = os.path.split(file_name)
127
  (fd, new_name) = tempfile.mkstemp(suffix=".new", prefix=base_name,
128
                                    dir=dir_name)
129
  try:
130
    try:
131
      if uid != -1 or gid != -1:
132
        os.chown(new_name, uid, gid)
133
      if mode:
134
        os.chmod(new_name, mode)
135
      if callable(prewrite):
136
        prewrite(fd)
137
      if data is not None:
138
        os.write(fd, data)
139
      else:
140
        fn(fd)
141
      if callable(postwrite):
142
        postwrite(fd)
143
      os.fsync(fd)
144
      if atime is not None and mtime is not None:
145
        os.utime(new_name, (atime, mtime))
146
    finally:
147
      # Close file unless the file descriptor should be returned
148
      if close:
149
        os.close(fd)
150
      else:
151
        result = fd
152

    
153
    # Rename file to destination name
154
    if not dry_run:
155
      os.rename(new_name, file_name)
156
      # Successful, no need to remove anymore
157
      do_remove = False
158
  finally:
159
    if do_remove:
160
      RemoveFile(new_name)
161

    
162
  return result
163

    
164

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

168
  Either the path to the file or the fd must be given.
169

170
  @param path: the file path
171
  @param fd: a file descriptor
172
  @return: a tuple of (device number, inode number, mtime)
173

174
  """
175
  if [path, fd].count(None) != 1:
176
    raise errors.ProgrammerError("One and only one of fd/path must be given")
177

    
178
  if fd is None:
179
    st = os.stat(path)
180
  else:
181
    st = os.fstat(fd)
182

    
183
  return (st.st_dev, st.st_ino, st.st_mtime)
184

    
185

    
186
def VerifyFileID(fi_disk, fi_ours):
187
  """Verifies that two file IDs are matching.
188

189
  Differences in the inode/device are not accepted, but and older
190
  timestamp for fi_disk is accepted.
191

192
  @param fi_disk: tuple (dev, inode, mtime) representing the actual
193
      file data
194
  @param fi_ours: tuple (dev, inode, mtime) representing the last
195
      written file data
196
  @rtype: boolean
197

198
  """
199
  (d1, i1, m1) = fi_disk
200
  (d2, i2, m2) = fi_ours
201

    
202
  return (d1, i1) == (d2, i2) and m1 <= m2
203

    
204

    
205
def SafeWriteFile(file_name, file_id, **kwargs):
206
  """Wraper over L{WriteFile} that locks the target file.
207

208
  By keeping the target file locked during WriteFile, we ensure that
209
  cooperating writers will safely serialise access to the file.
210

211
  @type file_name: str
212
  @param file_name: the target filename
213
  @type file_id: tuple
214
  @param file_id: a result from L{GetFileID}
215

216
  """
217
  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
218
  try:
219
    filelock.LockFile(fd)
220
    if file_id is not None:
221
      disk_id = GetFileID(fd=fd)
222
      if not VerifyFileID(disk_id, file_id):
223
        raise errors.LockError("Cannot overwrite file %s, it has been modified"
224
                               " since last written" % file_name)
225
    return WriteFile(file_name, **kwargs)
226
  finally:
227
    os.close(fd)
228

    
229

    
230
def ReadOneLineFile(file_name, strict=False):
231
  """Return the first non-empty line from a file.
232

233
  @type strict: boolean
234
  @param strict: if True, abort if the file has more than one
235
      non-empty line
236

237
  """
238
  file_lines = ReadFile(file_name).splitlines()
239
  full_lines = filter(bool, file_lines)
240
  if not file_lines or not full_lines:
241
    raise errors.GenericError("No data in one-liner file %s" % file_name)
242
  elif strict and len(full_lines) > 1:
243
    raise errors.GenericError("Too many lines in one-liner file %s" %
244
                              file_name)
245
  return full_lines[0]
246

    
247

    
248
def RemoveFile(filename):
249
  """Remove a file ignoring some errors.
250

251
  Remove a file, ignoring non-existing ones or directories. Other
252
  errors are passed.
253

254
  @type filename: str
255
  @param filename: the file to be removed
256

257
  """
258
  try:
259
    os.unlink(filename)
260
  except OSError, err:
261
    if err.errno not in (errno.ENOENT, errno.EISDIR):
262
      raise
263

    
264

    
265
def RemoveDir(dirname):
266
  """Remove an empty directory.
267

268
  Remove a directory, ignoring non-existing ones.
269
  Other errors are passed. This includes the case,
270
  where the directory is not empty, so it can't be removed.
271

272
  @type dirname: str
273
  @param dirname: the empty directory to be removed
274

275
  """
276
  try:
277
    os.rmdir(dirname)
278
  except OSError, err:
279
    if err.errno != errno.ENOENT:
280
      raise
281

    
282

    
283
def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
284
  """Renames a file.
285

286
  @type old: string
287
  @param old: Original path
288
  @type new: string
289
  @param new: New path
290
  @type mkdir: bool
291
  @param mkdir: Whether to create target directory if it doesn't exist
292
  @type mkdir_mode: int
293
  @param mkdir_mode: Mode for newly created directories
294

295
  """
296
  try:
297
    return os.rename(old, new)
298
  except OSError, err:
299
    # In at least one use case of this function, the job queue, directory
300
    # creation is very rare. Checking for the directory before renaming is not
301
    # as efficient.
302
    if mkdir and err.errno == errno.ENOENT:
303
      # Create directory and try again
304
      Makedirs(os.path.dirname(new), mode=mkdir_mode)
305

    
306
      return os.rename(old, new)
307

    
308
    raise
309

    
310

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

314
  This is a wrapper around C{os.makedirs} adding error handling not implemented
315
  before Python 2.5.
316

317
  """
318
  try:
319
    os.makedirs(path, mode)
320
  except OSError, err:
321
    # Ignore EEXIST. This is only handled in os.makedirs as included in
322
    # Python 2.5 and above.
323
    if err.errno != errno.EEXIST or not os.path.exists(path):
324
      raise
325

    
326

    
327
def TimestampForFilename():
328
  """Returns the current time formatted for filenames.
329

330
  The format doesn't contain colons as some shells and applications treat them
331
  as separators. Uses the local timezone.
332

333
  """
334
  return time.strftime("%Y-%m-%d_%H_%M_%S")
335

    
336

    
337
def CreateBackup(file_name):
338
  """Creates a backup of a file.
339

340
  @type file_name: str
341
  @param file_name: file to be backed up
342
  @rtype: str
343
  @return: the path to the newly created backup
344
  @raise errors.ProgrammerError: for invalid file names
345

346
  """
347
  if not os.path.isfile(file_name):
348
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
349
                                file_name)
350

    
351
  prefix = ("%s.backup-%s." %
352
            (os.path.basename(file_name), TimestampForFilename()))
353
  dir_name = os.path.dirname(file_name)
354

    
355
  fsrc = open(file_name, 'rb')
356
  try:
357
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
358
    fdst = os.fdopen(fd, 'wb')
359
    try:
360
      logging.debug("Backing up %s at %s", file_name, backup_name)
361
      shutil.copyfileobj(fsrc, fdst)
362
    finally:
363
      fdst.close()
364
  finally:
365
    fsrc.close()
366

    
367
  return backup_name
368

    
369

    
370
def ListVisibleFiles(path):
371
  """Returns a list of visible files in a directory.
372

373
  @type path: str
374
  @param path: the directory to enumerate
375
  @rtype: list
376
  @return: the list of all files not starting with a dot
377
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
378

379
  """
380
  if not IsNormAbsPath(path):
381
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
382
                                 " absolute/normalized: '%s'" % path)
383
  files = [i for i in os.listdir(path) if not i.startswith(".")]
384
  return files
385

    
386

    
387
def EnsureDirs(dirs):
388
  """Make required directories, if they don't exist.
389

390
  @param dirs: list of tuples (dir_name, dir_mode)
391
  @type dirs: list of (string, integer)
392

393
  """
394
  for dir_name, dir_mode in dirs:
395
    try:
396
      os.mkdir(dir_name, dir_mode)
397
    except EnvironmentError, err:
398
      if err.errno != errno.EEXIST:
399
        raise errors.GenericError("Cannot create needed directory"
400
                                  " '%s': %s" % (dir_name, err))
401
    try:
402
      os.chmod(dir_name, dir_mode)
403
    except EnvironmentError, err:
404
      raise errors.GenericError("Cannot change directory permissions on"
405
                                " '%s': %s" % (dir_name, err))
406
    if not os.path.isdir(dir_name):
407
      raise errors.GenericError("%s is not a directory" % dir_name)
408

    
409

    
410
def FindFile(name, search_path, test=os.path.exists):
411
  """Look for a filesystem object in a given path.
412

413
  This is an abstract method to search for filesystem object (files,
414
  dirs) under a given search path.
415

416
  @type name: str
417
  @param name: the name to look for
418
  @type search_path: str
419
  @param search_path: location to start at
420
  @type test: callable
421
  @param test: a function taking one argument that should return True
422
      if the a given object is valid; the default value is
423
      os.path.exists, causing only existing files to be returned
424
  @rtype: str or None
425
  @return: full path to the object if found, None otherwise
426

427
  """
428
  # validate the filename mask
429
  if constants.EXT_PLUGIN_MASK.match(name) is None:
430
    logging.critical("Invalid value passed for external script name: '%s'",
431
                     name)
432
    return None
433

    
434
  for dir_name in search_path:
435
    # FIXME: investigate switch to PathJoin
436
    item_name = os.path.sep.join([dir_name, name])
437
    # check the user test and that we're indeed resolving to the given
438
    # basename
439
    if test(item_name) and os.path.basename(item_name) == name:
440
      return item_name
441
  return None
442

    
443

    
444
def IsNormAbsPath(path):
445
  """Check whether a path is absolute and also normalized
446

447
  This avoids things like /dir/../../other/path to be valid.
448

449
  """
450
  return os.path.normpath(path) == path and os.path.isabs(path)
451

    
452

    
453
def PathJoin(*args):
454
  """Safe-join a list of path components.
455

456
  Requirements:
457
      - the first argument must be an absolute path
458
      - no component in the path must have backtracking (e.g. /../),
459
        since we check for normalization at the end
460

461
  @param args: the path components to be joined
462
  @raise ValueError: for invalid paths
463

464
  """
465
  # ensure we're having at least one path passed in
466
  assert args
467
  # ensure the first component is an absolute and normalized path name
468
  root = args[0]
469
  if not IsNormAbsPath(root):
470
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
471
  result = os.path.join(*args)
472
  # ensure that the whole path is normalized
473
  if not IsNormAbsPath(result):
474
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
475
  # check that we're still under the original prefix
476
  prefix = os.path.commonprefix([root, result])
477
  if prefix != root:
478
    raise ValueError("Error: path joining resulted in different prefix"
479
                     " (%s != %s)" % (prefix, root))
480
  return result
481

    
482

    
483
def TailFile(fname, lines=20):
484
  """Return the last lines from a file.
485

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

490
  @param fname: the file name
491
  @type lines: int
492
  @param lines: the (maximum) number of lines to return
493

494
  """
495
  fd = open(fname, "r")
496
  try:
497
    fd.seek(0, 2)
498
    pos = fd.tell()
499
    pos = max(0, pos-4096)
500
    fd.seek(pos, 0)
501
    raw_data = fd.read()
502
  finally:
503
    fd.close()
504

    
505
  rows = raw_data.splitlines()
506
  return rows[-lines:]
507

    
508

    
509
def BytesToMebibyte(value):
510
  """Converts bytes to mebibytes.
511

512
  @type value: int
513
  @param value: Value in bytes
514
  @rtype: int
515
  @return: Value in mebibytes
516

517
  """
518
  return int(round(value / (1024.0 * 1024.0), 0))
519

    
520

    
521
def CalculateDirectorySize(path):
522
  """Calculates the size of a directory recursively.
523

524
  @type path: string
525
  @param path: Path to directory
526
  @rtype: int
527
  @return: Size in mebibytes
528

529
  """
530
  size = 0
531

    
532
  for (curpath, _, files) in os.walk(path):
533
    for filename in files:
534
      st = os.lstat(PathJoin(curpath, filename))
535
      size += st.st_size
536

    
537
  return BytesToMebibyte(size)
538

    
539

    
540
def GetFilesystemStats(path):
541
  """Returns the total and free space on a filesystem.
542

543
  @type path: string
544
  @param path: Path on filesystem to be examined
545
  @rtype: int
546
  @return: tuple of (Total space, Free space) in mebibytes
547

548
  """
549
  st = os.statvfs(path)
550

    
551
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
552
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
553
  return (tsize, fsize)
554

    
555

    
556
def ReadPidFile(pidfile):
557
  """Read a pid from a file.
558

559
  @type  pidfile: string
560
  @param pidfile: path to the file containing the pid
561
  @rtype: int
562
  @return: The process id, if the file exists and contains a valid PID,
563
           otherwise 0
564

565
  """
566
  try:
567
    raw_data = ReadOneLineFile(pidfile)
568
  except EnvironmentError, err:
569
    if err.errno != errno.ENOENT:
570
      logging.exception("Can't read pid file")
571
    return 0
572

    
573
  try:
574
    pid = int(raw_data)
575
  except (TypeError, ValueError), err:
576
    logging.info("Can't parse pid file contents", exc_info=True)
577
    return 0
578

    
579
  return pid
580

    
581

    
582
def ReadLockedPidFile(path):
583
  """Reads a locked PID file.
584

585
  This can be used together with L{utils.process.StartDaemon}.
586

587
  @type path: string
588
  @param path: Path to PID file
589
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
590

591
  """
592
  try:
593
    fd = os.open(path, os.O_RDONLY)
594
  except EnvironmentError, err:
595
    if err.errno == errno.ENOENT:
596
      # PID file doesn't exist
597
      return None
598
    raise
599

    
600
  try:
601
    try:
602
      # Try to acquire lock
603
      filelock.LockFile(fd)
604
    except errors.LockError:
605
      # Couldn't lock, daemon is running
606
      return int(os.read(fd, 100))
607
  finally:
608
    os.close(fd)
609

    
610
  return None
611

    
612

    
613
def AddAuthorizedKey(file_obj, key):
614
  """Adds an SSH public key to an authorized_keys file.
615

616
  @type file_obj: str or file handle
617
  @param file_obj: path to authorized_keys file
618
  @type key: str
619
  @param key: string containing key
620

621
  """
622
  key_fields = key.split()
623

    
624
  if isinstance(file_obj, basestring):
625
    f = open(file_obj, 'a+')
626
  else:
627
    f = file_obj
628

    
629
  try:
630
    nl = True
631
    for line in f:
632
      # Ignore whitespace changes
633
      if line.split() == key_fields:
634
        break
635
      nl = line.endswith('\n')
636
    else:
637
      if not nl:
638
        f.write("\n")
639
      f.write(key.rstrip('\r\n'))
640
      f.write("\n")
641
      f.flush()
642
  finally:
643
    f.close()
644

    
645

    
646
def RemoveAuthorizedKey(file_name, key):
647
  """Removes an SSH public key from an authorized_keys file.
648

649
  @type file_name: str
650
  @param file_name: path to authorized_keys file
651
  @type key: str
652
  @param key: string containing key
653

654
  """
655
  key_fields = key.split()
656

    
657
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
658
  try:
659
    out = os.fdopen(fd, 'w')
660
    try:
661
      f = open(file_name, 'r')
662
      try:
663
        for line in f:
664
          # Ignore whitespace changes while comparing lines
665
          if line.split() != key_fields:
666
            out.write(line)
667

    
668
        out.flush()
669
        os.rename(tmpname, file_name)
670
      finally:
671
        f.close()
672
    finally:
673
      out.close()
674
  except:
675
    RemoveFile(tmpname)
676
    raise
677

    
678

    
679
def DaemonPidFileName(name):
680
  """Compute a ganeti pid file absolute path
681

682
  @type name: str
683
  @param name: the daemon name
684
  @rtype: str
685
  @return: the full path to the pidfile corresponding to the given
686
      daemon name
687

688
  """
689
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
690

    
691

    
692
def WritePidFile(pidfile):
693
  """Write the current process pidfile.
694

695
  @type pidfile: string
696
  @param pidfile: the path to the file to be written
697
  @raise errors.LockError: if the pid file already exists and
698
      points to a live process
699
  @rtype: int
700
  @return: the file descriptor of the lock file; do not close this unless
701
      you want to unlock the pid file
702

703
  """
704
  # We don't rename nor truncate the file to not drop locks under
705
  # existing processes
706
  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
707

    
708
  # Lock the PID file (and fail if not possible to do so). Any code
709
  # wanting to send a signal to the daemon should try to lock the PID
710
  # file before reading it. If acquiring the lock succeeds, the daemon is
711
  # no longer running and the signal should not be sent.
712
  filelock.LockFile(fd_pidfile)
713

    
714
  os.write(fd_pidfile, "%d\n" % os.getpid())
715

    
716
  return fd_pidfile
717

    
718

    
719
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
720
  """Reads the watcher pause file.
721

722
  @type filename: string
723
  @param filename: Path to watcher pause file
724
  @type now: None, float or int
725
  @param now: Current time as Unix timestamp
726
  @type remove_after: int
727
  @param remove_after: Remove watcher pause file after specified amount of
728
    seconds past the pause end time
729

730
  """
731
  if now is None:
732
    now = time.time()
733

    
734
  try:
735
    value = ReadFile(filename)
736
  except IOError, err:
737
    if err.errno != errno.ENOENT:
738
      raise
739
    value = None
740

    
741
  if value is not None:
742
    try:
743
      value = int(value)
744
    except ValueError:
745
      logging.warning(("Watcher pause file (%s) contains invalid value,"
746
                       " removing it"), filename)
747
      RemoveFile(filename)
748
      value = None
749

    
750
    if value is not None:
751
      # Remove file if it's outdated
752
      if now > (value + remove_after):
753
        RemoveFile(filename)
754
        value = None
755

    
756
      elif now > value:
757
        value = None
758

    
759
  return value
760

    
761

    
762
def NewUUID():
763
  """Returns a random UUID.
764

765
  @note: This is a Linux-specific method as it uses the /proc
766
      filesystem.
767
  @rtype: str
768

769
  """
770
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")