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