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
|
# 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__
|
||||||
|
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,
|
('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),
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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