# Copyright (c) 2013 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 datetime import eventlet import six from oslo.config import cfg from stevedore import enabled from climate import context from climate.db import api as db_api from climate.db import exceptions as db_ex from climate import exceptions as common_ex from climate import manager from climate.manager import exceptions from climate.openstack.common.gettextutils import _ from climate.openstack.common import log as logging from climate.utils import service as service_utils from climate.utils import trusts manager_opts = [ cfg.ListOpt('plugins', default=['dummy.vm.plugin'], help='All plugins to use (one for every resource type to ' 'support.)'), ] CONF = cfg.CONF CONF.register_opts(manager_opts, 'manager') LOG = logging.getLogger(__name__) LEASE_DATE_FORMAT = "%Y-%m-%d %H:%M" class ManagerService(service_utils.RPCServer): """Service class for the climate-manager service. Responsible for working with Climate DB, scheduling logic, running events, working with plugins, etc. """ def __init__(self): target = manager.get_target() super(ManagerService, self).__init__(target) self.plugins = self._get_plugins() self.resource_actions = self._setup_actions() def start(self): super(ManagerService, self).start() self.tg.add_timer(10, self._event) def _get_plugins(self): """Return dict of resource-plugin class pairs.""" config_plugins = CONF.manager.plugins plugins = {} extension_manager = enabled.EnabledExtensionManager( check_func=lambda ext: ext.name in config_plugins, namespace='climate.resource.plugins', invoke_on_load=False ) for ext in extension_manager.extensions: try: plugin_obj = ext.plugin() except Exception as e: LOG.warning("Could not load {0} plugin " "for resource type {1} '{2}'".format( ext.name, ext.plugin.resource_type, e)) else: if plugin_obj.resource_type in plugins: msg = "You have provided several plugins for " \ "one resource type in configuration file. " \ "Please set one plugin per resource type." raise exceptions.PluginConfigurationError(error=msg) plugins[plugin_obj.resource_type] = plugin_obj return plugins def _setup_actions(self): """Setup actions for each resource type supported. BasePlugin interface provides only on_start and on_end behaviour now. If there are some configs needed by plugin, they should be returned from get_plugin_opts method. These flags are registered in [resource_type] group of configuration file. """ actions = {} for resource_type, plugin in six.iteritems(self.plugins): plugin = self.plugins[resource_type] CONF.register_opts(plugin.get_plugin_opts(), group=resource_type) actions[resource_type] = {} actions[resource_type]['on_start'] = plugin.on_start actions[resource_type]['on_end'] = plugin.on_end plugin.setup(None) return actions @service_utils.with_empty_context def _event(self): """Tries to commit event. If there is an event in Climate DB to be done, do it and change its status to 'DONE'. """ LOG.debug(_('Trying to get event from DB.')) event = db_api.event_get_first_sorted_by_filters( sort_key='time', sort_dir='asc', filters={'status': 'UNDONE'} ) if not event: return if event['time'] < datetime.datetime.utcnow(): db_api.event_update(event['id'], {'status': 'IN_PROGRESS'}) event_type = event['event_type'] event_fn = getattr(self, event_type, None) if event_fn is None: raise exceptions.EventError(error='Event type %s is not ' 'supported' % event_type) try: eventlet.spawn_n(service_utils.with_empty_context(event_fn), event['lease_id'], event['id']) except Exception: db_api.event_update(event['id'], {'status': 'ERROR'}) LOG.exception(_('Error occurred while event handling.')) def _date_from_string(self, date_string, date_format=LEASE_DATE_FORMAT): try: date = datetime.datetime.strptime(date_string, date_format) except ValueError: raise exceptions.InvalidDate(date=date_string, date_format=date_format) return date def get_lease(self, lease_id): return db_api.lease_get(lease_id) def list_leases(self, project_id=None): return db_api.lease_list(project_id) def create_lease(self, lease_values): """Create a lease with reservations. Return either the model of created lease or None if any error. """ # Remove and keep reservation values reservations = lease_values.pop("reservations", []) # Create the lease without the reservations start_date = lease_values['start_date'] end_date = lease_values['end_date'] now = datetime.datetime.utcnow() now = datetime.datetime(now.year, now.month, now.day, now.hour, now.minute) if start_date == 'now': start_date = now else: start_date = self._date_from_string(start_date) end_date = self._date_from_string(end_date) if start_date < now: raise common_ex.NotAuthorized( 'Start date must later than current date') ctx = context.current() lease_values['user_id'] = ctx.user_id lease_values['project_id'] = ctx.project_id lease_values['start_date'] = start_date lease_values['end_date'] = end_date if not lease_values.get('events'): lease_values['events'] = [] lease_values['events'].append({'event_type': 'start_lease', 'time': start_date, 'status': 'UNDONE'}) lease_values['events'].append({'event_type': 'end_lease', 'time': end_date, 'status': 'UNDONE'}) try: lease = db_api.lease_create(lease_values) lease_id = lease['id'] except db_ex.ClimateDBDuplicateEntry: LOG.exception('Cannot create a lease - duplicated lease name') raise exceptions.LeaseNameAlreadyExists(name=lease_values['name']) except db_ex.ClimateDBException: LOG.exception('Cannot create a lease') raise else: try: for reservation in reservations: reservation['lease_id'] = lease['id'] reservation['start_date'] = lease['start_date'] reservation['end_date'] = lease['end_date'] resource_type = reservation['resource_type'] if resource_type in self.plugins: self.plugins[resource_type].create_reservation( reservation) else: raise exceptions.UnsupportedResourceType(resource_type) except (exceptions.UnsupportedResourceType, common_ex.ClimateException): LOG.exception("Failed to create reservation for a lease. " "Rollback the lease and associated reservations") db_api.lease_destroy(lease_id) raise else: return db_api.lease_get(lease['id']) def update_lease(self, lease_id, values): if not values: return db_api.lease_get(lease_id) if len(values) == 1 and 'name' in values: db_api.lease_update(lease_id, values) return db_api.lease_get(lease_id) lease = db_api.lease_get(lease_id) start_date = values.get( 'start_date', datetime.datetime.strftime(lease['start_date'], LEASE_DATE_FORMAT)) end_date = values.get( 'end_date', datetime.datetime.strftime(lease['end_date'], LEASE_DATE_FORMAT)) now = datetime.datetime.utcnow() now = datetime.datetime(now.year, now.month, now.day, now.hour, now.minute) if start_date == 'now': start_date = now else: start_date = self._date_from_string(start_date) end_date = self._date_from_string(end_date) values['start_date'] = start_date values['end_date'] = end_date if (lease['start_date'] < now and values['start_date'] != lease['start_date']): raise common_ex.NotAuthorized( 'Cannot modify the start date of already started leases') if (lease['start_date'] > now and values['start_date'] < now): raise common_ex.NotAuthorized( 'Start date must later than current date') if lease['end_date'] < now: raise common_ex.NotAuthorized( 'Terminated leases can only be renamed') if (values['end_date'] < now or values['end_date'] < values['start_date']): raise common_ex.NotAuthorized( 'End date must be later than current and start date') #TODO(frossigneux) rollback if an exception is raised for reservation in \ db_api.reservation_get_all_by_lease_id(lease_id): reservation['start_date'] = values['start_date'] reservation['end_date'] = values['end_date'] resource_type = reservation['resource_type'] self.plugins[resource_type].update_reservation( reservation['id'], reservation) event = db_api.event_get_first_sorted_by_filters( 'lease_id', 'asc', { 'lease_id': lease_id, 'event_type': 'start_lease' } ) if not event: raise common_ex.ClimateException( 'Start lease event not found') db_api.event_update(event['id'], {'time': values['start_date']}) event = db_api.event_get_first_sorted_by_filters( 'lease_id', 'asc', { 'lease_id': lease_id, 'event_type': 'end_lease' } ) if not event: raise common_ex.ClimateException( 'End lease event not found') db_api.event_update(event['id'], {'time': values['end_date']}) db_api.lease_update(lease_id, values) return db_api.lease_get(lease_id) def delete_lease(self, lease_id): lease = self.get_lease(lease_id) if (datetime.datetime.utcnow() < lease['start_date'] or datetime.datetime.utcnow() > lease['end_date']): with trusts.create_ctx_from_trust(lease['trust_id']): for reservation in lease['reservations']: try: self.plugins[reservation['resource_type']]\ .on_end(reservation['resource_id']) except (db_ex.ClimateDBException, RuntimeError): LOG.exception("Failed to delete a reservation " "for a lease.") raise db_api.lease_destroy(lease_id) else: raise common_ex.NotAuthorized( 'Already started lease cannot be deleted') def start_lease(self, lease_id, event_id): lease = self.get_lease(lease_id) with trusts.create_ctx_from_trust(lease['trust_id']): self._basic_action(lease_id, event_id, 'on_start', 'active') def end_lease(self, lease_id, event_id): lease = self.get_lease(lease_id) with trusts.create_ctx_from_trust(lease['trust_id']): self._basic_action(lease_id, event_id, 'on_end', 'deleted') def _basic_action(self, lease_id, event_id, action_time, reservation_status=None): """Commits basic lease actions such as starting and ending.""" lease = self.get_lease(lease_id) for reservation in lease['reservations']: resource_type = reservation['resource_type'] try: self.resource_actions[resource_type][action_time]( reservation['resource_id'] ) except common_ex.ClimateException: LOG.exception("Failed to execute action %(action)s " "for lease %(lease)s" % { 'action': action_time, 'lease': lease_id, }) if reservation_status is not None: db_api.reservation_update(reservation['id'], {'status': reservation_status}) db_api.event_update(event_id, {'status': 'DONE'}) def __getattr__(self, name): """RPC Dispatcher for plugins methods.""" fn = None try: resource_type, method = name.rsplit(':', 1) except ValueError: # NOTE(sbauza) : the dispatcher needs to know which plugin to use, # raising error if consequently not raise AttributeError(name) try: try: fn = getattr(self.plugins[resource_type], method) except KeyError: LOG.error("Plugin with resource type %s not found", resource_type) raise exceptions.UnsupportedResourceType(resource_type) except AttributeError: LOG.error("Plugin %s doesn't include method %s", self.plugins[resource_type], method) if fn is not None: return fn raise AttributeError(name)