From 1ec1496693c95e0a884b9035cfeeac5a4cd9ab0d Mon Sep 17 00:00:00 2001 From: Ian Gable Date: Thu, 25 Jun 2015 16:58:26 -0700 Subject: [PATCH] Add multiple endpoint support Add an Endpoint class as a part of the resource module. An Endpoint can be optionally passed to any of of the resource objects to allow them to access a different DCM endpoint then the one specified either by environment variables or in configuration files as loaded by the config class. - Issue #210 heavily inspired this feature, thanks @thijs-creemers - Fixes Issue #207 --- mixcoatl/admin/account.py | 8 +- mixcoatl/admin/api_key.py | 10 +- mixcoatl/admin/billing_code.py | 8 +- mixcoatl/admin/customer.py | 8 +- mixcoatl/admin/group.py | 8 +- mixcoatl/admin/job.py | 8 +- mixcoatl/admin/role.py | 6 +- mixcoatl/admin/user.py | 8 +- mixcoatl/analytics/server_analytics.py | 4 +- mixcoatl/analytics/tier_analytics.py | 8 +- mixcoatl/auth.py | 20 +++- .../configuration_management_account.py | 8 +- .../configuration_management_service.py | 8 +- .../configuration_management_system.py | 8 +- mixcoatl/automation/environment.py | 8 +- mixcoatl/automation/personality.py | 8 +- mixcoatl/automation/script.py | 8 +- mixcoatl/geography/cloud.py | 8 +- mixcoatl/geography/datacenter.py | 8 +- mixcoatl/geography/region.py | 8 +- mixcoatl/geography/subscription.py | 10 +- mixcoatl/infrastructure/machine_image.py | 6 +- mixcoatl/infrastructure/server.py | 9 +- mixcoatl/infrastructure/server_product.py | 8 +- mixcoatl/infrastructure/snapshot.py | 8 +- mixcoatl/infrastructure/volume.py | 8 +- mixcoatl/network/firewall.py | 8 +- mixcoatl/network/firewall_rule.py | 8 +- mixcoatl/network/load_balancer.py | 8 +- mixcoatl/network/network.py | 8 +- mixcoatl/platform/relational_database.py | 8 +- .../platform/relational_database_product.py | 8 +- mixcoatl/platform/storage_object.py | 8 +- mixcoatl/resource.py | 101 +++++++++++++++++- tests/data/unit/admin/endpoint.json | 8 ++ tests/data/unit/admin/endpoints.json | 18 ++++ tests/unit/admin/test_endpoint.py | 46 ++++++++ 37 files changed, 310 insertions(+), 136 deletions(-) create mode 100644 tests/data/unit/admin/endpoint.json create mode 100644 tests/data/unit/admin/endpoints.json create mode 100644 tests/unit/admin/test_endpoint.py diff --git a/mixcoatl/admin/account.py b/mixcoatl/admin/account.py index 6df85f1..09d28b1 100644 --- a/mixcoatl/admin/account.py +++ b/mixcoatl/admin/account.py @@ -18,8 +18,8 @@ class Account(Resource): COLLECTION_NAME = 'accounts' PRIMARY_KEY = 'account_id' - def __init__(self, account_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, account_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__account_id = account_id @property @@ -166,7 +166,7 @@ def add(self): raise CreateAccountException(self.last_error) @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Get all accounts >>> Account.all(detail='basic') @@ -184,7 +184,7 @@ def all(cls, keys_only=False, **kwargs): :returns: `list` of :class:`Account` or :attr:`account_id` :raises: :class:`AccountException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH,endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/admin/api_key.py b/mixcoatl/admin/api_key.py index 73cb9aa..0ee1c16 100644 --- a/mixcoatl/admin/api_key.py +++ b/mixcoatl/admin/api_key.py @@ -18,8 +18,8 @@ class ApiKey(Resource): COLLECTION_NAME = 'apiKeys' PRIMARY_KEY = 'access_key' - def __init__(self, access_key=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, access_key=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__access_key = access_key @property @@ -147,7 +147,7 @@ def generate_api_key(cls, key_name, description, expiration=None): return a @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Get all api keys .. note:: @@ -166,10 +166,10 @@ def all(cls, keys_only=False, **kwargs): """ if 'access_key' in kwargs: - r = Resource(cls.PATH + "/" + kwargs['access_key']) + r = Resource(cls.PATH + "/" + kwargs['access_key'], endpoint=endpoint) params = {} else: - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) if 'detail' in kwargs: r.request_details = kwargs['detail'] diff --git a/mixcoatl/admin/billing_code.py b/mixcoatl/admin/billing_code.py index 15d9ea0..3241de7 100644 --- a/mixcoatl/admin/billing_code.py +++ b/mixcoatl/admin/billing_code.py @@ -19,8 +19,8 @@ class BillingCode(Resource): COLLECTION_NAME = 'billingCodes' PRIMARY_KEY = 'billing_code_id' - def __init__(self, billing_code_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, billing_code_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__billing_code_id = billing_code_id @property @@ -99,7 +99,7 @@ def soft_quota(self, s): self.__soft_quota = s @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Get all visible billing codes .. note:: @@ -113,7 +113,7 @@ def all(cls, keys_only=False, **kwargs): :returns: `list` - of :class:`BillingCode` or :attr:`billing_code_id` :raises: :class:`BillingCodeException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'details' in kwargs: diff --git a/mixcoatl/admin/customer.py b/mixcoatl/admin/customer.py index df71335..31b2378 100644 --- a/mixcoatl/admin/customer.py +++ b/mixcoatl/admin/customer.py @@ -19,8 +19,8 @@ class Customer(Resource): COLLECTION_NAME = 'customers' PRIMARY_KEY = 'customer_id' - def __init__(self, role_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, role_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__customer_id = customer_id @property @@ -29,9 +29,9 @@ def customer_id(self): return self.__customer_id @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Get all customers""" - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/admin/group.py b/mixcoatl/admin/group.py index f66b5d2..0aee4b8 100644 --- a/mixcoatl/admin/group.py +++ b/mixcoatl/admin/group.py @@ -20,8 +20,8 @@ class Group(Resource): COLLECTION_NAME = 'groups' PRIMARY_KEY = 'group_id' - def __init__(self, group_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, group_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__group_id = group_id @property @@ -90,7 +90,7 @@ def role_assignments(self): return self.__role_assignments @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Get all groups .. note:: @@ -106,7 +106,7 @@ def all(cls, keys_only=False, **kwargs): :returns: `list` - List of :class:`Group` or :attr:`group_id` :raises: :class:`GroupException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/admin/job.py b/mixcoatl/admin/job.py index 08d1235..068bdd0 100644 --- a/mixcoatl/admin/job.py +++ b/mixcoatl/admin/job.py @@ -18,8 +18,8 @@ class Job(Resource): COLLECTION_NAME = 'jobs' PRIMARY_KEY = 'job_id' - def __init__(self, job_id=None, **kwargs): - Resource.__init__(self) + def __init__(self, job_id=None, endpoint=None, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__job_id = job_id @property @@ -59,14 +59,14 @@ def job_message(self): return self.__job_message @classmethod - def all(cls, keys_only=False): + def all(cls, keys_only=False, endpoint=None): """Get all jobs :param keys_only: Only return :attr:`job_id` instead of :class:`Job` :type keys_only: bool. :returns: `list` of :class:`Job` or :attr:`job_id` :raises: :class:`JobException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) x = r.get() if r.last_error is None: if keys_only is True: diff --git a/mixcoatl/admin/role.py b/mixcoatl/admin/role.py index 37a6461..774db9c 100644 --- a/mixcoatl/admin/role.py +++ b/mixcoatl/admin/role.py @@ -19,8 +19,8 @@ class Role(Resource): COLLECTION_NAME = 'roles' PRIMARY_KEY = 'role_id' - def __init__(self, role_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, role_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__role_id = role_id @property @@ -92,7 +92,7 @@ def create(self): raise RoleCreationException(self.last_error) @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Get all roles .. note:: diff --git a/mixcoatl/admin/user.py b/mixcoatl/admin/user.py index df87493..9a47a70 100644 --- a/mixcoatl/admin/user.py +++ b/mixcoatl/admin/user.py @@ -19,8 +19,8 @@ class User(Resource): COLLECTION_NAME = 'users' PRIMARY_KEY = 'user_id' - def __init__(self, user_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, user_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__user_id = user_id @property @@ -252,7 +252,7 @@ def create(self): raise UserCreationException(self.last_error) @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Return all users .. note:: @@ -266,7 +266,7 @@ def all(cls, keys_only=False, **kwargs): :returns: `list` of :class:`User` or :attr:`user_id` :raises: :class:`UserException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) if 'detail' in kwargs: r.request_details = kwargs['detail'] else: diff --git a/mixcoatl/analytics/server_analytics.py b/mixcoatl/analytics/server_analytics.py index 059e607..3c81693 100644 --- a/mixcoatl/analytics/server_analytics.py +++ b/mixcoatl/analytics/server_analytics.py @@ -65,7 +65,7 @@ def interval_in_minutes(self): return self.__interval_in_minutes @classmethod - def all(cls, server_id, keys_only=False, **kwargs): + def all(cls, server_id, keys_only=False, endpoint=None, **kwargs): """Get all analytics for `server_id` :param server_id: The server represented in the analytics @@ -80,7 +80,7 @@ def all(cls, server_id, keys_only=False, **kwargs): :param period_end: int. :returns: :class:`ServerAnalytics` or `list` of :attr:`data_points` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: r.request_details = kwargs['detail'] diff --git a/mixcoatl/analytics/tier_analytics.py b/mixcoatl/analytics/tier_analytics.py index 444d95c..169163e 100644 --- a/mixcoatl/analytics/tier_analytics.py +++ b/mixcoatl/analytics/tier_analytics.py @@ -7,8 +7,8 @@ class TierAnalytics(Resource): COLLECTION_NAME = 'analytics' PRIMARY_KEY = 'tier_id' - def __init__(self, tier_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, tier_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__tier_id = tier_id @property @@ -32,8 +32,8 @@ def interval_in_minutes(self): return self.__interval_in_minutes @classmethod - def all(cls, tier_id, **kwargs): - r = Resource(cls.PATH+'/'+str(tier_id)) + def all(cls, tier_id, endpoint=None, **kwargs): + r = Resource(cls.PATH+'/'+str(tier_id), endpoint=endpoint) if 'details' in kwargs: r.request_details = kwargs['details'] else: diff --git a/mixcoatl/auth.py b/mixcoatl/auth.py index 21dc56e..7c7f148 100644 --- a/mixcoatl/auth.py +++ b/mixcoatl/auth.py @@ -5,16 +5,26 @@ from mixcoatl.settings.load_settings import settings -def get_sig(http_method, path): +def get_sig(http_method, path, endpoint=None): """Return a signature valid for use in making DCM API calls :param http_method: The `http_method` used to make the API call :param path: The `path` used in the API call """ + + if endpoint: + access_key = endpoint.access_key + secret_key = endpoint.secret_key + basepath = endpoint.basepath + else: + access_key = settings.access_key + secret_key = settings.secret_key + basepath = settings.basepath + timestamp = int(round(time.time() * 1000)) - signpath = settings.basepath + '/' + path + signpath = basepath + '/' + path parts = [] - parts.append(settings.access_key) + parts.append(access_key) parts.append(http_method) parts.append(signpath) parts.append(timestamp) @@ -22,9 +32,9 @@ def get_sig(http_method, path): # pylint: disable-msg=E1101 dm = hashlib.sha256 to_sign = ':'.join([str(x) for x in parts]) - d = hmac.new(settings.secret_key, msg=to_sign, digestmod=dm).digest() + d = hmac.new(secret_key, msg=to_sign, digestmod=dm).digest() b64auth = base64.b64encode(d).decode() return {'timestamp': timestamp, 'signature': b64auth, - 'access_key': settings.access_key, + 'access_key': access_key, 'ua': settings.user_agent} diff --git a/mixcoatl/automation/configuration_management_account.py b/mixcoatl/automation/configuration_management_account.py index dbbb6d9..4a55ae9 100644 --- a/mixcoatl/automation/configuration_management_account.py +++ b/mixcoatl/automation/configuration_management_account.py @@ -12,8 +12,8 @@ class ConfigurationManagementAccount(Resource): COLLECTION_NAME = 'cmAccounts' PRIMARY_KEY = 'cm_account_id' - def __init__(self, cm_account_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, cm_account_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__cm_account_id = cm_account_id @property @@ -77,8 +77,8 @@ def owning_user(self): return self.__owning_user @classmethod - def all(cls, **kwargs): - r = Resource(cls.PATH) + def all(cls, endpoint=None, **kwargs): + r = Resource(cls.PATH, endpoint=endpoint) if 'details' in kwargs: r.request_details = kwargs['details'] else: diff --git a/mixcoatl/automation/configuration_management_service.py b/mixcoatl/automation/configuration_management_service.py index 5a2d603..dfefdbe 100644 --- a/mixcoatl/automation/configuration_management_service.py +++ b/mixcoatl/automation/configuration_management_service.py @@ -15,8 +15,8 @@ class ConfigurationManagementService(Resource): COLLECTION_NAME = 'cmServices' PRIMARY_KEY = 'cm_system_id' - def __init__(self, cm_account_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, cm_account_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) @lazy_property def cm_system(self): @@ -91,8 +91,8 @@ def create(self): raise CMCreationException(self.last_error) @classmethod - def all(cls, **kwargs): - r = Resource(cls.PATH) + def all(cls, endpoint=None, **kwargs): + r = Resource(cls.PATH, endpoint=endpoint) if 'details' in kwargs: r.request_details = kwargs['details'] diff --git a/mixcoatl/automation/configuration_management_system.py b/mixcoatl/automation/configuration_management_system.py index fc959e2..7a8fe2f 100644 --- a/mixcoatl/automation/configuration_management_system.py +++ b/mixcoatl/automation/configuration_management_system.py @@ -17,12 +17,12 @@ class ConfigurationManagementSystem(Resource): COLLECTION_NAME = 'cmSystems' PRIMARY_KEY = 'cm_system_id' - def __init__(self, cm_account_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, cm_account_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) @classmethod - def all(cls, **kwargs): - r = Resource(cls.PATH) + def all(cls, endpoint=None, **kwargs): + r = Resource(cls.PATH, endpoint=endpoint) if 'details' in kwargs: r.request_details = kwargs['details'] diff --git a/mixcoatl/automation/environment.py b/mixcoatl/automation/environment.py index cdc3cf4..8bd8d18 100644 --- a/mixcoatl/automation/environment.py +++ b/mixcoatl/automation/environment.py @@ -15,13 +15,13 @@ class Environment(Resource): COLLECTION_NAME = 'environments' PRIMARY_KEY = 'environmentId' - def __init__(self, environmentId=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, environmentId=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__environmentId = environmentId @classmethod - def all(cls, cmAccountId, **kwargs): - r = Resource(cls.PATH) + def all(cls, cmAccountId, endpoint=None, **kwargs): + r = Resource(cls.PATH, endpoint=endpoint) if 'detail' in kwargs: r.request_details = kwargs['detail'] diff --git a/mixcoatl/automation/personality.py b/mixcoatl/automation/personality.py index 1b3212b..8c92878 100644 --- a/mixcoatl/automation/personality.py +++ b/mixcoatl/automation/personality.py @@ -12,8 +12,8 @@ class Personality(Resource): COLLECTION_NAME = 'personalities' PRIMARY_KEY = 'cmAccountId' - def __init__(self, cmAccountId=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, cmAccountId=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__cmAccountId = cmAccountId @property @@ -21,8 +21,8 @@ def cmAccountId(self): return self.__cmAccountId @classmethod - def all(cls, cmAccountId, **kwargs): - r = Resource(cls.PATH) + def all(cls, cmAccountId, endpoint=None, **kwargs): + r = Resource(cls.PATH, endpoint=endpoint) if 'detail' in kwargs: r.request_details = kwargs['detail'] diff --git a/mixcoatl/automation/script.py b/mixcoatl/automation/script.py index 33a1556..75740f5 100644 --- a/mixcoatl/automation/script.py +++ b/mixcoatl/automation/script.py @@ -15,8 +15,8 @@ class Script(Resource): COLLECTION_NAME = 'scripts' PRIMARY_KEY = 'cmAccountId' - def __init__(self, cmAccountId=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, cmAccountId=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__cmAccountId = cmAccountId @property @@ -24,8 +24,8 @@ def cmAccountId(self): return self.__cmAccountId @classmethod - def all(cls, cmAccountId, **kwargs): - r = Resource(cls.PATH) + def all(cls, cmAccountId, endpoint=None, **kwargs): + r = Resource(cls.PATH, endpoint=endpoint) if 'detail' in kwargs: r.request_details = kwargs['detail'] diff --git a/mixcoatl/geography/cloud.py b/mixcoatl/geography/cloud.py index 0116eb4..13ceff8 100644 --- a/mixcoatl/geography/cloud.py +++ b/mixcoatl/geography/cloud.py @@ -12,8 +12,8 @@ class Cloud(Resource): COLLECTION_NAME = 'clouds' PRIMARY_KEY = 'cloud_id' - def __init__(self, cloud_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, cloud_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__cloud_id = cloud_id @property @@ -102,7 +102,7 @@ def status(self): return self.__status @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False, endpoint=None, **kwargs): """Return all clouds :param keys_only: Return :attr:`cloud_id` instead of :class:`Cloud` @@ -111,7 +111,7 @@ def all(cls, keys_only=False, **kwargs): :type detail: str. :returns: `list` of :class:`Cloud` or :attr:`cloud_id` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/geography/datacenter.py b/mixcoatl/geography/datacenter.py index b6fbab3..bbf8211 100644 --- a/mixcoatl/geography/datacenter.py +++ b/mixcoatl/geography/datacenter.py @@ -14,8 +14,8 @@ class DataCenter(Resource): COLLECTION_NAME = 'dataCenters' PRIMARY_KEY = 'data_center_id' - def __init__(self, data_center_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, data_center_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__data_center_id = data_center_id @property @@ -49,7 +49,7 @@ def status(self): return self.__status @classmethod - def all(cls, region_id, **kwargs): + def all(cls, region_id, endpoint=None, **kwargs): """Return all data centers :param region_id: Required. The region to query against @@ -61,7 +61,7 @@ def all(cls, region_id, **kwargs): :returns: `list` of :class:`DataCenter` or :attr:`data_center_id` :raises: :class:`DataCenterException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) if 'detail' in kwargs: r.request_details = kwargs['detail'] diff --git a/mixcoatl/geography/region.py b/mixcoatl/geography/region.py index 27c685e..e0819a5 100644 --- a/mixcoatl/geography/region.py +++ b/mixcoatl/geography/region.py @@ -11,8 +11,8 @@ class Region(Resource): COLLECTION_NAME = 'regions' PRIMARY_KEY = 'region_id' - def __init__(self, region_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, region_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__region_id = region_id @property @@ -56,7 +56,7 @@ def description(self): return self.__description @classmethod - def all(cls, **kwargs): + def all(cls, endpoint=None, **kwargs): """Return all regions :param account_id: Limit results to regions with the specified account @@ -73,7 +73,7 @@ def all(cls, **kwargs): :returns: `list` of :class:`Region` or :attr:`region_id` :raises: :class:`RegionException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/geography/subscription.py b/mixcoatl/geography/subscription.py index 71d1abc..f3d78fe 100644 --- a/mixcoatl/geography/subscription.py +++ b/mixcoatl/geography/subscription.py @@ -14,16 +14,16 @@ class Subscription(Resource): COLLECTION_NAME = 'subscriptions' PRIMARY_KEY = 'region_id' - def __init__(self, region_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, region_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__region_id = region_id @classmethod - def all(cls, keys_only=False, **kwargs): + def all(cls, keys_only=False,endpoint=None, **kwargs): if 'region_id' in kwargs: - r = Resource(cls.PATH + "/" + str(kwargs['region_id'])) + r = Resource(cls.PATH + "/" + str(kwargs['region_id']), endpoint=endpoint) else: - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) if 'details' in kwargs: r.request_details = kwargs['details'] diff --git a/mixcoatl/infrastructure/machine_image.py b/mixcoatl/infrastructure/machine_image.py index 5e877ff..0668f83 100644 --- a/mixcoatl/infrastructure/machine_image.py +++ b/mixcoatl/infrastructure/machine_image.py @@ -282,7 +282,7 @@ def unregister_agent(self, **kwargs): return self.put(p, data=json.dumps(payload)) @classmethod - def all(cls, **kwargs): + def all(cls, endpoint=None, **kwargs): """Return all machine images :param machine_image_id: The id of the machine image @@ -301,9 +301,9 @@ def all(cls, **kwargs): :raises: :class:`MachineImageException` """ if 'machine_image_id' in kwargs: - r = Resource(cls.PATH + "/" + str(kwargs['machine_image_id'])) + r = Resource(cls.PATH + "/" + str(kwargs['machine_image_id']), endpoint=endpoint) else: - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} diff --git a/mixcoatl/infrastructure/server.py b/mixcoatl/infrastructure/server.py index ec6e600..ddf54e2 100644 --- a/mixcoatl/infrastructure/server.py +++ b/mixcoatl/infrastructure/server.py @@ -15,10 +15,11 @@ class Server(Resource): COLLECTION_NAME = 'servers' PRIMARY_KEY = 'server_id' - def __init__(self, server_id=None): - Resource.__init__(self) + def __init__(self, server_id=None, endpoint=None): + Resource.__init__(self, endpoint=endpoint) self.__server_id = server_id + @property def server_id(self): """`int` - The DCM ID of this server""" @@ -540,7 +541,7 @@ def wait_for(self, status='RUNNING', callback=None): return self @classmethod - def all(cls, **kwargs): + def all(cls, endpoint=None, **kwargs): """Get a list of all known servers >>> Server.all() @@ -549,7 +550,7 @@ def all(cls, **kwargs): :returns: list -- a list of :class:`Server` :raises: ServerException """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/infrastructure/server_product.py b/mixcoatl/infrastructure/server_product.py index f9f8d8d..59defbd 100644 --- a/mixcoatl/infrastructure/server_product.py +++ b/mixcoatl/infrastructure/server_product.py @@ -13,8 +13,8 @@ class ServerProduct(Resource): COLLECTION_NAME = 'serverProducts' PRIMARY_KEY = 'product_id' - def __init__(self, product_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, product_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__product_id = product_id @property @@ -93,7 +93,7 @@ def software(self): return self.__software @classmethod - def all(cls, region_id, **kwargs): + def all(cls, region_id, endpoint=None, **kwargs): """Return all server products :param region_id: The region id to search in @@ -105,7 +105,7 @@ def all(cls, region_id, **kwargs): :returns: `list` of :attr:`product_id` or :class:`ServerProduct` :raises: :class:`ServerProductException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) r.request_details = 'basic' params = {'regionId': region_id} if 'keys_only' in kwargs: diff --git a/mixcoatl/infrastructure/snapshot.py b/mixcoatl/infrastructure/snapshot.py index 66f7dd8..8c1ec0b 100644 --- a/mixcoatl/infrastructure/snapshot.py +++ b/mixcoatl/infrastructure/snapshot.py @@ -14,9 +14,9 @@ class Snapshot(Resource): COLLECTION_NAME = 'snapshots' PRIMARY_KEY = 'snapshot_id' - def __init__(self, snapshot_id=None, *args, **kwargs): + def __init__(self, snapshot_id=None, endpoint=None, *args, **kwargs): # pylint: disable-msg=W0613 - Resource.__init__(self) + Resource.__init__(self, endpoint=endpoint) self.__snapshot_id = snapshot_id @property @@ -229,7 +229,7 @@ def create(self, callback=None): raise SnapshotException(self.last_error) @classmethod - def all(cls, **kwargs): + def all(cls, endpoint=None, **kwargs): """Return a list of snapshots :param account_id: Restrict to snapshots owned by `account_id` @@ -245,7 +245,7 @@ def all(cls, **kwargs): :returns: `list` of :attr:`snapshot_id` or :class:`Snapshot` :raises: :class:`SnapshotException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/infrastructure/volume.py b/mixcoatl/infrastructure/volume.py index 7943190..5741ae3 100644 --- a/mixcoatl/infrastructure/volume.py +++ b/mixcoatl/infrastructure/volume.py @@ -17,9 +17,9 @@ class Volume(Resource): COLLECTION_NAME = 'volumes' PRIMARY_KEY = 'volume_id' - def __init__(self, volume_id=None, **kwargs): + def __init__(self, volume_id=None, endpoint=None, **kwargs): # pylint: disable-msg=W0613 - Resource.__init__(self) + Resource.__init__(self, endpoint=endpoint) if 'detail' in kwargs: self.request_details = kwargs['detail'] self.__volume_id = volume_id @@ -362,7 +362,7 @@ def snapshot(self, **kwargs): raise VolumeSnapshotException(str(e)) @classmethod - def all(cls, **kwargs): + def all(cls, endpoint=None, **kwargs): """List all volumes :param account_id: Restrict to volumes owned by `account_id` :type account_id: int. @@ -377,7 +377,7 @@ def all(cls, **kwargs): :returns: `list` of :attr:`volume_id` or :class:`Volume` :raises: :class:`VolumeException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/network/firewall.py b/mixcoatl/network/firewall.py index 53d64cd..75686b5 100644 --- a/mixcoatl/network/firewall.py +++ b/mixcoatl/network/firewall.py @@ -15,8 +15,8 @@ class Firewall(Resource): COLLECTION_NAME = 'firewalls' PRIMARY_KEY = "firewall_id" - def __init__(self, firewall_id=None, **kwargs): - Resource.__init__(self) + def __init__(self, firewall_id=None, endpoint=None, **kwargs): + Resource.__init__(self, endpoint=endpoint) if 'detail' in kwargs: self.request_details = kwargs['detail'] @@ -179,7 +179,7 @@ def describe(self): """Describe a firewall""" @classmethod - def all(cls, **kwargs): + def all(cls, endpoint=None, **kwargs): """List all firewalls in `region_id` :param region_id: Limit results to `region_id` :type region_id: int. @@ -192,7 +192,7 @@ def all(cls, **kwargs): :returns: `list` of :attr:`firewall_id` or :class:`Firewall` :raises: :class:`FirewallException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=None) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/network/firewall_rule.py b/mixcoatl/network/firewall_rule.py index a97b890..6eb597a 100644 --- a/mixcoatl/network/firewall_rule.py +++ b/mixcoatl/network/firewall_rule.py @@ -14,11 +14,11 @@ class FirewallRule(Resource): COLLECTION_NAME = 'rules' PRIMARY_KEY = 'firewall_rule_id' - def __init__(self, firewall_rule_id=None, *args, **kwargs): + def __init__(self, firewall_rule_id=None, endpoint=None, *args, **kwargs): if 'detail' in kwargs: self.request_details = kwargs['detail'] - Resource.__init__(self) + Resource.__init__(self, endpoint=endpoint) self.__firewall_rule_id = firewall_rule_id @property @@ -174,7 +174,7 @@ def remove(self, reason): raise FirewallRuleException(self.last_error) @classmethod - def all(cls, firewall_id, **kwargs): + def all(cls, firewall_id, endpoint=None, **kwargs): """List all rules for `firewall_id` :param firewall_id: The id of the firewall to list rules for @@ -187,7 +187,7 @@ def all(cls, firewall_id, **kwargs): :raises: :class:`FirewallRuleException` """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {} if 'detail' in kwargs: diff --git a/mixcoatl/network/load_balancer.py b/mixcoatl/network/load_balancer.py index 14a67b4..c91eacb 100644 --- a/mixcoatl/network/load_balancer.py +++ b/mixcoatl/network/load_balancer.py @@ -13,8 +13,8 @@ class LoadBalancer(Resource): COLLECTION_NAME = 'loadBalancers' PRIMARY_KEY = 'load_balancer_id' - def __init__(self, load_balancer_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, load_balancer_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__load_balancer_id = load_balancer_id @property @@ -86,8 +86,8 @@ def listeners(self): return self.__listeners @classmethod - def all(cls, **kwargs): - r = Resource(cls.PATH) + def all(cls, endpoint=None, **kwargs): + r = Resource(cls.PATH, endpoint=endpoint) if 'details' in kwargs: r.request_details = kwargs['details'] else: diff --git a/mixcoatl/network/network.py b/mixcoatl/network/network.py index c3ee36c..e887efd 100644 --- a/mixcoatl/network/network.py +++ b/mixcoatl/network/network.py @@ -20,8 +20,8 @@ class Network(Resource): COLLECTION_NAME = 'networks' PRIMARY_KEY = "network_id" - def __init__(self, network_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, network_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) if 'detail' in kwargs: self.request_details = kwargs['detail'] @@ -267,7 +267,7 @@ def destroy(self, reason='no reason provided'): return self.delete(p, params=qopts) @classmethod - def all(cls, **kwargs): + def all(cls, endpoint=None, **kwargs): """List all networks in `region_id` :param region_id: Limit results to `region_id` @@ -286,7 +286,7 @@ def all(cls, **kwargs): :raises: :class:`NetworkException` """ params = {} - r = Resource(cls.PATH) + r = Resource(cls.PATH,endpoint=endpoint) if 'detail' in kwargs: request_details = kwargs['detail'] diff --git a/mixcoatl/platform/relational_database.py b/mixcoatl/platform/relational_database.py index e221242..d25cec6 100644 --- a/mixcoatl/platform/relational_database.py +++ b/mixcoatl/platform/relational_database.py @@ -11,8 +11,8 @@ class RelationalDatabase(Resource): COLLECTION_NAME = 'relational_databases' PRIMARY_KEY = 'relational_database_id' - def __init__(self, relational_database_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, relational_database_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__relational_database_id = relational_database_id @property @@ -312,8 +312,8 @@ def wait_for(self, status='RUNNING', callback = None): return self @classmethod - def all(cls, **kwargs): - r = Resource(cls.PATH) + def all(cls, endpoint=None, **kwargs): + r = Resource(cls.PATH,endpoint=endpoint) params = {} if 'detail' in kwargs: r.request_details = kwargs['detail'] diff --git a/mixcoatl/platform/relational_database_product.py b/mixcoatl/platform/relational_database_product.py index 701e626..53f68a2 100644 --- a/mixcoatl/platform/relational_database_product.py +++ b/mixcoatl/platform/relational_database_product.py @@ -11,8 +11,8 @@ class RelationalDatabaseProduct(Resource): COLLECTION_NAME = 'rdbmsProducts' PRIMARY_KEY = 'product_id' - def __init__(self, product_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, product_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__product_id = product_id @property @@ -130,7 +130,7 @@ def reload(self): return self.last_error @classmethod - def all(cls, region_id, engine, **kwargs): + def all(cls, region_id, engine, endpoint=None, **kwargs): """Get a list of all known relational_databases >>> RelationalDatabaseProduct.all(region_id=100, engine='MYSQL51') @@ -139,7 +139,7 @@ def all(cls, region_id, engine, **kwargs): :returns: list -- a list of :class:`RelationalDatabaseProduct` :raises: RelationalDatabaseProductException """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) r.request_details = 'basic' params = {'regionId': region_id, 'engine': engine} diff --git a/mixcoatl/platform/storage_object.py b/mixcoatl/platform/storage_object.py index 8993505..944fadd 100644 --- a/mixcoatl/platform/storage_object.py +++ b/mixcoatl/platform/storage_object.py @@ -10,8 +10,8 @@ class StorageObject(Resource): COLLECTION_NAME = 'storageObjects' PRIMARY_KEY = 'storage_object_id' - def __init__(self, storage_object_id=None, *args, **kwargs): - Resource.__init__(self) + def __init__(self, storage_object_id=None, endpoint=None, *args, **kwargs): + Resource.__init__(self, endpoint=endpoint) self.__storage_object_id = storage_object_id @property @@ -139,7 +139,7 @@ def reload(self): return self.last_error @classmethod - def all(cls, region_id, **kwargs): + def all(cls, region_id, endpoint=None, **kwargs): """Get a list of all known storage objects. >>> StorageObject.all(region_id=100) @@ -148,7 +148,7 @@ def all(cls, region_id, **kwargs): :returns: list -- a list of :class:`StorageObject` :raises: StorageObjectException """ - r = Resource(cls.PATH) + r = Resource(cls.PATH, endpoint=endpoint) params = {'regionId': region_id} if 'detail' in kwargs: diff --git a/mixcoatl/resource.py b/mixcoatl/resource.py index 98cc9d2..faa84ae 100644 --- a/mixcoatl/resource.py +++ b/mixcoatl/resource.py @@ -8,7 +8,89 @@ from mixcoatl.settings.load_settings import settings from mixcoatl.decorators.lazy import lazy_property from mixcoatl.utils import camel_keys +import json +class Endpoint(object): + """ A class for representing DCM endpoints and loading them from json files + """ + + def __init__(self, url = None, basepath=None, api_version=None, secret_key=None, access_key=None, ssl_verify=True, nickname=None): + """ + :param url: The url of the DCM endpoint (e.g https://dcm.enstratius.com/api/enstratus/2015-05-25 ) + :param basepath: Optional server path to the API (e.g. /api/enstratus/2015-05-25) + :param api_version: required API version (e.g. 2015-05-25) + :param access_key: DCM access key + :param secret_key: DCM secret key + :param ssl_verify: Should the endpoints SSL key be verified. Default is True + :param nickname: optional arbitrary short nick name to assign this endpoint + : + """ + if url: + self.url = url + else: + raise ValueError("endpoint must be specified") + if api_version: + self.api_version = api_version + else: + raise ValueError("api version must be specified") + if basepath: + self.basepath = basepath + else: + self.basepath = "/api/enstratus/%s" % self.api_version + if access_key: + self.access_key = access_key + else: + raise ValueError("access_key must be specified") + if secret_key: + self.secret_key = secret_key + else: + raise ValueError("secret_key must be specified") + + self.ssl_verify = ssl_verify + self.nickname = nickname + + @classmethod + def from_file(cls,filename): + """ Load a DCM endpoint definition from a json file and return and Endpoint object. For an example file see: + TODO: add github pointed to example file + + :param filename: the full path to a json file containing and endpoint definition + :returns: Endpoint - the endpoint loaded from the file + """ + endpoint_json = open(filename).read() + e = json.loads(endpoint_json) + + return Endpoint(url=e['url'], + basepath=e['basepath'] if 'basepath' in e else None, + api_version= e['api_version'], + secret_key=str(e['secret_key']), + access_key=str(e['access_key']), + ssl_verify=e['ssl_verify'] if 'ssl_verify' in e else None, + nickname=e['nickname'] if 'nickname' in e else None) + + @classmethod + def multiple_from_file(cls,filename): + """ Load multiple DCM endpoint definitions from a json file and return a dictionary indexed by endpont nicknames + For an example file see: + TODO: add github pointed to example file + + :param filename: the full path to a json file containing and multiple endpoint definitions + :returns: Endpoint dictionary - a dictionary indexed by endpoint nicknames + """ + endpoint_json = open(filename).read() + endpoint_in = json.loads(endpoint_json) + endpoint_dict ={} + + for e in endpoint_in: + endpoint_dict[e['nickname']] = Endpoint(url=e['url'], + basepath=e['basepath'] if 'basepath' in e else None, + api_version=e['api_version'], + secret_key=str(e['secret_key']), + access_key=str(e['access_key']), + ssl_verify=e['ssl_verify'] if 'ssl_verify' in e else None, + nickname=e['nickname'] if 'nickname' in e else None) + + return endpoint_dict class Resource(object): @@ -52,7 +134,7 @@ class Resource(object): #: The unique identifier of an individual resource PRIMARY_KEY = None - def __init__(self, base_path=None, request_details='basic', **kwargs): + def __init__(self, base_path=None, request_details='basic', endpoint=None, **kwargs): if base_path is None: try: self.__path = self.__class__.PATH @@ -71,7 +153,16 @@ def __init__(self, base_path=None, request_details='basic', **kwargs): self.__params = kwargs['params'] else: self.__params = {} - + if endpoint: + self.__endpoint = endpoint + else: + # no endpoint provided, pull from settings + self.__endpoint = Endpoint(url=settings.endpoint, + basepath=settings.basepath, + api_version=settings.api_version, + secret_key=settings.secret_key, + access_key=settings.access_key, + ssl_verify=settings.ssl_verify) self.__last_request = None self.__last_error = None self.__current_job = None @@ -226,9 +317,9 @@ def __doreq(self, method, **kwargs): based on DCM API documentation """ failures = [400, 403, 404, 409, 500, 501, 503] - sig = auth.get_sig(method, self.path) - url = settings.endpoint + '/' + self.path - ssl_verify = settings.ssl_verify + sig = auth.get_sig(method, self.path, endpoint=self.__endpoint) + url = self.__endpoint.url + '/' + self.path + ssl_verify = self.__endpoint.ssl_verify if self.payload_format == 'xml': payload_format = 'application/xml' diff --git a/tests/data/unit/admin/endpoint.json b/tests/data/unit/admin/endpoint.json new file mode 100644 index 0000000..0e733f2 --- /dev/null +++ b/tests/data/unit/admin/endpoint.json @@ -0,0 +1,8 @@ +{ + "nickname":"saas", + "url":"https://dcm.enstratius.com/api/enstratus/2015-05-25", + "api_version":"2015-05-25", + "access_key":"abcdefg", + "secret_key":"gfedcba", + "ssl_verify": true +} \ No newline at end of file diff --git a/tests/data/unit/admin/endpoints.json b/tests/data/unit/admin/endpoints.json new file mode 100644 index 0000000..cab82d4 --- /dev/null +++ b/tests/data/unit/admin/endpoints.json @@ -0,0 +1,18 @@ +[ + { + "nickname": "saas", + "url": "https://dcm.enstratius.com/api/enstratus/2015-05-25", + "api_version": "2015-05-25", + "access_key": "abcdefg", + "secret_key": "gfedcba", + "ssl_verify": true + }, + { + "nickname": "vagrant", + "url": "https://vagrant.vm/api/enstratus/2015-05-25", + "api_version": "2015-05-25", + "access_key": "abcdefg", + "secret_key": "gfedcba", + "ssl_verify": true + } +] \ No newline at end of file diff --git a/tests/unit/admin/test_endpoint.py b/tests/unit/admin/test_endpoint.py new file mode 100644 index 0000000..5a92b2b --- /dev/null +++ b/tests/unit/admin/test_endpoint.py @@ -0,0 +1,46 @@ +import sys +# These have to be set before importing any mixcoatl modules +import os +os.environ['ES_ACCESS_KEY'] = 'abcdefg' +os.environ['ES_SECRET_KEY'] = 'gfedcba' + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + +from mixcoatl.resource import Endpoint + +class TestEndpoint(unittest.TestCase): + + def setUp(self): + self.cls = Endpoint + + def test_from_file(self): + '''test loading endpoint definitions from a json file''' + + e = Endpoint.from_file("../../tests/data/unit/admin/endpoint.json") + assert e.nickname == "saas" + assert e.url == "https://dcm.enstratius.com/api/enstratus/2015-05-25" + assert e.api_version == "2015-05-25" + assert e.access_key == "abcdefg" + assert e.secret_key == "gfedcba" + assert e.ssl_verify == True + + def test_multiple_from_file(self): + '''test loading multiple endpoints from file''' + e = Endpoint.multiple_from_file("../../tests/data/unit/admin/endpoints.json") + assert e['saas'].nickname == "saas" + assert e['saas'].url == "https://dcm.enstratius.com/api/enstratus/2015-05-25" + assert e['saas'].api_version == "2015-05-25" + assert e['saas'].access_key == "abcdefg" + assert e['saas'].secret_key == "gfedcba" + assert e['saas'].ssl_verify is True + + assert e['vagrant'].nickname == "vagrant" + assert e['vagrant'].url == "https://vagrant.vm/api/enstratus/2015-05-25" + assert e['vagrant'].api_version == "2015-05-25" + assert e['vagrant'].access_key == "abcdefg" + assert e['vagrant'].secret_key == "gfedcba" + assert e['vagrant'].ssl_verify is True + \ No newline at end of file