Merge "Add openstack based server provider"
This commit is contained in:
commit
3921b4faa5
25
doc/sample/sample-openstack.json
Normal file
25
doc/sample/sample-openstack.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"deploy": {
|
||||
"name": "DevstackEngine",
|
||||
"provider": {
|
||||
"name": "OpenStackProvider",
|
||||
"user": "admin",
|
||||
"tenant": "admin",
|
||||
"flavor_id": "2",
|
||||
"password": "admin",
|
||||
"auth_url": "http://example.net:5000/v2.0",
|
||||
"amount": 1,
|
||||
"image": {
|
||||
"checksum": "5101b2013b31d9f2f96f64f728926054",
|
||||
"name": "Ubuntu raring(added by rally)",
|
||||
"format": "qcow2",
|
||||
"userdata": "#cloud-config\r\n disable_root: false\r\n manage_etc_hosts: true\r\n",
|
||||
"url": "http://cloud-images.ubuntu.com/raring/current/raring-server-cloudimg-amd64-disk1.img"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"verify": ["sanity", "smoke"],
|
||||
"benchmark": {}
|
||||
}
|
||||
}
|
@ -141,3 +141,7 @@ class SSHError(RallyException):
|
||||
class TaskInvalidStatus(RallyException):
|
||||
msg_fmt = _("Task `%(uuid)s` in `%(actual)s` status but `%(require)s` is "
|
||||
"required.")
|
||||
|
||||
|
||||
class ChecksumMismatch(RallyException):
|
||||
msg_fmt = _("Checksum mismatch for image: %(url)s")
|
||||
|
180
rally/serverprovider/providers/openstack.py
Normal file
180
rally/serverprovider/providers/openstack.py
Normal file
@ -0,0 +1,180 @@
|
||||
# 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 jsonschema
|
||||
import os
|
||||
import time
|
||||
import urllib2
|
||||
|
||||
from rally.benchmark.scenarios.nova import utils as nova_utils
|
||||
from rally import exceptions
|
||||
from rally.openstack.common.gettextutils import _ # noqa
|
||||
from rally.openstack.common import log as logging
|
||||
from rally import osclients
|
||||
from rally.serverprovider import provider
|
||||
from rally import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SCHEMA_TEMPLATE = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'deployment_name': {'type': 'string'},
|
||||
'amount': {'type': 'integer'},
|
||||
'user': {'type': 'string'},
|
||||
'password': {'type': 'string'},
|
||||
'tenant': {'type': 'string'},
|
||||
'auth_url': {'type': 'string'},
|
||||
'flavor_id': {'type': 'string'},
|
||||
'image': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'checksum': {'type': 'string'},
|
||||
'name': {'type': 'string'},
|
||||
'format': {'type': 'string'},
|
||||
'userdata': {'type': 'string'},
|
||||
'url': {'type': 'string'},
|
||||
'uuid': {'type': 'string'},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['name', 'format', 'url', 'checksum'],
|
||||
},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
'required': ['user', 'password', 'tenant', 'deployment_name',
|
||||
'auth_url', 'flavor_id', 'image']
|
||||
}
|
||||
|
||||
|
||||
class OpenStackProvider(provider.ProviderFactory):
|
||||
"""Provides VMs using existing OpenStack cloud.
|
||||
|
||||
Sample configuration:
|
||||
|
||||
{
|
||||
"name": "OpenStackProvider",
|
||||
"amount": 42
|
||||
"user": "admin",
|
||||
"tenant": "admin",
|
||||
"password": "secret",
|
||||
"auth_url": "http://example.com/",
|
||||
"flavor_id": 2,
|
||||
"image": {
|
||||
"checksum": "75846dd06e9fcfd2b184aba7fa2b2a8d",
|
||||
"url": "http://example.com/disk1.img",
|
||||
"name": "Ubuntu Precise(added by rally)",
|
||||
"format": "qcow2",
|
||||
"userdata": "#cloud-config\r\n disable_root: false"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
jsonschema.validate(config, SCHEMA_TEMPLATE)
|
||||
self.config = dict(config)
|
||||
clients = osclients.Clients(config['user'], config['password'],
|
||||
config['tenant'], config['auth_url'])
|
||||
self.nova = clients.get_nova_client()
|
||||
self.glance = clients.get_glance_client()
|
||||
|
||||
def get_image_uuid(self):
|
||||
"""Get image uuid. Download image if necessary."""
|
||||
|
||||
image_uuid = self.config['image'].get('uuid', None)
|
||||
if image_uuid:
|
||||
return image_uuid
|
||||
|
||||
for image in self.glance.images.list():
|
||||
if image.checksum == self.config['image']['checksum']:
|
||||
LOG.info(_('Found image with appropriate checksum. Using it.'))
|
||||
return image.id
|
||||
|
||||
LOG.info(_('Downloading new image %s') % self.config['image']['url'])
|
||||
image = self.glance.images.create(name=self.config['image']['name'])
|
||||
try:
|
||||
image.update(data=urllib2.urlopen(self.config['image']['url']),
|
||||
disk_format=self.config['image']['format'],
|
||||
container_format='bare')
|
||||
except urllib2.URLError:
|
||||
LOG.error(_('Unable to retrieve %s') % self.config['image']['url'])
|
||||
raise
|
||||
image.get()
|
||||
|
||||
if image.checksum != self.config['image']['checksum']:
|
||||
raise exceptions.ChecksumMismatch(url=self.config['image']['url'])
|
||||
|
||||
return image.id
|
||||
|
||||
def get_userdata(self):
|
||||
userdata = self.config['image'].get('userdata', None)
|
||||
if userdata is not None:
|
||||
return userdata
|
||||
userdata = self.config['image'].get('userdata_file', None)
|
||||
if userdata is not None:
|
||||
userdata = open(userdata, 'r')
|
||||
return userdata
|
||||
|
||||
def create_vms(self):
|
||||
"""Create VMs with chosen image."""
|
||||
|
||||
image_uuid = self.get_image_uuid()
|
||||
userdata = self.get_userdata()
|
||||
flavor = self.config['flavor_id']
|
||||
|
||||
public_key_path = self.config.get(
|
||||
'ssh_public_key_file', os.path.expanduser('~/.ssh/id_rsa.pub'))
|
||||
public_key = open(public_key_path, 'r').read().strip()
|
||||
key_name = self.config['deployment_name'] + '-key'
|
||||
self.keypair = self.nova.keypairs.create(key_name, public_key)
|
||||
|
||||
self.os_servers = []
|
||||
for i in range(self.config.get('amount', 1)):
|
||||
name = "%s-%d" % (self.config['deployment_name'], i)
|
||||
server = self.nova.servers.create(name, image_uuid, flavor,
|
||||
key_name=self.keypair.name,
|
||||
userdata=userdata)
|
||||
self.os_servers.append(server)
|
||||
|
||||
kwargs = {
|
||||
'is_ready': nova_utils._resource_is("ACTIVE"),
|
||||
'update_resource': nova_utils._get_from_manager,
|
||||
'timeout': 120,
|
||||
'check_interval': 5
|
||||
}
|
||||
|
||||
for os_server in self.os_servers:
|
||||
utils.wait_for(os_server, **kwargs)
|
||||
|
||||
servers = [provider.ServerDTO(s.id,
|
||||
s.addresses.values()[0][0]['addr'],
|
||||
'root',
|
||||
public_key_path)
|
||||
for s in self.os_servers]
|
||||
for s in servers:
|
||||
s.ssh.wait(timeout=120, interval=5)
|
||||
|
||||
# NOTE(eyerediskin): usually ssh is ready much earlier then cloud-init
|
||||
time.sleep(8)
|
||||
return servers
|
||||
|
||||
def destroy_vms(self):
|
||||
for server in getattr(self, 'os_servers', []):
|
||||
server.delete()
|
||||
if hasattr(self, 'keypair'):
|
||||
self.keypair.delete()
|
163
tests/serverprovider/test_openstack_provider.py
Normal file
163
tests/serverprovider/test_openstack_provider.py
Normal file
@ -0,0 +1,163 @@
|
||||
# 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.
|
||||
|
||||
"""Tests for OpenStack VM provider."""
|
||||
|
||||
import jsonschema
|
||||
import mock
|
||||
|
||||
from rally.serverprovider.providers import openstack as provider
|
||||
from rally import test
|
||||
|
||||
|
||||
MOD_NAME = 'rally.serverprovider.providers.openstack'
|
||||
OSProvider = provider.OpenStackProvider
|
||||
|
||||
|
||||
class FakeOSClients(object):
|
||||
|
||||
def get_nova_client(self):
|
||||
return "nova"
|
||||
|
||||
def get_glance_client(self):
|
||||
return "glance"
|
||||
|
||||
|
||||
class OpenStackProviderTestCase(test.TestCase):
|
||||
|
||||
def _get_valid_config(self):
|
||||
return {
|
||||
'image': {
|
||||
'url': 'http://example.net/img.qcow2',
|
||||
'format': 'qcow2',
|
||||
'name': 'Image',
|
||||
'checksum': '0123456789abcdef',
|
||||
},
|
||||
'deployment_name': 'rally-dep-1',
|
||||
'auth_url': 'urlto',
|
||||
'user': 'name',
|
||||
'password': 'mypass',
|
||||
'tenant': 'tenant',
|
||||
'flavor_id': '22'}
|
||||
|
||||
def _init_mock_clients(self):
|
||||
|
||||
def g():
|
||||
raise Exception('ooke')
|
||||
|
||||
self.clients = mock.MagicMock()
|
||||
|
||||
self.image = mock.MagicMock()
|
||||
self.image.checksum = '0123456789abcdef'
|
||||
self.image.get = mock.MagicMock(return_value=self.image)
|
||||
self.image.id = 'fake-uuid'
|
||||
self.glance_client = mock.Mock(return_value=self.image)
|
||||
self.glance_client.images.create = mock.Mock(return_value=self.image)
|
||||
self.glance_client.images.list = mock.Mock(return_value=[self.image])
|
||||
self.clients.get_glance_client = mock.Mock(
|
||||
return_value=self.glance_client)
|
||||
|
||||
self.instance = mock.MagicMock()
|
||||
self.instance.status = "ACTIVE"
|
||||
|
||||
self.nova_client = mock.MagicMock()
|
||||
self.nova_client.servers.create = mock.MagicMock(
|
||||
return_value=self.instance)
|
||||
|
||||
self.clients.get_nova_client = mock.MagicMock(
|
||||
return_value=self.nova_client)
|
||||
|
||||
def test_openstack_provider_init(self):
|
||||
cfg = self._get_valid_config()
|
||||
|
||||
mod = "rally.serverprovider.providers.openstack."
|
||||
with mock.patch(mod + "osclients") as os_cli:
|
||||
os_cli.Clients = mock.MagicMock(return_value=FakeOSClients())
|
||||
os_provider = OSProvider(cfg)
|
||||
expected_calls = [
|
||||
mock.call.Clients(cfg['user'], cfg['password'],
|
||||
cfg['tenant'], cfg['auth_url'])]
|
||||
self.assertEqual(expected_calls, os_cli.mock_calls)
|
||||
self.assertEqual('nova', os_provider.nova)
|
||||
self.assertEqual('glance', os_provider.glance)
|
||||
|
||||
def test_openstack_provider_init_with_invalid_conf_no_user(self):
|
||||
cfg = self._get_valid_config()
|
||||
cfg.pop("user")
|
||||
with mock.patch("rally.serverprovider.providers.openstack.osclients"):
|
||||
self.assertRaises(jsonschema.ValidationError, OSProvider, cfg)
|
||||
|
||||
def test_openstack_provider_init_with_invalid_conf_extra_key(self):
|
||||
cfg = self._get_valid_config()
|
||||
cfg["aaaaa"] = "bbbbb"
|
||||
with mock.patch("rally.serverprovider.providers.openstack.osclients"):
|
||||
self.assertRaises(jsonschema.ValidationError, OSProvider, cfg)
|
||||
|
||||
def test_openstack_provider_init_with_invalid_conf_flavor_(self):
|
||||
cfg = self._get_valid_config()
|
||||
cfg["user"] = 1111
|
||||
with mock.patch("rally.serverprovider.providers.openstack.osclients"):
|
||||
self.assertRaises(jsonschema.ValidationError, OSProvider, cfg)
|
||||
|
||||
def test_openstack_provider_with_valid_config(self):
|
||||
cfg = self._get_valid_config()
|
||||
with mock.patch("rally.serverprovider.providers.openstack.osclients"):
|
||||
OSProvider(cfg)
|
||||
|
||||
@mock.patch(MOD_NAME + '.osclients')
|
||||
@mock.patch(MOD_NAME + '.open', create=True)
|
||||
@mock.patch(MOD_NAME + '.provider')
|
||||
@mock.patch(MOD_NAME + '.nova_utils._get_from_manager', new=lambda r: r)
|
||||
def test_openstack_provider_create_vms(self, g, provider, clients):
|
||||
self._init_mock_clients()
|
||||
clients.Clients = mock.MagicMock(return_value=self.clients)
|
||||
provider.ServerDTO = mock.MagicMock()
|
||||
prov = OSProvider(self._get_valid_config())
|
||||
prov.get_image_uuid = mock.Mock()
|
||||
prov.create_vms()
|
||||
self.assertEqual(['keypairs.create', 'servers.create'],
|
||||
[call[0] for call in self.nova_client.mock_calls])
|
||||
|
||||
@mock.patch(MOD_NAME + '.osclients')
|
||||
@mock.patch(MOD_NAME + '.urllib2')
|
||||
def test_openstack_provider_get_image_found_by_checksum(self, u, oscl):
|
||||
self._init_mock_clients()
|
||||
oscl.Clients = mock.MagicMock(return_value=self.clients)
|
||||
prov = OSProvider(self._get_valid_config())
|
||||
image_uuid = prov.get_image_uuid()
|
||||
self.assertEqual(image_uuid, 'fake-uuid')
|
||||
|
||||
@mock.patch(MOD_NAME + '.osclients')
|
||||
@mock.patch(MOD_NAME + '.urllib2')
|
||||
def test_openstack_provider_get_image_download(self, u, oscl):
|
||||
self._init_mock_clients()
|
||||
self.glance_client.images.list = mock.Mock(return_value=[])
|
||||
oscl.Clients = mock.MagicMock(return_value=self.clients)
|
||||
prov = OSProvider(self._get_valid_config())
|
||||
image_uuid = prov.get_image_uuid()
|
||||
self.assertEqual(image_uuid, 'fake-uuid')
|
||||
self.assertEqual(u.mock_calls,
|
||||
[mock.call.urlopen('http://example.net/img.qcow2')])
|
||||
|
||||
def test_openstack_provider_destroy_vms(self):
|
||||
with mock.patch(MOD_NAME + '.osclients'):
|
||||
prov = OSProvider(self._get_valid_config())
|
||||
server = mock.MagicMock()
|
||||
keypair = mock.MagicMock()
|
||||
prov.os_servers = [server]
|
||||
prov.keypair = keypair
|
||||
prov.destroy_vms()
|
||||
self.assertEqual(server.mock_calls, [mock.call.delete()])
|
||||
self.assertEqual(keypair.mock_calls, [mock.call.delete()])
|
Loading…
x
Reference in New Issue
Block a user