Revision 1a2eb2dc lib/backend.py

b/lib/backend.py
87 87
_MASTER_START = "start"
88 88
_MASTER_STOP = "stop"
89 89

  
90
#: Maximum file permissions for remote command directory and executables
91
_RCMD_MAX_MODE = (stat.S_IRWXU |
92
                  stat.S_IRGRP | stat.S_IXGRP |
93
                  stat.S_IROTH | stat.S_IXOTH)
94

  
95
#: Delay before returning an error for remote commands
96
_RCMD_INVALID_DELAY = 10
97

  
98
#: How long to wait to acquire lock for remote commands (shorter than
99
#: L{_RCMD_INVALID_DELAY}) to reduce blockage of noded forks when many
100
#: command requests arrive
101
_RCMD_LOCK_TIMEOUT = _RCMD_INVALID_DELAY * 0.8
102

  
90 103

  
91 104
class RPCFail(Exception):
92 105
  """Class denoting RPC failure.
......
3567 3580
  hyper.PowercycleNode()
3568 3581

  
3569 3582

  
3583
def _VerifyRemoteCommandName(cmd):
3584
  """Verifies a remote command name.
3585

  
3586
  @type cmd: string
3587
  @param cmd: Command name
3588
  @rtype: tuple; (boolean, string or None)
3589
  @return: The tuple's first element is the status; if C{False}, the second
3590
    element is an error message string, otherwise it's C{None}
3591

  
3592
  """
3593
  if not cmd.strip():
3594
    return (False, "Missing command name")
3595

  
3596
  if os.path.basename(cmd) != cmd:
3597
    return (False, "Invalid command name")
3598

  
3599
  if not constants.EXT_PLUGIN_MASK.match(cmd):
3600
    return (False, "Command name contains forbidden characters")
3601

  
3602
  return (True, None)
3603

  
3604

  
3605
def _CommonRemoteCommandCheck(path, owner):
3606
  """Common checks for remote command file system directories and files.
3607

  
3608
  @type path: string
3609
  @param path: Path to check
3610
  @param owner: C{None} or tuple containing UID and GID
3611
  @rtype: tuple; (boolean, string or C{os.stat} result)
3612
  @return: The tuple's first element is the status; if C{False}, the second
3613
    element is an error message string, otherwise it's the result of C{os.stat}
3614

  
3615
  """
3616
  if owner is None:
3617
    # Default to root as owner
3618
    owner = (0, 0)
3619

  
3620
  try:
3621
    st = os.stat(path)
3622
  except EnvironmentError, err:
3623
    return (False, "Can't stat(2) '%s': %s" % (path, err))
3624

  
3625
  if stat.S_IMODE(st.st_mode) & (~_RCMD_MAX_MODE):
3626
    return (False, "Permissions on '%s' are too permissive" % path)
3627

  
3628
  if (st.st_uid, st.st_gid) != owner:
3629
    (owner_uid, owner_gid) = owner
3630
    return (False, "'%s' is not owned by %s:%s" % (path, owner_uid, owner_gid))
3631

  
3632
  return (True, st)
3633

  
3634

  
3635
def _VerifyRemoteCommandDirectory(path, _owner=None):
3636
  """Verifies remote command directory.
3637

  
3638
  @type path: string
3639
  @param path: Path to check
3640
  @rtype: tuple; (boolean, string or None)
3641
  @return: The tuple's first element is the status; if C{False}, the second
3642
    element is an error message string, otherwise it's C{None}
3643

  
3644
  """
3645
  (status, value) = _CommonRemoteCommandCheck(path, _owner)
3646

  
3647
  if not status:
3648
    return (False, value)
3649

  
3650
  if not stat.S_ISDIR(value.st_mode):
3651
    return (False, "Path '%s' is not a directory" % path)
3652

  
3653
  return (True, None)
3654

  
3655

  
3656
def _VerifyRemoteCommand(path, cmd, _owner=None):
3657
  """Verifies a whole remote command and returns its executable filename.
3658

  
3659
  @type path: string
3660
  @param path: Directory containing remote commands
3661
  @type cmd: string
3662
  @param cmd: Command name
3663
  @rtype: tuple; (boolean, string)
3664
  @return: The tuple's first element is the status; if C{False}, the second
3665
    element is an error message string, otherwise the second element is the
3666
    absolute path to the executable
3667

  
3668
  """
3669
  executable = utils.PathJoin(path, cmd)
3670

  
3671
  (status, msg) = _CommonRemoteCommandCheck(executable, _owner)
3672

  
3673
  if not status:
3674
    return (False, msg)
3675

  
3676
  if not utils.IsExecutable(executable):
3677
    return (False, "access(2) thinks '%s' can't be executed" % executable)
3678

  
3679
  return (True, executable)
3680

  
3681

  
3682
def _PrepareRemoteCommand(path, cmd,
3683
                          _verify_dir=_VerifyRemoteCommandDirectory,
3684
                          _verify_name=_VerifyRemoteCommandName,
3685
                          _verify_cmd=_VerifyRemoteCommand):
3686
  """Performs a number of tests on a remote command.
3687

  
3688
  @type path: string
3689
  @param path: Directory containing remote commands
3690
  @type cmd: string
3691
  @param cmd: Command name
3692
  @return: Same as L{_VerifyRemoteCommand}
3693

  
3694
  """
3695
  # Verify the directory first
3696
  (status, msg) = _verify_dir(path)
3697
  if status:
3698
    # Check command if everything was alright
3699
    (status, msg) = _verify_name(cmd)
3700

  
3701
  if not status:
3702
    return (False, msg)
3703

  
3704
  # Check actual executable
3705
  return _verify_cmd(path, cmd)
3706

  
3707

  
3708
def RunRemoteCommand(cmd,
3709
                     _lock_timeout=_RCMD_LOCK_TIMEOUT,
3710
                     _lock_file=pathutils.REMOTE_COMMANDS_LOCK_FILE,
3711
                     _path=pathutils.REMOTE_COMMANDS_DIR,
3712
                     _sleep_fn=time.sleep,
3713
                     _prepare_fn=_PrepareRemoteCommand,
3714
                     _runcmd_fn=utils.RunCmd,
3715
                     _enabled=constants.ENABLE_REMOTE_COMMANDS):
3716
  """Executes a remote command after performing strict tests.
3717

  
3718
  @type cmd: string
3719
  @param cmd: Command name
3720
  @rtype: string
3721
  @return: Command output
3722
  @raise RPCFail: In case of an error
3723

  
3724
  """
3725
  logging.info("Preparing to run remote command '%s'", cmd)
3726

  
3727
  if not _enabled:
3728
    _Fail("Remote commands disabled at configure time")
3729

  
3730
  lock = None
3731
  try:
3732
    cmdresult = None
3733
    try:
3734
      lock = utils.FileLock.Open(_lock_file)
3735
      lock.Exclusive(blocking=True, timeout=_lock_timeout)
3736

  
3737
      (status, value) = _prepare_fn(_path, cmd)
3738

  
3739
      if status:
3740
        cmdresult = _runcmd_fn([value], env={}, reset_env=True,
3741
                               postfork_fn=lambda _: lock.Unlock())
3742
      else:
3743
        logging.error(value)
3744
    except Exception: # pylint: disable=W0703
3745
      # Keep original error in log
3746
      logging.exception("Caught exception")
3747

  
3748
    if cmdresult is None:
3749
      logging.info("Sleeping for %0.1f seconds before returning",
3750
                   _RCMD_INVALID_DELAY)
3751
      _sleep_fn(_RCMD_INVALID_DELAY)
3752

  
3753
      # Do not include original error message in returned error
3754
      _Fail("Executing command '%s' failed" % cmd)
3755
    elif cmdresult.failed or cmdresult.fail_reason:
3756
      _Fail("Remote command '%s' failed: %s; output: %s",
3757
            cmd, cmdresult.fail_reason, cmdresult.output)
3758
    else:
3759
      return cmdresult.output
3760
  finally:
3761
    if lock is not None:
3762
      # Release lock at last
3763
      lock.Close()
3764
      lock = None
3765

  
3766

  
3570 3767
class HooksRunner(object):
3571 3768
  """Hook runner.
3572 3769

  

Also available in: Unified diff