Merge pull request #106 from sacharya/async-create

Async instance create operation
This commit is contained in:
Michael Basnight 2012-06-08 07:22:26 -07:00
commit c3380d3735
7 changed files with 401 additions and 140 deletions

View File

@ -0,0 +1,43 @@
# Copyright 2012 OpenStack LLC.
#
# 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 sqlalchemy.schema import Column
from sqlalchemy.schema import MetaData
from reddwarf.db.sqlalchemy.migrate_repo.schema import String
from reddwarf.db.sqlalchemy.migrate_repo.schema import Table
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
# add column:
instances = Table('instances', meta, autoload=True)
volume_size = Column('volume_size', String(36))
flavor_id = Column('flavor_id', String(36))
instances.create_column(flavor_id)
instances.create_column(volume_size)
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
# drop column:
instances = Table('instances', meta, autoload=True)
instances.drop_column('flavor_id')
instances.drop_column('volume_size')

View File

@ -77,10 +77,12 @@ class DnsManager(object):
Use instance by default Use instance by default
""" """
dns_support = config.Config.get('reddwarf_dns_support', 'False') dns_support = config.Config.get('reddwarf_dns_support', 'False')
LOG.debug(_("reddwarf dns support = %s") % dns_support)
if utils.bool_from_string(dns_support): if utils.bool_from_string(dns_support):
entry = self.entry_factory.create_entry(instance.id) entry = self.entry_factory.create_entry(instance.id)
instance.hostname = entry.name instance.hostname = entry.name
instance.save() instance.save()
LOG.debug("Saved the hostname as %s " % instance.hostname)
else: else:
instance.hostname = instance.name instance.hostname = instance.name
instance.save() instance.save()

View File

@ -131,7 +131,119 @@ SERVER_INVALID_ACTION_STATUSES = ["BUILD", "REBOOT", "REBUILD"]
VALID_ACTION_STATUSES = ["ACTIVE"] VALID_ACTION_STATUSES = ["ACTIVE"]
class Instance(object): class SimpleInstance(object):
"""
Simple model is a quick hack for when server/volumes is not available, and
all we have is database info. Example is create instance response, when the
async call to server/volume may not have completed yet.
"""
def __init__(self, context, db_info, service_status):
self.context = context
self.db_info = db_info
self.service_status = service_status
self.volumes = [{'size': self.db_info.volume_size }]
@staticmethod
def load(context, id):
if context is None:
raise TypeError("Argument context not defined.")
elif id is None:
raise TypeError("Argument id not defined.")
try:
db_info = DBInstance.find_by(id=id)
except rd_exceptions.NotFound:
raise rd_exceptions.NotFound(uuid=id)
task_status = db_info.task_status
service_status = InstanceServiceStatus.find_by(instance_id=id)
LOG.info("service status=%s" % service_status)
return SimpleInstance(context, db_info, service_status)
@property
def name(self):
return self.db_info.name
@property
def id(self):
return self.db_info.id
@property
def hostname(self):
return self.db_info.hostname
@property
def status(self):
# If the service status is NEW, then we are building.
if ServiceStatuses.NEW == self.service_status.status:
return InstanceStatus.BUILD
@property
def created(self):
return self.db_info.created
@property
def updated(self):
return self.db_info.updated
@property
def addresses(self):
return None
@property
def is_building(self):
return self.status in [InstanceStatus.BUILD]
@property
def is_sql_running(self):
"""True if the service status indicates MySQL is up and running."""
return self.service_status.status in MYSQL_RESPONSIVE_STATUSES
@property
def links(self):
"""
The links here are just used for structural format. The actual link is
created in the views by replacing the matching pieces from the request
"""
links = [
{
"href": "https://localhost/v1.0/tenant_id/instances/instance_id",
"rel": "self"
},
{
"href": "https://localhost/instances/instance_id",
"rel": "bookmark"
}
]
return links
@property
def flavor_links(self):
links = [
{
"href": "https://localhost/v1.0/tenant_id/flavors/flavor_id",
"rel": "self"
},
{
"href": "https://localhost/flavors/flavor_id",
"rel": "bookmark"
}
]
return links
def restart(self):
# Just so it doesnt blow if restart is accidentally called
raise rd_exceptions.UnprocessableEntity("Instance %s is not ready."
% self.id)
def delete(self):
# Just so it doesnt blow if delete is accidentally called
raise rd_exceptions.UnprocessableEntity("Instance %s is not ready."
% self.id)
class Instance(SimpleInstance):
"""Represents an instance. """Represents an instance.
The life span of this object should be limited. Do not store them or The life span of this object should be limited. Do not store them or
@ -139,10 +251,8 @@ class Instance(object):
""" """
def __init__(self, context, db_info, server, service_status, volumes): def __init__(self, context, db_info, server, service_status, volumes):
self.context = context super(Instance, self).__init__(context, db_info, service_status)
self.db_info = db_info
self.server = server self.server = server
self.service_status = service_status
self.volumes = volumes self.volumes = volumes
@staticmethod @staticmethod
@ -155,11 +265,19 @@ class Instance(object):
db_info = DBInstance.find_by(id=id) db_info = DBInstance.find_by(id=id)
except rd_exceptions.NotFound: except rd_exceptions.NotFound:
raise rd_exceptions.NotFound(uuid=id) raise rd_exceptions.NotFound(uuid=id)
server, volumes = load_server_with_volumes(context, db_info.id,
db_info.compute_instance_id)
task_status = db_info.task_status task_status = db_info.task_status
service_status = InstanceServiceStatus.find_by(instance_id=id) service_status = InstanceServiceStatus.find_by(instance_id=id)
LOG.info("service status=%s" % service_status) LOG.info("service status=%s" % service_status)
if db_info.compute_instance_id is None:
LOG.debug("Missing server_id for instance %s " % db_info.id)
# TODO: Should it raise exception or return SimpleInstance?
# If I return SimpleInstance, somebody will invoke delete on it and
# it will be method not found.
return SimpleInstance(context, db_info, service_status)
server, volumes = load_server_with_volumes(context, db_info.id,
db_info.compute_instance_id)
return Instance(context, db_info, server, service_status, volumes) return Instance(context, db_info, server, service_status, volumes)
def delete(self, force=False): def delete(self, force=False):
@ -173,142 +291,33 @@ class Instance(object):
self.db_info.save() self.db_info.save()
task_api.API(self.context).delete_instance(self.id) task_api.API(self.context).delete_instance(self.id)
@classmethod
def _create_volume(cls, context, db_info, volume_size):
volume_support = config.Config.get("reddwarf_volume_support", 'False')
LOG.debug(_("reddwarf volume support = %s") % volume_support)
if utils.bool_from_string(volume_support):
LOG.debug(_("Starting to create the volume for the instance"))
volume_client = create_nova_volume_client(context)
volume_desc = ("mysql volume for %s" % db_info.id)
volume_ref = volume_client.volumes.create(
volume_size,
display_name="mysql-%s" % db_info.id,
display_description=volume_desc)
# Record the volume ID in case something goes wrong.
db_info.volume_id = volume_ref.id
db_info.save()
#TODO(cp16net) this is bad to wait here for the volume create
# before returning but this was a quick way to get it working
# for now we need this to go into the task manager
v_ref = volume_client.volumes.get(volume_ref.id)
while not v_ref.status in ['available', 'error']:
LOG.debug(_("waiting for volume [volume.status=%s]") %
v_ref.status)
greenthread.sleep(1)
v_ref = volume_client.volumes.get(volume_ref.id)
if v_ref.status in ['error']:
raise rd_exceptions.VolumeCreationFailure()
LOG.debug(_("Created volume %s") % v_ref)
# The mapping is in the format:
# <id>:[<type>]:[<size(GB)>]:[<delete_on_terminate>]
# setting the delete_on_terminate instance to true=1
mapping = "%s:%s:%s:%s" % (v_ref.id, '', v_ref.size, 1)
bdm = CONFIG.get('block_device_mapping', 'vdb')
block_device = {bdm: mapping}
volumes = [{'id': v_ref.id,
'size': v_ref.size}]
LOG.debug("block_device = %s" % block_device)
LOG.debug("volume = %s" % volumes)
device_path = CONFIG.get('device_path', '/dev/vdb')
mount_point = CONFIG.get('mount_point', '/var/lib/mysql')
LOG.debug(_("device_path = %s") % device_path)
LOG.debug(_("mount_point = %s") % mount_point)
else:
LOG.debug(_("Skipping setting up the volume"))
block_device = None
device_path = None
mount_point = None
volumes = None
#end volume_support
#block_device = ""
#device_path = /dev/vdb
#mount_point = /var/lib/mysql
volume_info = {'block_device': block_device,
'device_path': device_path,
'mount_point': mount_point,
'volumes': volumes}
return volume_info
@classmethod @classmethod
def create(cls, context, name, flavor_ref, image_id, def create(cls, context, name, flavor_ref, image_id,
databases, service_type, volume_size): databases, service_type, volume_size):
db_info = DBInstance.create(name=name, flavor_id = utils.get_id_from_href(flavor_ref)
task_status=InstanceTasks.NONE) db_info = DBInstance.create(name=name, volume_size=volume_size,
flavor_id =flavor_id, task_status=InstanceTasks.NONE)
LOG.debug(_("Created new Reddwarf instance %s...") % db_info.id) LOG.debug(_("Created new Reddwarf instance %s...") % db_info.id)
if volume_size: task_api.API(context).create_instance(db_info.id, name,
volume_info = cls._create_volume(context, db_info, volume_size) flavor_ref, image_id, databases, service_type,
block_device_mapping = volume_info['block_device'] volume_size)
device_path = volume_info['device_path'] # Defaults the hostname to instance name of dns is disabled.
mount_point = volume_info['mount_point']
volumes = volume_info['volumes']
else:
block_device_mapping = None
device_path = None
mount_point = None
volumes = []
client = create_nova_client(context)
files = {"/etc/guest_info": "guest_id=%s\nservice_type=%s\n" %
(db_info.id, service_type)}
server = client.servers.create(name, image_id, flavor_ref,
files=files,
block_device_mapping=block_device_mapping)
LOG.debug(_("Created new compute instance %s.") % server.id)
db_info.compute_instance_id = server.id
db_info.save()
service_status = InstanceServiceStatus.create(instance_id=db_info.id,
status=ServiceStatuses.NEW)
# Now wait for the response from the create to do additional work
guest = create_guest_client(context, db_info.id)
# populate the databases
model_schemas = populate_databases(databases)
guest.prepare(512, model_schemas, users=[],
device_path=device_path,
mount_point=mount_point)
dns_support = config.Config.get("reddwarf_dns_support", 'False')
LOG.debug(_("reddwarf dns support = %s") % dns_support)
dns_client = create_dns_client(context) dns_client = create_dns_client(context)
# Default the hostname to instance name if no dns support
dns_client.update_hostname(db_info) dns_client.update_hostname(db_info)
if utils.bool_from_string(dns_support):
def get_server(): #Check to see if a New status has already been created
return client.servers.get(server.id) service_status = InstanceServiceStatus.get_by(instance_id=db_info.id)
if service_status is None:
service_status = InstanceServiceStatus.create(
instance_id=db_info.id,
status=ServiceStatuses.NEW)
def ip_is_available(server): return SimpleInstance(context, db_info, service_status)
if server.addresses != {}:
return True
elif server.addresses == {} and\
server.status != InstanceStatus.ERROR:
return False
elif server.addresses == {} and\
server.status == InstanceStatus.ERROR:
LOG.error(_("Instance IP not available, instance (%s): server had "
" status (%s).") % (db_info['id'], server.status))
raise rd_exceptions.ReddwarfError(
status=server.status)
poll_until(get_server, ip_is_available, sleep_time=1, time_out=60*2)
dns_client.create_instance_entry(db_info['id'],
get_ip_address(server.addresses))
return Instance(context, db_info, server, service_status, volumes)
def get_guest(self): def get_guest(self):
return create_guest_client(self.context, self.db_info.id) return create_guest_client(self.context, self.db_info.id)
@property
def id(self):
return self.db_info.id
@property @property
def is_building(self): def is_building(self):
return self.status in [InstanceStatus.BUILD] return self.status in [InstanceStatus.BUILD]
@ -350,14 +359,6 @@ class Instance(object):
# For everything else we can look at the service status mapping. # For everything else we can look at the service status mapping.
return self.service_status.status.api_status return self.service_status.status.api_status
@property
def created(self):
return self.db_info.created
@property
def updated(self):
return self.db_info.updated
@property @property
def flavor(self): def flavor(self):
return self.server.flavor return self.server.flavor

View File

@ -59,7 +59,7 @@ class InstanceView(object):
} }
dns_support = config.Config.get("reddwarf_dns_support", 'False') dns_support = config.Config.get("reddwarf_dns_support", 'False')
if utils.bool_from_string(dns_support): if utils.bool_from_string(dns_support):
instance_dict['hostname'] = self.instance.db_info.hostname instance_dict['hostname'] = self.instance.hostname
if self.add_addresses and ip is not None and len(ip) > 0: if self.add_addresses and ip is not None and len(ip) > 0:
instance_dict['ip'] = ip instance_dict['ip'] = ip
if self.add_volumes and volumes is not None: if self.add_volumes and volumes is not None:
@ -118,10 +118,51 @@ class InstanceDetailView(InstanceView):
def data(self): def data(self):
result = super(InstanceDetailView, self).data() result = super(InstanceDetailView, self).data()
result['instance']['created'] = self.instance.created result['instance']['created'] = self.instance.created
result['instance']['flavor'] = self.instance.flavor result['instance']['flavor'] = self._build_flavor()
result['instance']['updated'] = self.instance.updated result['instance']['updated'] = self.instance.updated
return result return result
def _build_flavor(self):
try:
return self.instance.flavor
except:
return {
'id': self.instance.db_info.flavor_id,
'links': self._build_flavor_links(),
}
def _build_flavor_links(self):
result = []
#scheme = self.req.scheme
scheme = 'https' # Forcing https
endpoint = self.req.host
splitpath = self.req.path.split('/')
detailed = ''
if splitpath[-1] == 'detail':
detailed = '/detail'
splitpath.pop(-1)
flavorid = self.instance.db_info.flavor_id
if str(splitpath[-1]) == str(flavorid):
splitpath.pop(-1)
href_template = "%(scheme)s://%(endpoint)s%(path)s/%(flavorid)s"
for link in self.instance.flavor_links:
rlink = link
href = rlink['href']
if rlink['rel'] == 'self':
path = '/'.join(splitpath)
href = href_template % locals()
elif rlink['rel'] == 'bookmark':
splitpath.pop(2) # Remove the version.
splitpath.pop(1) # Remove the tenant id.
path = '/'.join(splitpath)
href = href_template % locals()
rlink['href'] = href
result.append(rlink)
for link in result:
link['href'] = link['href'].replace('instances', 'flavors')
return result
class InstancesView(object): class InstancesView(object):

View File

@ -73,3 +73,11 @@ class API(object):
def delete_instance(self, instance_id): def delete_instance(self, instance_id):
LOG.debug("Making async call to delete instance: %s" % instance_id) LOG.debug("Making async call to delete instance: %s" % instance_id)
self._cast("delete_instance", instance_id=instance_id) self._cast("delete_instance", instance_id=instance_id)
def create_instance(self, instance_id, name, flavor_ref, image_id,
databases, service_type, volume_size):
LOG.debug("Making async call to create instance %s " % instance_id)
self._cast("create_instance", instance_id=instance_id, name=name,
flavor_ref=flavor_ref, image_id=image_id,
databases=databases, service_type=service_type,
volume_size=volume_size)

View File

@ -23,6 +23,7 @@ from eventlet import greenthread
from reddwarf.common import service from reddwarf.common import service
from reddwarf.taskmanager import models from reddwarf.taskmanager import models
from reddwarf.taskmanager.models import InstanceTasks
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -67,4 +68,11 @@ class TaskManager(service.Manager):
instance_tasks = models.InstanceTasks.load(context, instance_id) instance_tasks = models.InstanceTasks.load(context, instance_id)
instance_tasks.delete_instance() instance_tasks.delete_instance()
def create_instance(self, context, instance_id, name, flavor_ref,
image_id, databases, service_type, volume_size):
instance_tasks = InstanceTasks(context)
instance_tasks.create_instance(instance_id, name, flavor_ref,
image_id, databases,
service_type, volume_size)

View File

@ -21,9 +21,21 @@ from reddwarf.common import config
from reddwarf.common import remote from reddwarf.common import remote
from reddwarf.common import utils from reddwarf.common import utils
from reddwarf.common.exception import PollTimeOut from reddwarf.common.exception import PollTimeOut
from reddwarf.common.exception import VolumeCreationFailure
from reddwarf.common.exception import NotFound
from reddwarf.common.exception import ReddwarfError from reddwarf.common.exception import ReddwarfError
from reddwarf.common.remote import create_dns_client from reddwarf.common.remote import create_dns_client
from reddwarf.common.remote import create_nova_client
from reddwarf.common.remote import create_nova_volume_client
from reddwarf.common.remote import create_guest_client
from reddwarf.common.utils import poll_until
from reddwarf.instance import models as inst_models from reddwarf.instance import models as inst_models
from reddwarf.instance.models import DBInstance
from reddwarf.instance.models import InstanceStatus
from reddwarf.instance.models import InstanceServiceStatus
from reddwarf.instance.models import populate_databases
from reddwarf.instance.models import ServiceStatuses
from reddwarf.instance.views import get_ip_address
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -34,7 +46,7 @@ class InstanceTasks:
Performs the various asynchronous instance related tasks. Performs the various asynchronous instance related tasks.
""" """
def __init__(self, context, db_info, server, volumes, def __init__(self, context, db_info=None, server=None, volumes=None,
nova_client=None, volume_client=None, guest=None): nova_client=None, volume_client=None, guest=None):
self.context = context self.context = context
self.db_info = db_info self.db_info = db_info
@ -179,6 +191,152 @@ class InstanceTasks:
self.db_info.task_status = inst_models.InstanceTasks.NONE self.db_info.task_status = inst_models.InstanceTasks.NONE
self.db_info.save() self.db_info.save()
def create_instance(self, instance_id, name, flavor_ref,
image_id, databases, service_type, volume_size):
LOG.info("Entering create_instance")
try:
db_info = DBInstance.find_by(id=instance_id)
volume_info = self._create_volume(instance_id,
volume_size)
block_device_mapping = volume_info['block_device']
server = self._create_server(instance_id, name,
flavor_ref, image_id, service_type, block_device_mapping)
LOG.info("server id: %s" % server)
server_id = server.id
self._create_dns_entry(instance_id, server_id)
LOG.info("volume_info %s " % volume_info)
self._guest_prepare(server, db_info, volume_info, databases)
except Exception, e:
LOG.error(e)
self._log_service_status(instance_id, ServiceStatuses.UNKNOWN)
def _create_volume(self, instance_id, volume_size):
LOG.info("Entering create_volume")
LOG.debug(_("Starting to create the volume for the instance"))
volume_support = config.Config.get("reddwarf_volume_support", 'False')
LOG.debug(_("reddwarf volume support = %s") % volume_support)
if volume_size is None or \
utils.bool_from_string(volume_support) is False:
volume_info = {'block_device': None,
'device_path': None,
'mount_point': None,
'volumes': None}
return volume_info
db_info = DBInstance.find_by(id=instance_id)
volume_client = create_nova_volume_client(self.context)
volume_desc = ("mysql volume for %s" % instance_id)
volume_ref = volume_client.volumes.create(
volume_size,
display_name="mysql-%s" % db_info.id,
display_description=volume_desc)
# Record the volume ID in case something goes wrong.
db_info.volume_id = volume_ref.id
db_info.save()
utils.poll_until(
lambda: volume_client.volumes.get(volume_ref.id),
lambda v_ref: v_ref.status in ['available', 'error'],
sleep_time=2,
time_out=2 * 60)
v_ref = volume_client.volumes.get(volume_ref.id)
if v_ref.status in ['error']:
raise VolumeCreationFailure()
LOG.debug(_("Created volume %s") % v_ref)
# The mapping is in the format:
# <id>:[<type>]:[<size(GB)>]:[<delete_on_terminate>]
# setting the delete_on_terminate instance to true=1
mapping = "%s:%s:%s:%s" % (v_ref.id, '', v_ref.size, 1)
bdm = config.Config.get('block_device_mapping', 'vdb')
block_device = {bdm: mapping}
volumes = [{'id': v_ref.id,
'size': v_ref.size}]
LOG.debug("block_device = %s" % block_device)
LOG.debug("volume = %s" % volumes)
device_path = config.Config.get('device_path', '/dev/vdb')
mount_point = config.Config.get('mount_point', '/var/lib/mysql')
LOG.debug(_("device_path = %s") % device_path)
LOG.debug(_("mount_point = %s") % mount_point)
volume_info = {'block_device': block_device,
'device_path': device_path,
'mount_point': mount_point,
'volumes': volumes}
return volume_info
def _create_server(self, instance_id, name, flavor_ref, image_id,
service_type, block_device_mapping):
nova_client = create_nova_client(self.context)
files = {"/etc/guest_info": "guest_id=%s\nservice_type=%s\n" %
(instance_id, service_type)}
server = nova_client.servers.create(name, image_id, flavor_ref,
files=files, block_device_mapping=block_device_mapping)
LOG.debug(_("Created new compute instance %s.") % server.id)
return server
def _guest_prepare(self, server, db_info, volume_info, databases):
LOG.info("Entering guest_prepare")
db_info.compute_instance_id = server.id
db_info.save()
self._log_service_status(db_info, ServiceStatuses.NEW)
# Now wait for the response from the create to do additional work
guest = create_guest_client(self.context, db_info.id)
# populate the databases
model_schemas = populate_databases(databases)
guest.prepare(512, model_schemas, users=[],
device_path=volume_info['device_path'],
mount_point=volume_info['mount_point'])
def _create_dns_entry(self, instance_id, server_id):
LOG.debug("%s: Creating dns entry for instance: %s"
% (greenthread.getcurrent(), instance_id))
dns_client = create_dns_client(self.context)
dns_support = config.Config.get("reddwarf_dns_support", 'False')
LOG.debug(_("reddwarf dns support = %s") % dns_support)
nova_client = create_nova_client(self.context)
if utils.bool_from_string(dns_support):
def get_server():
return nova_client.servers.get(server_id)
def ip_is_available(server):
LOG.info("Polling for ip addresses: $%s " % server.addresses)
if server.addresses != {}:
return True
elif server.addresses == {} and\
server.status != InstanceStatus.ERROR:
return False
elif server.addresses == {} and\
server.status == InstanceStatus.ERROR:
LOG.error(_("Instance IP not available, instance (%s): "
"server had status (%s).")
% (instance_id, server.status))
raise ReddwarfError(status=server.status)
poll_until(get_server, ip_is_available,
sleep_time=1, time_out=60 * 2)
server = nova_client.servers.get(server_id)
LOG.info("Creating dns entry...")
dns_client.create_instance_entry(instance_id,
get_ip_address(server.addresses))
def _log_service_status(self, instance_id, status):
LOG.info("Saving service status %s for instance %s "
% (status, instance_id))
service_status = InstanceServiceStatus.get_by(instance_id=instance_id)
if service_status:
service_status.status = status
service_status.save()
else:
InstanceServiceStatus.create(instance_id=instance_id,
status=status)
def _refresh_compute_server_info(self): def _refresh_compute_server_info(self):
"""Refreshes the compute server field.""" """Refreshes the compute server field."""
server = self.nova_client.servers.get(self.server.id) server = self.nova_client.servers.get(self.server.id)