6b26ecaf75
Refernces from tenant to project were made in several places, including DB, HTTP Headers, configuration items, json in responses and devstack integration. Additionally, a migration script using Alembic was included. Implements: blueprint tenant-to-project-ref Change-Id: I4b21182f555ccd412a4ca4e0ce753f07edcd07f8
398 lines
15 KiB
Python
398 lines
15 KiB
Python
# 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)
|