From f8907f0c4fc0ba4bb97a1d636a50c5b40c2642f2 Mon Sep 17 00:00:00 2001 From: Edan Binshtok Date: Wed, 4 Oct 2017 09:33:23 +0300 Subject: Initial seed Initial upload of django test framework Change-Id: I643a7f4efc52cfafe4cc6d92e3178f36a0c1837c Issue-Id: VVP-1 Signed-off-by: Edan Binshtok --- rados/__init__.py | 38 +++ rados/rgwa_client.py | 593 +++++++++++++++++++++++++++++++++++++++++++ rados/rgwa_client_factory.py | 87 +++++++ 3 files changed, 718 insertions(+) create mode 100644 rados/__init__.py create mode 100644 rados/rgwa_client.py create mode 100644 rados/rgwa_client_factory.py (limited to 'rados') diff --git a/rados/__init__.py b/rados/__init__.py new file mode 100644 index 0000000..30d7152 --- /dev/null +++ b/rados/__init__.py @@ -0,0 +1,38 @@ + +# ============LICENSE_START========================================== +# org.onap.vvp/test-engine +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. diff --git a/rados/rgwa_client.py b/rados/rgwa_client.py new file mode 100644 index 0000000..1f10a98 --- /dev/null +++ b/rados/rgwa_client.py @@ -0,0 +1,593 @@ + +# ============LICENSE_START========================================== +# org.onap.vvp/test-engine +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. +"""A Ceph Rados Gateway Admin Operations API client.""" + +# Design goals: +# +# - Minimal abstractions over the raw Requests calls +# - Python method signatures enforce optional/required API parameters +# - DRY by procedurally mapping kwargs to API parameters +# - (TODO) procedurally generate this library directly from Ceph docs + +import os + +from awsauth import S3Auth +from requests import request + + +def _validate_args(valid_args, **kwargs): + """Validate kwargs conforms to a specification of allowable values. + + Ensures that any keyword arguments either: + - are unconstrained by valid_args, or + - have a value of None, or + - have a value that matches one of the corresponding specified values. + + This is useful for limiting several common keyword arguments to a set of + values across many methods, while ignoring those set to None. (Typically, + these are optional and were unspecified by the caller.) + + This is a validator function: it either returns None on success, or raises + an exception on failure. + + """ + for keyword, value in kwargs.items(): + if keyword not in valid_args: + continue + if value is None: + continue + if value in valid_args[keyword]: + continue + raise ValueError( + "Invalid parameter {:s}={!r}; must be one of: {!r}".format( + keyword, value, valid_args[keyword])) + + +class RGWAClient(object): + """A client for the Ceph Rados Gateway Admin Operations API. + + This class is implemented as a simplistic/mechanical wrapper around the + Python Requests library. Calling its methods triggers HTTP(S) calls to the + specified API endpoint, and the responses are decoded from JSON to Python + objects before being returned. + + The methods available on this object should mirror the endpoints of the API + closely enough that its documentation may be used as a reference: + + http://docs.ceph.com/docs/master/radosgw/adminops/ + + """ + valid_args = { + 'quota_type': ['user', 'bucket'], + 'key_type': ['s3', 'swift'], + }, + + def __init__(self, base_url, access_key=None, secret_key=None, verify='/opt/secrets/site-crt/site.crt', + return_raw_response=False): + """ + + base_url (string): + + The full URL to your admin entry point. Should include the protocol + ("http://" or "https://"), and optionally the port as well. The + URL-path to the admin entry point is configurable using "rgw admin + entry" in your Ceph configuration. Example: + + "https://s3.example.com:8080/admin" + + access_key (string): Your AWS Access Key ID + secret_key (string): Your AWS Secret Access Key + + If either of access_key or secret_key are omitted, this class will + attempt to look the values in the environment variables + AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY respectively. + + verify (boolean): + + Set to False to disable SSL Certificate verification, or optionally + set to the path to a CA Certificate bundle. This is passed directly + to the underlying call to the requests library; see: + http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification + + return_raw_response (boolean): + + All of the methods of this class return, upon success, the objects + resulting from parsing the JSON data returned by the API. On error, + they raise an exception. This is meant for caller convenience, but + may be undesirable in some situations because callers have no + access to the additional data and methods available in the raw + Response object. + + from ice_rgwa_client import ( + RGWAClient, HTTPError) + # ... + rgw = RGWAClient( + access_key='...', + secret_key='...', + base_url='...', + ) + # ... + try: + user = rgw.get_user('nonexistent') + except HTTPError as exc: + if exc.response.status_code == 404: + print("No such user") + continue + else: + print("Problem loading user") + raise + + If return_raw_response is set to True, the methods will instead + return the raw Response object from the Requests library, and it + will be up to the caller to check the error status as needed. + + See + http://docs.python-requests.org/en/master/user/quickstart/#json-response-content + + from ice_rgwa_client import RGWAClient + # ... + rgw = RGWAClient( + access_key='...', + secret_key='...', + base_url='...', + return_raw_response=True, + ) + # ... + response = rgw.get_user('nonexistent') + if response.status_code == 404: + print("No such user") + elif response.status_code != 200: + print("Problem loading user") + else: + user = response.json() + + """ + if not access_key: + access_key = os.environ.get('AWS_ACCESS_KEY_ID') + if not secret_key: + secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') + self.base_url = base_url + self.verify = verify + self.auth = S3Auth(access_key, secret_key, service_url=base_url) + self.return_raw_response = return_raw_response + + def _request(self, method, endpoint, action=None, data=None, **kwargs): + """Helper method to factor out actions common to Ceph Rados Gateway + Admin requests. + + "data" is a dictionary that, if provided, will be JSON-encoded and + submitted in the body of the request. + + Other keyword arguments will be encoded and used as URL parameters. + + "_" in kwargs will be converted to "-" in URL parameter keys. + + """ + # We can validate some arguments before the round trip to the server + _validate_args(self.valid_args, **kwargs) + + # We never want to pass literal None to the API, so we can use None to + # indicate "do not use this k/v pair at all." Using this, optional + # parameters still appear in the function signature but will be omitted + # from the request when unspecified. + params = { + k.replace('_', '-'): v + for k, v in kwargs.items() + if v is not None} + + # Same for body data but without _/- conversion... + data = {} if data is None else { + k: v + for k, v in data.items() + if v is not None} + + # The Ceph Object Gateway Admin Operations API specifies, for some + # operations, a key-only URL parameter (that we call "action") with no + # associated value. For simplicity, we violate the spec slightly by + # assigning it a value of "". It seems to work. + if action: + params[action] = '' + + # JSON output is the default, so there's no need to specify format=json + # parameter despite all the examples in the docs making it explicit. + + url = '%s/%s' % (self.base_url, endpoint) + response = request( + method=method, + url=url, + params=params, + json=data, + auth=self.auth, + verify=self.verify, + ) + if self.return_raw_response: + return response + else: + if response.status_code == 404: + return None + + response.raise_for_status() + try: + return response.json() + except ValueError: + # At this point we have a successful 200 status but a problem + # decoding the json. Some responses are empty: + if not response.content: + return {} + raise + + # + # These methods appear in the same order as the corresponding endpoints in + # the documentation. The docstrings are copied verbatim from that + # documentation. See: + # http://docs.ceph.com/docs/master/radosgw/adminops/ + # + + def get_usage(self, uid=None, start=None, end=None, show_entries=False, show_summary=False): + """Request bandwidth usage information. + + Note: this feature is disabled by default, can be enabled by setting + 'rgw enable usage log = true' in the appropriate section of ceph.conf. + For changes in ceph.conf to take effect, radosgw process restart is + needed. + + """ + return self._request( + 'get', 'usage', + uid=uid, + start=start, + end=end, + show_entries=show_entries, + show_summary=show_summary, + ) + + def trim_usage(self, uid=None, start=None, end=None, remove_all=False): + """Remove usage information. + + With no dates specified, removes all usage information. + + Note: this feature is disabled by default, can be enabled by setting + 'rgw enable usage log = true' in the appropriate section of ceph.conf. + For changes in ceph.conf to take effect, radosgw process restart is + needed. + + """ + return self._request( + 'delete', 'usage', + uid=uid, + start=start, + end=end, + remove_all=remove_all, + ) + + def get_user(self, uid): + """Get user information.""" + return self._request('get', 'user', uid=uid) + + def create_user(self, uid, display_name, email=None, key_type='s3', + access_key=None, secret_key=None, user_caps=None, + generate_key=True, max_buckets=None, suspended=False): + """Create a new user. + + By default, a S3 key pair will be created automatically and returned in + the response. If only one of access_key or secret_key is provided, the + omitted key will be automatically generated. By default, a generated + key is added to the keyring without replacing an existing key pair. If + access_key is specified and refers to an existing key owned by the user + then it will be modified. + + """ + return self._request( + 'put', 'user', + uid=uid, + display_name=display_name, + email=email, + key_type=key_type, + access_key=access_key, + secret_key=secret_key, + user_caps=user_caps, + generate_key=generate_key, + max_buckets=max_buckets, + suspended=suspended, + ) + + def modify_user(self, uid, display_name=None, email=None, key_type='s3', + access_key=None, secret_key=None, user_caps=None, + generate_key=True, max_buckets=None, suspended=False): + """Modify a user.""" + return self._request( + 'post', 'user', + uid=uid, + display_name=display_name, + email=email, + key_type=key_type, + access_key=access_key, + secret_key=secret_key, + user_caps=user_caps, + generate_key=generate_key, + max_buckets=max_buckets, + suspended=suspended, + ) + + def remove_user(self, uid, purge_data=False): + """Remove an existing user.""" + return self._request( + 'delete', 'user', + uid=uid, + purge_data=purge_data, + ) + + def create_subuser(self, uid, subuser=None, secret_key=None, access_key=None, + key_type=None, access=None, generate_secret=False): + """Create a new subuser. + + (Primarily useful for clients using the Swift API). Note that in + general for a subuser to be useful, it must be granted permissions by + specifying access. As with user creation if subuser is specified + without secret, then a secret key will be automatically generated. + + """ + return self._request( + 'put', 'user', 'subuser', + uid=uid, + subuser=subuser, + secret_key=secret_key, + access_key=access_key, + key_type=key_type, + access=access, + generate_secret=generate_secret, + ) + + def modify_subuser(self, uid, subuser, secret=None, key_type='swift', access=None, + generate_secret=False): + """Modify an existing subuser.""" + return self._request( + 'post', 'user', 'subuser', + uid=uid, + subuser=subuser, + secret=secret, + key_type=key_type, + access=access, + generate_secret=generate_secret, + ) + + def remove_subuser(self, uid, subuser, purge_keys=True): + """Remove an existing subuser.""" + return self._request( + 'delete', 'user', 'subuser', + uid=uid, + subuser=subuser, + purge_keys=purge_keys, + ) + + def create_key(self, uid, subuser=None, key_type='s3', access_key=None, + secret_key=None, generate_key=True): + """Create a new key. + + If a subuser is specified then by default created keys will be swift + type. If only one of access_key or secret_key is provided the committed + key will be automatically generated, that is if only secret_key is + specified then access_key will be automatically generated. By default, + a generated key is added to the keyring without replacing an existing + key pair. If access_key is specified and refers to an existing key + owned by the user then it will be modified. The response is a container + listing all keys of the same type as the key created. Note that when + creating a swift key, specifying the option access_key will have no + effect. Additionally, only one swift key may be held by each user or + subuser. + + """ + return self._request( + 'put', 'user', 'key', + uid=uid, + subuser=subuser, + key_type=key_type, + access_key=access_key, + secret_key=secret_key, + generate_key=generate_key, + ) + + def remove_key(self, access_key, key_type=None, uid=None, subuser=None): + """Remove an existing key.""" + return self._request( + 'delete', 'user', 'key', + access_key=access_key, + key_type=key_type, + uid=uid, + subuser=subuser, + ) + + def get_bucket(self, bucket=None, uid=None, stats=False): + """Get information about a subset of the existing buckets. + + If uid is specified without bucket then all buckets beloning to the + user will be returned. If bucket alone is specified, information for + that particular bucket will be retrieved. + + """ + return self._request( + 'get', 'bucket', + bucket=bucket, + uid=uid, + stats=stats, + ) + + def check_bucket_index(self, bucket, check_objects=False, fix=False): + """Check the index of an existing bucket. + + NOTE: to check multipart object accounting with check-objects, fix must + be set to True. + + """ + return self._request( + 'get', 'bucket', 'index', + bucket=bucket, + check_objects=check_objects, + fix=fix, + ) + + def remove_bucket(self, bucket, purge_objects=False): + """Delete an existing bucket.""" + return self._request( + 'delete', 'bucket', + bucket=bucket, + purge_objects=purge_objects, + ) + + def unlink_bucket(self, bucket, uid): + """Unlink a bucket from a specified user. + + Primarily useful for changing bucket ownership. + + """ + return self._request( + 'post', 'bucket', + bucket=bucket, + uid=uid, + ) + + def link_bucket(self, bucket, bucket_id, uid): + """Link a bucket to a specified user, unlinking the bucket from any + previous user. + + """ + # Both bucket and bucket_id are really required. Use get_bucket() to + # discover the id of a bucket from its name. + # + # FIXME: add a convenience method to look up the id? + return self._request( + 'put', 'bucket', + bucket=bucket, + bucket_id=bucket_id, + uid=uid, + ) + + def remove_object(self, bucket, object_name): + """Remove an existing object. + + NOTE: Does not require owner to be non-suspended. + + """ + return self._request( + 'delete', 'bucket', 'object', + bucket=bucket, + object_name=object_name, + ) + + def get_policy(self, bucket, object_name=None): + """Read the policy of an object or bucket.""" + return self._request( + 'get', 'bucket', 'policy', + bucket=bucket, + object_name=object_name, + ) + + def add_capability(self, uid, user_caps): + """Add an administrative capability to a specified user. + + uid (string): + The user ID to add an administrative capability to. + + user_caps (string): + The administrative capability to add to the user. Example: + "usage=read,write;user=write" + + """ + return self._request( + 'put', 'user', 'caps', + uid=uid, + user_caps=user_caps, + ) + + def remove_capability(self, uid, user_caps): + """Remove an administrative capability from a specified user.""" + return self._request( + 'delete', 'user', 'caps', + uid=uid, + user_caps=user_caps, + ) + + def get_quota(self, uid, quota_type): + return self._request( + 'get', 'user', 'quota', + uid=uid, + quota_type=quota_type, + ) + + def set_quota(self, uid, quota_type, bucket=None, max_size_kb=None, + max_objects=None, enabled=None): + return self._request( + 'put', 'user', 'quota', + quota_type=quota_type, + uid=uid, + bucket=bucket, + max_size_kb=max_size_kb, + max_objects=max_objects, + enabled=enabled, + ) + + # + # Convenience methods + # + + def get_user_quota(self, uid): + return self.get_quota(uid=uid, quota_type='user') + + def set_user_quota(self, uid, max_size_kb=None, max_objects=None, enabled=None): + return self.set_quota( + uid=uid, + quota_type='user', + max_size_kb=max_size_kb, + max_objects=max_objects, + enabled=enabled, + ) + + def get_user_bucket_quota(self, uid): + return self.get_quota(uid=uid, quota_type='bucket') + + def set_user_bucket_quota(self, uid, bucket, max_size_kb=None, max_objects=None, + enabled=None): + return self.set_quota( + uid=uid, + bucket=bucket, + quota_type='bucket', + max_size_kb=max_size_kb, + max_objects=max_objects, + enabled=enabled, + ) diff --git a/rados/rgwa_client_factory.py b/rados/rgwa_client_factory.py new file mode 100644 index 0000000..b53aa4d --- /dev/null +++ b/rados/rgwa_client_factory.py @@ -0,0 +1,87 @@ + +# ============LICENSE_START========================================== +# org.onap.vvp/test-engine +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software 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. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# 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. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. +from boto.s3.connection import S3Connection, OrdinaryCallingFormat +from django.conf import settings +from rados.rgwa_client import RGWAClient + + +class RGWAClientFactory(object): + __standard_client = None + __admin_client = None + + def __init__(self): + if self.__standard_client is None: + self.__set_standard_client() + + if self.__admin_client is None: + self.__set_admin_client() + + @classmethod + def __set_standard_client(cls): + if cls.__standard_client is None: + cls.__standard_client = S3Connection( + host=settings.AWS_S3_HOST, + port=settings.AWS_S3_PORT, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + calling_format=OrdinaryCallingFormat(), + is_secure=True) + cls.__standard_client.num_retries = 0 + + + @classmethod + def __set_admin_client(cls): + if cls.__admin_client is None: + cls.__admin_client = RGWAClient( + base_url='https://{S3_HOST}:{S3_PORT}/admin'.format( + S3_HOST=settings.AWS_S3_HOST, + S3_PORT=settings.AWS_S3_PORT, )) + + @classmethod + def standard(cls): + if cls.__standard_client is None: + cls.__set_standard_client() + return cls.__standard_client + + + @classmethod + def admin(cls): + if cls.__admin_client is None: + cls.__set_admin_client() + return cls.__admin_client -- cgit 1.2.3-korg