diff --git a/doc/source/dsl/dsl_v2.rst b/doc/source/dsl/dsl_v2.rst index 29ba0fc6..64a5f3ec 100644 --- a/doc/source/dsl/dsl_v2.rst +++ b/doc/source/dsl/dsl_v2.rst @@ -765,7 +765,9 @@ Input parameters: *Required*. - **username** - User name to authenticate on the host. - **password** - User password to to authenticate on the host. *Optional.* -- **private_key** - Private key string which will be used for authentication on remote host. *Optional.* +- **private_key_filename** - Private key file name which will be used for authentication on remote host. +All private keys should be on executor host in **/.ssh/**. +**** should refer to user directory under which service is running. *Optional.* **NOTE**: Authentication using key pairs is supported, key should be on Mistral Executor server machine. diff --git a/functionaltests/post_test_hook.sh b/functionaltests/post_test_hook.sh index c91aa90e..0cb7fc49 100755 --- a/functionaltests/post_test_hook.sh +++ b/functionaltests/post_test_hook.sh @@ -17,6 +17,7 @@ RETVAL=0 +sudo chmod -R a+rw /opt/stack/new/ cd /opt/stack/new/ echo "Repository: $ZUUL_PROJECT" @@ -25,7 +26,7 @@ echo "Repository: $ZUUL_PROJECT" if [[ "$ZUUL_PROJECT" == "openstack/mistral" ]]; then cd mistral/ echo "Run mistral API tests" - sudo bash ./functionaltests/run_tests.sh + ./functionaltests/run_tests.sh RETVAL=$? fi @@ -33,7 +34,7 @@ fi if [[ RETVAL -eq 0 ]]; then cd /opt/stack/new/python-mistralclient/ echo "Run mistralclient tests" - sudo bash ./functionaltests/run_tests.sh + ./functionaltests/run_tests.sh RETVAL=$? fi diff --git a/functionaltests/run_tests.sh b/functionaltests/run_tests.sh index d719cad2..beb6d2fd 100755 --- a/functionaltests/run_tests.sh +++ b/functionaltests/run_tests.sh @@ -32,8 +32,5 @@ MISTRALCLIENT_DIR=/opt/stack/new/python-mistralclient # Define PYTHONPATH export PYTHONPATH=$PYTHONPATH:$TEMPEST_DIR -#installing requirements for tempest -pip install -r $TEMPEST_DIR/requirements.txt - pwd nosetests -sv mistral/tests/functional/ diff --git a/mistral/actions/std_actions.py b/mistral/actions/std_actions.py index 07df918a..55af6666 100644 --- a/mistral/actions/std_actions.py +++ b/mistral/actions/std_actions.py @@ -330,19 +330,20 @@ class SSHAction(base.Action): def _execute_cmd_method(self): return ssh_utils.execute_command - def __init__(self, cmd, host, username, password=None, private_key=None): + def __init__(self, cmd, host, username, + password=None, private_key_filename=None): self.cmd = cmd self.host = host self.username = username self.password = password - self.private_key = private_key + self.private_key_filename = private_key_filename self.params = { 'cmd': self.cmd, 'host': self.host, 'username': self.username, 'password': self.password, - 'private_key': self.private_key + 'private_key_filename': self.private_key_filename } def run(self): @@ -386,14 +387,15 @@ class SSHProxiedAction(SSHAction): def _execute_cmd_method(self): return ssh_utils.execute_command_via_gateway - def __init__(self, cmd, host, username, private_key, gateway_host, - gateway_username=None, password=None, proxy_command=None): + def __init__(self, cmd, host, username, private_key_filename, + gateway_host, gateway_username=None, + password=None, proxy_command=None): super(SSHProxiedAction, self).__init__( cmd, host, username, password, - private_key + private_key_filename ) self.gateway_host = gateway_host diff --git a/mistral/tests/functional/engine/actions/v2/test_ssh_actions.py b/mistral/tests/functional/engine/actions/v2/test_ssh_actions.py index cf24bfce..34089cc9 100644 --- a/mistral/tests/functional/engine/actions/v2/test_ssh_actions.py +++ b/mistral/tests/functional/engine/actions/v2/test_ssh_actions.py @@ -13,6 +13,8 @@ # limitations under the License. import json +import os +from os import path import time from oslo_log import log as logging @@ -27,6 +29,7 @@ from mistral.utils import ssh_utils LOG = logging.getLogger(__name__) CONF = config.CONF +SSH_KEYS_DIRECTORY = path.expanduser("~/.ssh/") class SSHActionsTestsV2(base.TestCaseAdvanced): @@ -138,6 +141,27 @@ class SSHActionsTestsV2(base.TestCaseAdvanced): cls.private_key, cls.public_key = utils.generate_key_pair() cls.key_name = 'mistral-functional-tests-key' + # If ZUUL_PROJECT is specified, it means + # tests are running on Jenkins gate. + + if os.environ.get('ZUUL_PROJECT'): + cls.key_dir = "/opt/stack/new/.ssh/" + + if not path.exists(cls.key_dir): + os.mkdir(cls.key_dir) + else: + cls.key_dir = SSH_KEYS_DIRECTORY + + utils.save_text_to( + cls.private_key, + cls.key_dir + cls.key_name, + overwrite=True + ) + + LOG.info( + "Private key saved to %s" % cls.key_dir + cls.key_name + ) + # Create keypair in nova. cls.mgr.keypairs_client.create_keypair( name=cls.key_name, @@ -184,6 +208,7 @@ class SSHActionsTestsV2(base.TestCaseAdvanced): cls.mgr.security_group_rules_client.delete_security_group_rule( cls.ssh_rule_id ) + os.remove(cls.key_dir + cls.key_name) super(SSHActionsTestsV2, cls).resource_cleanup() @@ -193,7 +218,7 @@ class SSHActionsTestsV2(base.TestCaseAdvanced): 'cmd': 'hostname', 'host': self.public_vm_ip, 'username': CONF.scenario.ssh_user, - 'private_key': self.private_key + 'private_key_filename': self.key_name } resp, body = self.client.create_action_execution( @@ -217,7 +242,7 @@ class SSHActionsTestsV2(base.TestCaseAdvanced): 'cmd': 'hostname', 'host': guest_vm_ip, 'username': CONF.scenario.ssh_user, - 'private_key': self.private_key, + 'private_key_filename': self.key_name, 'gateway_host': self.public_vm_ip, 'gateway_username': CONF.scenario.ssh_user } diff --git a/mistral/tests/unit/utils/test_utils.py b/mistral/tests/unit/utils/test_utils.py index f80e9443..d668c48d 100644 --- a/mistral/tests/unit/utils/test_utils.py +++ b/mistral/tests/unit/utils/test_utils.py @@ -16,8 +16,10 @@ import copy +from mistral import exceptions as exc from mistral.tests import base from mistral import utils +from mistral.utils import ssh_utils LEFT = { 'key1': { @@ -117,3 +119,15 @@ class UtilsTest(base.BaseTest): self.assertEqual(2, input_dict.get('param2')) self.assertEqual('var3', input_dict.get('param3')) self.assertIs(input_dict.get('param1'), utils.NotDefined) + + def test_paramiko_to_private_key(self): + self.assertRaises( + exc.DataAccessException, + ssh_utils._to_paramiko_private_key, + "../dir" + ) + self.assertRaises( + exc.DataAccessException, + ssh_utils._to_paramiko_private_key, + "..\\dir" + ) diff --git a/mistral/utils/__init__.py b/mistral/utils/__init__.py index cf677661..eb0c36c3 100644 --- a/mistral/utils/__init__.py +++ b/mistral/utils/__init__.py @@ -267,6 +267,7 @@ def tempdir(**kwargs): if 'dir' not in argdict: argdict['dir'] = '/tmp/' + tmpdir = tempfile.mkdtemp(**argdict) try: @@ -281,6 +282,16 @@ def tempdir(**kwargs): ) +def save_text_to(text, file_path, overwrite=False): + if os.path.exists(file_path) and not overwrite: + raise exc.DataAccessException( + "Cannot save data to file. File %s already exists." + ) + + with open(file_path, 'w') as f: + f.write(text) + + def generate_key_pair(key_length=2048): """Create RSA key pair with specified number of bits in key. Returns tuple of private and public keys. @@ -295,15 +306,20 @@ def generate_key_pair(key_length=2048): '-f', keyfile, # filename of the key file '-C', 'Generated-by-Mistral' # key comment ] + if key_length is not None: args.extend(['-b', key_length]) + processutils.execute(*args) + if not os.path.exists(keyfile): raise exc.DataAccessException( "Private key file hasn't been created" ) + private_key = open(keyfile).read() public_key_path = keyfile + '.pub' + if not os.path.exists(public_key_path): raise exc.DataAccessException( "Public key file hasn't been created" diff --git a/mistral/utils/ssh_utils.py b/mistral/utils/ssh_utils.py index eb526016..dc5c440a 100644 --- a/mistral/utils/ssh_utils.py +++ b/mistral/utils/ssh_utils.py @@ -14,12 +14,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from os import path + import six from oslo_log import log as logging import paramiko +from mistral import exceptions as exc + +KEY_PATH = path.expanduser("~/.ssh/") LOG = logging.getLogger(__name__) @@ -33,9 +38,17 @@ def _read_paramimko_stream(recv_func): return result -def _to_paramiko_private_key(private_key_raw, password): +def _to_paramiko_private_key(private_key_filename, password=None): + if '../' in private_key_filename or '..\\' in private_key_filename: + raise exc.DataAccessException( + "Private key filename must not contain '..'. " + "Actual: %s" % private_key_filename + ) + + private_key_path = KEY_PATH + private_key_filename + return paramiko.RSAKey( - file_obj=six.StringIO(private_key_raw), + filename=private_key_path, password=password ) @@ -87,13 +100,12 @@ def _execute_command(ssh_client, cmd, get_stderr=False, _cleanup(ssh_client) -def execute_command_via_gateway(cmd, host, username, private_key, +def execute_command_via_gateway(cmd, host, username, private_key_filename, gateway_host, gateway_username=None, proxy_command=None, password=None): LOG.debug('Creating SSH connection') - if isinstance(private_key, six.string_types): - private_key = _to_paramiko_private_key(private_key, password) + private_key = _to_paramiko_private_key(private_key_filename, password) proxy = None @@ -138,9 +150,10 @@ def execute_command_via_gateway(cmd, host, username, private_key, _cleanup(_proxy_ssh_client) -def execute_command(cmd, host, username, password=None, private_key=None, - get_stderr=False, raise_when_error=True): - ssh_client = _connect(host, username, password, private_key) +def execute_command(cmd, host, username, password=None, + private_key_filename=None, get_stderr=False, + raise_when_error=True): + ssh_client = _connect(host, username, password, private_key_filename) LOG.debug("Executing command %s" % cmd)