Make devstack engine more flexible

Ability to make custom localrc.
Ability to specify devstack repository in config file.
Added sshutils.

Change-Id: I0e1286e4c9a9674abd4cd96323903b1a64d9d101
This commit is contained in:
Sergey Skripnick 2013-10-07 11:55:52 +03:00
parent 66a012eb81
commit 9e3454aa3f
7 changed files with 220 additions and 71 deletions

View File

@ -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": {}

View File

@ -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

View File

@ -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 %}

45
rally/sshutils.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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)

66
tests/test_sshutils.py Normal file
View File

@ -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)