Add enforcement filter using an external HTTP service

Co-Authored-By: Jacob Colleran <jakecoll@uchicago.edu>
Co-Authored-By: Jason Anderson <jasonanderson@uchicago.edu>
Co-Authored-By: Pierre Riteau <pierre@stackhpc.com>

Change-Id: I0728f556829ba84e222c27bd8c407738b4be2f76
This commit is contained in:
Pierre Riteau 2022-08-30 22:06:54 +02:00
parent 42e4b7639f
commit 22e99c1827
5 changed files with 252 additions and 5 deletions

View File

@ -13,9 +13,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from blazar.enforcement.filters.external_service_filter import (
ExternalServiceFilter)
from blazar.enforcement.filters.max_lease_duration_filter import ( from blazar.enforcement.filters.max_lease_duration_filter import (
MaxLeaseDurationFilter) MaxLeaseDurationFilter)
__all__ = ['MaxLeaseDurationFilter'] __all__ = ['ExternalServiceFilter', 'MaxLeaseDurationFilter']
all_filters = __all__ all_filters = __all__

View File

@ -0,0 +1,156 @@
# Copyright (c) 2022 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.enforcement.filters import base_filter
from blazar import exceptions
from blazar.i18n import _
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 enforcement filter returned a %(status)s '
'HTTP response. Only 204 and 403 responses are supported.')
class ExternalServiceUnsupportedDeniedResponse(exceptions.BlazarException):
code = 400
msg_fmt = _('External service enforcement filter returned a 403 HTTP '
'response %(response)s without a valid JSON dictionary '
'containing a "message" key.')
class ExternalServiceFilterException(exceptions.NotAuthorized):
code = 400
msg_fmt = _('%(message)s')
class ExternalServiceFilter(base_filter.BaseFilter):
enforcement_opts = [
cfg.StrOpt(
'external_service_endpoint',
default=None,
help='The URL of the external service API.'),
cfg.StrOpt(
'external_service_check_create',
default=None,
help='Overwrite check-create endpoint with absolute URL.'),
cfg.StrOpt(
'external_service_check_update',
default=None,
help='Overwrite check-update endpoint with absolute URL.'),
cfg.StrOpt(
'external_service_on_end',
default=None,
help='Overwrite on-end endpoint with absolute URL.'),
cfg.StrOpt(
'external_service_token',
default="",
help='Token used for authentication with the external service.')
]
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:
client = BlazarKeystoneClient()
headers['X-Auth-Token'] = client.session.get_token()
return headers
def _get_absolute_url(self, path):
url = self.external_service_endpoint
if url[-1] == '/':
url += path[1:]
else:
url += path
return url
def post(self, url, body):
body = json.dumps(body, cls=DateTimeEncoder)
req = requests.post(url, headers=self.get_headers(), data=body)
if req.status_code == 204:
return True
elif req.status_code == 403:
try:
message = req.json()['message']
except (requests.JSONDecodeError, KeyError):
raise ExternalServiceUnsupportedDeniedResponse(
response=req.content)
raise ExternalServiceFilterException(message=message)
else:
raise ExternalServiceUnsupportedHTTPResponse(
status=req.status_code)
def check_create(self, context, lease_values):
body = dict(context=context, lease=lease_values)
if self.external_service_check_create:
self.post(self.external_service_check_create, body)
return
if self.external_service_endpoint:
path = '/check-create'
self.post(self._get_absolute_url(path), body)
return
def check_update(self, context, current_lease_values, new_lease_values):
body = dict(context=context, current_lease=current_lease_values,
lease=new_lease_values)
if self.external_service_check_update:
self.post(self.external_service_check_update, body)
return
if self.external_service_endpoint:
path = '/check-update'
self.post(self._get_absolute_url(path), body)
return
def on_end(self, context, lease_values):
body = dict(context=context, lease=lease_values)
if self.external_service_on_end:
self.post(self.external_service_on_end, body)
return
if self.external_service_endpoint:
path = '/on-end'
self.post(self._get_absolute_url(path), body)
return

View File

@ -45,6 +45,8 @@ def list_opts():
('manager', itertools.chain(blazar.manager.opts, ('manager', itertools.chain(blazar.manager.opts,
blazar.manager.service.manager_opts)), blazar.manager.service.manager_opts)),
('enforcement', itertools.chain( ('enforcement', itertools.chain(
blazar.enforcement.filters.external_service_filter
.ExternalServiceFilter.enforcement_opts,
blazar.enforcement.filters.max_lease_duration_filter.MaxLeaseDurationFilter.enforcement_opts, # noqa blazar.enforcement.filters.max_lease_duration_filter.MaxLeaseDurationFilter.enforcement_opts, # noqa
blazar.enforcement.enforcement.enforcement_opts)), blazar.enforcement.enforcement.enforcement_opts)),
('notifications', blazar.notification.notifier.notification_opts), ('notifications', blazar.notification.notifier.notification_opts),

View File

@ -6,16 +6,17 @@ Synopsis
======== ========
Usage enforcement and lease constraints can be implemented by operators via Usage enforcement and lease constraints can be implemented by operators via
custom usage enforcement filters. custom usage enforcement filters or an external service.
Description Description
=========== ===========
Usage enforcement filters are called on ``lease_create``, ``lease_update`` and Usage enforcement filters are called on ``lease_create``, ``lease_update`` and
``on_end`` operations. The filters check whether or not lease values or ``on_end`` operations. The filters check whether or not lease values or
allocation criteria pass admin defined thresholds. There is currently one allocation criteria pass admin defined thresholds. There are currently two
filter provided out-of-the-box. The ``MaxLeaseDurationFilter`` restricts the filters provided out-of-the-box. ``MaxLeaseDurationFilter`` restricts the
duration of leases. duration of leases. ``ExternalServiceFilter`` calls a third-party service for
implementing policies using a URL configured in ``blazar.conf``.
Options Options
======= =======
@ -48,3 +49,84 @@ supports two configuration options:
See the :doc:`../configuration/blazar-conf` page for a description of these See the :doc:`../configuration/blazar-conf` page for a description of these
options. options.
ExternalServiceFilter
---------------------
This filter delegates the decision for each API to an external HTTP service.
The service must use token-based authentication and implement the following
endpoints for POST method:
* ``POST /v1/check-create``
* ``POST /v1/check-update``
* ``POST /v1/on-end``
The external service should return ``204 No Content`` if the parameters meet
defined criteria and ``403 Forbidden`` if not.
Example format of data the external service will receive in a request body:
* Request example:
.. sourcecode:: json
{
"context": {
"user_id": "c631173e-dec0-4bb7-a0c3-f7711153c06c",
"project_id": "a0b86a98-b0d3-43cb-948e-00689182efd4",
"auth_url": "https://api.example.com:5000/v3",
"region_name": "RegionOne"
},
"current_lease": {
"start_date": "2020-05-13 00:00",
"end_time": "2020-05-14 23:59",
"reservations": [
{
"resource_type": "physical:host",
"min": 1,
"max": 2,
"hypervisor_properties": "[]",
"resource_properties": "[\"==\", \"$availability_zone\", \"az1\"]",
"allocations": [
{
"id": "1",
"hypervisor_hostname": "32af5a7a-e7a3-4883-a643-828e3f63bf54",
"extra": {
"availability_zone": "az1"
}
}
]
}
]
},
"lease": {
"start_date": "2020-05-13 00:00",
"end_time": "2020-05-14 23:59",
"reservations": [
{
"resource_type": "physical:host",
"min": 2,
"max": 3,
"hypervisor_properties": "[]",
"resource_properties": "[\"==\", \"$availability_zone\", \"az1\"]",
"allocations": [
{
"id": "1",
"hypervisor_hostname": "32af5a7a-e7a3-4883-a643-828e3f63bf54",
"extra": {
"availability_zone": "az1"
}
},
{
"id": "2",
"hypervisor_hostname": "af69aabd-8386-4053-a6dd-1a983787bd7f",
"extra": {
"availability_zone": "az1"
}
}
]
}
]
}
}

View File

@ -0,0 +1,5 @@
---
features:
- |
Add a usage enforcement filter delegating decisions to an external HTTP
service. This new filter is called ``ExternalServiceFilter``.