satori/satori/bash.py

258 lines
9.9 KiB
Python

# Copyright 2012-2013 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Shell classes for executing commands on a system.
Execute commands over ssh or using the python subprocess module.
"""
import logging
import shlex
from satori.common import popen
from satori import errors
from satori import smb
from satori import ssh
from satori import utils
LOG = logging.getLogger(__name__)
class ShellMixin(object):
"""Handle platform detection and define execute command."""
def execute(self, command, **kwargs):
"""Execute a (shell) command on the target.
:param command: Shell command to be executed
:param with_exit_code: Include the exit_code in the return body.
:param cwd: The child's current directory will be changed
to `cwd` before it is executed. Note that this
directory is not considered when searching the
executable, so you can't specify the program's
path relative to this argument
:returns: a dict with stdin, stdout, and
(optionally), the exit_code of the call
See SSH.remote_execute(), SMB.remote_execute(), and
LocalShell.execute() for client-specific keyword arguments.
"""
pass
@property
def platform_info(self):
"""Provide distro, version, architecture."""
pass
def is_debian(self):
"""Indicate whether the system is Debian based.
Uses the platform_info property.
"""
if not self.platform_info['dist']:
raise errors.UndeterminedPlatform(
'Unable to determine whether the system is Debian based.')
return self.platform_info['dist'].lower() in ['debian', 'ubuntu']
def is_fedora(self):
"""Indicate whether the system in Fedora based.
Uses the platform_info property.
"""
if not self.platform_info['dist']:
raise errors.UndeterminedPlatform(
'Unable to determine whether the system is Fedora based.')
return (self.platform_info['dist'].lower() in
['redhat', 'centos', 'fedora', 'el'])
def is_osx(self):
"""Indicate whether the system is Apple OSX based.
Uses the platform_info property.
"""
if not self.platform_info['dist']:
raise errors.UndeterminedPlatform(
'Unable to determine whether the system is OS X based.')
return (self.platform_info['dist'].lower() in
['darwin', 'macosx'])
def is_windows(self):
"""Indicate whether the system is Windows based.
Uses the platform_info property.
"""
if hasattr(self, '_client'):
if isinstance(self._client, smb.SMBClient):
return True
if not self.platform_info['dist']:
raise errors.UndeterminedPlatform(
'Unable to determine whether the system is Windows based.')
return self.platform_info['dist'].startswith('win')
class LocalShell(ShellMixin):
"""Execute shell commands on local machine."""
def __init__(self, user=None, password=None, interactive=False):
"""An interface for executing shell commands locally.
:param user: The user to execute the command as.
Defaults to the current user.
:param password: The password for `user`
:param interactive: If true, prompt for password if missing.
"""
self.user = user
self.password = password
self.interactive = interactive
# properties
self._platform_info = None
@property
def platform_info(self):
"""Return distro, version, and system architecture."""
if not self._platform_info:
self._platform_info = utils.get_platform_info()
return self._platform_info
def execute(self, command, **kwargs):
"""Execute a command (containing no shell operators) locally.
:param command: Shell command to be executed.
:param with_exit_code: Include the exit_code in the return body.
Default is False.
:param cwd: The child's current directory will be changed
to `cwd` before it is executed. Note that this
directory is not considered when searching the
executable, so you can't specify the program's
path relative to this argument
:returns: A dict with stdin, stdout, and
(optionally) the exit code.
"""
cwd = kwargs.get('cwd')
with_exit_code = kwargs.get('with_exit_code')
spipe = popen.PIPE
cmd = shlex.split(command)
LOG.debug("Executing `%s` on local machine", command)
result = popen.popen(
cmd, stdout=spipe, stderr=spipe, cwd=cwd)
out, err = result.communicate()
resultdict = {
'stdout': out.strip(),
'stderr': err.strip(),
}
if with_exit_code:
resultdict.update({'exit_code': result.returncode})
return resultdict
class RemoteShell(ShellMixin):
"""Execute shell commands on a remote machine over ssh."""
def __init__(self, address, password=None, username=None,
private_key=None, key_filename=None, port=None,
timeout=None, gateway=None, options=None, interactive=False,
protocol='ssh', root_password=None, **kwargs):
"""An interface for executing shell commands on remote machines.
:param str host: The ip address or host name of the server
to connect to
:param str password: A password to use for authentication
or for unlocking a private key
:param username: The username to authenticate as
:param private_key: Private SSH Key string to use
(instead of using a filename)
:param root_password: root user password to be used if username is
not root. This will use username and password
to login and then 'su' to root using
root_password
:param key_filename: a private key filename (path)
:param port: tcp/ip port to use (defaults to 22)
:param float timeout: an optional timeout (in seconds) for the
TCP connection
:param socket gateway: an existing SSH instance to use
for proxying
:param dict options: A dictionary used to set ssh options
(when proxying).
e.g. for `ssh -o StrictHostKeyChecking=no`,
you would provide
(.., options={'StrictHostKeyChecking': 'no'})
Conversion of booleans is also supported,
(.., options={'StrictHostKeyChecking': False})
is equivalent.
:keyword interactive: If true, prompt for password if missing.
"""
if kwargs:
LOG.warning("Satori RemoteClient received unrecognized "
"keyword arguments: %s", kwargs.keys())
if protocol == 'smb':
self._client = smb.connect(address, password=password,
username=username,
port=port, timeout=timeout,
gateway=gateway)
else:
self._client = ssh.connect(address, password=password,
username=username,
private_key=private_key,
key_filename=key_filename,
port=port, timeout=timeout,
gateway=gateway,
options=options,
interactive=interactive,
root_password=root_password)
self.host = self._client.host
self.port = self._client.port
@property
def platform_info(self):
"""Return distro, version, architecture."""
return self._client.platform_info
def __del__(self):
"""Destructor which should close the connection."""
self.close()
def __enter__(self):
"""Context manager establish connection."""
self.connect()
return self
def __exit__(self, *exc_info):
"""Context manager close connection."""
self.close()
def connect(self):
"""Connect to the remote host."""
return self._client.connect()
def test_connection(self):
"""Test the connection to the remote host."""
return self._client.test_connection()
def execute(self, command, **kwargs):
"""Execute given command over ssh."""
return self._client.remote_execute(command, **kwargs)
def close(self):
"""Close the connection to the remote host."""
return self._client.close()