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:
parent
42e4b7639f
commit
22e99c1827
@ -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__
|
||||
|
156
blazar/enforcement/filters/external_service_filter.py
Normal file
156
blazar/enforcement/filters/external_service_filter.py
Normal 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
|
@ -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),
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add a usage enforcement filter delegating decisions to an external HTTP
|
||||
service. This new filter is called ``ExternalServiceFilter``.
|
Loading…
Reference in New Issue
Block a user