Implement image builder in Python

Change-Id: I7bd8b99b93b70e87a3780dfe535a425a6942f4b4
This commit is contained in:
Ilya Shakhat
2015-02-20 00:51:15 +03:00
parent 9c61061550
commit 12027d7461
14 changed files with 401 additions and 50 deletions

View File

@@ -24,12 +24,24 @@
#os_password =
# Authentication region name, defaults to env[OS_REGION_NAME]. (string value)
#os_region_name =
#os_region_name = RegionOne
# Name or ID of external network. If not set the network is chosen randomly.
# (string value)
#external_net = <None>
# Name of image to use. The default is created by shaker-image-builder (string
# value)
#image_name = shaker-image
# Name of image flavor. The default is created by shaker-image-builder (string
# value)
#flavor_name = shaker-flavor
#
# From shaker.engine.config
#
# Scenario file name (string value)
#scenario = <None>

View File

@@ -11,6 +11,7 @@ oslo.config>=1.6.0 # Apache-2.0
oslo.i18n>=1.3.0 # Apache-2.0
oslo.serialization>=1.2.0 # Apache-2.0
oslo.utils>=1.2.0 # Apache-2.0
python-glanceclient>=0.15.0
python-keystoneclient>=1.0.0
python-neutronclient>=2.3.6,<3
python-novaclient>=2.18.0

View File

@@ -1,6 +1,6 @@
[metadata]
name = shaker
summary = Shake VMs with our sheer-class tests!
summary = Distributed data-plane performance testing tool
description-file =
README.rst
author = OpenStack
@@ -26,6 +26,8 @@ packages =
console_scripts =
shaker = shaker.engine.server:main
shaker-agent = shaker.agent.agent:main
shaker-image-builder = shaker.engine.installer:build_image
shaker-cleanup = shaker.engine.installer:cleanup
oslo.config.opts =
shaker.openstack.common.log = shaker.openstack.common.log:list_opts

View File

@@ -25,7 +25,7 @@ COMMON_OPTS = [
help='Address for server connections (host:port)'),
]
SERVER_OPTS = [
OPENSTACK_OPTS = [
cfg.StrOpt('os-auth-url', metavar='<auth-url>',
default=utils.env('OS_AUTH_URL'),
help='Authentication URL, defaults to env[OS_AUTH_URL].'),
@@ -40,7 +40,7 @@ SERVER_OPTS = [
default=utils.env('OS_PASSWORD'),
help='Authentication password, defaults to env[OS_PASSWORD].'),
cfg.StrOpt('os-region-name', metavar='<auth-region-name>',
default=utils.env('OS_REGION_NAME'),
default=utils.env('OS_REGION_NAME') or 'RegionOne',
help='Authentication region name, defaults to '
'env[OS_REGION_NAME].'),
@@ -48,6 +48,17 @@ SERVER_OPTS = [
help='Name or ID of external network. If not set the network '
'is chosen randomly.'),
cfg.StrOpt('image-name',
default='shaker-image',
help='Name of image to use. The default is created by '
'shaker-image-builder'),
cfg.StrOpt('flavor-name',
default='shaker-flavor',
help='Name of image flavor. The default is created by '
'shaker-image-builder'),
]
SERVER_OPTS = [
cfg.StrOpt('scenario',
required=True,
help='Scenario file name'),
@@ -67,5 +78,6 @@ AGENT_OPTS = [
def list_opts():
yield (None, copy.deepcopy(COMMON_OPTS))
yield (None, copy.deepcopy(OPENSTACK_OPTS))
yield (None, copy.deepcopy(SERVER_OPTS))
yield (None, copy.deepcopy(AGENT_OPTS))

View File

@@ -30,23 +30,23 @@ LOG = logging.getLogger(__name__)
class Deployment(object):
def __init__(self, os_username, os_password, os_tenant_name, os_auth_url,
os_region_name, server_endpoint, external_net):
keystone_kwargs = {'username': os_username,
'password': os_password,
'tenant_name': os_tenant_name,
'auth_url': os_auth_url,
}
self.keystone_client = keystone.create_keystone_client(keystone_kwargs)
self.heat_client = heat.create_heat_client(
os_region_name, server_endpoint, external_net, flavor_name,
image_name):
self.keystone_client = keystone.create_keystone_client(
username=os_username, password=os_password,
tenant_name=os_tenant_name, auth_url=os_auth_url)
self.heat_client = heat.create_client(
self.keystone_client, os_region_name)
self.nova_client = nova.create_nova_client(
self.nova_client = nova.create_client(
self.keystone_client, os_region_name)
self.neutron_client = neutron.create_neutron_client(
self.neutron_client = neutron.create_client(
self.keystone_client, os_region_name)
self.server_endpoint = server_endpoint
self.external_net = (external_net or
neutron.choose_external_net(self.neutron_client))
self.flavor_name = flavor_name
self.image_name = image_name
self.stack_name = 'shaker_%s' % uuid.uuid4()
self.stack_deployed = False
@@ -91,7 +91,7 @@ class Deployment(object):
for param in params:
o = stack_outputs.get(vm_name + '_' + param)
if o:
result[param] = o['output_value']
result[param] = o
return result
def convert_instance_name_to_agent_id(self, instance_name):
@@ -133,36 +133,35 @@ class Deployment(object):
return agents
def _fill_missing_template_parameters(self, template_parameters):
template_parameters['private_net_name'] = 'net_%s' % uuid.uuid4()
template_parameters['server_endpoint'] = self.server_endpoint
if not template_parameters.get('external_net'):
template_parameters['external_net'] = self.external_net
def _deploy_from_hot(self, specification):
vm_accommodation = specification['vm_accommodation']
heat_template_name = specification['template']
template_parameters = specification['template_parameters']
heat_template = utils.read_file(heat_template_name)
groups = self._make_groups(vm_accommodation)
groups = self._make_groups(specification['vm_accommodation'])
# render template by jinja
vars_values = {
'groups': groups,
}
heat_template = utils.read_file(specification['template'])
compiled_template = jinja2.Template(heat_template)
rendered_template = compiled_template.render(vars_values)
LOG.debug('Rendered template: %s', rendered_template)
# create stack by Heat
self._fill_missing_template_parameters(template_parameters)
merged_parameters = {
'private_net_name': 'net_%s' % uuid.uuid4(),
'private_net_cidr': '10.0.0.0/16',
'server_endpoint': self.server_endpoint,
'external_net': self.external_net,
'image': self.image_name,
'flavor': self.flavor_name,
}
merged_parameters.update(specification['template_parameters'])
stack_params = {
'stack_name': self.stack_name,
'parameters': template_parameters,
'parameters': merged_parameters,
'template': rendered_template,
}
LOG.debug('Creating stack with parameters: %s', stack_params)
stack = self.heat_client.stacks.create(**stack_params)['stack']
LOG.info('New stack: %s', stack)
@@ -171,9 +170,7 @@ class Deployment(object):
self.stack_deployed = True
# get info about deployed objects
outputs_list = self.heat_client.stacks.get(
stack['id']).to_dict()['outputs']
outputs = dict((item['output_key'], item) for item in outputs_list)
outputs = heat.get_stack_outputs(self.heat_client, stack['id'])
# convert groups into agents
return self._make_agents(groups, outputs)

113
shaker/engine/installer.py Normal file
View File

@@ -0,0 +1,113 @@
# Copyright (c) 2015 Mirantis Inc.
#
# 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 uuid
from oslo_config import cfg
from shaker.engine import config
from shaker.engine import utils
from shaker.openstack.clients import glance
from shaker.openstack.clients import heat
from shaker.openstack.clients import neutron
from shaker.openstack.clients import nova
from shaker.openstack.clients import openstack
from shaker.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def init():
# init conf and logging
conf = cfg.CONF
conf.register_cli_opts(config.OPENSTACK_OPTS)
conf.register_opts(config.OPENSTACK_OPTS)
conf(project='shaker')
logging.setup('shaker')
LOG.info('Logging enabled')
openstack_client = openstack.OpenStackClient(
username=cfg.CONF.os_username, password=cfg.CONF.os_password,
tenant_name=cfg.CONF.os_tenant_name, auth_url=cfg.CONF.os_auth_url,
region_name=cfg.CONF.os_region_name)
return openstack_client
def build_image():
openstack_client = init()
flavor_name = cfg.CONF.flavor_name
image_name = cfg.CONF.image_name
if nova.is_flavor_exists(openstack_client.nova, flavor_name):
LOG.info('Using existing flavor: %s', flavor_name)
else:
openstack_client.nova.flavors.create(name=flavor_name,
ram=1024, vcpus=1, disk=3)
LOG.info('Created flavor %s', flavor_name)
if glance.get_image(openstack_client.glance, image_name):
LOG.info('Using existing image: %s', image_name)
else:
external_net = (cfg.CONF.external_net or
neutron.choose_external_net(openstack_client.neutron))
stack_params = {
'stack_name': 'shaker_%s' % uuid.uuid4(),
'parameters': {'external_net': external_net,
'flavor': flavor_name},
'template': utils.read_file('shaker/engine/installer.yaml'),
}
stack = openstack_client.heat.stacks.create(**stack_params)['stack']
LOG.debug('New stack: %s', stack)
heat.wait_stack_completion(openstack_client.heat, stack['id'])
outputs = heat.get_stack_outputs(openstack_client.heat, stack['id'])
LOG.debug('Stack outputs: %s', outputs)
LOG.debug('Waiting for server to shutdown')
server_id = outputs['server_info'].get('id')
nova.wait_server_shutdown(openstack_client.nova, server_id)
LOG.debug('Making snapshot')
openstack_client.nova.servers.create_image(
server_id, image_name)
LOG.debug('Waiting for server to snapshot')
nova.wait_server_snapshot(openstack_client.nova, server_id)
LOG.debug('Clearing up')
openstack_client.heat.stacks.delete(stack['id'])
LOG.info('Created image: %s', image_name)
def cleanup():
openstack_client = init()
flavor_name = cfg.CONF.flavor_name
image_name = cfg.CONF.image_name
image = glance.get_image(openstack_client.glance, image_name)
if image:
openstack_client.glance.images.delete(image)
if nova.is_flavor_exists(openstack_client.nova, flavor_name):
openstack_client.nova.flavors.delete(name=flavor_name)
if __name__ == "__main__":
build_image()

View File

@@ -0,0 +1,82 @@
heat_template_version: 2013-05-23
description: >
HOT template to create a new neutron network plus a router to the public
network, and for deploying servers into the new network.
parameters:
external_net:
type: string
description: ID or name of public network for which floating IP addresses will be allocated
flavor:
type: string
description: Flavor to use for servers
resources:
private_net:
type: OS::Neutron::Net
properties:
name: shaker_image_builder_net
private_subnet:
type: OS::Neutron::Subnet
properties:
network_id: { get_resource: private_net }
cidr: 10.0.0.0/29
dns_nameservers: [ 8.8.8.8, 8.8.4.4 ]
router:
type: OS::Neutron::Router
properties:
external_gateway_info:
network: { get_param: external_net }
router_interface:
type: OS::Neutron::RouterInterface
properties:
router_id: { get_resource: router }
subnet_id: { get_resource: private_subnet }
master_image:
type: OS::Glance::Image
properties:
container_format: bare
disk_format: qcow2
location: https://cloud-images.ubuntu.com/releases/14.04.1/release/ubuntu-14.04-server-cloudimg-amd64-disk1.img
min_disk: 3
min_ram: 1000
name: shaker_image_build_template
master_image_server_port:
type: OS::Neutron::Port
properties:
network_id: { get_resource: private_net }
fixed_ips:
- subnet_id: { get_resource: private_subnet }
master_image_server:
type: OS::Nova::Server
properties:
name: shaker_image_builder_server
image: { get_resource: master_image }
flavor: { get_param: flavor }
networks:
- port: { get_resource: master_image_server_port }
user_data_format: RAW
user_data:
str_replace:
template: |
#!/bin/sh
sudo apt-add-repository "deb http://nova.clouds.archive.ubuntu.com/ubuntu/ trusty multiverse"
sudo apt-get update
sudo apt-get -y install iperf netperf git python-dev libzmq-dev screen
wget -O get-pip.py https://bootstrap.pypa.io/get-pip.py && sudo python get-pip.py
sudo pip install pbr netperf-wrapper
git clone git://git.openstack.org/stackforge/shaker && cd shaker && sudo pip install -r requirements.txt && sudo python setup.py develop
sudo shutdown -P -f now
params:
"$UNUSED": foo
outputs:
server_info:
value: { get_attr: [ master_image_server, show ] }

View File

@@ -192,8 +192,10 @@ def main():
# init conf and logging
conf = cfg.CONF
conf.register_cli_opts(config.COMMON_OPTS)
conf.register_cli_opts(config.OPENSTACK_OPTS)
conf.register_cli_opts(config.SERVER_OPTS)
conf.register_opts(config.COMMON_OPTS)
conf.register_opts(config.OPENSTACK_OPTS)
conf.register_opts(config.SERVER_OPTS)
try:
@@ -211,9 +213,11 @@ def main():
cfg.CONF.os_password,
cfg.CONF.os_tenant_name,
cfg.CONF.os_auth_url,
cfg.CONF.os_region_name or 'RegionOne',
cfg.CONF.os_region_name,
cfg.CONF.server_endpoint,
cfg.CONF.external_net)
cfg.CONF.external_net,
cfg.CONF.flavor_name,
cfg.CONF.image_name)
agents = deployment.deploy(scenario['deployment'])
if not agents:

View File

@@ -0,0 +1,34 @@
# Copyright (c) 2015 Mirantis Inc.
#
# 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.
from glanceclient import client as glance_client_pkg
GLANCE_VERSION = '1'
def create_client(keystone_client, os_region_name):
image_api_url = keystone_client.service_catalog.url_for(
service_type='image', region_name=os_region_name)
return glance_client_pkg.Client(GLANCE_VERSION,
endpoint=image_api_url,
token=keystone_client.auth_token)
def get_image(glance_client, image_name):
for image in glance_client.images.list():
if image.name == image_name:
return image
return None

View File

@@ -26,13 +26,12 @@ LOG = logging.getLogger(__name__)
HEAT_VERSION = '1'
def create_heat_client(keystone_client, os_region_name):
def create_client(keystone_client, os_region_name):
orchestration_api_url = keystone_client.service_catalog.url_for(
service_type='orchestration', region_name=os_region_name)
client = heat_client_pkg.Client(HEAT_VERSION,
endpoint=orchestration_api_url,
token=keystone_client.auth_token, )
return client
return heat_client_pkg.Client(HEAT_VERSION,
endpoint=orchestration_api_url,
token=keystone_client.auth_token)
def wait_stack_completion(heat_client, stack_id):
@@ -48,3 +47,9 @@ def wait_stack_completion(heat_client, stack_id):
if status != 'COMPLETE':
raise Exception(status)
def get_stack_outputs(heat_client, stack_id):
outputs_list = heat_client.stacks.get(stack_id).to_dict()['outputs']
return dict((item['output_key'], item['output_value'])
for item in outputs_list)

View File

@@ -18,13 +18,13 @@ from keystoneclient.v2_0 import client as keystone_v2
from keystoneclient.v3 import client as keystone_v3
def create_keystone_client(args):
discover = keystone_discover.Discover(**args)
def create_keystone_client(**kwargs):
discover = keystone_discover.Discover(**kwargs)
for version_data in discover.version_data():
version = version_data["version"]
if version[0] <= 2:
return keystone_v2.Client(**args)
return keystone_v2.Client(**kwargs)
elif version[0] == 3:
return keystone_v3.Client(**args)
return keystone_v3.Client(**kwargs)
raise Exception(
'Failed to discover keystone version for url %(auth_url)s.', **args)
'Failed to discover keystone version for url %(auth_url)s.', **kwargs)

View File

@@ -24,13 +24,12 @@ LOG = logging.getLogger(__name__)
NEUTRON_VERSION = '2.0'
def create_neutron_client(keystone_client, os_region_name):
def create_client(keystone_client, os_region_name):
network_api_url = keystone_client.service_catalog.url_for(
service_type='network', region_name=os_region_name)
client = neutron_client_pkg.Client(NEUTRON_VERSION,
endpoint_url=network_api_url,
token=keystone_client.auth_token, )
return client
return neutron_client_pkg.Client(NEUTRON_VERSION,
endpoint_url=network_api_url,
token=keystone_client.auth_token)
def choose_external_net(neutron_client):

View File

@@ -13,13 +13,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import time
from novaclient import client as nova_client_pkg
from shaker.openstack.common import log as logging
LOG = logging.getLogger(__name__)
NOVA_VERSION = '2'
def create_nova_client(keystone_client, os_region_name):
def create_client(keystone_client, os_region_name):
compute_api_url = keystone_client.service_catalog.url_for(
service_type='compute', region_name=os_region_name)
client = nova_client_pkg.Client(NOVA_VERSION,
@@ -30,3 +36,43 @@ def create_nova_client(keystone_client, os_region_name):
def get_compute_nodes(nova_client):
return nova_client.services.list(binary='nova-compute')
def is_flavor_exists(nova_client, flavor_name):
for flavor in nova_client.flavors.list():
if flavor.to_dict()['name'] == flavor_name:
return True
return False
def _poll_for_status(poll_fn, obj_id, final_ok_states, poll_period=20,
status_field="status"):
LOG.debug('Poll server %(id)s, waiting for any of statuses %(statuses)s',
dict(id=obj_id, statuses=final_ok_states))
while True:
obj = poll_fn(obj_id)
status = getattr(obj, status_field)
if status:
status = status.lower()
LOG.debug('Server %(id)s has status %(status)s',
dict(id=obj_id, status=status))
if status in final_ok_states:
break
elif status == "error":
raise Exception(obj.fault['message'])
time.sleep(poll_period)
def wait_server_shutdown(nova_client, server_id):
_poll_for_status(nova_client.servers.get, server_id, ['shutoff'])
def wait_server_snapshot(nova_client, server_id):
task_state_field = "OS-EXT-STS:task_state"
server = nova_client.servers.get(server_id)
if hasattr(server, task_state_field):
_poll_for_status(nova_client.servers.get, server.id, [None, '-', ''],
status_field=task_state_field)

View File

@@ -0,0 +1,44 @@
# Copyright (c) 2015 Mirantis Inc.
#
# 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.
from shaker.openstack.clients import glance
from shaker.openstack.clients import heat
from shaker.openstack.clients import keystone
from shaker.openstack.clients import neutron
from shaker.openstack.clients import nova
CLIENT_MAKERS = {
'glance': glance.create_client,
'heat': heat.create_client,
'neutron': neutron.create_client,
'nova': nova.create_client,
}
class OpenStackClient(object):
def __init__(self, username, password, tenant_name, auth_url, region_name):
super(OpenStackClient, self).__init__()
self.keystone_client = keystone.create_keystone_client(
username=username, password=password, tenant_name=tenant_name,
auth_url=auth_url)
self.region_name = region_name or 'RegionOne'
def __getattribute__(self, name):
if name in CLIENT_MAKERS:
return CLIENT_MAKERS[name](self.keystone_client, self.region_name)
else:
return super(OpenStackClient, self).__getattribute__(name)