From 9e3454aa3ffd24e676565b5e92ea7836fd690c19 Mon Sep 17 00:00:00 2001 From: Sergey Skripnick Date: Mon, 7 Oct 2013 11:55:52 +0300 Subject: [PATCH] Make devstack engine more flexible Ability to make custom localrc. Ability to specify devstack repository in config file. Added sshutils. Change-Id: I0e1286e4c9a9674abd4cd96323903b1a64d9d101 --- doc/sample/sample-task.json | 18 ++--- rally/deploy/engines/devstack.py | 88 ++++++++++++----------- rally/deploy/engines/devstack/localrc.tpl | 14 ---- rally/sshutils.py | 45 ++++++++++++ requirements.txt | 1 - tests/deploy/test_devstack_engine.py | 59 +++++++++++++++ tests/test_sshutils.py | 66 +++++++++++++++++ 7 files changed, 220 insertions(+), 71 deletions(-) delete mode 100644 rally/deploy/engines/devstack/localrc.tpl create mode 100644 rally/sshutils.py create mode 100644 tests/deploy/test_devstack_engine.py create mode 100644 tests/test_sshutils.py diff --git a/doc/sample/sample-task.json b/doc/sample/sample-task.json index 05133eab91..960cb6198a 100644 --- a/doc/sample/sample-task.json +++ b/doc/sample/sample-task.json @@ -1,19 +1,9 @@ { "deploy": { - "name": "DevstackDeployment", - "vm_provider": { - "name": "VirshProvider", - "connection": "alex@performance-01", - "template_name": "stack-01-devstack-template" - "template_user": "alex", - }, - "vm_count": 1, - "services": { - "admin_password": "71789845d5ceb06f9609", - "nova": { - "repo": "https://github.com/Alexei-Kornienko/nova", - "branch": "rally" - } + "name": "DevstackEngine", + "provider": { + "name": "DummyProvider", + "credentials": ["eye@10.2.250.35"] } }, "tests": {} diff --git a/rally/deploy/engines/devstack.py b/rally/deploy/engines/devstack.py index ae1c1f6ae5..7b81c61cd3 100644 --- a/rally/deploy/engines/devstack.py +++ b/rally/deploy/engines/devstack.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013: Mirantis Inc. # All Rights Reserved. # @@ -15,84 +13,95 @@ # License for the specific language governing permissions and limitations # under the License. -import jinja2 import os -import subprocess import tempfile from rally.deploy import engine from rally.openstack.common.gettextutils import _ # noqa from rally.openstack.common import log as logging from rally.serverprovider import provider +from rally import sshutils LOG = logging.getLogger(__name__) +DEVSTACK_REPO = 'https://github.com/openstack-dev/devstack.git' -class DevstackDeployment(engine.EngineFactory): +class DevstackEngine(engine.EngineFactory): '''Deploys Devstack cloud. deploy config example: "deploy": { - "vm_provider": { + "name": "DevstackEngine", + "openrc": { + "ADMIN_PASSWORD": "secret" + }, + "devstack_repo": "git://example.com/devstack/", + "provider": { "name": "%name%", ... } - "vm_count": 1, }, ''' - def __init__(self, config): + def __init__(self, task, config): + self.task = task self._config = config self._vms = [] - provider_config = config['vm_provider'] + provider_config = config['provider'] self._vm_provider = provider.ProviderFactory.get_provider( provider_config) + self.localrc = { + 'DATABASE_PASSWORD': 'rally', + 'RABBIT_PASSWORD': 'rally', + 'SERVICE_TOKEN': 'rally', + 'SERVICE_PASSWORD': 'rally', + 'ADMIN_PASSWORD': 'admin', + 'RECLONE': 'yes', + 'SYSLOG': 'yes', + } + if 'localrc' in config: + self.localrc.update(config['localrc']) def deploy(self): - self._vms = self._vm_provider.create_vms( - amount=int(self._config['vm_count'])) + self._vms = self._vm_provider.create_vms() + devstack_repo = self._config.get('devstack_repo', DEVSTACK_REPO) for vm in self._vms: - self.patch_devstack(vm) + sshutils.execute_command(vm.user, vm.ip, + ['git', 'clone', devstack_repo]) + self.configure_devstack(vm) self.start_devstack(vm) - self._vms.append(vm) + self._vms.append(vm) - identity_host = {'host': self._vms[0].ip} + identity_host = self._vms[0].ip return { 'identity': { 'url': 'http://%s/' % identity_host, 'uri': 'http://%s:5000/v2.0/' % identity_host, 'admin_username': 'admin', - 'admin_password': self._config['services']['admin_password'], - 'admin_tenant_name': 'service', + 'admin_password': self.localrc['ADMIN_PASSWORD'], + 'admin_tenant_name': 'admin', + }, + 'compute': { + 'controller_nodes': self._vms[0].ip, + 'compute_nodes': self._vms[0].ip, + 'controller_node_ssh_user': self._vms[0].user, } } def cleanup(self): - for vm in self._vms: - self._vm_provider.destroy_vm(vm) + self._vm_provider.destroy_vms() - def patch_devstack(self, vm): + def configure_devstack(self, vm): task_uuid = self.task['uuid'] LOG.info(_('Task %(uuid)s: Patching DevStack for VM %(vm_ip)s...') % {'uuid': task_uuid, 'vm_ip': vm.ip}) - template_path = os.path.dirname(__file__) + '/devstack/' - env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_path)) - - config_file, config_path = tempfile.mkstemp() - config_file = os.fdopen(config_file, 'w') - - config_template = env.get_template('localrc.tpl') - config_file.write(config_template.render(self._config['services'])) + fd, config_path = tempfile.mkstemp() + config_file = open(config_path, "w") + for k, v in self.localrc.iteritems(): + config_file.write('%s=%s\n' % (k, v)) config_file.close() - - cmd = 'scp %(opts)s %(config)s %(usr)s@%(host)s:~/devstack/localrc' % { - 'opts': '-o StrictHostKeyChecking=no', - 'config': config_path, - 'usr': vm.user, - 'host': vm.ip - } - subprocess.check_call(cmd, shell=True) - + os.close(fd) + sshutils.upload_file(vm.user, vm.ip, config_path, "~/devstack/localrc") os.unlink(config_path) LOG.info(_('Task %(uuid)s: DevStack for VM %(vm_ip)s successfully ' 'patched.') % {'uuid': task_uuid, 'vm_ip': vm.ip}) @@ -102,12 +111,7 @@ class DevstackDeployment(engine.EngineFactory): task_uuid = self.task['uuid'] LOG.info(_('Task %(uuid)s: Starting DevStack for VM %(vm_ip)s...') % {'uuid': task_uuid, 'vm_ip': vm.ip}) - cmd = 'ssh %(opts)s %(usr)s@%(host)s devstack/stack.sh' % { - 'opts': '-o StrictHostKeyChecking=no', - 'usr': vm.user, - 'host': vm.ip - } - subprocess.check_call(cmd, shell=True) + sshutils.execute_command(vm.user, vm.ip, ['~/devstack/stack.sh']) LOG.info(_('Task %(uuid)s: DevStack for VM %(vm_ip)s successfully ' 'started.') % {'uuid': task_uuid, 'vm_ip': vm.ip}) return True diff --git a/rally/deploy/engines/devstack/localrc.tpl b/rally/deploy/engines/devstack/localrc.tpl deleted file mode 100644 index d89e42bcc3..0000000000 --- a/rally/deploy/engines/devstack/localrc.tpl +++ /dev/null @@ -1,14 +0,0 @@ -DATABASE_PASSWORD=b63a9cca3cd359cc32ed -RABBIT_PASSWORD=a9fd294d3977ec2eb41e -SERVICE_TOKEN=95f65562216379062794 -SERVICE_PASSWORD=c2ec0d6c0aae31959ead -ADMIN_PASSWORD={{ admin_password }} - -RECLONE=yes -SYSLOG=True - -#Nova configuration -{% if nova %} -NOVA_REPO={{ nova.repo }} -NOVA_BRANCH={{ nova.branch }} -{% endif %} diff --git a/rally/sshutils.py b/rally/sshutils.py new file mode 100644 index 0000000000..19f5ab4854 --- /dev/null +++ b/rally/sshutils.py @@ -0,0 +1,45 @@ +# Copyright 2013: Mirantis Inc. +# All Rights Reserved. +# +# 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. + +import subprocess + +DEFAULT_OPTIONS = ['-o', 'StrictHostKeyChecking=no'] + + +class SSHException(Exception): + pass + + +def upload_file(user, host, source, destination): + cmd = ['scp'] + DEFAULT_OPTIONS + [ + source, '%s@%s:%s' % (user, host, destination)] + pipe = subprocess.Popen(cmd, stderr=subprocess.PIPE) + (so, se) = pipe.communicate() + if pipe.returncode: + raise SSHException(se) + + +def execute_script(user, host, script, enterpreter='/bin/sh'): + cmd = ['ssh'] + DEFAULT_OPTIONS + ['%s@%s' % (user, host), enterpreter] + subprocess.check_call(cmd, stdin=open(script, 'r')) + + +def execute_command(user, host, cmd): + pipe = subprocess.Popen(['ssh'] + DEFAULT_OPTIONS + + ['%s@%s' % (user, host)] + cmd, + stderr=subprocess.PIPE) + (so, se) = pipe.communicate() + if pipe.returncode: + raise SSHException(se) diff --git a/requirements.txt b/requirements.txt index 0eaa5c336d..e7b94b98bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ Babel>=0.9.6 eventlet>=0.9.17 iso8601>=0.1.4 jsonschema>=2.0.0 -Jinja2 netaddr>=0.7.6 oslo.config>=1.2.0 paramiko>=1.8.0 diff --git a/tests/deploy/test_devstack_engine.py b/tests/deploy/test_devstack_engine.py new file mode 100644 index 0000000000..21e73253dc --- /dev/null +++ b/tests/deploy/test_devstack_engine.py @@ -0,0 +1,59 @@ +# Copyright 2013: Mirantis Inc. +# All Rights Reserved. +# +# 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. + +import mock + +from rally.deploy.engines import devstack +from rally.openstack.common import test + + +SAMPLE_CONFIG = { + 'provider': { + 'name': 'DummyProvider', + 'credentials': ['root@example.com'], + }, + 'localrc': { + 'ADMIN_PASSWORD': 'secret', + }, +} + +DEVSTACK_REPO = 'https://github.com/openstack-dev/devstack.git' + + +class DevstackEngineTestCase(test.BaseTestCase): + + def setUp(self): + self.task = mock.MagicMock() + self.task['uuid'] = mock.MagicMock() + self.de = devstack.DevstackEngine(self.task, SAMPLE_CONFIG) + super(DevstackEngineTestCase, self).setUp() + + def test_construct(self): + self.assertEqual(self.de.localrc['ADMIN_PASSWORD'], 'secret') + + def test_deploy(self): + with mock.patch('rally.deploy.engines.devstack.sshutils') as ssh: + self.de.deploy() + + config_tmp_filename = ssh.mock_calls[1][1][2] + call = mock.call + expected = [ + call.execute_command('root', 'example.com', ['git', 'clone', + DEVSTACK_REPO]), + call.upload_file('root', 'example.com', + config_tmp_filename, '~/devstack/localrc'), + call.execute_command('root', 'example.com', + ['~/devstack/stack.sh'])] + self.assertEqual(expected, ssh.mock_calls) diff --git a/tests/test_sshutils.py b/tests/test_sshutils.py new file mode 100644 index 0000000000..ca6cb50663 --- /dev/null +++ b/tests/test_sshutils.py @@ -0,0 +1,66 @@ +# Copyright 2013: Mirantis Inc. +# All Rights Reserved. +# +# 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. + + +import mock + +from rally.openstack.common import test +from rally import sshutils + + +class SshutilsTestCase(test.BaseTestCase): + + def setUp(self): + super(SshutilsTestCase, self).setUp() + self.pipe = mock.MagicMock() + self.pipe.communicate = mock.MagicMock(return_value=(mock.MagicMock(), + mock.MagicMock())) + self.pipe.returncode = 0 + self.new = mock.MagicMock() + self.new.PIPE = self.pipe + self.new.Popen = mock.MagicMock(return_value=self.pipe) + + def test_upload_file(self): + with mock.patch('rally.sshutils.subprocess', new=self.new) as sp: + sshutils.upload_file('root', 'example.com', '/tmp/s', '/tmp/d') + + expected = [mock.call.Popen(['scp', '-o', + 'StrictHostKeyChecking=no', + '/tmp/s', 'root@example.com:/tmp/d'], + stderr=self.pipe), + mock.call.PIPE.communicate()] + self.assertEqual(sp.mock_calls, expected) + + def test_execute_script_no_file(self): + self.assertRaises(IOError, sshutils.execute_script, 'user', 'host', + '/ioerror') + + def test_execute_script(self): + with mock.patch('rally.sshutils.subprocess', new=self.new) as sp: + with mock.patch('rally.sshutils.open', create=True) as op: + sshutils.execute_script('user', 'example.com', '/tmp/s') + expected = [ + mock.call.check_call(['ssh', '-o', 'StrictHostKeyChecking=no', + 'user@example.com', '/bin/sh'], stdin=op())] + self.assertEqual(sp.mock_calls, expected) + + def test_execute_command(self): + with mock.patch('rally.sshutils.subprocess', new=self.new) as sp: + sshutils.execute_command('user', 'host', ['command', 'arg']) + expected = [ + mock.call.Popen(['ssh', '-o', 'StrictHostKeyChecking=no', + 'user@host', 'command', 'arg'], stderr=self.pipe), + mock.call.PIPE.communicate()] + self.assertEqual(sp.mock_calls, expected)