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:
parent
66a012eb81
commit
9e3454aa3f
@ -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": {}
|
||||
|
@ -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
|
||||
|
@ -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
45
rally/sshutils.py
Normal 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)
|
@ -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
|
||||
|
59
tests/deploy/test_devstack_engine.py
Normal file
59
tests/deploy/test_devstack_engine.py
Normal 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
66
tests/test_sshutils.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user