# Copyright (C) 2015-2020 OpenIO SAS, as part of OpenIO SDS
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from werkzeug.wrappers import Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import NotFound, BadRequest, Conflict
from functools import wraps
from oio.account.backend import AccountBackend
from oio.common.constants import REQID_HEADER, STRLEN_REQID
from oio.common.json import json
from oio.common.logger import get_logger
from oio.common.wsgi import WerkzeugApp
from oio.common.easy_value import int_value, true_value
ACCOUNT_LISTING_DEFAULT_LIMIT = 1000
ACCOUNT_LISTING_MAX_LIMIT = 10000
[docs]def access_log(func):
@wraps(func)
def _access_log_wrapper(self, req, *args, **kwargs):
from time import time
pre = time()
rc = func(self, req, *args, **kwargs)
post = time()
reqid = req.headers.get(REQID_HEADER, '-')[:STRLEN_REQID]
# func time size user reqid
self.logger.info("%s %0.6f %s %s %s",
func.__name__, post - pre, '-', '-', reqid)
return rc
return _access_log_wrapper
[docs]class Account(WerkzeugApp):
# pylint: disable=no-member
def __init__(self, conf, backend, logger=None):
self.conf = conf
self.backend = backend
self.logger = logger or get_logger(conf)
self.url_map = Map([
Rule('/status', endpoint='status'),
Rule('/v1.0/account/create', endpoint='account_create',
methods=['PUT']),
Rule('/v1.0/account/delete', endpoint='account_delete',
methods=['POST']),
Rule('/v1.0/account/list', endpoint='account_list',
methods=['GET']),
Rule('/v1.0/account/update', endpoint='account_update',
methods=['PUT', 'POST']), # FIXME(adu) only PUT
Rule('/v1.0/account/update-bucket', endpoint='bucket_update',
methods=['PUT']),
Rule('/v1.0/account/show', endpoint='account_show',
methods=['GET']),
Rule('/v1.0/account/show-bucket', endpoint='bucket_show',
methods=['GET']),
Rule('/v1.0/account/show-container',
endpoint='account_container_show',
methods=['GET']),
Rule('/v1.0/account/buckets', endpoint='account_buckets',
methods=['GET']),
Rule('/v1.0/account/containers', endpoint='account_containers',
methods=['GET']),
Rule('/v1.0/account/refresh', endpoint='account_refresh',
methods=['POST']),
Rule('/v1.0/account/flush', endpoint='account_flush',
methods=['POST']),
Rule('/v1.0/account/container/reset',
endpoint='account_container_reset',
methods=['PUT', 'POST']), # FIXME(adu) only PUT
Rule('/v1.0/account/container/show',
endpoint='account_container_show',
methods=['GET']),
Rule('/v1.0/account/container/update',
endpoint='account_container_update',
methods=['PUT', 'POST']), # FIXME(adu) only PUT
Rule('/v1.0/bucket/show', endpoint='bucket_show',
methods=['GET']),
Rule('/v1.0/bucket/update', endpoint='bucket_update',
methods=['PUT']),
])
super(Account, self).__init__(self.url_map, self.logger)
def _get_item_id(self, req, key='id', what='account'):
"""Fetch the name of the requested item, raise an error if missing."""
item_id = req.args.get(key)
if not item_id:
raise BadRequest('Missing %s ID' % what)
return item_id
def _get_account_id(self, req):
"""Fetch account name from request query string."""
return self._get_item_id(req, what='account')
# ACCT{{
# GET /status
# ~~~~~~~~~~~
# Return a summary of the target account service. The body of the reply
# will present a count of the objects in the databse, formatted as a JSON
# object.
#
# Sample request:
#
# .. code-block:: http
#
# GET /status HTTP/1.1
# Host: 127.0.0.1:6021
# User-Agent: curl/7.55.1
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.7.1
# Date: Wed, 22 Nov 2017 09:45:03 GMT
# Connection: keep-alive
# Content-Type: text/json; charset=utf-8
# Content-Length: 20
#
# {"account_count": 0}
#
# }}ACCT
[docs] def on_status(self, req):
status = self.backend.status()
return Response(json.dumps(status), mimetype='text/json')
# ACCT{{
# PUT /v1.0/account/create?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create a new account with the specified name.
#
# Sample request:
#
# .. code-block:: http
#
# PUT /v1.0/account/create?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 201 CREATED
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
# Content-Length: 9
#
# myaccount
#
# }}ACCT
[docs] def on_account_create(self, req):
account_id = self._get_account_id(req)
id = self.backend.create_account(account_id)
if id:
return Response(id, 201)
else:
return Response(status=202)
# ACCT{{
# GET /v1.0/account/list
# ~~~~~~~~~~~~~~~~~~~~~~
# Get the list of existing accounts.
#
# Sample request:
#
# .. code-block:: http
#
# GET /v1.0/account/list HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/json; charset=utf-8
# Content-Length: 13
#
# ["myaccount"]
#
# }}ACCT
[docs] @access_log
def on_account_list(self, req):
accounts = self.backend.list_accounts()
if accounts is None:
return NotFound('No account found')
return Response(json.dumps(accounts), mimetype='text/json')
# ACCT{{
# POST /v1.0/account/delete?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Delete the specified account.
#
# Sample request:
#
# .. code-block:: http
#
# POST /v1.0/account/delete?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 204 No Content
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
#
# }}ACCT
[docs] def on_account_delete(self, req):
account_id = self._get_account_id(req)
result = self.backend.delete_account(account_id)
if result is None:
return NotFound('No such account')
if result is False:
return Conflict('Account not empty')
else:
return Response(status=204)
# ACCT{{
# POST /v1.0/account/update?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Update metadata of the specified account.
#
# Sample request:
#
# .. code-block:: http
#
# PUT /v1.0/account/update?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
# Content-Length: 41
# Content-Type: application/x-www-for-urlencoded
#
# .. code-block:: json
#
# {
# "metadata": {"key":"value"},
# "to_delete": ["key"]
# }
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 204 NO CONTENT
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
#
# }}ACCT
[docs] def on_account_update(self, req):
account_id = self._get_account_id(req)
decoded = json.loads(req.get_data())
metadata = decoded.get('metadata')
to_delete = decoded.get('to_delete')
success = self.backend.update_account_metadata(
account_id, metadata, to_delete)
if success:
return Response(status=204)
return NotFound('Account not found')
# ACCT{{
# GET /v1.0/account/show?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Get information about the specified account.
#
# Sample request:
#
# .. code-block:: http
#
# GET /v1.0/account/show?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
# Content-Length: 107
#
# .. code-block:: json
#
# {
# "ctime": "1533127401.08165",
# "bytes": 0,
# "objects": 0,
# "id": "myaccount",
# "containers": 0,
# "metadata": {}
# }
#
# }}ACCT
[docs] @access_log
def on_account_show(self, req):
account_id = self._get_account_id(req)
raw = self.backend.info_account(account_id)
if raw is not None:
return Response(json.dumps(raw), mimetype='text/json')
return NotFound('Account not found')
# ACCT{{
# GET /v1.0/account/buckets?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Get information about the buckets belonging to the specified account.
#
# Sample request:
#
# .. code-block:: http
#
# GET /v1.0/account/buckets?id=AUTH_demo HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
# Content-Length: 122
#
# .. code-block:: json
#
# {
# "buckets": 3,
# "bytes": 132573,
# "ctime": "1582628089.30237",
# "objects": 3,
# "listing": [
# {
# "bytes": 132573,
# "mtime": 1582641887.75408,
# "name": "bucket0",
# "objects": 3
# },
# {
# "bytes": 0,
# "mtime": 1582641712.44969,
# "name": "bucket1",
# "objects": 0
# },
# {
# "bytes": 0,
# "mtime": 1582641715.19412,
# "name": "bucket2",
# "objects": 0
# }
# ],
# "id": "AUTH_demo",
# "containers": 5,
# "metadata": {}
# }
#
# }}ACCT
[docs] @access_log
def on_account_buckets(self, req):
account_id = self._get_account_id(req)
info = self.backend.info_account(account_id)
if not info:
return NotFound('Account not found')
marker = req.args.get('marker', '')
end_marker = req.args.get('end_marker', '')
prefix = req.args.get('prefix', '')
limit = int(req.args.get('limit', '1000'))
bucket_list, next_marker = self.backend.list_buckets(
account_id, limit=limit, marker=marker, end_marker=end_marker,
prefix=prefix)
info['listing'] = bucket_list
info['truncated'] = next_marker is not None
if next_marker is not None:
info['next_marker'] = next_marker
result = json.dumps(info)
return Response(result, mimetype='text/json')
# ACCT{{
# GET /v1.0/account/containers?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Get information about the containers belonging to the specified account.
#
# Sample request:
#
# .. code-block:: http
#
# GET /v1.0/account/containers?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
# Content-Length: 122
#
# .. code-block:: json
#
# {
# "ctime": "1533127401.08165",
# "bytes": 0,
# "objects": 0,
# "listing": [],
# "id": "account",
# "containers": 0,
# "metadata": {}
# }
#
# }}ACCT
[docs] @access_log
def on_account_containers(self, req):
account_id = self._get_account_id(req)
info = self.backend.info_account(account_id)
if not info:
return NotFound('Account not found')
marker = req.args.get('marker', '')
end_marker = req.args.get('end_marker', '')
prefix = req.args.get('prefix', '')
limit = int(req.args.get('limit', '1000'))
limit = max(0, min(ACCOUNT_LISTING_MAX_LIMIT, int_value(
req.args.get('limit'), 0)))
if limit <= 0:
limit = ACCOUNT_LISTING_DEFAULT_LIMIT
delimiter = req.args.get('delimiter', '')
s3_buckets_only = true_value(req.args.get('s3_buckets_only', False))
user_list = self.backend.list_containers(
account_id, limit=limit, marker=marker, end_marker=end_marker,
prefix=prefix, delimiter=delimiter,
s3_buckets_only=s3_buckets_only)
info['listing'] = user_list
# TODO(FVE): add "truncated" entry telling if the listing is truncated
result = json.dumps(info)
return Response(result, mimetype='text/json')
# ACCT{{
# PUT /v1.0/account/container/update?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Update account with container-related metadata.
#
# Request example:
#
# .. code-block:: http
#
# PUT /v1.0/account/container/update?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
# Content-Length: 84
# Content-Type: application/x-www-form-urlencoded
#
# .. code-block:: json
#
# {
# "mtime": "123456789",
# "dtime": "1223456789",
# "name": "user1bucket%2Ffiles",
# "objects": 0,
# "bytes": 0,
# "bucket": "user1bucket"
# }
#
# Response example:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
# Content-Length: 117
#
# }}ACCT
[docs] def on_account_container_update(self, req):
account_id = self._get_account_id(req)
data = json.loads(req.get_data())
name = data.get('name')
mtime = data.get('mtime')
dtime = data.get('dtime')
object_count = data.get('objects')
bytes_used = data.get('bytes')
damaged_objects = data.get('damaged_objects')
missing_chunks = data.get('missing_chunks')
bucket_name = data.get('bucket') # can be None
# Exceptions are catched by dispatch_request
info = self.backend.update_container(
account_id, name, mtime, dtime,
object_count, bytes_used, damaged_objects, missing_chunks,
bucket_name=bucket_name)
result = json.dumps(info)
return Response(result)
# ACCT{{
# PUT /v1.0/account/container/reset?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Reset statistics of the specified container.
# Usually followed by a "container touch" operation.
#
# Request example:
#
# .. code-block:: http
#
# PUT /v1.0/account/container/reset?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
# Content-Length: 45
# Content-Type: application/x-www-form-urlencoded
#
# .. code-block:: json
#
# {
# "name": "container name",
# "mtime": 1234567891011
# }
#
# Response example:
#
# .. code-block:: http
#
# HTTP/1.1 204 NO CONTENT
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
#
# }}ACCT
[docs] def on_account_container_reset(self, req):
account_id = self._get_account_id(req)
data = json.loads(req.get_data())
name = data.get('name')
mtime = data.get('mtime')
dtime = None
object_count = 0
bytes_used = 0
damaged_objects = 0
missing_chunks = 0
# Exceptions are catched by dispatch_request
self.backend.update_container(
account_id, name, mtime, dtime,
object_count, bytes_used, damaged_objects, missing_chunks,
autocreate_container=False)
return Response(status=204)
# ACCT{{
# POST /v1.0/account/refresh?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Refresh counter of an account named account_name
#
# .. code-block:: http
#
# POST /v1.0/account/refresh?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# .. code-block:: http
#
# HTTP/1.1 204 NO CONTENT
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
#
# }}ACCT
[docs] def on_account_refresh(self, req):
account_id = self._get_account_id(req)
self.backend.refresh_account(account_id)
return Response(status=204)
# ACCT{{
# POST /v1.0/account/flush?id=<account_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Flush all container of an account named account_name
#
# .. code-block:: http
#
# POST /v1.0/account/flush?id=myaccount HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# .. code-block:: http
#
# HTTP/1.1 204 NO CONTENT
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
#
# }}ACCT
[docs] def on_account_flush(self, req):
account_id = self._get_account_id(req)
self.backend.flush_account(account_id)
return Response(status=204)
# ACCT{{
# GET /v1.0/bucket/show?id=<bucket_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Get information about the specified bucket.
#
# Sample request:
#
# .. code-block:: http
#
# GET /v1.0/bucket/show?id=mybucket HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
# Content-Length: 128
#
# .. code-block:: json
#
# {
# "account": "myaccount",
# "bytes": 11300,
# "damaged_objects": 0,
# "missing_objects": 0,
# "mtime": "1533127401.08165",
# "objects": 100,
# "replication_enabled": false
# }
#
# }}ACCT
[docs] @access_log
def on_bucket_show(self, req):
"""
Get all available information about a bucket.
"""
bname = self._get_item_id(req, what='bucket')
raw = self.backend.get_bucket_info(bname)
if raw is not None:
return Response(json.dumps(raw), mimetype='text/json')
return NotFound('Bucket not found')
# ACCT{{
# POST /v1.0/bucket/update?id=<bucket_name>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Update metadata of the specified bucket.
#
# Sample request:
#
# .. code-block:: http
#
# PUT /v1.0/bucket/update?id=mybucket HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
# Content-Length: 41
# Content-Type: application/x-www-for-urlencoded
#
# .. code-block:: json
#
# {
# "metadata": {"key":"value"},
# "to_delete": ["oldkey"]
# }
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
#
# .. code-block:: json
#
# {
# "account": "myaccount",
# "bytes": 11300,
# "damaged_objects": 0,
# "missing_objects": 0,
# "mtime": "1533127401.08165",
# "objects": 100,
# "replication_enabled": false
# }
#
# }}ACCT
[docs] def on_bucket_update(self, req):
"""
Update (or delete) bucket metadata.
"""
bname = self._get_item_id(req, what='bucket')
decoded = json.loads(req.get_data())
metadata = decoded.get('metadata')
to_delete = decoded.get('to_delete')
info = self.backend.update_bucket_metadata(
bname, metadata, to_delete)
if info is not None:
return Response(json.dumps(info), mimetype='text/json')
return NotFound('Bucket not found')
# ACCT{{
# GET /v1.0/account/container/show?id=<account_name>&container=<container>
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Get information about the specified container.
#
# Sample request:
#
# .. code-block:: http
#
# GET /v1.0/account/show-container?id=AUTH_demo&container=buck0 HTTP/1.1
# Host: 127.0.0.1:6013
# User-Agent: curl/7.47.0
# Accept: */*
#
# Sample response:
#
# .. code-block:: http
#
# HTTP/1.1 200 OK
# Server: gunicorn/19.9.0
# Date: Wed, 01 Aug 2018 12:17:25 GMT
# Connection: keep-alive
# Content-Type: text/plain; charset=utf-8
# Content-Length: 128
#
# .. code-block:: json
#
# {
# "bucket": "buck0",
# "bytes": 2052,
# "damaged_objects": 0,
# "dtime": "0",
# "missing_chunks": 0,
# "mtime": "1583772880.48631",
# "name": "buck0",
# "objects": 2,
# "replication_enabled": false
# }
#
# }}ACCT
[docs] @access_log
def on_account_container_show(self, req):
account_id = self._get_account_id(req)
cname = self._get_item_id(req, key='container', what='container')
raw = self.backend.get_container_info(account_id, cname)
if raw is not None:
return Response(json.dumps(raw), mimetype='text/json')
return NotFound('Container not found')
[docs]def create_app(conf, **kwargs):
backend = AccountBackend(conf)
app = Account(conf, backend)
return app