Skip to content

Commit

Permalink
Merge pull request #15 from objectrocket/acls-interface
Browse files Browse the repository at this point in the history
Finished initial buildout of ACLs interface.
  • Loading branch information
thedodd committed Nov 16, 2015
2 parents c711aa7 + ed08fb3 commit fa2c19d
Show file tree
Hide file tree
Showing 11 changed files with 508 additions and 51 deletions.
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ ObjectRocket API interface library for Python.

**NOTICE:** this client is still undergoing intial stages of development, and some public interfaces may change as development continues. We will increment the version of this package to 1.0.0 once the public interface to this library is deemed stable.


### examples
To use the library, simply do the following:


```python
>>> import objectrocket

Expand All @@ -32,17 +30,16 @@ To use the library, simply do the following:
[<MongodbInstance {...} at 0x10aedb980>]
```


### installation

pip install objectrocket

```bash
pip install objectrocket
```

### development
#### test
Testing against local you will want to export a couple environment variables:

```sh
```bash
export OR_DEFAULT_API_URL='http://localhost:5050/v2/'
export OR_DEFAULT_ADMIN_API_URL='http://localhost:5050/admin/'
```
Expand Down
257 changes: 257 additions & 0 deletions objectrocket/acls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
"""ACL operations and objects."""
import copy
import json
import logging

import requests

from objectrocket import bases
from objectrocket import util

logger = logging.getLogger(__name__)


class Acls(bases.BaseOperationsLayer):
"""ACL operations.
:param objectrocket.client.Client base_client: An instance of objectrocket.client.Client.
"""

def __init__(self, base_client):
super(Acls, self).__init__(base_client=base_client)

#####################
# Public interface. #
#####################
@util.token_auto_auth
def all(self, instance):
"""Get all ACLs associated with the instance specified by name.
:param str instance: The name of the instance from which to fetch ACLs.
:returns: A list of :py:class:`Acl` objects associated with the specified instance.
:rtype: list
"""
url = self._url.format(instance=instance)
response = requests.get(url, **self._default_request_kwargs)
data = self._get_response_data(response)
return self._concrete_acl_list(data)

@util.token_auto_auth
def create(self, instance, cidr_mask, description, **kwargs):
"""Create an ACL entry for the specified instance.
:param str instance: The name of the instance to associate the new ACL entry with.
:param str cidr_mask: The IPv4 CIDR mask for the new ACL entry.
:param str description: A short description for the new ACL entry.
:param collector kwargs: (optional) Additional key=value pairs to be supplied to the
creation payload. **Caution:** fields unrecognized by the API will cause this request
to fail with a 400 from the API.
"""
# Build up request data.
url = self._url.format(instance=instance)
request_data = {
'cidr_mask': cidr_mask,
'description': description
}
request_data.update(kwargs)

# Call to create an instance.
response = requests.post(
url,
data=json.dumps(request_data),
**self._default_request_kwargs
)

# Log outcome of instance creation request.
if response.status_code == 200:
logger.info('Successfully created a new ACL for instance {} with: {}.'
.format(instance, request_data))
else:
logger.info('Failed to create a new ACL for instance {} with: {}.'
.format(instance, request_data))

data = self._get_response_data(response)
return self._concrete_acl(data)

@util.token_auto_auth
def get(self, instance, acl):
"""Get an ACL by ID belonging to the instance specified by name.
:param str instance: The name of the instance from which to fetch the ACL.
:param str acl: The ID of the ACL to fetch.
:returns: An :py:class:`Acl` object, or None if ACL does not exist.
:rtype: :py:class:`Acl`
"""
base_url = self._url.format(instance=instance)
url = '{base}{aclid}/'.format(base=base_url, aclid=acl)
response = requests.get(url, **self._default_request_kwargs)
data = self._get_response_data(response)
return self._concrete_acl(data)

######################
# Private interface. #
######################
def _concrete_acl(self, acl_doc):
"""Concretize an ACL document.
:param dict acl_doc: A document describing an ACL entry. Should come from the API.
:returns: An :py:class:`Acl`, or None.
:rtype: :py:class:`bases.BaseInstance`
"""
if not isinstance(acl_doc, dict):
return None

# Attempt to instantiate an Acl object with the given dict.
try:
return Acl(document=acl_doc, acls=self)

# If construction fails, log the exception and return None.
except Exception as ex:
logger.exception(ex)
logger.error('Could not instantiate ACL document. You probably need to upgrade to a '
'recent version of the client. Document which caused this error: {}'
.format(acl_doc))
return None

def _concrete_acl_list(self, acl_docs):
"""Concretize a list of ACL documents.
:param list acl_docs: A list of ACL documents. Should come from the API.
:returns: A list of :py:class:`ACL` objects.
:rtype: list
"""
if not acl_docs:
return []

return list(filter(None, [self._concrete_acl(acl_doc=doc) for doc in acl_docs]))

@property
def _default_request_kwargs(self):
"""The default request keyword arguments to be passed to the requests library."""
defaults = copy.deepcopy(super(Acls, self)._default_request_kwargs)
defaults.setdefault('headers', {}).update({
'X-Auth-Token': self._client.auth._token
})
return defaults

@property
def _url(self):
"""The base URL for ACL operations."""
base_url = self._client._url.rstrip('/')
return '{}/instances/{{instance}}/acls/'.format(base_url)


class Acl(object):
"""An Access Control List entry object.
:param document dict: The dict representing this object.
:param Acls acls: The Acls operations layer instance from which this object came.
"""

def __init__(self, document, acls):
self.__client = acls._client
self.__acls = acls
self.__document = document

# Bind required pseudo private attributes from API response document.
self._cidr_mask = document['cidr_mask']
self._description = document['description']
self._id = document['id']
self._instance_name = document['instance']
self._login = document['login']
self._port = document['port']

# Bind attributes which may be present in API response document.
self._date_created = document.get('date_created', None)
self._instance = document.get('instance_id', None)
self._instance_type = document.get('instance_type', None)
self._metadata = document.get('metadata', {})
self._service_type = document.get('service_type', None)

def __repr__(self):
"""Represent this object as a string."""
_id = hex(id(self))
rep = (
'<{!s} cidr={!s} port={!s} instance={!s} id={!s} at {!s}>'
.format(self.__class__.__name__, self.cidr_mask, self.port,
self.instance_name, self.id, _id)
)
return rep

@property
def cidr_mask(self):
"""This ACL entry's CIDR mask."""
return self._cidr_mask

@property
def date_created(self):
"""The date which this ACL entry was created on."""
return self._date_created

@property
def description(self):
"""This ACL entry's description."""
return self._description

@property
def _document(self):
"""This ACL entry's document."""
return self.__document

@property
def id(self):
"""This ACL entry's ID."""
return self._id

@property
def instance(self):
"""The ID of the instance to which this ACL entry is associated."""
return self._instance

@property
def instance_name(self):
"""The name of the instance to which this ACL entry is associated."""
return self._instance_name

@property
def instance_type(self):
"""The type of the instance to which this ACL entry is associated."""
return self._instance_type

@property
def login(self):
"""The login of the user to which this ACL entry belongs."""
return self._login

@property
def metadata(self):
"""This ACL entry's metadata."""
return self._metadata

@property
def port(self):
"""This ACL entry's port number."""
return self._port

@property
def service_type(self):
"""The service of the instance to which this ACL entry is associated."""
return self._service_type

def to_dict(self):
"""Render this object as a dictionary."""
return self._document

######################
# Private interface. #
######################
@property
def _client(self):
"""An instance of the objectrocket.client.Client."""
return self.__client

@property
def _url(self):
"""The URL of this ACL object."""
base_url = self._client._url.rstrip('/')
return '{}/instances/{}/acls/{}/'.format(base_url, self.instance_name, self.id)
17 changes: 17 additions & 0 deletions objectrocket/bases.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Base classes used throughout the library."""
import abc
import logging

import requests
import six

Expand All @@ -8,6 +10,8 @@

from stevedore.extension import ExtensionManager

log = logging.getLogger(__name__)


@six.add_metaclass(abc.ABCMeta)
class BaseOperationsLayer(object):
Expand Down Expand Up @@ -37,6 +41,19 @@ def _default_request_kwargs(self):
}
return default_kwargs

def _get_response_data(self, response):
"""Return the data from a ``requests.Response`` object.
:param requests.Response response: The ``Response`` object from which to get the data.
"""
try:
_json = response.json()
data = _json.get('data')
return data
except ValueError as ex:
log.exception(ex)
return None

@abc.abstractproperty
def _url(self):
"""The URL this operations layer is to interface with."""
Expand Down
2 changes: 2 additions & 0 deletions objectrocket/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""ObjectRocket Python client."""
from objectrocket import acls
from objectrocket import auth
from objectrocket import bases
from objectrocket import constants
Expand All @@ -16,6 +17,7 @@ def __init__(self, base_url=constants.OR_DEFAULT_API_URL):
self.__url = base_url

# Public interface attributes.
self.acls = acls.Acls(base_client=self)
self.auth = auth.Auth(base_client=self)
self.instances = instances.Instances(base_client=self)

Expand Down
38 changes: 13 additions & 25 deletions objectrocket/instances/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,21 @@ def _concrete_instance(self, instance_doc):
if not isinstance(instance_doc, dict):
return None

service = instance_doc.setdefault('service', 'unknown')
inst = None

# If service key is a recognized service type, instantiate its respective instance.
if service in self._service_class_map:
# Attempt to instantiate the appropriate class for the given instance document.
try:
service = instance_doc['service']
cls = self._service_class_map[service]
inst = cls(instance_document=instance_doc, instances=self)
return cls(instance_document=instance_doc, instances=self)

# If service is not recognized, log a warning and return None.
else:
logger.warning(
'Could not determine instance service. You probably need to upgrade to a more '
# If construction fails, log the exception and return None.
except Exception as ex:
logger.exception(ex)
logger.error(
'Instance construction failed. You probably need to upgrade to a more '
'recent version of the client. Instance document which generated this '
'warning: {}'.format(instance_doc)
)

return inst
return None

def _concrete_instance_list(self, instance_docs):
"""Concretize a list of instance documents.
Expand All @@ -130,19 +128,9 @@ def _concrete_instance_list(self, instance_docs):
if not instance_docs:
return []

return filter(None, [self._concrete_instance(instance_doc=doc) for doc in instance_docs])

def _get_response_data(self, response):
"""Return the data from a ``requests.Response`` object.
:param requests.Response response: The ``Response`` object from which to get the data.
"""
try:
_json = response.json()
data = _json.get('data')
return data
except ValueError:
return None
return list(
filter(None, [self._concrete_instance(instance_doc=doc) for doc in instance_docs])
)

@property
def _default_request_kwargs(self):
Expand Down

0 comments on commit fa2c19d

Please sign in to comment.