Revision c047a981

b/Makefile.am
26 26
serverdir = $(pkgpythondir)/server
27 27
watcherdir = $(pkgpythondir)/watcher
28 28
impexpddir = $(pkgpythondir)/impexpd
29
utilsdir = $(pkgpythondir)/utils
29 30
toolsdir = $(pkglibdir)/tools
30 31
docdir = $(datadir)/doc/$(PACKAGE)
31 32

  
......
50 51
	lib/masterd \
51 52
	lib/rapi \
52 53
	lib/server \
54
	lib/utils \
53 55
	lib/watcher \
54 56
	man \
55 57
	qa \
......
149 151
	lib/ssconf.py \
150 152
	lib/ssh.py \
151 153
	lib/storage.py \
152
	lib/utils.py \
153 154
	lib/uidpool.py \
154 155
	lib/workerpool.py
155 156

  
......
210 211
	lib/server/noded.py \
211 212
	lib/server/rapi.py
212 213

  
214
utils_PYTHON = \
215
	lib/utils/__init__.py
216

  
213 217
docrst = \
214 218
	doc/admin.rst \
215 219
	doc/design-2.0.rst \
......
514 518
	$(confd_PYTHON) \
515 519
	$(masterd_PYTHON) \
516 520
	$(impexpd_PYTHON) \
521
	$(utils_PYTHON) \
517 522
	$(watcher_PYTHON) \
518 523
	$(noinst_PYTHON) \
519 524
	$(qa_scripts)
/dev/null
1
#
2
#
3

  
4
# Copyright (C) 2006, 2007, 2010 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

  
21

  
22
"""Ganeti utility module.
23

  
24
This module holds functions that can be used in both daemons (all) and
25
the command line scripts.
26

  
27
"""
28

  
29

  
30
import os
31
import sys
32
import time
33
import subprocess
34
import re
35
import socket
36
import tempfile
37
import shutil
38
import errno
39
import pwd
40
import itertools
41
import select
42
import fcntl
43
import resource
44
import logging
45
import logging.handlers
46
import signal
47
import OpenSSL
48
import datetime
49
import calendar
50
import hmac
51
import collections
52

  
53
from cStringIO import StringIO
54

  
55
try:
56
  # pylint: disable-msg=F0401
57
  import ctypes
58
except ImportError:
59
  ctypes = None
60

  
61
from ganeti import errors
62
from ganeti import constants
63
from ganeti import compat
64

  
65

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

  
69
debug_locks = False
70

  
71
#: when set to True, L{RunCmd} is disabled
72
no_fork = False
73

  
74
_RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
75

  
76
HEX_CHAR_RE = r"[a-zA-Z0-9]"
77
VALID_X509_SIGNATURE_SALT = re.compile("^%s+$" % HEX_CHAR_RE, re.S)
78
X509_SIGNATURE = re.compile(r"^%s:\s*(?P<salt>%s+)/(?P<sign>%s+)$" %
79
                            (re.escape(constants.X509_CERT_SIGNATURE_HEADER),
80
                             HEX_CHAR_RE, HEX_CHAR_RE),
81
                            re.S | re.I)
82

  
83
_VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
84

  
85
UUID_RE = re.compile('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-'
86
                     '[a-f0-9]{4}-[a-f0-9]{12}$')
87

  
88
# Certificate verification results
89
(CERT_WARNING,
90
 CERT_ERROR) = range(1, 3)
91

  
92
# Flags for mlockall() (from bits/mman.h)
93
_MCL_CURRENT = 1
94
_MCL_FUTURE = 2
95

  
96
#: MAC checker regexp
97
_MAC_CHECK = re.compile("^([0-9a-f]{2}:){5}[0-9a-f]{2}$", re.I)
98

  
99
(_TIMEOUT_NONE,
100
 _TIMEOUT_TERM,
101
 _TIMEOUT_KILL) = range(3)
102

  
103
#: Shell param checker regexp
104
_SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
105

  
106
#: Unit checker regexp
107
_PARSEUNIT_REGEX = re.compile(r"^([.\d]+)\s*([a-zA-Z]+)?$")
108

  
109
#: ASN1 time regexp
110
_ASN1_TIME_REGEX = re.compile(r"^(\d+)([-+]\d\d)(\d\d)$")
111

  
112
_SORTER_RE = re.compile("^%s(.*)$" % (8 * "(\D+|\d+)?"))
113
_SORTER_DIGIT = re.compile("^\d+$")
114

  
115

  
116
class RunResult(object):
117
  """Holds the result of running external programs.
118

  
119
  @type exit_code: int
120
  @ivar exit_code: the exit code of the program, or None (if the program
121
      didn't exit())
122
  @type signal: int or None
123
  @ivar signal: the signal that caused the program to finish, or None
124
      (if the program wasn't terminated by a signal)
125
  @type stdout: str
126
  @ivar stdout: the standard output of the program
127
  @type stderr: str
128
  @ivar stderr: the standard error of the program
129
  @type failed: boolean
130
  @ivar failed: True in case the program was
131
      terminated by a signal or exited with a non-zero exit code
132
  @ivar fail_reason: a string detailing the termination reason
133

  
134
  """
135
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
136
               "failed", "fail_reason", "cmd"]
137

  
138

  
139
  def __init__(self, exit_code, signal_, stdout, stderr, cmd, timeout_action,
140
               timeout):
141
    self.cmd = cmd
142
    self.exit_code = exit_code
143
    self.signal = signal_
144
    self.stdout = stdout
145
    self.stderr = stderr
146
    self.failed = (signal_ is not None or exit_code != 0)
147

  
148
    fail_msgs = []
149
    if self.signal is not None:
150
      fail_msgs.append("terminated by signal %s" % self.signal)
151
    elif self.exit_code is not None:
152
      fail_msgs.append("exited with exit code %s" % self.exit_code)
153
    else:
154
      fail_msgs.append("unable to determine termination reason")
155

  
156
    if timeout_action == _TIMEOUT_TERM:
157
      fail_msgs.append("terminated after timeout of %.2f seconds" % timeout)
158
    elif timeout_action == _TIMEOUT_KILL:
159
      fail_msgs.append(("force termination after timeout of %.2f seconds"
160
                        " and linger for another %.2f seconds") %
161
                       (timeout, constants.CHILD_LINGER_TIMEOUT))
162

  
163
    if fail_msgs and self.failed:
164
      self.fail_reason = CommaJoin(fail_msgs)
165

  
166
    if self.failed:
167
      logging.debug("Command '%s' failed (%s); output: %s",
168
                    self.cmd, self.fail_reason, self.output)
169

  
170
  def _GetOutput(self):
171
    """Returns the combined stdout and stderr for easier usage.
172

  
173
    """
174
    return self.stdout + self.stderr
175

  
176
  output = property(_GetOutput, None, None, "Return full output")
177

  
178

  
179
def _BuildCmdEnvironment(env, reset):
180
  """Builds the environment for an external program.
181

  
182
  """
183
  if reset:
184
    cmd_env = {}
185
  else:
186
    cmd_env = os.environ.copy()
187
    cmd_env["LC_ALL"] = "C"
188

  
189
  if env is not None:
190
    cmd_env.update(env)
191

  
192
  return cmd_env
193

  
194

  
195
def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False,
196
           interactive=False, timeout=None):
197
  """Execute a (shell) command.
198

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

  
202
  @type cmd: string or list
203
  @param cmd: Command to run
204
  @type env: dict
205
  @param env: Additional environment variables
206
  @type output: str
207
  @param output: if desired, the output of the command can be
208
      saved in a file instead of the RunResult instance; this
209
      parameter denotes the file name (if not None)
210
  @type cwd: string
211
  @param cwd: if specified, will be used as the working
212
      directory for the command; the default will be /
213
  @type reset_env: boolean
214
  @param reset_env: whether to reset or keep the default os environment
215
  @type interactive: boolean
216
  @param interactive: weather we pipe stdin, stdout and stderr
217
                      (default behaviour) or run the command interactive
218
  @type timeout: int
219
  @param timeout: If not None, timeout in seconds until child process gets
220
                  killed
221
  @rtype: L{RunResult}
222
  @return: RunResult instance
223
  @raise errors.ProgrammerError: if we call this when forks are disabled
224

  
225
  """
226
  if no_fork:
227
    raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
228

  
229
  if output and interactive:
230
    raise errors.ProgrammerError("Parameters 'output' and 'interactive' can"
231
                                 " not be provided at the same time")
232

  
233
  if isinstance(cmd, basestring):
234
    strcmd = cmd
235
    shell = True
236
  else:
237
    cmd = [str(val) for val in cmd]
238
    strcmd = ShellQuoteArgs(cmd)
239
    shell = False
240

  
241
  if output:
242
    logging.debug("RunCmd %s, output file '%s'", strcmd, output)
243
  else:
244
    logging.debug("RunCmd %s", strcmd)
245

  
246
  cmd_env = _BuildCmdEnvironment(env, reset_env)
247

  
248
  try:
249
    if output is None:
250
      out, err, status, timeout_action = _RunCmdPipe(cmd, cmd_env, shell, cwd,
251
                                                     interactive, timeout)
252
    else:
253
      timeout_action = _TIMEOUT_NONE
254
      status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
255
      out = err = ""
256
  except OSError, err:
257
    if err.errno == errno.ENOENT:
258
      raise errors.OpExecError("Can't execute '%s': not found (%s)" %
259
                               (strcmd, err))
260
    else:
261
      raise
262

  
263
  if status >= 0:
264
    exitcode = status
265
    signal_ = None
266
  else:
267
    exitcode = None
268
    signal_ = -status
269

  
270
  return RunResult(exitcode, signal_, out, err, strcmd, timeout_action, timeout)
271

  
272

  
273
def SetupDaemonEnv(cwd="/", umask=077):
274
  """Setup a daemon's environment.
275

  
276
  This should be called between the first and second fork, due to
277
  setsid usage.
278

  
279
  @param cwd: the directory to which to chdir
280
  @param umask: the umask to setup
281

  
282
  """
283
  os.chdir(cwd)
284
  os.umask(umask)
285
  os.setsid()
286

  
287

  
288
def SetupDaemonFDs(output_file, output_fd):
289
  """Setups up a daemon's file descriptors.
290

  
291
  @param output_file: if not None, the file to which to redirect
292
      stdout/stderr
293
  @param output_fd: if not None, the file descriptor for stdout/stderr
294

  
295
  """
296
  # check that at most one is defined
297
  assert [output_file, output_fd].count(None) >= 1
298

  
299
  # Open /dev/null (read-only, only for stdin)
300
  devnull_fd = os.open(os.devnull, os.O_RDONLY)
301

  
302
  if output_fd is not None:
303
    pass
304
  elif output_file is not None:
305
    # Open output file
306
    try:
307
      output_fd = os.open(output_file,
308
                          os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
309
    except EnvironmentError, err:
310
      raise Exception("Opening output file failed: %s" % err)
311
  else:
312
    output_fd = os.open(os.devnull, os.O_WRONLY)
313

  
314
  # Redirect standard I/O
315
  os.dup2(devnull_fd, 0)
316
  os.dup2(output_fd, 1)
317
  os.dup2(output_fd, 2)
318

  
319

  
320
def StartDaemon(cmd, env=None, cwd="/", output=None, output_fd=None,
321
                pidfile=None):
322
  """Start a daemon process after forking twice.
323

  
324
  @type cmd: string or list
325
  @param cmd: Command to run
326
  @type env: dict
327
  @param env: Additional environment variables
328
  @type cwd: string
329
  @param cwd: Working directory for the program
330
  @type output: string
331
  @param output: Path to file in which to save the output
332
  @type output_fd: int
333
  @param output_fd: File descriptor for output
334
  @type pidfile: string
335
  @param pidfile: Process ID file
336
  @rtype: int
337
  @return: Daemon process ID
338
  @raise errors.ProgrammerError: if we call this when forks are disabled
339

  
340
  """
341
  if no_fork:
342
    raise errors.ProgrammerError("utils.StartDaemon() called with fork()"
343
                                 " disabled")
344

  
345
  if output and not (bool(output) ^ (output_fd is not None)):
346
    raise errors.ProgrammerError("Only one of 'output' and 'output_fd' can be"
347
                                 " specified")
348

  
349
  if isinstance(cmd, basestring):
350
    cmd = ["/bin/sh", "-c", cmd]
351

  
352
  strcmd = ShellQuoteArgs(cmd)
353

  
354
  if output:
355
    logging.debug("StartDaemon %s, output file '%s'", strcmd, output)
356
  else:
357
    logging.debug("StartDaemon %s", strcmd)
358

  
359
  cmd_env = _BuildCmdEnvironment(env, False)
360

  
361
  # Create pipe for sending PID back
362
  (pidpipe_read, pidpipe_write) = os.pipe()
363
  try:
364
    try:
365
      # Create pipe for sending error messages
366
      (errpipe_read, errpipe_write) = os.pipe()
367
      try:
368
        try:
369
          # First fork
370
          pid = os.fork()
371
          if pid == 0:
372
            try:
373
              # Child process, won't return
374
              _StartDaemonChild(errpipe_read, errpipe_write,
375
                                pidpipe_read, pidpipe_write,
376
                                cmd, cmd_env, cwd,
377
                                output, output_fd, pidfile)
378
            finally:
379
              # Well, maybe child process failed
380
              os._exit(1) # pylint: disable-msg=W0212
381
        finally:
382
          _CloseFDNoErr(errpipe_write)
383

  
384
        # Wait for daemon to be started (or an error message to
385
        # arrive) and read up to 100 KB as an error message
386
        errormsg = RetryOnSignal(os.read, errpipe_read, 100 * 1024)
387
      finally:
388
        _CloseFDNoErr(errpipe_read)
389
    finally:
390
      _CloseFDNoErr(pidpipe_write)
391

  
392
    # Read up to 128 bytes for PID
393
    pidtext = RetryOnSignal(os.read, pidpipe_read, 128)
394
  finally:
395
    _CloseFDNoErr(pidpipe_read)
396

  
397
  # Try to avoid zombies by waiting for child process
398
  try:
399
    os.waitpid(pid, 0)
400
  except OSError:
401
    pass
402

  
403
  if errormsg:
404
    raise errors.OpExecError("Error when starting daemon process: %r" %
405
                             errormsg)
406

  
407
  try:
408
    return int(pidtext)
409
  except (ValueError, TypeError), err:
410
    raise errors.OpExecError("Error while trying to parse PID %r: %s" %
411
                             (pidtext, err))
412

  
413

  
414
def _StartDaemonChild(errpipe_read, errpipe_write,
415
                      pidpipe_read, pidpipe_write,
416
                      args, env, cwd,
417
                      output, fd_output, pidfile):
418
  """Child process for starting daemon.
419

  
420
  """
421
  try:
422
    # Close parent's side
423
    _CloseFDNoErr(errpipe_read)
424
    _CloseFDNoErr(pidpipe_read)
425

  
426
    # First child process
427
    SetupDaemonEnv()
428

  
429
    # And fork for the second time
430
    pid = os.fork()
431
    if pid != 0:
432
      # Exit first child process
433
      os._exit(0) # pylint: disable-msg=W0212
434

  
435
    # Make sure pipe is closed on execv* (and thereby notifies
436
    # original process)
437
    SetCloseOnExecFlag(errpipe_write, True)
438

  
439
    # List of file descriptors to be left open
440
    noclose_fds = [errpipe_write]
441

  
442
    # Open PID file
443
    if pidfile:
444
      fd_pidfile = WritePidFile(pidfile)
445

  
446
      # Keeping the file open to hold the lock
447
      noclose_fds.append(fd_pidfile)
448

  
449
      SetCloseOnExecFlag(fd_pidfile, False)
450
    else:
451
      fd_pidfile = None
452

  
453
    SetupDaemonFDs(output, fd_output)
454

  
455
    # Send daemon PID to parent
456
    RetryOnSignal(os.write, pidpipe_write, str(os.getpid()))
457

  
458
    # Close all file descriptors except stdio and error message pipe
459
    CloseFDs(noclose_fds=noclose_fds)
460

  
461
    # Change working directory
462
    os.chdir(cwd)
463

  
464
    if env is None:
465
      os.execvp(args[0], args)
466
    else:
467
      os.execvpe(args[0], args, env)
468
  except: # pylint: disable-msg=W0702
469
    try:
470
      # Report errors to original process
471
      WriteErrorToFD(errpipe_write, str(sys.exc_info()[1]))
472
    except: # pylint: disable-msg=W0702
473
      # Ignore errors in error handling
474
      pass
475

  
476
  os._exit(1) # pylint: disable-msg=W0212
477

  
478

  
479
def WriteErrorToFD(fd, err):
480
  """Possibly write an error message to a fd.
481

  
482
  @type fd: None or int (file descriptor)
483
  @param fd: if not None, the error will be written to this fd
484
  @param err: string, the error message
485

  
486
  """
487
  if fd is None:
488
    return
489

  
490
  if not err:
491
    err = "<unknown error>"
492

  
493
  RetryOnSignal(os.write, fd, err)
494

  
495

  
496
def _CheckIfAlive(child):
497
  """Raises L{RetryAgain} if child is still alive.
498

  
499
  @raises RetryAgain: If child is still alive
500

  
501
  """
502
  if child.poll() is None:
503
    raise RetryAgain()
504

  
505

  
506
def _WaitForProcess(child, timeout):
507
  """Waits for the child to terminate or until we reach timeout.
508

  
509
  """
510
  try:
511
    Retry(_CheckIfAlive, (1.0, 1.2, 5.0), max(0, timeout), args=[child])
512
  except RetryTimeout:
513
    pass
514

  
515

  
516
def _RunCmdPipe(cmd, env, via_shell, cwd, interactive, timeout,
517
                _linger_timeout=constants.CHILD_LINGER_TIMEOUT):
518
  """Run a command and return its output.
519

  
520
  @type  cmd: string or list
521
  @param cmd: Command to run
522
  @type env: dict
523
  @param env: The environment to use
524
  @type via_shell: bool
525
  @param via_shell: if we should run via the shell
526
  @type cwd: string
527
  @param cwd: the working directory for the program
528
  @type interactive: boolean
529
  @param interactive: Run command interactive (without piping)
530
  @type timeout: int
531
  @param timeout: Timeout after the programm gets terminated
532
  @rtype: tuple
533
  @return: (out, err, status)
534

  
535
  """
536
  poller = select.poll()
537

  
538
  stderr = subprocess.PIPE
539
  stdout = subprocess.PIPE
540
  stdin = subprocess.PIPE
541

  
542
  if interactive:
543
    stderr = stdout = stdin = None
544

  
545
  child = subprocess.Popen(cmd, shell=via_shell,
546
                           stderr=stderr,
547
                           stdout=stdout,
548
                           stdin=stdin,
549
                           close_fds=True, env=env,
550
                           cwd=cwd)
551

  
552
  out = StringIO()
553
  err = StringIO()
554

  
555
  linger_timeout = None
556

  
557
  if timeout is None:
558
    poll_timeout = None
559
  else:
560
    poll_timeout = RunningTimeout(timeout, True).Remaining
561

  
562
  msg_timeout = ("Command %s (%d) run into execution timeout, terminating" %
563
                 (cmd, child.pid))
564
  msg_linger = ("Command %s (%d) run into linger timeout, killing" %
565
                (cmd, child.pid))
566

  
567
  timeout_action = _TIMEOUT_NONE
568

  
569
  if not interactive:
570
    child.stdin.close()
571
    poller.register(child.stdout, select.POLLIN)
572
    poller.register(child.stderr, select.POLLIN)
573
    fdmap = {
574
      child.stdout.fileno(): (out, child.stdout),
575
      child.stderr.fileno(): (err, child.stderr),
576
      }
577
    for fd in fdmap:
578
      SetNonblockFlag(fd, True)
579

  
580
    while fdmap:
581
      if poll_timeout:
582
        pt = poll_timeout() * 1000
583
        if pt < 0:
584
          if linger_timeout is None:
585
            logging.warning(msg_timeout)
586
            if child.poll() is None:
587
              timeout_action = _TIMEOUT_TERM
588
              IgnoreProcessNotFound(os.kill, child.pid, signal.SIGTERM)
589
            linger_timeout = RunningTimeout(_linger_timeout, True).Remaining
590
          pt = linger_timeout() * 1000
591
          if pt < 0:
592
            break
593
      else:
594
        pt = None
595

  
596
      pollresult = RetryOnSignal(poller.poll, pt)
597

  
598
      for fd, event in pollresult:
599
        if event & select.POLLIN or event & select.POLLPRI:
600
          data = fdmap[fd][1].read()
601
          # no data from read signifies EOF (the same as POLLHUP)
602
          if not data:
603
            poller.unregister(fd)
604
            del fdmap[fd]
605
            continue
606
          fdmap[fd][0].write(data)
607
        if (event & select.POLLNVAL or event & select.POLLHUP or
608
            event & select.POLLERR):
609
          poller.unregister(fd)
610
          del fdmap[fd]
611

  
612
  if timeout is not None:
613
    assert callable(poll_timeout)
614

  
615
    # We have no I/O left but it might still run
616
    if child.poll() is None:
617
      _WaitForProcess(child, poll_timeout())
618

  
619
    # Terminate if still alive after timeout
620
    if child.poll() is None:
621
      if linger_timeout is None:
622
        logging.warning(msg_timeout)
623
        timeout_action = _TIMEOUT_TERM
624
        IgnoreProcessNotFound(os.kill, child.pid, signal.SIGTERM)
625
        lt = _linger_timeout
626
      else:
627
        lt = linger_timeout()
628
      _WaitForProcess(child, lt)
629

  
630
    # Okay, still alive after timeout and linger timeout? Kill it!
631
    if child.poll() is None:
632
      timeout_action = _TIMEOUT_KILL
633
      logging.warning(msg_linger)
634
      IgnoreProcessNotFound(os.kill, child.pid, signal.SIGKILL)
635

  
636
  out = out.getvalue()
637
  err = err.getvalue()
638

  
639
  status = child.wait()
640
  return out, err, status, timeout_action
641

  
642

  
643
def _RunCmdFile(cmd, env, via_shell, output, cwd):
644
  """Run a command and save its output to a file.
645

  
646
  @type  cmd: string or list
647
  @param cmd: Command to run
648
  @type env: dict
649
  @param env: The environment to use
650
  @type via_shell: bool
651
  @param via_shell: if we should run via the shell
652
  @type output: str
653
  @param output: the filename in which to save the output
654
  @type cwd: string
655
  @param cwd: the working directory for the program
656
  @rtype: int
657
  @return: the exit status
658

  
659
  """
660
  fh = open(output, "a")
661
  try:
662
    child = subprocess.Popen(cmd, shell=via_shell,
663
                             stderr=subprocess.STDOUT,
664
                             stdout=fh,
665
                             stdin=subprocess.PIPE,
666
                             close_fds=True, env=env,
667
                             cwd=cwd)
668

  
669
    child.stdin.close()
670
    status = child.wait()
671
  finally:
672
    fh.close()
673
  return status
674

  
675

  
676
def SetCloseOnExecFlag(fd, enable):
677
  """Sets or unsets the close-on-exec flag on a file descriptor.
678

  
679
  @type fd: int
680
  @param fd: File descriptor
681
  @type enable: bool
682
  @param enable: Whether to set or unset it.
683

  
684
  """
685
  flags = fcntl.fcntl(fd, fcntl.F_GETFD)
686

  
687
  if enable:
688
    flags |= fcntl.FD_CLOEXEC
689
  else:
690
    flags &= ~fcntl.FD_CLOEXEC
691

  
692
  fcntl.fcntl(fd, fcntl.F_SETFD, flags)
693

  
694

  
695
def SetNonblockFlag(fd, enable):
696
  """Sets or unsets the O_NONBLOCK flag on on a file descriptor.
697

  
698
  @type fd: int
699
  @param fd: File descriptor
700
  @type enable: bool
701
  @param enable: Whether to set or unset it
702

  
703
  """
704
  flags = fcntl.fcntl(fd, fcntl.F_GETFL)
705

  
706
  if enable:
707
    flags |= os.O_NONBLOCK
708
  else:
709
    flags &= ~os.O_NONBLOCK
710

  
711
  fcntl.fcntl(fd, fcntl.F_SETFL, flags)
712

  
713

  
714
def RetryOnSignal(fn, *args, **kwargs):
715
  """Calls a function again if it failed due to EINTR.
716

  
717
  """
718
  while True:
719
    try:
720
      return fn(*args, **kwargs)
721
    except EnvironmentError, err:
722
      if err.errno != errno.EINTR:
723
        raise
724
    except (socket.error, select.error), err:
725
      # In python 2.6 and above select.error is an IOError, so it's handled
726
      # above, in 2.5 and below it's not, and it's handled here.
727
      if not (err.args and err.args[0] == errno.EINTR):
728
        raise
729

  
730

  
731
def RunParts(dir_name, env=None, reset_env=False):
732
  """Run Scripts or programs in a directory
733

  
734
  @type dir_name: string
735
  @param dir_name: absolute path to a directory
736
  @type env: dict
737
  @param env: The environment to use
738
  @type reset_env: boolean
739
  @param reset_env: whether to reset or keep the default os environment
740
  @rtype: list of tuples
741
  @return: list of (name, (one of RUNDIR_STATUS), RunResult)
742

  
743
  """
744
  rr = []
745

  
746
  try:
747
    dir_contents = ListVisibleFiles(dir_name)
748
  except OSError, err:
749
    logging.warning("RunParts: skipping %s (cannot list: %s)", dir_name, err)
750
    return rr
751

  
752
  for relname in sorted(dir_contents):
753
    fname = PathJoin(dir_name, relname)
754
    if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
755
            constants.EXT_PLUGIN_MASK.match(relname) is not None):
756
      rr.append((relname, constants.RUNPARTS_SKIP, None))
757
    else:
758
      try:
759
        result = RunCmd([fname], env=env, reset_env=reset_env)
760
      except Exception, err: # pylint: disable-msg=W0703
761
        rr.append((relname, constants.RUNPARTS_ERR, str(err)))
762
      else:
763
        rr.append((relname, constants.RUNPARTS_RUN, result))
764

  
765
  return rr
766

  
767

  
768
def RemoveFile(filename):
769
  """Remove a file ignoring some errors.
770

  
771
  Remove a file, ignoring non-existing ones or directories. Other
772
  errors are passed.
773

  
774
  @type filename: str
775
  @param filename: the file to be removed
776

  
777
  """
778
  try:
779
    os.unlink(filename)
780
  except OSError, err:
781
    if err.errno not in (errno.ENOENT, errno.EISDIR):
782
      raise
783

  
784

  
785
def RemoveDir(dirname):
786
  """Remove an empty directory.
787

  
788
  Remove a directory, ignoring non-existing ones.
789
  Other errors are passed. This includes the case,
790
  where the directory is not empty, so it can't be removed.
791

  
792
  @type dirname: str
793
  @param dirname: the empty directory to be removed
794

  
795
  """
796
  try:
797
    os.rmdir(dirname)
798
  except OSError, err:
799
    if err.errno != errno.ENOENT:
800
      raise
801

  
802

  
803
def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
804
  """Renames a file.
805

  
806
  @type old: string
807
  @param old: Original path
808
  @type new: string
809
  @param new: New path
810
  @type mkdir: bool
811
  @param mkdir: Whether to create target directory if it doesn't exist
812
  @type mkdir_mode: int
813
  @param mkdir_mode: Mode for newly created directories
814

  
815
  """
816
  try:
817
    return os.rename(old, new)
818
  except OSError, err:
819
    # In at least one use case of this function, the job queue, directory
820
    # creation is very rare. Checking for the directory before renaming is not
821
    # as efficient.
822
    if mkdir and err.errno == errno.ENOENT:
823
      # Create directory and try again
824
      Makedirs(os.path.dirname(new), mode=mkdir_mode)
825

  
826
      return os.rename(old, new)
827

  
828
    raise
829

  
830

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

  
834
  This is a wrapper around C{os.makedirs} adding error handling not implemented
835
  before Python 2.5.
836

  
837
  """
838
  try:
839
    os.makedirs(path, mode)
840
  except OSError, err:
841
    # Ignore EEXIST. This is only handled in os.makedirs as included in
842
    # Python 2.5 and above.
843
    if err.errno != errno.EEXIST or not os.path.exists(path):
844
      raise
845

  
846

  
847
def ResetTempfileModule():
848
  """Resets the random name generator of the tempfile module.
849

  
850
  This function should be called after C{os.fork} in the child process to
851
  ensure it creates a newly seeded random generator. Otherwise it would
852
  generate the same random parts as the parent process. If several processes
853
  race for the creation of a temporary file, this could lead to one not getting
854
  a temporary name.
855

  
856
  """
857
  # pylint: disable-msg=W0212
858
  if hasattr(tempfile, "_once_lock") and hasattr(tempfile, "_name_sequence"):
859
    tempfile._once_lock.acquire()
860
    try:
861
      # Reset random name generator
862
      tempfile._name_sequence = None
863
    finally:
864
      tempfile._once_lock.release()
865
  else:
866
    logging.critical("The tempfile module misses at least one of the"
867
                     " '_once_lock' and '_name_sequence' attributes")
868

  
869

  
870
def _FingerprintFile(filename):
871
  """Compute the fingerprint of a file.
872

  
873
  If the file does not exist, a None will be returned
874
  instead.
875

  
876
  @type filename: str
877
  @param filename: the filename to checksum
878
  @rtype: str
879
  @return: the hex digest of the sha checksum of the contents
880
      of the file
881

  
882
  """
883
  if not (os.path.exists(filename) and os.path.isfile(filename)):
884
    return None
885

  
886
  f = open(filename)
887

  
888
  fp = compat.sha1_hash()
889
  while True:
890
    data = f.read(4096)
891
    if not data:
892
      break
893

  
894
    fp.update(data)
895

  
896
  return fp.hexdigest()
897

  
898

  
899
def FingerprintFiles(files):
900
  """Compute fingerprints for a list of files.
901

  
902
  @type files: list
903
  @param files: the list of filename to fingerprint
904
  @rtype: dict
905
  @return: a dictionary filename: fingerprint, holding only
906
      existing files
907

  
908
  """
909
  ret = {}
910

  
911
  for filename in files:
912
    cksum = _FingerprintFile(filename)
913
    if cksum:
914
      ret[filename] = cksum
915

  
916
  return ret
917

  
918

  
919
def ForceDictType(target, key_types, allowed_values=None):
920
  """Force the values of a dict to have certain types.
921

  
922
  @type target: dict
923
  @param target: the dict to update
924
  @type key_types: dict
925
  @param key_types: dict mapping target dict keys to types
926
                    in constants.ENFORCEABLE_TYPES
927
  @type allowed_values: list
928
  @keyword allowed_values: list of specially allowed values
929

  
930
  """
931
  if allowed_values is None:
932
    allowed_values = []
933

  
934
  if not isinstance(target, dict):
935
    msg = "Expected dictionary, got '%s'" % target
936
    raise errors.TypeEnforcementError(msg)
937

  
938
  for key in target:
939
    if key not in key_types:
940
      msg = "Unknown key '%s'" % key
941
      raise errors.TypeEnforcementError(msg)
942

  
943
    if target[key] in allowed_values:
944
      continue
945

  
946
    ktype = key_types[key]
947
    if ktype not in constants.ENFORCEABLE_TYPES:
948
      msg = "'%s' has non-enforceable type %s" % (key, ktype)
949
      raise errors.ProgrammerError(msg)
950

  
951
    if ktype in (constants.VTYPE_STRING, constants.VTYPE_MAYBE_STRING):
952
      if target[key] is None and ktype == constants.VTYPE_MAYBE_STRING:
953
        pass
954
      elif not isinstance(target[key], basestring):
955
        if isinstance(target[key], bool) and not target[key]:
956
          target[key] = ''
957
        else:
958
          msg = "'%s' (value %s) is not a valid string" % (key, target[key])
959
          raise errors.TypeEnforcementError(msg)
960
    elif ktype == constants.VTYPE_BOOL:
961
      if isinstance(target[key], basestring) and target[key]:
962
        if target[key].lower() == constants.VALUE_FALSE:
963
          target[key] = False
964
        elif target[key].lower() == constants.VALUE_TRUE:
965
          target[key] = True
966
        else:
967
          msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
968
          raise errors.TypeEnforcementError(msg)
969
      elif target[key]:
970
        target[key] = True
971
      else:
972
        target[key] = False
973
    elif ktype == constants.VTYPE_SIZE:
974
      try:
975
        target[key] = ParseUnit(target[key])
976
      except errors.UnitParseError, err:
977
        msg = "'%s' (value %s) is not a valid size. error: %s" % \
978
              (key, target[key], err)
979
        raise errors.TypeEnforcementError(msg)
980
    elif ktype == constants.VTYPE_INT:
981
      try:
982
        target[key] = int(target[key])
983
      except (ValueError, TypeError):
984
        msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
985
        raise errors.TypeEnforcementError(msg)
986

  
987

  
988
def _GetProcStatusPath(pid):
989
  """Returns the path for a PID's proc status file.
990

  
991
  @type pid: int
992
  @param pid: Process ID
993
  @rtype: string
994

  
995
  """
996
  return "/proc/%d/status" % pid
997

  
998

  
999
def IsProcessAlive(pid):
1000
  """Check if a given pid exists on the system.
1001

  
1002
  @note: zombie status is not handled, so zombie processes
1003
      will be returned as alive
1004
  @type pid: int
1005
  @param pid: the process ID to check
1006
  @rtype: boolean
1007
  @return: True if the process exists
1008

  
1009
  """
1010
  def _TryStat(name):
1011
    try:
1012
      os.stat(name)
1013
      return True
1014
    except EnvironmentError, err:
1015
      if err.errno in (errno.ENOENT, errno.ENOTDIR):
1016
        return False
1017
      elif err.errno == errno.EINVAL:
1018
        raise RetryAgain(err)
1019
      raise
1020

  
1021
  assert isinstance(pid, int), "pid must be an integer"
1022
  if pid <= 0:
1023
    return False
1024

  
1025
  # /proc in a multiprocessor environment can have strange behaviors.
1026
  # Retry the os.stat a few times until we get a good result.
1027
  try:
1028
    return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5,
1029
                 args=[_GetProcStatusPath(pid)])
1030
  except RetryTimeout, err:
1031
    err.RaiseInner()
1032

  
1033

  
1034
def _ParseSigsetT(sigset):
1035
  """Parse a rendered sigset_t value.
1036

  
1037
  This is the opposite of the Linux kernel's fs/proc/array.c:render_sigset_t
1038
  function.
1039

  
1040
  @type sigset: string
1041
  @param sigset: Rendered signal set from /proc/$pid/status
1042
  @rtype: set
1043
  @return: Set of all enabled signal numbers
1044

  
1045
  """
1046
  result = set()
1047

  
1048
  signum = 0
1049
  for ch in reversed(sigset):
1050
    chv = int(ch, 16)
1051

  
1052
    # The following could be done in a loop, but it's easier to read and
1053
    # understand in the unrolled form
1054
    if chv & 1:
1055
      result.add(signum + 1)
1056
    if chv & 2:
1057
      result.add(signum + 2)
1058
    if chv & 4:
1059
      result.add(signum + 3)
1060
    if chv & 8:
1061
      result.add(signum + 4)
1062

  
1063
    signum += 4
1064

  
1065
  return result
1066

  
1067

  
1068
def _GetProcStatusField(pstatus, field):
1069
  """Retrieves a field from the contents of a proc status file.
1070

  
1071
  @type pstatus: string
1072
  @param pstatus: Contents of /proc/$pid/status
1073
  @type field: string
1074
  @param field: Name of field whose value should be returned
1075
  @rtype: string
1076

  
1077
  """
1078
  for line in pstatus.splitlines():
1079
    parts = line.split(":", 1)
1080

  
1081
    if len(parts) < 2 or parts[0] != field:
1082
      continue
1083

  
1084
    return parts[1].strip()
1085

  
1086
  return None
1087

  
1088

  
1089
def IsProcessHandlingSignal(pid, signum, status_path=None):
1090
  """Checks whether a process is handling a signal.
1091

  
1092
  @type pid: int
1093
  @param pid: Process ID
1094
  @type signum: int
1095
  @param signum: Signal number
1096
  @rtype: bool
1097

  
1098
  """
1099
  if status_path is None:
1100
    status_path = _GetProcStatusPath(pid)
1101

  
1102
  try:
1103
    proc_status = ReadFile(status_path)
1104
  except EnvironmentError, err:
1105
    # In at least one case, reading /proc/$pid/status failed with ESRCH.
1106
    if err.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL, errno.ESRCH):
1107
      return False
1108
    raise
1109

  
1110
  sigcgt = _GetProcStatusField(proc_status, "SigCgt")
1111
  if sigcgt is None:
1112
    raise RuntimeError("%s is missing 'SigCgt' field" % status_path)
1113

  
1114
  # Now check whether signal is handled
1115
  return signum in _ParseSigsetT(sigcgt)
1116

  
1117

  
1118
def ReadPidFile(pidfile):
1119
  """Read a pid from a file.
1120

  
1121
  @type  pidfile: string
1122
  @param pidfile: path to the file containing the pid
1123
  @rtype: int
1124
  @return: The process id, if the file exists and contains a valid PID,
1125
           otherwise 0
1126

  
1127
  """
1128
  try:
1129
    raw_data = ReadOneLineFile(pidfile)
1130
  except EnvironmentError, err:
1131
    if err.errno != errno.ENOENT:
1132
      logging.exception("Can't read pid file")
1133
    return 0
1134

  
1135
  try:
1136
    pid = int(raw_data)
1137
  except (TypeError, ValueError), err:
1138
    logging.info("Can't parse pid file contents", exc_info=True)
1139
    return 0
1140

  
1141
  return pid
1142

  
1143

  
1144
def ReadLockedPidFile(path):
1145
  """Reads a locked PID file.
1146

  
1147
  This can be used together with L{StartDaemon}.
1148

  
1149
  @type path: string
1150
  @param path: Path to PID file
1151
  @return: PID as integer or, if file was unlocked or couldn't be opened, None
1152

  
1153
  """
1154
  try:
1155
    fd = os.open(path, os.O_RDONLY)
1156
  except EnvironmentError, err:
1157
    if err.errno == errno.ENOENT:
1158
      # PID file doesn't exist
1159
      return None
1160
    raise
1161

  
1162
  try:
1163
    try:
1164
      # Try to acquire lock
1165
      LockFile(fd)
1166
    except errors.LockError:
1167
      # Couldn't lock, daemon is running
1168
      return int(os.read(fd, 100))
1169
  finally:
1170
    os.close(fd)
1171

  
1172
  return None
1173

  
1174

  
1175
def MatchNameComponent(key, name_list, case_sensitive=True):
1176
  """Try to match a name against a list.
1177

  
1178
  This function will try to match a name like test1 against a list
1179
  like C{['test1.example.com', 'test2.example.com', ...]}. Against
1180
  this list, I{'test1'} as well as I{'test1.example'} will match, but
1181
  not I{'test1.ex'}. A multiple match will be considered as no match
1182
  at all (e.g. I{'test1'} against C{['test1.example.com',
1183
  'test1.example.org']}), except when the key fully matches an entry
1184
  (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
1185

  
1186
  @type key: str
1187
  @param key: the name to be searched
1188
  @type name_list: list
1189
  @param name_list: the list of strings against which to search the key
1190
  @type case_sensitive: boolean
1191
  @param case_sensitive: whether to provide a case-sensitive match
1192

  
1193
  @rtype: None or str
1194
  @return: None if there is no match I{or} if there are multiple matches,
1195
      otherwise the element from the list which matches
1196

  
1197
  """
1198
  if key in name_list:
1199
    return key
1200

  
1201
  re_flags = 0
1202
  if not case_sensitive:
1203
    re_flags |= re.IGNORECASE
1204
    key = key.upper()
1205
  mo = re.compile("^%s(\..*)?$" % re.escape(key), re_flags)
1206
  names_filtered = []
1207
  string_matches = []
1208
  for name in name_list:
1209
    if mo.match(name) is not None:
1210
      names_filtered.append(name)
1211
      if not case_sensitive and key == name.upper():
1212
        string_matches.append(name)
1213

  
1214
  if len(string_matches) == 1:
1215
    return string_matches[0]
1216
  if len(names_filtered) == 1:
1217
    return names_filtered[0]
1218
  return None
1219

  
1220

  
1221
def ValidateServiceName(name):
1222
  """Validate the given service name.
1223

  
1224
  @type name: number or string
1225
  @param name: Service name or port specification
1226

  
1227
  """
1228
  try:
1229
    numport = int(name)
1230
  except (ValueError, TypeError):
1231
    # Non-numeric service name
1232
    valid = _VALID_SERVICE_NAME_RE.match(name)
1233
  else:
1234
    # Numeric port (protocols other than TCP or UDP might need adjustments
1235
    # here)
1236
    valid = (numport >= 0 and numport < (1 << 16))
1237

  
1238
  if not valid:
1239
    raise errors.OpPrereqError("Invalid service name '%s'" % name,
1240
                               errors.ECODE_INVAL)
1241

  
1242
  return name
1243

  
1244

  
1245
def ListVolumeGroups():
1246
  """List volume groups and their size
1247

  
1248
  @rtype: dict
1249
  @return:
1250
       Dictionary with keys volume name and values
1251
       the size of the volume
1252

  
1253
  """
1254
  command = "vgs --noheadings --units m --nosuffix -o name,size"
1255
  result = RunCmd(command)
1256
  retval = {}
1257
  if result.failed:
1258
    return retval
1259

  
1260
  for line in result.stdout.splitlines():
1261
    try:
1262
      name, size = line.split()
1263
      size = int(float(size))
1264
    except (IndexError, ValueError), err:
1265
      logging.error("Invalid output from vgs (%s): %s", err, line)
1266
      continue
1267

  
1268
    retval[name] = size
1269

  
1270
  return retval
1271

  
1272

  
1273
def BridgeExists(bridge):
1274
  """Check whether the given bridge exists in the system
1275

  
1276
  @type bridge: str
1277
  @param bridge: the bridge name to check
1278
  @rtype: boolean
1279
  @return: True if it does
1280

  
1281
  """
1282
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
1283

  
1284

  
1285
def _NiceSortTryInt(val):
1286
  """Attempts to convert a string to an integer.
1287

  
1288
  """
1289
  if val and _SORTER_DIGIT.match(val):
1290
    return int(val)
1291
  else:
1292
    return val
1293

  
1294

  
1295
def _NiceSortKey(value):
1296
  """Extract key for sorting.
1297

  
1298
  """
1299
  return [_NiceSortTryInt(grp)
1300
          for grp in _SORTER_RE.match(value).groups()]
1301

  
1302

  
1303
def NiceSort(values, key=None):
1304
  """Sort a list of strings based on digit and non-digit groupings.
1305

  
1306
  Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
1307
  will sort the list in the logical order C{['a1', 'a2', 'a10',
1308
  'a11']}.
1309

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

  
1314
  @type values: list
1315
  @param values: the names to be sorted
1316
  @type key: callable or None
1317
  @param key: function of one argument to extract a comparison key from each
1318
    list element, must return string
1319
  @rtype: list
1320
  @return: a copy of the name list sorted with our algorithm
1321

  
1322
  """
1323
  if key is None:
1324
    keyfunc = _NiceSortKey
1325
  else:
1326
    keyfunc = lambda value: _NiceSortKey(key(value))
1327

  
1328
  return sorted(values, key=keyfunc)
1329

  
1330

  
1331
def TryConvert(fn, val):
1332
  """Try to convert a value ignoring errors.
1333

  
1334
  This function tries to apply function I{fn} to I{val}. If no
1335
  C{ValueError} or C{TypeError} exceptions are raised, it will return
1336
  the result, else it will return the original value. Any other
1337
  exceptions are propagated to the caller.
1338

  
1339
  @type fn: callable
1340
  @param fn: function to apply to the value
1341
  @param val: the value to be converted
1342
  @return: The converted value if the conversion was successful,
1343
      otherwise the original value.
1344

  
1345
  """
1346
  try:
1347
    nv = fn(val)
1348
  except (ValueError, TypeError):
1349
    nv = val
1350
  return nv
1351

  
1352

  
1353
def IsValidShellParam(word):
1354
  """Verifies is the given word is safe from the shell's p.o.v.
1355

  
1356
  This means that we can pass this to a command via the shell and be
1357
  sure that it doesn't alter the command line and is passed as such to
1358
  the actual command.
1359

  
1360
  Note that we are overly restrictive here, in order to be on the safe
1361
  side.
1362

  
1363
  @type word: str
1364
  @param word: the word to check
1365
  @rtype: boolean
1366
  @return: True if the word is 'safe'
1367

  
1368
  """
1369
  return bool(_SHELLPARAM_REGEX.match(word))
1370

  
1371

  
1372
def BuildShellCmd(template, *args):
1373
  """Build a safe shell command line from the given arguments.
1374

  
1375
  This function will check all arguments in the args list so that they
1376
  are valid shell parameters (i.e. they don't contain shell
1377
  metacharacters). If everything is ok, it will return the result of
1378
  template % args.
1379

  
1380
  @type template: str
1381
  @param template: the string holding the template for the
1382
      string formatting
1383
  @rtype: str
1384
  @return: the expanded command line
1385

  
1386
  """
1387
  for word in args:
1388
    if not IsValidShellParam(word):
1389
      raise errors.ProgrammerError("Shell argument '%s' contains"
1390
                                   " invalid characters" % word)
1391
  return template % args
1392

  
1393

  
1394
def FormatUnit(value, units):
1395
  """Formats an incoming number of MiB with the appropriate unit.
1396

  
1397
  @type value: int
1398
  @param value: integer representing the value in MiB (1048576)
1399
  @type units: char
1400
  @param units: the type of formatting we should do:
1401
      - 'h' for automatic scaling
1402
      - 'm' for MiBs
1403
      - 'g' for GiBs
1404
      - 't' for TiBs
1405
  @rtype: str
1406
  @return: the formatted value (with suffix)
1407

  
1408
  """
1409
  if units not in ('m', 'g', 't', 'h'):
1410
    raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
1411

  
1412
  suffix = ''
1413

  
1414
  if units == 'm' or (units == 'h' and value < 1024):
1415
    if units == 'h':
1416
      suffix = 'M'
1417
    return "%d%s" % (round(value, 0), suffix)
1418

  
1419
  elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
1420
    if units == 'h':
1421
      suffix = 'G'
1422
    return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
1423

  
1424
  else:
1425
    if units == 'h':
1426
      suffix = 'T'
1427
    return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
1428

  
1429

  
1430
def ParseUnit(input_string):
1431
  """Tries to extract number and scale from the given string.
1432

  
1433
  Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
1434
  [UNIT]}. If no unit is specified, it defaults to MiB. Return value
1435
  is always an int in MiB.
1436

  
1437
  """
1438
  m = _PARSEUNIT_REGEX.match(str(input_string))
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff