Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions blazar/db/sqlalchemy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ def get_reservation_allocations_by_host_ids(host_ids, start_date, end_date,
return query.all()


def get_reservation_allocations_by_fip_ids(fip_ids, start_date, end_date,
lease_id=None, reservation_id=None):
session = get_session()
border0 = start_date <= models.Lease.end_date

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the type of start_date? I'm surprised this works; I would have thought that you'd need to do it like:

models.Lease.end_date >= start_date

... because this looks like it has to be an operator overload, the way it's working here. And I thought those used the implementation defined by the first item's type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I was never too sure myself. Upstream uses this border implementation for get_reservation_allocations_by_host_ids and I implemented it this way for floating ips for consistency.

border1 = models.Lease.start_date <= end_date
query = (session.query(models.Reservation.id,
models.Reservation.lease_id,
models.FloatingIPAllocation.floatingip_id)
.join(models.Lease,
models.Lease.id == models.Reservation.lease_id)
.join(models.FloatingIPAllocation,
models.FloatingIPAllocation.reservation_id ==
models.Reservation.id)
.filter(models.FloatingIPAllocation.floatingip_id.in_(fip_ids))
.filter(sa.and_(border0, border1)))
if lease_id:
query = query.filter(models.Reservation.lease_id == lease_id)
if reservation_id:
query = query.filter(models.Reservation.id == reservation_id)
return query.all()


def get_plugin_reservation(resource_type, resource_id):
if resource_type == host_plugin.RESOURCE_TYPE:
return api.host_reservation_get(resource_id)
Expand Down
6 changes: 6 additions & 0 deletions blazar/db/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ def get_reservation_allocations_by_host_ids(host_ids, start_date, end_date,
reservation_id)


def get_reservation_allocations_by_fip_ids(fip_ids, start_date, end_date,
lease_id=None, reservation_id=None):
return IMPL.get_reservation_allocations_by_fip_ids(
fip_ids, start_date, end_date, lease_id, reservation_id)


def get_plugin_reservation(resource_type, resource_id):
return IMPL.get_plugin_reservation(resource_type, resource_id)

Expand Down
18 changes: 18 additions & 0 deletions blazar/enforcement/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (c) 2020 University of Chicago.
#
# 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 blazar.enforcement.enforcement import UsageEnforcement

__all__ = ['UsageEnforcement']
99 changes: 99 additions & 0 deletions blazar/enforcement/enforcement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright (c) 2020 University of Chicago.
#
# 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 blazar.enforcement import filters
from blazar.utils.openstack import base

from oslo_config import cfg
from oslo_log import log as logging

CONF = cfg.CONF

enforcement_opts = [
cfg.ListOpt('enabled_filters',
default=[],
help='List of enabled usage enforcement filters.'),
]

CONF.register_opts(enforcement_opts, group='enforcement')
LOG = logging.getLogger(__name__)


class UsageEnforcement:

def __init__(self):
self.load_filters()

def load_filters(self):
self.enabled_filters = set()
for filter_name in CONF.enforcement.enabled_filters:
_filter = getattr(filters, filter_name)

if filter_name in filters.all_filters:
self.enabled_filters.add(_filter(conf=CONF))
else:
LOG.error("{} not in filters module.".format(filter_name))

self.enabled_filters = list(self.enabled_filters)

def format_context(self, context, lease_values):
ctx = context.to_dict()
region_name = CONF.os_region_name
auth_url = base.url_for(
ctx['service_catalog'], CONF.identity_service,
os_region_name=region_name)

return dict(user_id=lease_values['user_id'],
project_id=lease_values['project_id'],
auth_url=auth_url, region_name=region_name)

def format_lease(self, lease_values, reservations, allocations):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, could be private

lease = lease_values.copy()
lease['reservations'] = []

for reservation in reservations:
res = reservation.copy()
resource_type = res['resource_type']
res['allocations'] = allocations[resource_type]
lease['reservations'].append(res)

return lease

def check_create(self, context, lease_values, reservations, allocations):
context = self.format_context(context, lease_values)
lease = self.format_lease(lease_values, reservations, allocations)

for filter_ in self.enabled_filters:
filter_.check_create(context, lease)

def check_update(self, context, current_lease, new_lease,
current_allocations, new_allocations,
current_reservations, new_reservations):
context = self.format_context(context, current_lease)
current_lease = self.format_lease(current_lease, current_reservations,
current_allocations)
new_lease = self.format_lease(new_lease, new_reservations,
new_allocations)

for filter_ in self.enabled_filters:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supreme nitpick: filter_ vs _filter? My OCD wants there to be one convention :shame:

filter_.check_update(context, current_lease, new_lease)

def on_end(self, context, lease, allocations):
context = self.format_context(context, lease)
lease_values = self.format_lease(lease, lease['reservations'],
allocations)

for filter_ in self.enabled_filters:
filter_.on_end(context, lease_values)
23 changes: 23 additions & 0 deletions blazar/enforcement/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) 2013 Bull.
#
# 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 blazar.enforcement.filters.external_service_filter import (
ExternalServiceFilter)
from blazar.enforcement.filters.max_reservation_length_filter import (
MaxReservationLengthFilter)

__all__ = ['MaxReservationLengthFilter', 'ExternalServiceFilter']

all_filters = __all__
45 changes: 45 additions & 0 deletions blazar/enforcement/filters/base_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) 2020 University of Chicago.
#
# 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 abc
import six


@six.add_metaclass(abc.ABCMeta)
class BaseFilter:

enforcement_opts = []

def __init__(self, conf=None):
self.conf = conf

for opt in self.enforcement_opts:
self.conf.register_opt(opt, 'enforcement')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the spec we outlined that some filters may define their own group. E.g., the external filter has its options all under a new enforcement_external group. Not sure what the best way to express that is, maybe the filter can define a enforcement_opt_group, which defaults to 'enforcement'?


def __getattr__(self, name):
func = getattr(self.conf.enforcement, name)
return func

@abc.abstractmethod
def check_create(self, context, lease_values):
pass

@abc.abstractmethod
def check_update(self, context, current_lease_values, new_lease_values):
pass

@abc.abstractmethod
def on_end(self, context, lease_values):
pass
128 changes: 128 additions & 0 deletions blazar/enforcement/filters/external_service_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Copyright (c) 2020 University of Chicago.
#
# 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 json
import requests

from blazar import context
from blazar.enforcement.filters import base_filter
from blazar import exceptions
from blazar.i18n import _
from blazar.utils.openstack import base
from blazar.utils.openstack.keystone import BlazarKeystoneClient

from oslo_config import cfg
from oslo_log import log as logging

LOG = logging.getLogger(__name__)


class DateTimeEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime.datetime):
return str(o)

return json.JSONEncoder.default(self, o)


class ExternalServiceUnsupportedHTTPResponse(exceptions.BlazarException):
code = 400
msg_fmt = _('External Service Filter returned a %(status)s http response. '
'Only 204 and 403 responses are supported.')


class ExternalServiceFilterException(exceptions.BlazarException):
code = 400
msg_fmt = _('%(message)s')


class ExternalServiceFilter(base_filter.BaseFilter):

enforcement_opts = [
cfg.StrOpt(
'external_service_endpoint',
default="",
help='The url of the external service API. A value of -1 will '
'disabled the service.'),
cfg.StrOpt(
'external_service_token',
default=False,
help='Authentication token for token based authentication.')
]

def __init__(self, conf=None):
super(ExternalServiceFilter, self).__init__(conf=conf)

def get_headers(self):
headers = {'Content-Type': 'application/json'}

if self.external_service_token:
headers['X-Auth-Token'] = self.external_service_token
else:
auth_url = "%s://%s:%s/%s" % (self.conf.os_auth_protocol,
base.get_os_auth_host(self.conf),
self.conf.os_auth_port,
self.conf.os_auth_prefix)
client = BlazarKeystoneClient(
password=self.conf.os_admin_password,
auth_url=auth_url,
ctx=context.admin())

headers['X-Auth-Token'] = client.auth_token

return headers

def post(self, path, body):
url = self.external_service_endpoint

if url[-1] == '/':
url += path[1:]
else:
url += path

body = json.dumps(body, cls=DateTimeEncoder)
req = requests.post(url, headers=self.get_headers(), date=body)

if req.status_code == 204:
return True
elif req.status_code == 403:
raise ExternalServiceFilterException(
message=req.json().get('message'))
else:
raise ExternalServiceUnsupportedHTTPResponse(
status=req.status_code)

def check_create(self, context, lease_values):
if self.external_service_endpoint:
path = '/v1/check-create'
body = dict(context=context, lease=lease_values)

self.post(path, body)

def check_update(self, context, current_lease_values, new_lease_values):
if self.external_service_endpoint:
path = '/v1/check-update'
body = dict(context=context, current_lease=current_lease_values,
lease=new_lease_values)

self.post(path, body)

def on_end(self, context, lease_values):
if self.external_service_endpoint:
path = '/v1/on-end'
body = dict(context=context, lease=lease_values)

self.post(path, body)
Loading