From c7d93e7ce74c20a9c59ee0d2d542ed5b54d9c39c Mon Sep 17 00:00:00 2001 From: Nirmal Ranganathan Date: Sun, 17 Jun 2012 11:01:14 -0500 Subject: [PATCH] Refactoring the instance load method Adding back the volume used parameter. Added timeouts for all guest sync calls. Refactored the instance models. --- bin/reddwarf-manage | 3 +- etc/reddwarf/reddwarf-taskmanager.conf.sample | 8 + etc/reddwarf/reddwarf.conf.sample | 7 +- etc/reddwarf/reddwarf.conf.test | 8 + reddwarf/common/exception.py | 15 ++ reddwarf/common/wsgi.py | 17 +- reddwarf/db/models.py | 92 +++++++ reddwarf/db/sqlalchemy/mappers.py | 2 + reddwarf/db/sqlalchemy/session.py | 2 + reddwarf/dns/models.py | 10 +- reddwarf/dns/rsdns/driver.py | 3 - reddwarf/extensions/mysql/service.py | 3 +- reddwarf/flavor/models.py | 8 +- reddwarf/guestagent/api.py | 53 ++-- reddwarf/guestagent/dbaas.py | 1 + reddwarf/guestagent/models.py | 65 +++++ reddwarf/guestagent/query.py | 3 +- reddwarf/instance/models.py | 247 ++++++++---------- reddwarf/instance/service.py | 10 +- reddwarf/instance/views.py | 10 +- reddwarf/taskmanager/api.py | 1 + reddwarf/taskmanager/manager.py | 2 - reddwarf/taskmanager/models.py | 2 +- reddwarf/tests/fakes/guestagent.py | 6 + reddwarf/tests/fakes/nova.py | 5 +- 25 files changed, 383 insertions(+), 200 deletions(-) create mode 100644 reddwarf/db/models.py create mode 100644 reddwarf/guestagent/models.py diff --git a/bin/reddwarf-manage b/bin/reddwarf-manage index 4a53e5bb3a..0bc1cf7f8b 100755 --- a/bin/reddwarf-manage +++ b/bin/reddwarf-manage @@ -74,7 +74,8 @@ class Commands(object): def image_update(self, service_name, image_id): db_api.configure_db(self.conf) - image = db_api.find_by(instance_models.ServiceImage, service_name=service_name) + image = db_api.find_by(instance_models.ServiceImage, + service_name=service_name) if image is None: # Create a new one image = instance_models.ServiceImage() diff --git a/etc/reddwarf/reddwarf-taskmanager.conf.sample b/etc/reddwarf/reddwarf-taskmanager.conf.sample index 49d6619c62..5255e4c77b 100644 --- a/etc/reddwarf/reddwarf-taskmanager.conf.sample +++ b/etc/reddwarf/reddwarf-taskmanager.conf.sample @@ -33,6 +33,9 @@ nova_volume_url = http://localhost:8776/v1 # Config options for enabling volume service reddwarf_volume_support = True +block_device_mapping = /var/lib/mysql +device_path = /var/lib/mysql +mount_point = /var/lib/mysql volume_time_out=30 # Configuration options for talking to nova via the novaclient. @@ -50,6 +53,11 @@ taskmanager_manager=reddwarf.taskmanager.manager.TaskManager # Reddwarf DNS reddwarf_dns_support = False +# Guest related conf +agent_heartbeat_time = 10 +agent_call_low_timeout = 5 +agent_call_high_timeout = 100 + # ============ notifer queue kombu connection options ======================== notifier_queue_hostname = localhost diff --git a/etc/reddwarf/reddwarf.conf.sample b/etc/reddwarf/reddwarf.conf.sample index 43cdd0bed9..120b415dbb 100644 --- a/etc/reddwarf/reddwarf.conf.sample +++ b/etc/reddwarf/reddwarf.conf.sample @@ -47,7 +47,7 @@ add_addresses = True # Config options for enabling volume service reddwarf_volume_support = True block_device_mapping = /var/lib/mysql -device_path = /dev/vdb +device_path = /var/lib/mysql mount_point = /var/lib/mysql max_accepted_volume_size = 10 volume_time_out=30 @@ -58,6 +58,11 @@ reddwarf_dns_support = False # Auth admin_roles = [admin] +# Guest related conf +agent_heartbeat_time = 10 +agent_call_low_timeout = 5 +agent_call_high_timeout = 100 + # ============ notifer queue kombu connection options ======================== notifier_queue_hostname = localhost diff --git a/etc/reddwarf/reddwarf.conf.test b/etc/reddwarf/reddwarf.conf.test index d821593441..07a530f168 100644 --- a/etc/reddwarf/reddwarf.conf.test +++ b/etc/reddwarf/reddwarf.conf.test @@ -64,6 +64,14 @@ mount_point = /var/lib/mysql max_accepted_volume_size = 10 volume_time_out=30 +# Auth +admin_roles = [admin] + +# Guest related conf +agent_heartbeat_time = 10 +agent_call_low_timeout = 5 +agent_call_high_timeout = 100 + # ============ notifer queue kombu connection options ======================== notifier_queue_hostname = localhost diff --git a/reddwarf/common/exception.py b/reddwarf/common/exception.py index 03d91a3002..c790960411 100644 --- a/reddwarf/common/exception.py +++ b/reddwarf/common/exception.py @@ -88,6 +88,11 @@ class GuestError(ReddwarfError): "%(original_message)s.") +class GuestTimeout(ReddwarfError): + + message = _("Timeout trying to connect to the Guest Agent.") + + class BadRequest(ReddwarfError): message = _("The server could not comply with the request since it is " @@ -147,3 +152,13 @@ class PollTimeOut(ReddwarfError): class Forbidden(ReddwarfError): message = _("User does not have admin privileges.") + + +class InvalidModelError(ReddwarfError): + + message = _("The following values are invalid: %(errors)s") + + +class ModelNotFoundError(NotFound): + + message = _("Not Found") diff --git a/reddwarf/common/wsgi.py b/reddwarf/common/wsgi.py index 56911eb02f..5392095c96 100644 --- a/reddwarf/common/wsgi.py +++ b/reddwarf/common/wsgi.py @@ -46,14 +46,15 @@ eventlet.patcher.monkey_patch(all=False, socket=True) LOG = logging.getLogger('reddwarf.common.wsgi') XMLNS = 'http://docs.openstack.org/database/api/v1.0' -CUSTOM_PLURALS_METADATA = {'databases':'', 'users':''} -CUSTOM_SERIALIZER_METADATA = {'instance': {'status':'', 'hostname':'', - 'id':'', 'name':'','created':'', 'updated':''}, - 'volume': {'size':'', 'used':''}, - 'flavor': {'id':'', 'ram': '', 'name': ''}, - 'link': {'href':'', 'rel': ''}, - 'database': {'name':''}, - 'user': {'name':'', 'password':''}} +CUSTOM_PLURALS_METADATA = {'databases': '', 'users': ''} +CUSTOM_SERIALIZER_METADATA = {'instance': {'status': '', 'hostname': '', + 'id': '', 'name': '', 'created': '', + 'updated': ''}, + 'volume': {'size': '', 'used': ''}, + 'flavor': {'id': '', 'ram': '', 'name': ''}, + 'link': {'href': '', 'rel': ''}, + 'database': {'name': ''}, + 'user': {'name': '', 'password': ''}} def versioned_urlmap(*args, **kwargs): diff --git a/reddwarf/db/models.py b/reddwarf/db/models.py new file mode 100644 index 0000000000..ff4b5c34c5 --- /dev/null +++ b/reddwarf/db/models.py @@ -0,0 +1,92 @@ +# Copyright 2011 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. + +import logging + +from reddwarf import db +from reddwarf.common import exception +from reddwarf.common import models +from reddwarf.common import pagination +from reddwarf.common import utils + + +LOG = logging.getLogger(__name__) + + +class DatabaseModelBase(models.ModelBase): + _auto_generated_attrs = ['id'] + + @classmethod + def create(cls, **values): + values['id'] = utils.generate_uuid() + values['created'] = utils.utcnow() + instance = cls(**values).save() + if not instance.is_valid(): + raise exception.InvalidModelError(errors=instance.errors) + return instance + + def save(self): + if not self.is_valid(): + raise exception.InvalidModelError(errors=self.errors) + self['updated'] = utils.utcnow() + LOG.debug(_("Saving %s: %s") % + (self.__class__.__name__, self.__dict__)) + return db.db_api.save(self) + + def delete(self): + self['updated'] = utils.utcnow() + LOG.debug(_("Deleting %s: %s") % + (self.__class__.__name__, self.__dict__)) + return db.db_api.delete(self) + + def __init__(self, **kwargs): + self.merge_attributes(kwargs) + if not self.is_valid(): + raise exception.InvalidModelError(errors=self.errors) + + def merge_attributes(self, values): + """dict.update() behaviour.""" + for k, v in values.iteritems(): + self[k] = v + + @classmethod + def find_by(cls, **conditions): + model = cls.get_by(**conditions) + if model is None: + raise exception.ModelNotFoundError(_("%s Not Found") % + cls.__name__) + return model + + @classmethod + def get_by(cls, **kwargs): + return db.db_api.find_by(cls, **cls._process_conditions(kwargs)) + + @classmethod + def find_all(cls, **kwargs): + return db.db_query.find_all(cls, **cls._process_conditions(kwargs)) + + @classmethod + def _process_conditions(cls, raw_conditions): + """Override in inheritors to format/modify any conditions.""" + return raw_conditions + + @classmethod + def find_by_pagination(cls, collection_type, collection_query, + paginated_url, **kwargs): + elements, next_marker = collection_query.paginated_collection(**kwargs) + + return pagination.PaginatedDataView(collection_type, + elements, + paginated_url, + next_marker) diff --git a/reddwarf/db/sqlalchemy/mappers.py b/reddwarf/db/sqlalchemy/mappers.py index 215f7222f0..0ab39e45cb 100644 --- a/reddwarf/db/sqlalchemy/mappers.py +++ b/reddwarf/db/sqlalchemy/mappers.py @@ -36,6 +36,8 @@ def map(engine, models): Table('service_statuses', meta, autoload=True)) orm.mapper(models['dns_records'], Table('dns_records', meta, autoload=True)) + orm.mapper(models['agent_heartbeats'], + Table('agent_heartbeats', meta, autoload=True)) def mapping_exists(model): diff --git a/reddwarf/db/sqlalchemy/session.py b/reddwarf/db/sqlalchemy/session.py index 4b779de4f0..e1cd0eef04 100644 --- a/reddwarf/db/sqlalchemy/session.py +++ b/reddwarf/db/sqlalchemy/session.py @@ -42,11 +42,13 @@ def configure_db(options, models_mapper=None): from reddwarf.instance import models as base_models from reddwarf.dns import models as dns_models from reddwarf.extensions.mysql import models as mysql_models + from reddwarf.guestagent import models as agent_models model_modules = [ base_models, dns_models, mysql_models, + agent_models, ] models = {} diff --git a/reddwarf/dns/models.py b/reddwarf/dns/models.py index 27ea887481..b98d7bb732 100644 --- a/reddwarf/dns/models.py +++ b/reddwarf/dns/models.py @@ -22,9 +22,8 @@ Model classes that map instance Ip to dns record. import logging from reddwarf import db +from reddwarf.common import exception from reddwarf.common.models import ModelBase -from reddwarf.instance.models import InvalidModelError -from reddwarf.instance.models import ModelNotFoundError LOG = logging.getLogger(__name__) @@ -48,12 +47,12 @@ class DnsRecord(ModelBase): def create(cls, **values): record = cls(**values).save() if not record.is_valid(): - raise InvalidModelError(record.errors) + raise exception.InvalidModelError(errors=record.errors) return record def save(self): if not self.is_valid(): - raise InvalidModelError(self.errors) + raise exception.InvalidModelError(errors=self.errors) LOG.debug(_("Saving %s: %s") % (self.__class__.__name__, self.__dict__)) return db.db_api.save(self) @@ -67,7 +66,8 @@ class DnsRecord(ModelBase): def find_by(cls, **conditions): model = cls.get_by(**conditions) if model is None: - raise ModelNotFoundError(_("%s Not Found") % cls.__name__) + raise exception.ModelNotFoundError(_("%s Not Found") % + cls.__name__) return model @classmethod diff --git a/reddwarf/dns/rsdns/driver.py b/reddwarf/dns/rsdns/driver.py index e652b13747..aca7b9e4e8 100644 --- a/reddwarf/dns/rsdns/driver.py +++ b/reddwarf/dns/rsdns/driver.py @@ -218,6 +218,3 @@ class RsDnsZone(object): def __str__(self): return "%s:%s" % (self.id, self.name) - - - diff --git a/reddwarf/extensions/mysql/service.py b/reddwarf/extensions/mysql/service.py index a528d9cb8e..b36d8b0720 100644 --- a/reddwarf/extensions/mysql/service.py +++ b/reddwarf/extensions/mysql/service.py @@ -22,7 +22,6 @@ from reddwarf.common import exception from reddwarf.common import pagination from reddwarf.common import wsgi from reddwarf.guestagent.db import models as guest_models -from reddwarf.instance import models as instance_models from reddwarf.extensions.mysql import models from reddwarf.extensions.mysql import views @@ -44,7 +43,7 @@ class BaseController(wsgi.Controller): ], webob.exc.HTTPNotFound: [ exception.NotFound, - instance_models.ModelNotFoundError, + exception.ModelNotFoundError, ], webob.exc.HTTPConflict: [ ], diff --git a/reddwarf/flavor/models.py b/reddwarf/flavor/models.py index 43d88ae746..df1a1f41b6 100644 --- a/reddwarf/flavor/models.py +++ b/reddwarf/flavor/models.py @@ -20,7 +20,7 @@ from reddwarf import db from novaclient import exceptions as nova_exceptions -from reddwarf.common import exception as rd_exceptions +from reddwarf.common import exception from reddwarf.common import utils from reddwarf.common.models import NovaRemoteModelBase from reddwarf.common.remote import create_nova_client @@ -39,13 +39,13 @@ class Flavor(object): client = create_nova_client(context) self.flavor = client.flavors.get(flavor_id) except nova_exceptions.NotFound, e: - raise rd_exceptions.NotFound(uuid=flavor_id) + raise exception.NotFound(uuid=flavor_id) except nova_exceptions.ClientException, e: - raise rd_exceptions.ReddwarfError(str(e)) + raise exception.ReddwarfError(str(e)) return msg = ("Flavor is not defined, and" " context and flavor_id were not specified.") - raise InvalidModelError(msg) + raise exception.InvalidModelError(errors=msg) @property def id(self): diff --git a/reddwarf/guestagent/api.py b/reddwarf/guestagent/api.py index e09333d733..9dd211e512 100644 --- a/reddwarf/guestagent/api.py +++ b/reddwarf/guestagent/api.py @@ -19,16 +19,19 @@ Handles all request to the Platform or Guest VM """ - import logging + +from eventlet import Timeout + from reddwarf import rpc from reddwarf.common import config from reddwarf.common import exception from reddwarf.common import utils -# from nova.db import api as dbapi LOG = logging.getLogger(__name__) +AGENT_LOW_TIMEOUT = int(config.Config.get('agent_call_low_timeout', 5)) +AGENT_HIGH_TIMEOUT = int(config.Config.get('agent_call_high_timeout', 60)) class API(object): @@ -38,22 +41,30 @@ class API(object): self.context = context self.id = id - def _call(self, method_name, **kwargs): + def _call(self, method_name, timeout_sec, **kwargs): LOG.debug("Calling %s" % method_name) + + timeout = Timeout(timeout_sec) try: result = rpc.call(self.context, self._get_routing_key(), - {"method": method_name, "args": kwargs}) + {'method': method_name, 'args': kwargs}) LOG.debug("Result is %s" % result) return result except Exception as e: LOG.error(e) raise exception.GuestError(original_message=str(e)) + except Timeout as t: + if t is not timeout: + raise + else: + raise exception.GuestTimeout() + finally: + timeout.cancel() def _cast(self, method_name, **kwargs): try: rpc.cast(self.context, self._get_routing_key(), - {"method": method_name, - "args": kwargs}) + {'method': method_name, 'args': kwargs}) except Exception as e: LOG.error(e) raise exception.GuestError(original_message=str(e)) @@ -61,7 +72,7 @@ class API(object): def _cast_with_consumer(self, method_name, **kwargs): try: rpc.cast_with_consumer(self.context, self._get_routing_key(), - {"method": method_name, "args": kwargs}) + {'method': method_name, 'args': kwargs}) except Exception as e: LOG.error(e) raise exception.GuestError(original_message=str(e)) @@ -78,8 +89,8 @@ class API(object): def list_users(self, limit=None, marker=None, include_marker=False): """Make an asynchronous call to list database users""" LOG.debug(_("Listing Users for Instance %s"), self.id) - return self._call("list_users", limit=limit, marker=marker, - include_marker=include_marker) + return self._call("list_users", AGENT_LOW_TIMEOUT, limit=limit, + marker=marker, include_marker=include_marker) def delete_user(self, user): """Make an asynchronous call to delete an existing database user""" @@ -95,8 +106,8 @@ class API(object): def list_databases(self, limit=None, marker=None, include_marker=False): """Make an asynchronous call to list databases""" LOG.debug(_("Listing databases for Instance %s"), self.id) - return self._call("list_databases", limit=limit, marker=marker, - include_marker=include_marker) + return self._call("list_databases", AGENT_LOW_TIMEOUT, limit=limit, + marker=marker, include_marker=include_marker) def delete_database(self, database): """Make an asynchronous call to delete an existing database @@ -108,24 +119,24 @@ class API(object): """Make a synchronous call to enable the root user for access from anywhere""" LOG.debug(_("Enable root user for Instance %s"), self.id) - return self._call("enable_root") + return self._call("enable_root", AGENT_LOW_TIMEOUT) def disable_root(self): """Make a synchronous call to disable the root user for access from anywhere""" LOG.debug(_("Disable root user for Instance %s"), self.id) - return self._call("disable_root") + return self._call("disable_root", AGENT_LOW_TIMEOUT) def is_root_enabled(self): """Make a synchronous call to check if root access is available for the container""" LOG.debug(_("Check root access for Instance %s"), self.id) - return self._call("is_root_enabled") + return self._call("is_root_enabled", AGENT_LOW_TIMEOUT) def get_diagnostics(self): """Make a synchronous call to get diagnostics for the container""" LOG.debug(_("Check diagnostics on Instance %s"), self.id) - return self._call("get_diagnostics") + return self._call("get_diagnostics", AGENT_LOW_TIMEOUT) def prepare(self, memory_mb, databases, users, device_path='/dev/vdb', mount_point='/mnt/volume'): @@ -139,20 +150,26 @@ class API(object): def restart(self): """Restart the MySQL server.""" LOG.debug(_("Sending the call to restart MySQL on the Guest.")) - self._call("restart") + self._call("restart", AGENT_HIGH_TIMEOUT) def start_mysql_with_conf_changes(self, updated_memory_size): """Start the MySQL server.""" LOG.debug(_("Sending the call to start MySQL on the Guest.")) - self._call("start_mysql_with_conf_changes", + self._call("start_mysql_with_conf_changes", AGENT_HIGH_TIMEOUT, updated_memory_size=updated_memory_size) def stop_mysql(self): """Stop the MySQL server.""" LOG.debug(_("Sending the call to stop MySQL on the Guest.")) - self._call("stop_mysql") + self._call("stop_mysql", AGENT_HIGH_TIMEOUT) def upgrade(self): """Make an asynchronous call to self upgrade the guest agent""" LOG.debug(_("Sending an upgrade call to nova-guest")) self._cast_with_consumer("upgrade") + + def get_volume_info(self): + """Make a synchronous call to get volume info for the container""" + LOG.debug(_("Check Volume Info on Instance %s"), self.id) + return self._call("get_filesystem_stats", AGENT_LOW_TIMEOUT, + fs_path="/var/lib/mysql") diff --git a/reddwarf/guestagent/dbaas.py b/reddwarf/guestagent/dbaas.py index 7f5190fd8f..3db5a0d325 100644 --- a/reddwarf/guestagent/dbaas.py +++ b/reddwarf/guestagent/dbaas.py @@ -72,6 +72,7 @@ INCLUDE_MARKER_OPERATORS = { False: ">" } + def generate_random_password(): return str(uuid.uuid4()) diff --git a/reddwarf/guestagent/models.py b/reddwarf/guestagent/models.py new file mode 100644 index 0000000000..4e1375bafd --- /dev/null +++ b/reddwarf/guestagent/models.py @@ -0,0 +1,65 @@ +# Copyright 2011 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. + +import logging +from datetime import datetime +from datetime import timedelta + +from reddwarf import db +from reddwarf.common import config +from reddwarf.common import exception +from reddwarf.common import utils +from reddwarf.db import models as dbmodels + + +LOG = logging.getLogger(__name__) + +AGENT_HEARTBEAT = int(config.Config.get('agent_heartbeat_time', '10')) + + +def persisted_models(): + return { + 'agent_heartbeats': AgentHeartBeat, + } + + +class AgentHeartBeat(dbmodels.DatabaseModelBase): + """Defines the state of a Guest Agent.""" + + _data_fields = ['instance_id', 'updated_at'] + _table_name = 'agent_heartbeats' + + def __init__(self, **kwargs): + super(AgentHeartBeat, self).__init__(**kwargs) + + @classmethod + def create(cls, **values): + values['id'] = utils.generate_uuid() + heartbeat = cls(**values).save() + if not heartbeat.is_valid(): + raise exception.InvalidModelError(errors=heartbeat.errors) + return heartbeat + + def save(self): + if not self.is_valid(): + raise exception.InvalidModelError(errors=self.errors) + self['updated_at'] = utils.utcnow() + LOG.debug(_("Saving %s: %s") % + (self.__class__.__name__, self.__dict__)) + return db.db_api.save(self) + + @staticmethod + def is_active(agent): + return (datetime.now() - agent.updated_at) < \ + timedelta(seconds=AGENT_HEARTBEAT) diff --git a/reddwarf/guestagent/query.py b/reddwarf/guestagent/query.py index f883049b3b..f8234da934 100644 --- a/reddwarf/guestagent/query.py +++ b/reddwarf/guestagent/query.py @@ -24,7 +24,8 @@ Intermediary class for building SQL queries for use by the guest agent. class Query(object): - def __init__(self, columns=None, tables=None, where=None, order=None, group=None, limit=None): + def __init__(self, columns=None, tables=None, where=None, order=None, + group=None, limit=None): self.columns = columns or [] self.tables = tables or [] self.where = where or [] diff --git a/reddwarf/instance/models.py b/reddwarf/instance/models.py index 1b42dae2e3..d058772989 100644 --- a/reddwarf/instance/models.py +++ b/reddwarf/instance/models.py @@ -21,27 +21,21 @@ import eventlet import logging import netaddr -from reddwarf import db - from novaclient import exceptions as nova_exceptions from reddwarf.common import config -from reddwarf.common import exception as rd_exceptions -from reddwarf.common import pagination -from reddwarf.common import utils -from reddwarf.common.models import ModelBase -from novaclient import exceptions as nova_exceptions +from reddwarf.common import exception from reddwarf.common.remote import create_dns_client from reddwarf.common.remote import create_guest_client from reddwarf.common.remote import create_nova_client from reddwarf.common.remote import create_nova_volume_client -from reddwarf.common.utils import poll_until +from reddwarf.db import models as dbmodels from reddwarf.instance.tasks import InstanceTask from reddwarf.instance.tasks import InstanceTasks +from reddwarf.guestagent import models as agent_models from reddwarf.taskmanager import api as task_api from eventlet import greenthread -from reddwarf.instance.views import get_ip_address CONFIG = config.Config @@ -55,10 +49,10 @@ def load_server(context, instance_id, server_id): server = client.servers.get(server_id) except nova_exceptions.NotFound, e: LOG.debug("Could not find nova server_id(%s)" % server_id) - raise rd_exceptions.ComputeInstanceNotFound(instance_id=instance_id, - server_id=server_id) + raise exception.ComputeInstanceNotFound(instance_id=instance_id, + server_id=server_id) except nova_exceptions.ClientException, e: - raise rd_exceptions.ReddwarfError(str(e)) + raise exception.ReddwarfError(str(e)) return server @@ -80,7 +74,7 @@ def populate_databases(dbs): databases.append(mydb.serialize()) return databases except ValueError as ve: - raise rd_exceptions.BadRequest(ve.message) + raise exception.BadRequest(ve.message) class InstanceStatus(object): @@ -114,7 +108,7 @@ def load_simple_instance_server_status(context, db_info): # then assume the delete operation is done and raise an # exception. if InstanceTasks.DELETING == db_info.task_status: - raise rd_exceptions.NotFound(uuid=db_info.id) + raise exception.NotFound(uuid=db_info.id) # If the compute server is in any of these states we can't perform any @@ -124,6 +118,9 @@ SERVER_INVALID_ACTION_STATUSES = ["BUILD", "REBOOT", "REBUILD"] # Statuses in which an instance can have an action performed. VALID_ACTION_STATUSES = ["ACTIVE"] +# Invalid states to contact the agent +AGENT_INVALID_STATUSES = ["BUILD", "REBOOT", "RESIZE"] + class SimpleInstance(object): """A simple view of an instance. @@ -168,24 +165,6 @@ class SimpleInstance(object): """True if the service status indicates MySQL is up and running.""" return self.service_status.status in MYSQL_RESPONSIVE_STATUSES - @staticmethod - def load(context, id): - try: - db_info = DBInstance.find_by(id=id) - except ModelNotFoundError: - raise rd_exceptions.NotFound(uuid=id) - service_status = InstanceServiceStatus.find_by(instance_id=id) - LOG.info("service status=%s" % service_status) - # TODO(tim.simpson): In the future, we'll listen to notifications and - # update the RDL database when the server status changes, and add - # server_status as a property to db_info, but for now we have to resort - # to this. - db_info = DBInstance.find_by(id=id) - if not context.is_admin and db_info.tenant_id != context.tenant: - raise rd_exceptions.NotFound(uuid=id) - load_simple_instance_server_status(context, db_info) - return SimpleInstance(context, db_info, service_status) - @property def name(self): return self.db_info.name @@ -234,19 +213,46 @@ class SimpleInstance(object): return self.db_info.volume_size -def load_instance(cls, context, id, needs_server=False): +class DetailInstance(SimpleInstance): + """A detailed view of an Instnace. + + This loads a SimpleInstance and then adds additional data for the + instance from the guest. + """ + + def __init__(self, context, db_info, service_status): + super(DetailInstance, self).__init__(context, db_info, service_status) + self._volume_used = None + + @property + def volume_used(self): + return self._volume_used + + @volume_used.setter + def volume_used(self, value): + self._volume_used = value + + +def get_db_info(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) + except exception.NotFound: + raise exception.NotFound(uuid=id) + except exception.ModelNotFoundError: + raise exception.NotFound(uuid=id) if not context.is_admin and db_info.tenant_id != context.tenant: LOG.error("Tenant %s tried to access instance %s, owned by %s." % (context.tenant, id, db_info.tenant_id)) - raise rd_exceptions.NotFound(uuid=id) + raise exception.NotFound(uuid=id) + return db_info + + +def load_instance(cls, context, id, needs_server=False): + db_info = get_db_info(context, id) if not needs_server: # TODO(tim.simpson): When we have notifications this won't be # necessary and instead we'll just use the server_status field from @@ -260,17 +266,38 @@ def load_instance(cls, context, id, needs_server=False): #TODO(tim.simpson): Remove this hack when we have notifications! db_info.server_status = server.status db_info.addresses = server.addresses - except rd_exceptions.ComputeInstanceNotFound: + except exception.ComputeInstanceNotFound: LOG.error("COMPUTE ID = %s" % db_info.compute_instance_id) - raise rd_exceptions.UnprocessableEntity( - "Instance %s is not ready." % id) + raise exception.UnprocessableEntity("Instance %s is not ready." % + id) - task_status = db_info.task_status service_status = InstanceServiceStatus.find_by(instance_id=id) LOG.info("service status=%s" % service_status) return cls(context, db_info, server, service_status) +def load_instance_with_guest(cls, context, id): + db_info = get_db_info(context, id) + load_simple_instance_server_status(context, db_info) + service_status = InstanceServiceStatus.find_by(instance_id=id) + LOG.info("service status=%s" % service_status) + instance = cls(context, db_info, service_status) + try: + agent = agent_models.AgentHeartBeat.find_by(instance_id=id) + except exception.ModelNotFoundError as mnfe: + LOG.warn(mnfe) + return instance + + if instance.status not in AGENT_INVALID_STATUSES and \ + agent_models.AgentHeartBeat.is_active(agent): + guest = create_guest_client(context, id) + try: + instance.volume_used = guest.get_volume_info()['used'] + except Exception as e: + LOG.error(e) + return instance + + class BaseInstance(SimpleInstance): """Represents an instance.""" @@ -334,7 +361,7 @@ class Instance(BuiltInstance): def delete(self, force=False): if not force and \ self.db_info.server_status in SERVER_INVALID_ACTION_STATUSES: - raise rd_exceptions.UnprocessableEntity("Instance %s is not ready." + raise exception.UnprocessableEntity("Instance %s is not ready." % self.id) LOG.debug(_(" ... deleting compute id = %s") % self.db_info.compute_instance_id) @@ -349,7 +376,7 @@ class Instance(BuiltInstance): try: flavor = client.flavors.get(flavor_id) except nova_exceptions.NotFound: - raise rd_exceptions.FlavorNotFound(uuid=flavor_id) + raise exception.FlavorNotFound(uuid=flavor_id) db_info = DBInstance.create(name=name, flavor_id=flavor_id, tenant_id=context.tenant, @@ -375,7 +402,7 @@ class Instance(BuiltInstance): msg = "Instance is not currently available for an action to be " \ "performed. Status [%s]" LOG.debug(_(msg) % self.status) - raise rd_exceptions.UnprocessableEntity(_(msg) % self.status) + raise exception.UnprocessableEntity(_(msg) % self.status) def resize_flavor(self, new_flavor_id): self.validate_can_perform_resize() @@ -387,12 +414,12 @@ class Instance(BuiltInstance): try: new_flavor = client.flavors.get(new_flavor_id) except nova_exceptions.NotFound: - raise rd_exceptions.FlavorNotFound(uuid=new_flavor_id) + raise exception.FlavorNotFound(uuid=new_flavor_id) old_flavor = client.flavors.get(self.flavor_id) new_flavor_size = new_flavor.ram old_flavor_size = old_flavor.ram if new_flavor_size == old_flavor_size: - raise rd_exceptions.CannotResizeToSameSize() + raise exception.CannotResizeToSameSize() # Set the task to RESIZING and begin the async call before returning. self.update_db(task_status=InstanceTasks.RESIZING) @@ -403,24 +430,22 @@ class Instance(BuiltInstance): def resize_volume(self, new_size): LOG.info("Resizing volume of instance %s..." % self.id) if not self.volume_size: - raise rd_exceptions.BadRequest("Instance %s has no volume." - % self.id) + raise exception.BadRequest("Instance %s has no volume." % self.id) old_size = self.volume_size if int(new_size) <= old_size: - raise rd_exceptions.BadRequest("The new volume 'size' cannot be " + raise exception.BadRequest("The new volume 'size' cannot be " "less than the current volume size of '%s'" % old_size) # Set the task to Resizing before sending off to the taskmanager self.update_db(task_status=InstanceTasks.RESIZING) task_api.API(self.context).resize_volume(new_size, self.id) - def restart(self): if self.db_info.server_status in SERVER_INVALID_ACTION_STATUSES: msg = _("Restart instance not allowed while instance %s is in %s " "status.") % (self.id, instance_state) LOG.debug(msg) # If the state is building then we throw an exception back - raise rd_exceptions.UnprocessableEntity(msg) + raise exception.UnprocessableEntity(msg) else: LOG.info("Restarting instance %s..." % self.id) # Set our local status since Nova might not change it quick enough. @@ -442,7 +467,7 @@ class Instance(BuiltInstance): "performed (task status was %s, service status was %s)." \ % (self.db_info.task_status, self.service_status.status) LOG.error(msg) - raise rd_exceptions.UnprocessableEntity(msg) + raise exception.UnprocessableEntity(msg) def validate_can_perform_resize(self): """ @@ -452,7 +477,7 @@ class Instance(BuiltInstance): msg = "Instance is not currently available for an action to be " \ "performed (status was %s)." % self.status LOG.error(msg) - raise rd_exceptions.UnprocessableEntity(msg) + raise exception.UnprocessableEntity(msg) def create_server_list_matcher(server_list): @@ -465,13 +490,13 @@ def create_server_list_matcher(server_list): # The instance was not found in the list and # this can happen if the instance is deleted from # nova but still in reddwarf database - raise rd_exceptions.ComputeInstanceNotFound( + raise exception.ComputeInstanceNotFound( instance_id=instance_id, server_id=server_id) else: # Should never happen, but never say never. LOG.error(_("Server %s for instance %s was found twice!") % (server_id, instance_id)) - raise rd_exceptions.ReddwarfError(uuid=instance_id) + raise exception.ReddwarfError(uuid=instance_id) return find_server @@ -481,6 +506,10 @@ class Instances(object): @staticmethod def load(context): + + def load_simple_instance(context, db, status): + return SimpleInstance(context, db, status) + if context is None: raise TypeError("Argument context not defined.") client = create_nova_client(context) @@ -495,12 +524,20 @@ class Instances(object): marker=context.marker) next_marker = data_view.next_page_marker - ret = [] find_server = create_server_list_matcher(servers) for db in db_infos: LOG.debug("checking for db [id=%s, compute_instance_id=%s]" % (db.id, db.compute_instance_id)) - for db in data_view.collection: + ret = Instances._load_servers_status(load_simple_instance, context, + data_view.collection, + find_server) + return ret, next_marker + + @staticmethod + def _load_servers_status(load_instance, context, db_items, find_server): + ret = [] + for db in db_items: + server = None try: #TODO(tim.simpson): Delete when we get notifications working! if InstanceTasks.BUILDING == db.task_status: @@ -509,99 +546,32 @@ class Instances(object): try: server = find_server(db.id, db.compute_instance_id) db.server_status = server.status - except rd_exceptions.ComputeInstanceNotFound: + except exception.ComputeInstanceNotFound: if InstanceTasks.DELETING == db.task_status: #TODO(tim.simpson): This instance is actually # deleted, but without notifications we never # update our DB. continue - db.server_status = "SHUTDOWN" # Fake it... + db.server_status = "SHUTDOWN" # Fake it... #TODO(tim.simpson): End of hack. #volumes = find_volumes(server.id) status = InstanceServiceStatus.find_by(instance_id=db.id) LOG.info(_("Server api_status(%s)") % (status.status.api_status)) - if not status.status: # This should never happen. + if not status.status: # This should never happen. LOG.error(_("Server status could not be read for " "instance id(%s)") % (db.id)) continue - except ModelNotFoundError: + except exception.ModelNotFoundError: LOG.error(_("Server status could not be read for " "instance id(%s)") % (db.id)) continue - ret.append(SimpleInstance(context, db, status)) - return ret, next_marker + ret.append(load_instance(context, db, status)) + return ret -class DatabaseModelBase(ModelBase): - _auto_generated_attrs = ['id'] - - @classmethod - def create(cls, **values): - values['id'] = utils.generate_uuid() - values['created'] = utils.utcnow() - instance = cls(**values).save() - if not instance.is_valid(): - raise InvalidModelError(instance.errors) - return instance - - def save(self): - if not self.is_valid(): - raise InvalidModelError(self.errors) - self['updated'] = utils.utcnow() - LOG.debug(_("Saving %s: %s") % - (self.__class__.__name__, self.__dict__)) - return db.db_api.save(self) - - def delete(self): - self['updated'] = utils.utcnow() - LOG.debug(_("Deleting %s: %s") % - (self.__class__.__name__, self.__dict__)) - return db.db_api.delete(self) - - def __init__(self, **kwargs): - self.merge_attributes(kwargs) - if not self.is_valid(): - raise InvalidModelError(self.errors) - - def merge_attributes(self, values): - """dict.update() behaviour.""" - for k, v in values.iteritems(): - self[k] = v - - @classmethod - def find_by(cls, **conditions): - model = cls.get_by(**conditions) - if model is None: - raise ModelNotFoundError(_("%s Not Found") % cls.__name__) - return model - - @classmethod - def get_by(cls, **kwargs): - return db.db_api.find_by(cls, **cls._process_conditions(kwargs)) - - @classmethod - def find_all(cls, **kwargs): - return db.db_query.find_all(cls, **cls._process_conditions(kwargs)) - - @classmethod - def _process_conditions(cls, raw_conditions): - """Override in inheritors to format/modify any conditions.""" - return raw_conditions - - @classmethod - def find_by_pagination(cls, collection_type, collection_query, - paginated_url, **kwargs): - elements, next_marker = collection_query.paginated_collection(**kwargs) - - return pagination.PaginatedDataView(collection_type, - elements, - paginated_url, - next_marker) - - -class DBInstance(DatabaseModelBase): +class DBInstance(dbmodels.DatabaseModelBase): """Defines the task being executed plus the start time.""" #TODO(tim.simpson): Add start time. @@ -632,13 +602,13 @@ class DBInstance(DatabaseModelBase): task_status = property(get_task_status, set_task_status) -class ServiceImage(DatabaseModelBase): +class ServiceImage(dbmodels.DatabaseModelBase): """Defines the status of the service being run.""" _data_fields = ['service_name', 'image_id'] -class InstanceServiceStatus(DatabaseModelBase): +class InstanceServiceStatus(dbmodels.DatabaseModelBase): _data_fields = ['instance_id', 'status_id', 'status_description'] @@ -672,19 +642,6 @@ def persisted_models(): } -class InvalidModelError(rd_exceptions.ReddwarfError): - - message = _("The following values are invalid: %(errors)s") - - def __init__(self, errors, message=None): - super(InvalidModelError, self).__init__(message, errors=errors) - - -class ModelNotFoundError(rd_exceptions.ReddwarfError): - - message = _("Not Found") - - class ServiceStatus(object): """Represents the status of the app and in some rare cases the agent. diff --git a/reddwarf/instance/service.py b/reddwarf/instance/service.py index 9ab1a7bc68..03aeaf1bdb 100644 --- a/reddwarf/instance/service.py +++ b/reddwarf/instance/service.py @@ -40,7 +40,7 @@ class BaseController(wsgi.Controller): exception.UnprocessableEntity, ], webob.exc.HTTPBadRequest: [ - models.InvalidModelError, + exception.InvalidModelError, exception.BadRequest, exception.CannotResizeToSameSize, exception.BadValue @@ -48,7 +48,7 @@ class BaseController(wsgi.Controller): webob.exc.HTTPNotFound: [ exception.NotFound, exception.ComputeInstanceNotFound, - models.ModelNotFoundError, + exception.ModelNotFoundError, ], webob.exc.HTTPConflict: [ ], @@ -191,8 +191,8 @@ class InstanceController(BaseController): LOG.info(_("id : '%s'\n\n") % id) context = req.environ[wsgi.CONTEXT_KEY] - server = models.SimpleInstance.load(context=context, id=id) - # TODO(cp16net): need to set the return code correctly + server = models.load_instance_with_guest(models.DetailInstance, + context, id) return wsgi.Result(views.InstanceDetailView(server, req=req, add_addresses=self.add_addresses, add_volumes=self.add_volumes).data(), 200) @@ -242,7 +242,7 @@ class InstanceController(BaseController): try: volume_size = int(body['instance']['volume']['size']) except ValueError as e: - raise exception.BadValue(msg=e) + raise exception.BadValue(msg=e) else: volume_size = None instance = models.Instance.create(context, name, flavor_id, diff --git a/reddwarf/instance/views.py b/reddwarf/instance/views.py index 6a26156341..f12fd3e4ba 100644 --- a/reddwarf/instance/views.py +++ b/reddwarf/instance/views.py @@ -18,9 +18,8 @@ import logging from reddwarf.common import config from reddwarf.common import utils -from reddwarf.common import wsgi from reddwarf.common.views import create_links - +from reddwarf.instance import models LOG = logging.getLogger(__name__) @@ -87,6 +86,8 @@ class InstanceDetailView(InstanceView): self.add_addresses = add_addresses self.add_volumes = add_volumes + def _to_gb(self, bytes): + return bytes / 1024.0 ** 3 def data(self): result = super(InstanceDetailView, self).data() @@ -97,6 +98,11 @@ class InstanceDetailView(InstanceView): ip = get_ip_address(self.instance.addresses) if ip is not None and len(ip) > 0: result['instance']['ip'] = ip + if self.add_volumes: + if isinstance(self.instance, models.DetailInstance) and \ + self.instance.volume_used: + used = self._to_gb(self.instance.volume_used) + result['instance']['volume']['used'] = used return result diff --git a/reddwarf/taskmanager/api.py b/reddwarf/taskmanager/api.py index 3a02cbeb76..04dcb5ccbf 100644 --- a/reddwarf/taskmanager/api.py +++ b/reddwarf/taskmanager/api.py @@ -50,6 +50,7 @@ class API(object): from reddwarf.taskmanager.manager import TaskManager instance = TaskManager() method = getattr(instance, method_name) + def func(): try: method(self.context, **kwargs) diff --git a/reddwarf/taskmanager/manager.py b/reddwarf/taskmanager/manager.py index d08a2f6817..7324b85c5c 100644 --- a/reddwarf/taskmanager/manager.py +++ b/reddwarf/taskmanager/manager.py @@ -79,5 +79,3 @@ class TaskManager(service.Manager): instance_tasks = FreshInstanceTasks.load(context, instance_id) instance_tasks.create_instance(flavor_id, flavor_ram, image_id, databases, service_type, volume_size) - - diff --git a/reddwarf/taskmanager/models.py b/reddwarf/taskmanager/models.py index e2b01bae57..e281e259dd 100644 --- a/reddwarf/taskmanager/models.py +++ b/reddwarf/taskmanager/models.py @@ -106,7 +106,6 @@ class FreshInstanceTasks(FreshInstance): 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) @@ -146,6 +145,7 @@ class FreshInstanceTasks(FreshInstance): nova_client = create_nova_client(self.context) if utils.bool_from_string(dns_support): + def get_server(): return nova_client.servers.get(self.db_info.compute_instance_id) diff --git a/reddwarf/tests/fakes/guestagent.py b/reddwarf/tests/fakes/guestagent.py index 29516ad7b0..191b09d2cd 100644 --- a/reddwarf/tests/fakes/guestagent.py +++ b/reddwarf/tests/fakes/guestagent.py @@ -98,11 +98,13 @@ class FakeGuest(object): mount_point=None): from reddwarf.instance.models import InstanceServiceStatus from reddwarf.instance.models import ServiceStatuses + from reddwarf.guestagent.models import AgentHeartBeat def update_db(): status = InstanceServiceStatus.find_by(instance_id=self.id) status.status = ServiceStatuses.RUNNING status.save() + AgentHeartBeat.create(instance_id=self.id) EventSimulator.add_event(2.0, update_db) def restart(self): @@ -125,6 +127,10 @@ class FakeGuest(object): status.status = ServiceStatuses.SHUTDOWN status.save() + def get_volume_info(self): + """Return used volume information in bytes.""" + return {'used': 175756487} + def get_or_create(id): if id not in DB: diff --git a/reddwarf/tests/fakes/nova.py b/reddwarf/tests/fakes/nova.py index bf09733460..7bd401c199 100644 --- a/reddwarf/tests/fakes/nova.py +++ b/reddwarf/tests/fakes/nova.py @@ -289,8 +289,8 @@ class FakeVolume(object): for attachment in self.attachments: if attachment['server_id'] == server_id: return # Do nothing - self.attachments.append({'server_id':server_id, - 'device':self.device}) + self.attachments.append({'server_id': server_id, + 'device': self.device}) @property def status(self): @@ -348,6 +348,7 @@ class FakeVolumes(object): def resize(self, volume_id, new_size): volume = self.get(volume_id) + def finish_resize(): volume._current_status = "in-use" volume.size = new_size