From 98d3de26722c195b33f91168cf67ec35673069a4 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Sun, 25 Aug 2013 22:00:19 +0000 Subject: [PATCH] Implementing heat as an optional provisioning system * Adds a template * Adds create and delete functionality implements blueprint heat-integration Change-Id: I64b524c589620fb81867bee1ea5dd69ea9017132 --- requirements.txt | 1 + trove/common/cfg.py | 5 +++ trove/common/remote.py | 13 ++++++++ trove/common/template.py | 57 ++++++++++++++++++++++++++++++++ trove/taskmanager/models.py | 65 +++++++++++++++++++++++++++++++++++-- 5 files changed, 139 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d275fe19a7..22afb31779 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ sqlalchemy-migrate>=0.7.2 netaddr httplib2 lxml +python-heatclient>=0.2.3 python-novaclient python-cinderclient>=1.0.4 python-keystoneclient diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 3970750707..7705ef67fc 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -50,6 +50,7 @@ common_opts = [ help='Remote implementation for using fake integration code'), cfg.StrOpt('nova_compute_url', default='http://localhost:8774/v2'), cfg.StrOpt('cinder_url', default='http://localhost:8776/v2'), + cfg.StrOpt('heat_url', default='http://localhost:8004/v1'), cfg.StrOpt('swift_url', default='http://localhost:8080/v1/AUTH_'), cfg.StrOpt('trove_auth_url', default='http://0.0.0.0:5000/v2.0'), cfg.StrOpt('host', default='0.0.0.0'), @@ -100,6 +101,7 @@ common_opts = [ help='default driver to use for quota checks'), cfg.StrOpt('taskmanager_queue', default='taskmanager'), cfg.BoolOpt('use_nova_server_volume', default=False), + cfg.BoolOpt('use_heat', default=False), cfg.StrOpt('fake_mode_events', default='simulated'), cfg.StrOpt('device_path', default='/dev/vdb'), cfg.StrOpt('mount_point', default='/var/lib/mysql'), @@ -107,6 +109,7 @@ common_opts = [ cfg.StrOpt('block_device_mapping', default='vdb'), cfg.IntOpt('server_delete_time_out', default=2), cfg.IntOpt('volume_time_out', default=2), + cfg.IntOpt('heat_time_out', default=60), cfg.IntOpt('reboot_time_out', default=60 * 2), cfg.StrOpt('service_options', default=['mysql']), cfg.IntOpt('dns_time_out', default=60 * 2), @@ -166,6 +169,8 @@ common_opts = [ default='trove.common.remote.nova_client'), cfg.StrOpt('remote_cinder_client', default='trove.common.remote.cinder_client'), + cfg.StrOpt('remote_heat_client', + default='trove.common.remote.heat_client'), cfg.StrOpt('remote_swift_client', default='trove.common.remote.swift_client'), cfg.StrOpt('exists_notification_transformer', diff --git a/trove/common/remote.py b/trove/common/remote.py index 3fdf2a9f60..f6b91d75bb 100644 --- a/trove/common/remote.py +++ b/trove/common/remote.py @@ -18,6 +18,7 @@ from trove.common import cfg from trove.openstack.common.importutils import import_class from cinderclient.v2 import client as CinderClient +from heatclient.v1 import client as HeatClient from novaclient.v1_1.client import Client from swiftclient.client import Connection @@ -28,6 +29,7 @@ PROXY_AUTH_URL = CONF.trove_auth_url VOLUME_URL = CONF.cinder_url OBJECT_STORE_URL = CONF.swift_url USE_SNET = CONF.backup_use_snet +HEAT_URL = CONF.heat_url def dns_client(context): @@ -68,6 +70,16 @@ def cinder_client(context): return client +def heat_client(context): + endpoint = "%s/%s/" % (HEAT_URL, context.tenant) + client = HeatClient.Client(username=context.user, + password="radmin", + token=context.auth_token, + os_no_client_auth=True, + endpoint=endpoint) + return client + + def swift_client(context): client = Connection(preauthurl=OBJECT_STORE_URL + context.tenant, preauthtoken=context.auth_token, @@ -81,3 +93,4 @@ create_guest_client = import_class(CONF.remote_guest_client) create_nova_client = import_class(CONF.remote_nova_client) create_swift_client = import_class(CONF.remote_swift_client) create_cinder_client = import_class(CONF.remote_cinder_client) +create_heat_client = import_class(CONF.remote_heat_client) diff --git a/trove/common/template.py b/trove/common/template.py index f1ed754ddc..8edb355bfa 100644 --- a/trove/common/template.py +++ b/trove/common/template.py @@ -45,3 +45,60 @@ class SingleInstanceConfigTemplate(object): self.config_contents = self.template.render( flavor=self.flavor_dict) return self.config_contents + + +class HeatTemplate(object): + template_contents = """HeatTemplateFormatVersion: '2012-12-12' +Description: Instance creation +Parameters: + KeyName: {Type: String} + Flavor: {Type: String} + VolumeSize: {Type: Number} + ServiceType: {Type: String} + InstanceId: {Type: String} +Resources: + BaseInstance: + Type: AWS::EC2::Instance + Metadata: + AWS::CloudFormation::Init: + config: + files: + /etc/guest_info: + content: + Fn::Join: + - '' + - ["[DEFAULT]\\nguest_id=", {Ref: InstanceId}, + "\\nservice_type=", {Ref: ServiceType}] + mode: '000644' + owner: root + group: root + Properties: + ImageId: + Fn::Join: + - '' + - ["ubuntu_", {Ref: ServiceType}] + InstanceType: {Ref: Flavor} + KeyName: {Ref: KeyName} + UserData: + Fn::Base64: + Fn::Join: + - '' + - ["#!/bin/bash -v\\n", + "/opt/aws/bin/cfn-init\\n", + "sudo service trove-guest start\\n"] + DataVolume: + Type: AWS::EC2::Volume + Properties: + Size: {Ref: VolumeSize} + AvailabilityZone: nova + Tags: + - {Key: Usage, Value: Test} + MountPoint: + Type: AWS::EC2::VolumeAttachment + Properties: + InstanceId: {Ref: BaseInstance} + VolumeId: {Ref: DataVolume} + Device: /dev/vdb""" + + def template(self): + return self.template_contents diff --git a/trove/taskmanager/models.py b/trove/taskmanager/models.py index cc0957344a..55d8eda5aa 100644 --- a/trove/taskmanager/models.py +++ b/trove/taskmanager/models.py @@ -17,6 +17,9 @@ import os.path from cinderclient import exceptions as cinder_exceptions from eventlet import greenthread from novaclient import exceptions as nova_exceptions +from novaclient import base +from novaclient.v1_1 import servers +from novaclient.v1_1 import volumes from trove.common import cfg from trove.common import template from trove.common import utils @@ -26,12 +29,15 @@ from trove.common.exception import PollTimeOut from trove.common.exception import VolumeCreationFailure from trove.common.exception import TroveError from trove.common.remote import create_dns_client +from trove.common.remote import create_nova_client +from trove.common.remote import create_heat_client from trove.common.remote import create_cinder_client from swiftclient.client import ClientException from trove.common.utils import poll_until from trove.instance import models as inst_models from trove.instance.models import BuiltInstance from trove.instance.models import FreshInstance + from trove.instance.models import InstanceStatus from trove.instance.models import InstanceServiceStatus from trove.instance.models import ServiceStatuses @@ -49,10 +55,12 @@ VOLUME_TIME_OUT = CONF.volume_time_out # seconds. DNS_TIME_OUT = CONF.dns_time_out # seconds. RESIZE_TIME_OUT = CONF.resize_time_out # seconds. REVERT_TIME_OUT = CONF.revert_time_out # seconds. +HEAT_TIME_OUT = CONF.heat_time_out # seconds. USAGE_SLEEP_TIME = CONF.usage_sleep_time # seconds. USAGE_TIMEOUT = CONF.usage_timeout # seconds. use_nova_server_volume = CONF.use_nova_server_volume +use_heat = CONF.use_heat class NotifyMixin(object): @@ -128,7 +136,14 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def create_instance(self, flavor, image_id, databases, users, service_type, volume_size, security_groups, backup_id): - if use_nova_server_volume: + if use_heat: + server, volume_info = self._create_server_volume_heat( + flavor, + image_id, + security_groups, + service_type, + volume_size) + elif use_nova_server_volume: server, volume_info = self._create_server_volume( flavor['id'], image_id, @@ -142,6 +157,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): security_groups, service_type, volume_size) + try: self._create_dns_entry() except Exception as e: @@ -237,6 +253,42 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): return server, volume_info + def _create_server_volume_heat(self, flavor, image_id, + security_groups, service_type, + volume_size): + client = create_heat_client(self.context) + novaclient = create_nova_client(self.context) + cinderclient = create_cinder_client(self.context) + heat_template = template.HeatTemplate().template() + parameters = {"KeyName": "heatkey", + "Flavor": flavor["name"], + "VolumeSize": volume_size, + "ServiceType": "mysql", + "InstanceId": self.id} + stack_name = 'trove-%s' % self.id + stack = client.stacks.create(stack_name=stack_name, + template=heat_template, + parameters=parameters) + stack = client.stacks.get(stack_name) + + utils.poll_until( + lambda: client.stacks.get(stack_name), + lambda stack: stack.stack_status in ['CREATE_COMPLETE', + 'CREATE_FAILED'], + sleep_time=2, + time_out=HEAT_TIME_OUT) + + resource = client.resources.get(stack.id, 'BaseInstance') + server = novaclient.servers.get(resource.physical_resource_id) + + resource = client.resources.get(stack.id, 'DataVolume') + volume = cinderclient.volumes.get(resource.physical_resource_id) + volume_info = self._build_volume(volume) + + self.update_db(compute_instance_id=server.id, volume_id=volume.id) + + return server, volume_info + def _create_server_volume_individually(self, flavor_id, image_id, security_groups, service_type, volume_size): @@ -305,6 +357,9 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): v_ref = volume_client.volumes.get(volume_ref.id) if v_ref.status in ['error']: raise VolumeCreationFailure() + return self._build_volume(v_ref) + + def _build_volume(self, v_ref): LOG.debug(_("Created volume %s") % v_ref) # The mapping is in the format: # :[]:[]:[] @@ -417,7 +472,13 @@ class BuiltInstanceTasks(BuiltInstance, NotifyMixin, ConfigurationMixin): server_id = self.db_info.compute_instance_id old_server = self.nova_client.servers.get(server_id) try: - self.server.delete() + if use_heat: + # Delete the server via heat + heatclient = create_heat_client(self.context) + name = 'trove-%s' % self.id + heatclient.stacks.delete(name) + else: + self.server.delete() except Exception as ex: LOG.error("Error during delete compute server %s " % self.server.id)