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
# limitations under the License.
from blazar.enforcement.filters.external_service_filter import (
ExternalServiceFilter)
from blazar.enforcement.filters.max_lease_duration_filter import (
MaxLeaseDurationFilter)
__all__ = ['MaxLeaseDurationFilter']
__all__ = ['ExternalServiceFilter', 'MaxLeaseDurationFilter']
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,
blazar.manager.service.manager_opts)),
('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.enforcement.enforcement_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
custom usage enforcement filters.
custom usage enforcement filters or an external service.
Description
===========
Usage enforcement filters are called on ``lease_create``, ``lease_update`` and
``on_end`` operations. The filters check whether or not lease values or
allocation criteria pass admin defined thresholds. There is currently one
filter provided out-of-the-box. The ``MaxLeaseDurationFilter`` restricts the
duration of leases.
allocation criteria pass admin defined thresholds. There are currently two
filters provided out-of-the-box. ``MaxLeaseDurationFilter`` restricts the
duration of leases. ``ExternalServiceFilter`` calls a third-party service for
implementing policies using a URL configured in ``blazar.conf``.
Options
=======
@ -48,3 +49,84 @@ supports two configuration options:
See the :doc:`../configuration/blazar-conf` page for a description of these
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``.