From 354d397aca92ab8be97b8d42325014404a7ca8c8 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 22 Aug 2016 11:24:53 -0700 Subject: [PATCH 01/19] added wait and wait retries for delete_nat_gateway (otherwise releasing elastic ip will fail miserably). --- salt/modules/boto_vpc.py | 48 +++++++++++++++++++++++++++++++++++++++- salt/states/boto_vpc.py | 14 ++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index c93eeb8e60..ac066d6e69 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -131,12 +131,16 @@ from __future__ import absolute_import import logging import socket from distutils.version import LooseVersion as _LooseVersion # pylint: disable=import-error,no-name-in-module +import time +import random # Import Salt libs import salt.utils.boto import salt.utils.boto3 import salt.utils.compat from salt.exceptions import SaltInvocationError, CommandExecutionError +from salt.ext.six.moves import range # pylint: disable=import-error + # from salt.utils import exactly_one # TODO: Uncomment this and s/_exactly_one/exactly_one/ # See note in utils.boto @@ -1358,7 +1362,8 @@ def create_nat_gateway(subnet_id=None, def delete_nat_gateway(nat_gateway_id, release_eips=False, region=None, - key=None, keyid=None, profile=None): + key=None, keyid=None, profile=None, + wait_for_delete=False, wait_for_delete_retries=5): ''' Delete a nat gateway (by id). @@ -1368,6 +1373,35 @@ def delete_nat_gateway(nat_gateway_id, .. versionadded:: Carbon + nat_gateway_id + Id of the NAT Gateway + + releaes_eips + whether to release the elastic IPs associated with the given NAT Gateway Id + + region + Region to connect to. + + key + Secret key to be used. + + keyid + Access key to be used. + + profile + A dict with region, key and keyid, or a pillar key (string) that + contains a dict with region, key and keyid. + + wait_for_delete + whether to wait for delete of the NAT gateway to be in failed or deleted + state after issuing the delete call. + + wait_for_delete_retries + NAT gateway may take some time to be go into deleted or failed state. + During the deletion process, subsequent release of elastic IPs may fail; + this state will automatically retry this number of times to ensure + the NAT gateway is in deleted or failed state before proceeding. + CLI Examples: .. code-block:: bash @@ -1382,6 +1416,18 @@ def delete_nat_gateway(nat_gateway_id, if gwinfo: gwinfo = gwinfo.get('NatGateways', [None])[0] conn3.delete_nat_gateway(NatGatewayId=nat_gateway_id) + + # wait for deleting nat gateway to finish prior to attempt to release elastic ips + if wait_for_delete: + for retry in range(wait_for_delete_retries, 0, -1): + gwinfo = conn3.describe_nat_gateways(NatGatewayIds=[nat_gateway_id]) + if gwinfo: + gw = gwinfo.get('NatGateways', [None])[0] + if gw and gw['State'] not in ['deleted', 'failed']: + time.sleep((2 ** (wait_for_delete_retries - retry)) + (random.randint(0, 1000) / 1000)) + continue + break + if release_eips and gwinfo: for addr in gwinfo.get('NatGatewayAddresses'): conn3.release_address(AllocationId=addr.get('AllocationId')) diff --git a/salt/states/boto_vpc.py b/salt/states/boto_vpc.py index 5a5757734b..7050231614 100644 --- a/salt/states/boto_vpc.py +++ b/salt/states/boto_vpc.py @@ -1355,7 +1355,8 @@ def nat_gateway_present(name, subnet_name=None, subnet_id=None, def nat_gateway_absent(name=None, subnet_name=None, subnet_id=None, - region=None, key=None, keyid=None, profile=None): + region=None, key=None, keyid=None, profile=None, + wait_for_delete_retries=8): ''' Ensure the nat gateway in the named subnet is absent. @@ -1385,6 +1386,13 @@ def nat_gateway_absent(name=None, subnet_name=None, subnet_id=None, profile A dict with region, key and keyid, or a pillar key (string) that contains a dict with region, key and keyid. + + wait_for_delete_retries + NAT gateway may take some time to be go into deleted or failed state. + During the deletion process, subsequent release of elastic IPs may fail; + this state will automatically retry this number of times to ensure + the NAT gateway is in deleted or failed state before proceeding. + ''' ret = {'name': name, @@ -1412,7 +1420,9 @@ def nat_gateway_absent(name=None, subnet_name=None, subnet_id=None, release_eips=True, region=region, key=key, keyid=keyid, - profile=profile) + profile=profile, + wait_for_delete=True, + wait_for_delete_retries=wait_for_delete_retries) if 'error' in r: ret['result'] = False ret['comment'] = 'Failed to delete nat gateway: {0}'.format(r['error']['message']) From b6456437e2d986d29f6af7f622bf076809e38d27 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 22 Aug 2016 11:29:09 -0700 Subject: [PATCH 02/19] changed boto_vpc's nat_gateway absent state's wait_for_delete_retries to be 0 to be backward compatible with previous releases. --- salt/states/boto_vpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/states/boto_vpc.py b/salt/states/boto_vpc.py index 7050231614..7302dcb519 100644 --- a/salt/states/boto_vpc.py +++ b/salt/states/boto_vpc.py @@ -1356,7 +1356,7 @@ def nat_gateway_present(name, subnet_name=None, subnet_id=None, def nat_gateway_absent(name=None, subnet_name=None, subnet_id=None, region=None, key=None, keyid=None, profile=None, - wait_for_delete_retries=8): + wait_for_delete_retries=0): ''' Ensure the nat gateway in the named subnet is absent. @@ -1392,6 +1392,7 @@ def nat_gateway_absent(name=None, subnet_name=None, subnet_id=None, During the deletion process, subsequent release of elastic IPs may fail; this state will automatically retry this number of times to ensure the NAT gateway is in deleted or failed state before proceeding. + Default is set to 0 for backward compatibility. ''' From 46120a52690004e5001c120dd4649402d24c88b8 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 22 Aug 2016 12:02:36 -0700 Subject: [PATCH 03/19] fixed a bug in retry mechanism. --- salt/modules/boto_vpc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index ac066d6e69..42126769bf 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -1420,11 +1420,11 @@ def delete_nat_gateway(nat_gateway_id, # wait for deleting nat gateway to finish prior to attempt to release elastic ips if wait_for_delete: for retry in range(wait_for_delete_retries, 0, -1): - gwinfo = conn3.describe_nat_gateways(NatGatewayIds=[nat_gateway_id]) - if gwinfo: - gw = gwinfo.get('NatGateways', [None])[0] - if gw and gw['State'] not in ['deleted', 'failed']: - time.sleep((2 ** (wait_for_delete_retries - retry)) + (random.randint(0, 1000) / 1000)) + if gwinfo and gwinfo['State'] not in ['deleted', 'failed']: + time.sleep((2 ** (wait_for_delete_retries - retry)) + (random.randint(0, 1000) / 1000)) + gwinfo = conn3.describe_nat_gateways(NatGatewayIds=[nat_gateway_id]) + if gwinfo: + gwinfo = gwinfo.get('NatGateways', [None])[0] continue break From ad206abeb639520e1569a18afc90b0d607b58606 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 22 Aug 2016 12:07:39 -0700 Subject: [PATCH 04/19] fixed the randomization portion of the exponential backoff wait time. --- salt/modules/boto_vpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index 42126769bf..000217ea16 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -1421,7 +1421,7 @@ def delete_nat_gateway(nat_gateway_id, if wait_for_delete: for retry in range(wait_for_delete_retries, 0, -1): if gwinfo and gwinfo['State'] not in ['deleted', 'failed']: - time.sleep((2 ** (wait_for_delete_retries - retry)) + (random.randint(0, 1000) / 1000)) + time.sleep((2 ** (wait_for_delete_retries - retry)) + (random.randint(0, 1000) / 1000.0)) gwinfo = conn3.describe_nat_gateways(NatGatewayIds=[nat_gateway_id]) if gwinfo: gwinfo = gwinfo.get('NatGateways', [None])[0] From d18c05e947cfdeaada35f43c51bae99e3fb8b1cf Mon Sep 17 00:00:00 2001 From: kbelov Date: Wed, 14 Sep 2016 12:47:57 -0700 Subject: [PATCH 05/19] initial implementation of usage plan support in salt --- salt/modules/boto_apigateway.py | 150 ++++++++++++++++++++++++++++++++ salt/states/boto_apigateway.py | 141 ++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index c87a3e3f1f..79937d10db 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1388,3 +1388,153 @@ def create_api_integration_response(restApiId, resourcePath, httpMethod, statusC return {'created': False, 'error': 'no such resource'} except ClientError as e: return {'created': False, 'error': salt.utils.boto3.get_error(e)} + + +def _filter_plans(attr, name, plans): + ''' + Return list of usage plan items matching the given attribute value. + ''' + return [plan for plan in plans if plan[attr] == name] + +def describe_usage_plans(name=None, plan_id=None, region=None, key=None, keyid=None, profile=None): + ''' + Returns a list of existing usage plans, optionally filtered to match a given plan name + ''' + try: + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + plans = _multi_call(conn.get_usage_plans, 'items') + if name: + plans = _filter_plans('name', name, plans) + if plan_id: + plans = _filter_plans('id', plan_id, plans) + + return {'plans': [_convert_datetime_str(plan) for plan in plans]} + + except ClientError as e: + return {'error': salt.utils.boto3.get_error(e)} + +def _format_throttle(rate=None, burst=None): + throttle = {} + if rate: + throttle['rateLimit'] = float(rate) + if burst: + throttle['burstLimit'] = int(burst) + return throttle + +def _format_quotas(quota=None, offset=None, period=None): + quotas = {} + if quota: + quotas['limit'] = int(quota) + if offset: + quotas['offset'] = int(offset) + if period: + if period not in ['DAY', 'WEEK', 'MONTH']: + raise Exception('unsupported usage plan period, must be DAY, WEEK or MONTH') + quotas['period'] = period + return quotas + +def create_usage_plan(name, description=None, rate=None, burst=None, quota=None, offset=None, period=None, region=None, key=None, keyid=None, profile=None): + ''' + Creates a new usage plan with throttling and quotas optionally applied + rate: floating point number, number of reqeusts per second in steady state + burst: integer number, maximum rate limit + quota: integer number, maximum number of requests per day + offset: integer number, number of requests subtracted from quota on the initial time period + ''' + values = dict(name=name) + if description: + values['description'] = description + + throttle = _format_throttle(rate, burst) + if throttle: + values['throttle'] = throttle + + try: + quotas = _format_quotas(quota, offset, period) + if quotas: + values['quota'] = quotas + except Exception as e: + return {'error': '{0}'.format(e)} + + try: + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + res = conn.create_usage_plan(**values) + return {'created': True, 'result': res} + except ClientError as e: + return {'error': salt.utils.boto3.get_error(e)} + +def update_usage_plan(plan_id, rate=None, burst=None, quota=None, offset=None, period=None, region=None, key=None, keyid=None, profile=None): + try: + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + + patchOperations=[] + + if rate is None and burst is None: + patchOperations.append({'op': 'remove', 'path': '/throttle'}) + if quota is None and offset is None and period is None: + patchOperations.append({'op': 'remove', 'path': '/quota'}) + + # if rate: + # patchOperations.append({'op': 'replace', 'path': '/throttle/rateLimit', 'value': str(rate)}) + # patchOperations.append({'op': 'replace', 'path': '/quota/limit', 'value': str(quota)}) + # patchOperations.append({'op': 'replace', 'path': '/quota/period', 'value': str(period)}) + + if patchOperations: + res = conn.update_usage_plan(usagePlanId=plan_id, + patchOperations=patchOperations) + return {'updated': True, 'result': res} + + return {'updated': False} + + except ClientError as e: + return {'error': salt.utils.boto3.get_error(e)} + +def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): + ''' + Deletes usage plan identified by plan_id + ''' + try: + existing = describe_usage_plans(plan_id=plan_id, region=region, key=key, keyid=keyid, profile=profile) + # don't attempt to delete the usage plan if it does not exist + if 'plans' in existing and existing['plans']: + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + res = conn.delete_usage_plan(usagePlanId=plan_id) + return {'deleted': True, 'usagePlanId': plan_id} + except ClientError as e: + return {'error': salt.utils.boto3.get_error(e)} + +def attach_usage_plan_to_api_stage(plan_id, api_id, stage_name, region=None, key=None, keyid=None, profile=None): + ''' + Attaches the usage plan identified by plan_id to a stage stage_name in the REST API identified by api_id + ''' + try: + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + val = "{0}:{1}".format(api_id, stage_name) + res = conn.update_usage_plan(usagePlanId=plan_id, + patchOperations=[{ + 'op': 'add', + 'path': '/apiStages', + 'value': val + }]) + return {'success': True, 'result': res} + except ClientError as e: + return {'error': salt.utils.boto3.get_error(e)} + +def detach_usage_plan_from_api_stage(plan_id, api_id, stage_name, region=None, key=None, keyid=None, profile=None): + ''' + Removes the usage plan identified by plan_id from a stage stage_name in the REST API identified by api_id + ''' + try: + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + val = "{0}:{1}".format(api_id, stage_name) + res = conn.update_usage_plan(usagePlanId=plan_id, + patchOperations=[{ + 'op': 'remove', + 'path': '/apiStages', + 'value': val + }]) + return {'success': True, 'result': res} + except ClientError as e: + return {'error': salt.utils.boto3.get_error(e)} + + diff --git a/salt/states/boto_apigateway.py b/salt/states/boto_apigateway.py index 49d3dcf71f..1962242163 100644 --- a/salt/states/boto_apigateway.py +++ b/salt/states/boto_apigateway.py @@ -1615,3 +1615,144 @@ class _Swagger(object): ret = self._deploy_method(ret, path, method, method_data, api_key_required, lambda_integration_role, lambda_region, authorization_type) return ret + + +def usage_plan_present(name, plan_name, description=None, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): + ret = {'name': name, + 'result': True, + 'comment': '', + 'changes': {} + } + + try: + common_args = dict([('region', region), + ('key', key), + ('keyid', keyid), + ('profile', profile)]) + + existing = __salt__['boto_apigateway.describe_usage_plans'](name=plan_name, **common_args) + if 'error' in existing: + ret['result'] = False + ret['comment'] = 'Failed to describe existing usage plans' + return ret + + if not existing['plans']: + # plan does not exist, we need to create it + if __opts__['test']: + ret['comment'] = 'a new usage plan {0} would be created'.format(plan_name) + ret['result'] = None + return ret + + result = __salt__['boto_apigateway.create_usage_plan'](name=plan_name, + description=description, + rate=rate, + burst=burst, + quota=quota, + offset=offset, + period=period, + **common_args) + if 'error' in result: + ret['result'] = False + ret['comment'] = 'Failed to create a usage plan {0}, {1}'.format(plan_name, result['error']) + return ret + + ret['changes']['old'] = {'plan': None} + ret['comment'] = 'A new usage plan {0} has been created'.format(plan_name) + + else: + # need an existing plan modified to match given value + plan = existing['plans'][0] + needs_updating = False + + modifiable_params = (('throttle', ('rateLimit', 'burstLimit')), ('quota', ('limit', 'offset', 'period'))) + param_values = dict(rateLimit=rate, burstLimit=burst, limit=quota, offset=offset, period=period) + for p, fields in modifiable_params: + for f in fields: + if plan.get(p, {}).get(f, None) != param_values[f]: + needs_updating = True + break; + + if not needs_updating: + ret['comment'] = 'usage plan {0} is already in a correct state'.format(plan_name) + ret['result'] = True + return ret + + if __opts__['test']: + ret['comment'] = 'a new usage plan {0} would be updated'.format(plan_name) + ret['result'] = None + return ret + + result = __salt__['boto_apigateway.update_usage_plan'](plan['id'], + rate=rate, + burst=burst, + quota=quota, + offset=offset, + period=period, + **common_args) + if 'error' in result: + ret['result'] = False + ret['comment'] = 'Failed to update a usage plan {0}, {1}'.format(plan_name, result['error']) + return ret + + ret['changes']['old'] = {'plan': plan} + ret['comment'] = 'usage plan {0} has been updated'.format(plan_name) + + newstate = __salt__['boto_apigateway.describe_usage_plans'](name=plan_name, **common_args) + if 'error' in existing: + ret['result'] = False + ret['comment'] = 'Failed to describe existing usage plans after updates' + return ret + + ret['changes']['new'] = {'plan': newstate['plans'][0]} + + except (ValueError, IOError) as e: + ret['result'] = False + ret['comment'] = '{0}'.format(e.args) + + return ret + +def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profile=None): + ret = {'name': name, + 'result': True, + 'comment': '', + 'changes': {} + } + + try: + common_args = dict([('region', region), + ('key', key), + ('keyid', keyid), + ('profile', profile)]) + + existing = __salt__['boto_apigateway.describe_usage_plans'](name=plan_name, **common_args) + if 'error' in existing: + ret['result'] = False + ret['comment'] = 'Failed to describe existing usage plans' + return ret + + if not existing['plans']: + ret['comment'] = 'Usage plan {0} does not exist already'.format(plan_name) + return ret + + if __opts__['test']: + ret['comment'] = 'Usage plan {0} exists and would be deleted'.format(plan_name) + ret['result'] = None + return ret + + plan_id = existing['plans'][0]['id'] + result = __salt__['boto_apigateway.delete_usage_plan'](plan_id, **common_args) + + if 'error' in result: + ret['result'] = False + ret['comment'] = 'Failed to delete usage plan {0}, {1}'.format(plan_name, result) + return ret + + ret['comment'] = 'Usage plan {0} has been deleted'.format(plan_name) + ret['changes']['old'] = {'plan': existing['plans'][0]} + ret['changes']['new'] = {'plan': None} + + except (ValueError, IOError) as e: + ret['result'] = False + ret['comment'] = '{0}'.format(e.args) + + return ret From 42262044307fbf60b005feb751a7b8cf85307ed7 Mon Sep 17 00:00:00 2001 From: kbelov Date: Wed, 14 Sep 2016 13:42:12 -0700 Subject: [PATCH 06/19] renamed state parameters to better match AWS names --- salt/modules/boto_apigateway.py | 94 +++++++++++++++++---------------- salt/states/boto_apigateway.py | 69 ++++++++++++++++++++---- 2 files changed, 107 insertions(+), 56 deletions(-) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index 79937d10db..5de1dac726 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1413,27 +1413,22 @@ def describe_usage_plans(name=None, plan_id=None, region=None, key=None, keyid=N except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} -def _format_throttle(rate=None, burst=None): - throttle = {} - if rate: - throttle['rateLimit'] = float(rate) - if burst: - throttle['burstLimit'] = int(burst) - return throttle - -def _format_quotas(quota=None, offset=None, period=None): - quotas = {} - if quota: - quotas['limit'] = int(quota) - if offset: - quotas['offset'] = int(offset) - if period: - if period not in ['DAY', 'WEEK', 'MONTH']: - raise Exception('unsupported usage plan period, must be DAY, WEEK or MONTH') - quotas['period'] = period - return quotas +def _validate_throttle(throttle): + if throttle: + if not isinstance(throttle, dict): + raise TypeError('throttle must be a dictionary, provided value: {0}'.format(throttle)) -def create_usage_plan(name, description=None, rate=None, burst=None, quota=None, offset=None, period=None, region=None, key=None, keyid=None, profile=None): +def _validate_quota(quota): + if quota: + if not isinstance(quota, dict): + raise TypeError('quota must be a dictionary, provided value: {0}'.format(quota)) + periods = ['DAY', 'WEEK', 'MONTH'] + if 'period' not in quota or quota['period'] not in periods: + raise ValueError('quota must have a valid period specified, valid values are {0}'.format(','.join(periods))) + if 'limit' not in quota: + raise ValueError('quota limit must have a valid value') + +def create_usage_plan(name, description=None, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): ''' Creates a new usage plan with throttling and quotas optionally applied rate: floating point number, number of reqeusts per second in steady state @@ -1441,44 +1436,51 @@ def create_usage_plan(name, description=None, rate=None, burst=None, quota=None, quota: integer number, maximum number of requests per day offset: integer number, number of requests subtracted from quota on the initial time period ''' - values = dict(name=name) - if description: - values['description'] = description - - throttle = _format_throttle(rate, burst) - if throttle: - values['throttle'] = throttle - try: - quotas = _format_quotas(quota, offset, period) - if quotas: - values['quota'] = quotas - except Exception as e: - return {'error': '{0}'.format(e)} + _validate_throttle(throttle) + _validate_quota(quota) + + values = dict(name=name) + if description: + values['description'] = description + if throttle: + values['throttle'] = throttle + if quota: + values['quota'] = quota - try: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) res = conn.create_usage_plan(**values) return {'created': True, 'result': res} except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} + except (TypeError, ValueError) as e: + return {'error': '{0}'.format(e)} -def update_usage_plan(plan_id, rate=None, burst=None, quota=None, offset=None, period=None, region=None, key=None, keyid=None, profile=None): +def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): try: + _validate_throttle(throttle) + _validate_quota(quota) + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) - - patchOperations=[] - - if rate is None and burst is None: - patchOperations.append({'op': 'remove', 'path': '/throttle'}) - if quota is None and offset is None and period is None: - patchOperations.append({'op': 'remove', 'path': '/quota'}) - # if rate: - # patchOperations.append({'op': 'replace', 'path': '/throttle/rateLimit', 'value': str(rate)}) - # patchOperations.append({'op': 'replace', 'path': '/quota/limit', 'value': str(quota)}) - # patchOperations.append({'op': 'replace', 'path': '/quota/period', 'value': str(period)}) + patchOperations =[] + + if throttle is None: + patchOperations.append({'op': 'remove', 'path': '/throttle'}) + else: + if 'rateLimit' in throttle: + patchOperations.append({'op': 'replace', 'path': '/throttle/rateLimit', 'value': str(throttle['rateLimit'])}) + if 'burstLimit' in throttle: + patchOperations.append({'op': 'replace', 'path': '/throttle/burstLimit', 'value': str(throttle['burstLimit'])}) + if quota is None: + patchOperations.append({'op': 'remove', 'path': '/quota'}) + else: + patchOperations.append({'op': 'replace', 'path': '/quota/period', 'value': str(quota['period'])}) + patchOperations.append({'op': 'replace', 'path': '/quota/limit', 'value': str(quota['limit'])}) + if 'offset' in quota: + patchOperations.append({'op': 'replace', 'path': '/quota/offset', 'value': str(quota['offset'])}) + if patchOperations: res = conn.update_usage_plan(usagePlanId=plan_id, patchOperations=patchOperations) diff --git a/salt/states/boto_apigateway.py b/salt/states/boto_apigateway.py index 1962242163..2422a08fa1 100644 --- a/salt/states/boto_apigateway.py +++ b/salt/states/boto_apigateway.py @@ -1618,6 +1618,46 @@ class _Swagger(object): def usage_plan_present(name, plan_name, description=None, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): + ''' + Ensure the spcifieda usage plan with the corresponding metrics is deployed + + name + name of the state + plan_name + [Required] name of the usage plan + throttle + [Optional] throttling parameters expressed as a dictionary. + If provided, at least one of the throttling paramters must be present + rateLimit + rate per second at which capacity bucket is populated + burstLimit + maximum rate allowed + quota + [Optional] quota on the number of api calls permitted by the plan. + If provided, limit and period must be present + limit + [Required] number of calls permitted per quota period + offset + [Optional] number of calls to be subtracted from the limit at the beginning of the period + period + [Required] period to which quota applies. Must be DAY, WEEK or MONTH + + Example: + UsagePlanPresent: + boto_apigateway.usage_plan_present: + - name: my_usage_plan + - plan_name: my_usage_plan + - throttle: + rateLimit: 70 + burstLimit: 100 + - quota: + limit: 1000 + offset: 0 + period: DAY + - profile: cfg-aws-profile + ''' + func_params = locals() + ret = {'name': name, 'result': True, 'comment': '', @@ -1645,11 +1685,8 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N result = __salt__['boto_apigateway.create_usage_plan'](name=plan_name, description=description, - rate=rate, - burst=burst, + throttle=throttle, quota=quota, - offset=offset, - period=period, **common_args) if 'error' in result: ret['result'] = False @@ -1665,10 +1702,10 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N needs_updating = False modifiable_params = (('throttle', ('rateLimit', 'burstLimit')), ('quota', ('limit', 'offset', 'period'))) - param_values = dict(rateLimit=rate, burstLimit=burst, limit=quota, offset=offset, period=period) for p, fields in modifiable_params: for f in fields: - if plan.get(p, {}).get(f, None) != param_values[f]: + actual_param = {} if func_params.get(p) is None else func_params.get(p) + if plan.get(p, {}).get(f, None) != actual_param.get(f, None): needs_updating = True break; @@ -1683,11 +1720,8 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N return ret result = __salt__['boto_apigateway.update_usage_plan'](plan['id'], - rate=rate, - burst=burst, + throttle=throttle, quota=quota, - offset=offset, - period=period, **common_args) if 'error' in result: ret['result'] = False @@ -1712,6 +1746,21 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N return ret def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profile=None): + ''' + Ensures usage plan identified by name is no longer present + + name + name of the state + plan_name + name of the plan to remove + + Example + usage plan absent: + boto_apigateway.usage_plan_absent: + - name: my_usage_plan_state + - plan_name: my_usage_plan + - profile: cfg-aws-profile + ''' ret = {'name': name, 'result': True, 'comment': '', From 76af9186a91aad8da7fac62bf905d5ecbe8f0b40 Mon Sep 17 00:00:00 2001 From: kbelov Date: Wed, 14 Sep 2016 16:55:50 -0700 Subject: [PATCH 07/19] added usage_plan_association present and absent states --- salt/modules/boto_apigateway.py | 85 +++++++++++----- salt/states/boto_apigateway.py | 174 ++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 28 deletions(-) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index 5de1dac726..bac11f3923 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1414,11 +1414,17 @@ def describe_usage_plans(name=None, plan_id=None, region=None, key=None, keyid=N return {'error': salt.utils.boto3.get_error(e)} def _validate_throttle(throttle): + ''' + Verifies that throttling parameters are valid + ''' if throttle: if not isinstance(throttle, dict): raise TypeError('throttle must be a dictionary, provided value: {0}'.format(throttle)) def _validate_quota(quota): + ''' + Verifies that quota parameters are valid + ''' if quota: if not isinstance(quota, dict): raise TypeError('quota must be a dictionary, provided value: {0}'.format(quota)) @@ -1431,10 +1437,18 @@ def _validate_quota(quota): def create_usage_plan(name, description=None, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): ''' Creates a new usage plan with throttling and quotas optionally applied - rate: floating point number, number of reqeusts per second in steady state - burst: integer number, maximum rate limit - quota: integer number, maximum number of requests per day - offset: integer number, number of requests subtracted from quota on the initial time period + throttle + rateLimit + requests per second at steady rate, float + burstLimit + maximum number of requests per second, integer + quota + limit + number of allowed requests per specified quota period [required if quota parameter is present] + offset + number of requests to be subtracted from limit at the beginning of the period [optional] + period + quota period, must be one of DAY, WEEK, or MONTH. [required if quota parameter is present ''' try: _validate_throttle(throttle) @@ -1457,6 +1471,21 @@ def create_usage_plan(name, description=None, throttle=None, quota=None, region= return {'error': '{0}'.format(e)} def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): + ''' + Updates an existing usage plan with throttling and quotas + throttle + rateLimit + requests per second at steady rate, float + burstLimit + maximum number of requests per second, integer + quota + limit + number of allowed requests per specified quota period [required if quota parameter is present] + offset + number of requests to be subtracted from limit at the beginning of the period [optional] + period + quota period, must be one of DAY, WEEK, or MONTH. [required if quota parameter is present + ''' try: _validate_throttle(throttle) _validate_quota(quota) @@ -1505,38 +1534,38 @@ def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} -def attach_usage_plan_to_api_stage(plan_id, api_id, stage_name, region=None, key=None, keyid=None, profile=None): +def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None, profile=None): ''' - Attaches the usage plan identified by plan_id to a stage stage_name in the REST API identified by api_id + Updates the usage plan identified by plan_id by adding or removing it to each of the stages, specified by apis parameter. + apis + a list of dictionaries, containing apiId and stage + op + 'add' or 'remove' ''' try: - conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) - val = "{0}:{1}".format(api_id, stage_name) - res = conn.update_usage_plan(usagePlanId=plan_id, - patchOperations=[{ - 'op': 'add', - 'path': '/apiStages', - 'value': val - }]) - return {'success': True, 'result': res} - except ClientError as e: - return {'error': salt.utils.boto3.get_error(e)} + patchOperations = [] + for api in apis: + patchOperations.append({ + 'op': op, + 'path': '/apiStages', + 'value': '{0}:{1}'.format(api['apiId'], api['stage']) + }) -def detach_usage_plan_from_api_stage(plan_id, api_id, stage_name, region=None, key=None, keyid=None, profile=None): - ''' - Removes the usage plan identified by plan_id from a stage stage_name in the REST API identified by api_id - ''' - try: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) - val = "{0}:{1}".format(api_id, stage_name) res = conn.update_usage_plan(usagePlanId=plan_id, - patchOperations=[{ - 'op': 'remove', - 'path': '/apiStages', - 'value': val - }]) + patchOperations=patchOperations) return {'success': True, 'result': res} except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} +def attach_usage_plan_to_apis(plan_id, apis, region=None, key=None, keyid=None, profile=None): + ''' + Attaches given usage plan to each of the apis provided in a list of apiId and stage values + ''' + return _update_usage_plan_apis(plan_id, apis, 'add', region=region, key=key, keyid=keyid, profile=profile) +def detach_usage_plan_from_apis(plan_id, apis, region=None, key=None, keyid=None, profile=None): + ''' + Detaches given usage plan from each of the apis provided in a list of apiId and stage value + ''' + return _update_usage_plan_apis(plan_id, apis, 'remove', region=region, key=key, keyid=keyid, profile=profile) diff --git a/salt/states/boto_apigateway.py b/salt/states/boto_apigateway.py index 2422a08fa1..c7b8d632ca 100644 --- a/salt/states/boto_apigateway.py +++ b/salt/states/boto_apigateway.py @@ -1805,3 +1805,177 @@ def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profil ret['comment'] = '{0}'.format(e.args) return ret + + +def usage_plan_association_present(name, plan_name, apiStages, region=None, key=None, keyid=None, profile=None): + ''' + Ensures usage plan identified by name is added to provided apiStages + + name + name of the state + plan_name + name of the plan to use + apiStages + list of stages + apiId + apiId of the api to attach usage plan to + stage + stage name of the api to attach usage plan to + + Example + UsagePlanAssociationPresent: + boto_apigateway.usage_plan_association_present: + - name: usage_plan_association + - plan_name: planB + - apiStages: + - apiId: 9kb0404ec0 + stage: xxx + - apiId: l9v7o2aj90 + stage: xxx + - profile: cfg-aws-profile + ''' + ret = {'name': name, + 'result': True, + 'comment': '', + 'changes': {} + } + try: + common_args = dict([('region', region), + ('key', key), + ('keyid', keyid), + ('profile', profile)]) + + existing = __salt__['boto_apigateway.describe_usage_plans'](name=plan_name, **common_args) + if 'error' in existing: + ret['result'] = False + ret['comment'] = 'Failed to describe existing usage plans' + return ret + + if not existing['plans']: + ret['comment'] = 'Usage plan {0} does not exist'.format(plan_name) + ret['result'] = False + return ret + + if len(existing['plans']) != 1: + ret['comment'] = 'There are multiple usage plans with the same name - it is not supported' + ret['result'] = False + return ret + + plan = existing['plans'][0] + plan_id = plan['id'] + plan_stages = plan.get('apiStages', []) + + stages_to_add = [] + for api in apiStages: + if api not in plan_stages: + stages_to_add.append(api) + + if not stages_to_add: + ret['comment'] = 'Usage plan is already asssociated to all api stages' + return ret + + result = __salt__['boto_apigateway.attach_usage_plan_to_apis'](plan_id, stages_to_add, **common_args) + if 'error' in result: + ret['comment'] = 'Failed to associate a usage plan {0} to the apis {1}, {2}'.format(plan_name, stages_to_add, result['error']) + ret['result'] = False + return ret + + ret['comment'] = 'successfully associated usage plan to apis' + ret['changes']['old'] = plan_stages + ret['changes']['new'] = result.get('result', {}).get('apiStages', []) + + except (ValueError, IOError) as e: + ret['result'] = False + ret['comment'] = '{0}'.format(e.args) + + return ret + +def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=None, keyid=None, profile=None): + ''' + Ensures usage plan identified by name is removed from provided apiStages + If a plan is associated to stages not listed in apiStages parameter, + those associations remain intact + + name + name of the state + plan_name + name of the plan to use + apiStages + list of stages + apiId + apiId of the api to detach usage plan from + stage + stage name of the api to detach usage plan from + + Example + UsagePlanAssociationAbsent: + boto_apigateway.usage_plan_association_absent: + - name: usage_plan_association + - plan_name: planB + - apiStages: + - apiId: 9kb0404ec0 + stage: xxx + - apiId: l9v7o2aj90 + stage: xxx + - profile: cfg-aws-profile + ''' + ret = {'name': name, + 'result': True, + 'comment': '', + 'changes': {} + } + try: + common_args = dict([('region', region), + ('key', key), + ('keyid', keyid), + ('profile', profile)]) + + existing = __salt__['boto_apigateway.describe_usage_plans'](name=plan_name, **common_args) + if 'error' in existing: + ret['result'] = False + ret['comment'] = 'Failed to describe existing usage plans' + return ret + + if not existing['plans']: + ret['comment'] = 'Usage plan {0} does not exist'.format(plan_name) + ret['result'] = False + return ret + + if len(existing['plans']) != 1: + ret['comment'] = 'There are multiple usage plans with the same name - it is not supported' + ret['result'] = False + return ret + + plan = existing['plans'][0] + plan_id = plan['id'] + plan_stages = plan.get('apiStages', []) + + if not plan_stages: + ret['comment'] = 'Usage plan {0} has no associated stages already'.format(plan_name) + return ret + + stages_to_remove = [] + for api in apiStages: + if api in plan_stages: + stages_to_remove.append(api) + + if not stages_to_remove: + ret['comment'] = 'Usage plan is already not asssociated to any api stages' + return ret + + result = __salt__['boto_apigateway.detach_usage_plan_from_apis'](plan_id, stages_to_remove, **common_args) + if 'error' in result: + ret['comment'] = 'Failed to disassociate a usage plan {0} from the apis {1}, {2}'.format(plan_name, stages_to_add, result['error']) + ret['result'] = False + return ret + + ret['comment'] = 'successfully disassociated usage plan from apis' + ret['changes']['old'] = plan_stages + ret['changes']['new'] = result.get('result', {}).get('apiStages', []) + + except (ValueError, IOError) as e: + ret['result'] = False + ret['comment'] = '{0}'.format(e.args) + + return ret + From 83d07a8fb8b3e50888a6fd32f3da6f6cb8379238 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Thu, 15 Sep 2016 08:50:03 -0700 Subject: [PATCH 08/19] lint clean ups. --- salt/modules/boto_apigateway.py | 27 ++++++++++++++------- salt/states/boto_apigateway.py | 43 +++++++++++++++++---------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index bac11f3923..d1e1ccd4ec 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1396,6 +1396,7 @@ def _filter_plans(attr, name, plans): ''' return [plan for plan in plans if plan[attr] == name] + def describe_usage_plans(name=None, plan_id=None, region=None, key=None, keyid=None, profile=None): ''' Returns a list of existing usage plans, optionally filtered to match a given plan name @@ -1413,6 +1414,7 @@ def describe_usage_plans(name=None, plan_id=None, region=None, key=None, keyid=N except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} + def _validate_throttle(throttle): ''' Verifies that throttling parameters are valid @@ -1421,6 +1423,7 @@ def _validate_throttle(throttle): if not isinstance(throttle, dict): raise TypeError('throttle must be a dictionary, provided value: {0}'.format(throttle)) + def _validate_quota(quota): ''' Verifies that quota parameters are valid @@ -1434,6 +1437,7 @@ def _validate_quota(quota): if 'limit' not in quota: raise ValueError('quota limit must have a valid value') + def create_usage_plan(name, description=None, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): ''' Creates a new usage plan with throttling and quotas optionally applied @@ -1453,7 +1457,7 @@ def create_usage_plan(name, description=None, throttle=None, quota=None, region= try: _validate_throttle(throttle) _validate_quota(quota) - + values = dict(name=name) if description: values['description'] = description @@ -1470,9 +1474,10 @@ def create_usage_plan(name, description=None, throttle=None, quota=None, region= except (TypeError, ValueError) as e: return {'error': '{0}'.format(e)} + def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): ''' - Updates an existing usage plan with throttling and quotas + Updates an existing usage plan with throttling and quotas throttle rateLimit requests per second at steady rate, float @@ -1491,9 +1496,9 @@ def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, _validate_quota(quota) conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) - - patchOperations =[] - + + patchOperations = [] + if throttle is None: patchOperations.append({'op': 'remove', 'path': '/throttle'}) else: @@ -1509,7 +1514,7 @@ def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, patchOperations.append({'op': 'replace', 'path': '/quota/limit', 'value': str(quota['limit'])}) if 'offset' in quota: patchOperations.append({'op': 'replace', 'path': '/quota/offset', 'value': str(quota['offset'])}) - + if patchOperations: res = conn.update_usage_plan(usagePlanId=plan_id, patchOperations=patchOperations) @@ -1520,6 +1525,7 @@ def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} + def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): ''' Deletes usage plan identified by plan_id @@ -1530,16 +1536,17 @@ def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): if 'plans' in existing and existing['plans']: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) res = conn.delete_usage_plan(usagePlanId=plan_id) - return {'deleted': True, 'usagePlanId': plan_id} + return {'deleted': True, 'usagePlanId': plan_id} except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} + def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None, profile=None): ''' Updates the usage plan identified by plan_id by adding or removing it to each of the stages, specified by apis parameter. - apis + apis a list of dictionaries, containing apiId and stage - op + op 'add' or 'remove' ''' try: @@ -1558,12 +1565,14 @@ def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} + def attach_usage_plan_to_apis(plan_id, apis, region=None, key=None, keyid=None, profile=None): ''' Attaches given usage plan to each of the apis provided in a list of apiId and stage values ''' return _update_usage_plan_apis(plan_id, apis, 'add', region=region, key=key, keyid=keyid, profile=profile) + def detach_usage_plan_from_apis(plan_id, apis, region=None, key=None, keyid=None, profile=None): ''' Detaches given usage plan from each of the apis provided in a list of apiId and stage value diff --git a/salt/states/boto_apigateway.py b/salt/states/boto_apigateway.py index c7b8d632ca..79a0e60688 100644 --- a/salt/states/boto_apigateway.py +++ b/salt/states/boto_apigateway.py @@ -1626,7 +1626,7 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N plan_name [Required] name of the usage plan throttle - [Optional] throttling parameters expressed as a dictionary. + [Optional] throttling parameters expressed as a dictionary. If provided, at least one of the throttling paramters must be present rateLimit rate per second at which capacity bucket is populated @@ -1636,7 +1636,7 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N [Optional] quota on the number of api calls permitted by the plan. If provided, limit and period must be present limit - [Required] number of calls permitted per quota period + [Required] number of calls permitted per quota period offset [Optional] number of calls to be subtracted from the limit at the beginning of the period period @@ -1683,10 +1683,10 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N ret['result'] = None return ret - result = __salt__['boto_apigateway.create_usage_plan'](name=plan_name, - description=description, + result = __salt__['boto_apigateway.create_usage_plan'](name=plan_name, + description=description, throttle=throttle, - quota=quota, + quota=quota, **common_args) if 'error' in result: ret['result'] = False @@ -1695,7 +1695,7 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N ret['changes']['old'] = {'plan': None} ret['comment'] = 'A new usage plan {0} has been created'.format(plan_name) - + else: # need an existing plan modified to match given value plan = existing['plans'][0] @@ -1707,7 +1707,7 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N actual_param = {} if func_params.get(p) is None else func_params.get(p) if plan.get(p, {}).get(f, None) != actual_param.get(f, None): needs_updating = True - break; + break if not needs_updating: ret['comment'] = 'usage plan {0} is already in a correct state'.format(plan_name) @@ -1719,9 +1719,9 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N ret['result'] = None return ret - result = __salt__['boto_apigateway.update_usage_plan'](plan['id'], - throttle=throttle, - quota=quota, + result = __salt__['boto_apigateway.update_usage_plan'](plan['id'], + throttle=throttle, + quota=quota, **common_args) if 'error' in result: ret['result'] = False @@ -1745,6 +1745,7 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N return ret + def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profile=None): ''' Ensures usage plan identified by name is no longer present @@ -1782,18 +1783,18 @@ def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profil if not existing['plans']: ret['comment'] = 'Usage plan {0} does not exist already'.format(plan_name) return ret - + if __opts__['test']: - ret['comment'] = 'Usage plan {0} exists and would be deleted'.format(plan_name) - ret['result'] = None - return ret + ret['comment'] = 'Usage plan {0} exists and would be deleted'.format(plan_name) + ret['result'] = None + return ret plan_id = existing['plans'][0]['id'] - result = __salt__['boto_apigateway.delete_usage_plan'](plan_id, **common_args) + result = __salt__['boto_apigateway.delete_usage_plan'](plan_id, **common_args) if 'error' in result: ret['result'] = False - ret['comment'] = 'Failed to delete usage plan {0}, {1}'.format(plan_name, result) + ret['comment'] = 'Failed to delete usage plan {0}, {1}'.format(plan_name, result) return ret ret['comment'] = 'Usage plan {0} has been deleted'.format(plan_name) @@ -1867,7 +1868,7 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= stages_to_add = [] for api in apiStages: - if api not in plan_stages: + if api not in plan_stages: stages_to_add.append(api) if not stages_to_add: @@ -1882,7 +1883,7 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= ret['comment'] = 'successfully associated usage plan to apis' ret['changes']['old'] = plan_stages - ret['changes']['new'] = result.get('result', {}).get('apiStages', []) + ret['changes']['new'] = result.get('result', {}).get('apiStages', []) except (ValueError, IOError) as e: ret['result'] = False @@ -1890,10 +1891,11 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= return ret + def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=None, keyid=None, profile=None): ''' Ensures usage plan identified by name is removed from provided apiStages - If a plan is associated to stages not listed in apiStages parameter, + If a plan is associated to stages not listed in apiStages parameter, those associations remain intact name @@ -1965,7 +1967,7 @@ def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=N result = __salt__['boto_apigateway.detach_usage_plan_from_apis'](plan_id, stages_to_remove, **common_args) if 'error' in result: - ret['comment'] = 'Failed to disassociate a usage plan {0} from the apis {1}, {2}'.format(plan_name, stages_to_add, result['error']) + ret['comment'] = 'Failed to disassociate a usage plan {0} from the apis {1}, {2}'.format(plan_name, stages_to_remove, result['error']) ret['result'] = False return ret @@ -1978,4 +1980,3 @@ def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=N ret['comment'] = '{0}'.format(e.args) return ret - From 16f368f3047af32b57efa316e75016b1c601da2e Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Thu, 15 Sep 2016 11:44:40 -0700 Subject: [PATCH 09/19] reformatted docs comments. --- salt/modules/boto_apigateway.py | 118 ++++++++++++++++++++++++++++++-- salt/states/boto_apigateway.py | 67 ++++++++++++------ 2 files changed, 159 insertions(+), 26 deletions(-) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index d1e1ccd4ec..d4a5f13121 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1392,7 +1392,7 @@ def create_api_integration_response(restApiId, resourcePath, httpMethod, statusC def _filter_plans(attr, name, plans): ''' - Return list of usage plan items matching the given attribute value. + Helper to return list of usage plan items matching the given attribute value. ''' return [plan for plan in plans if plan[attr] == name] @@ -1400,6 +1400,19 @@ def _filter_plans(attr, name, plans): def describe_usage_plans(name=None, plan_id=None, region=None, key=None, keyid=None, profile=None): ''' Returns a list of existing usage plans, optionally filtered to match a given plan name + + CLI Example: + + .. code-block:: bash + + salt myminion boto_apigateway.describe_usage_plans + + salt myminion boto_apigateway.describe_usage_plans name='usage plan name' + + salt myminion boto_apigateway.describe_usage_plans plan_id='usage plan id' + + .. versionadded:: Carbon + ''' try: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) @@ -1417,7 +1430,7 @@ def describe_usage_plans(name=None, plan_id=None, region=None, key=None, keyid=N def _validate_throttle(throttle): ''' - Verifies that throttling parameters are valid + Helper to verify that throttling parameters are valid ''' if throttle: if not isinstance(throttle, dict): @@ -1426,7 +1439,7 @@ def _validate_throttle(throttle): def _validate_quota(quota): ''' - Verifies that quota parameters are valid + Helper to verify that quota parameters are valid ''' if quota: if not isinstance(quota, dict): @@ -1441,18 +1454,39 @@ def _validate_quota(quota): def create_usage_plan(name, description=None, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): ''' Creates a new usage plan with throttling and quotas optionally applied + + name + Name of the usage plan + throttle + A dictionary consisting of the following keys: + rateLimit requests per second at steady rate, float + burstLimit maximum number of requests per second, integer + quota + A dictionary consisting of the following keys: + limit number of allowed requests per specified quota period [required if quota parameter is present] + offset number of requests to be subtracted from limit at the beginning of the period [optional] + period quota period, must be one of DAY, WEEK, or MONTH. [required if quota parameter is present + + CLI Example: + + .. code-block:: bash + + salt myminion boto_apigateway.create_usage_plan name='usage plan name' throttle='{"rateLimit": 10.0, "burstLimit": 10}' + + .. versionadded:: Carbon + ''' try: _validate_throttle(throttle) @@ -1478,18 +1512,39 @@ def create_usage_plan(name, description=None, throttle=None, quota=None, region= def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, keyid=None, profile=None): ''' Updates an existing usage plan with throttling and quotas + + plan_id + Id of the created usage plan + throttle + A dictionary consisting of the following keys: + rateLimit requests per second at steady rate, float + burstLimit maximum number of requests per second, integer + quota + A dictionary consisting of the following keys: + limit number of allowed requests per specified quota period [required if quota parameter is present] + offset number of requests to be subtracted from limit at the beginning of the period [optional] + period quota period, must be one of DAY, WEEK, or MONTH. [required if quota parameter is present + + CLI Example: + + .. code-block:: bash + + salt myminion boto_apigateway.update_usage_plan plan_id='usage plan id' throttle='{"rateLimit": 10.0, "burstLimit": 10}' + + .. versionadded:: Carbon + ''' try: _validate_throttle(throttle) @@ -1529,6 +1584,15 @@ def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): ''' Deletes usage plan identified by plan_id + + CLI Example: + + .. code-block:: bash + + salt myminion boto_apigateway.delete_usage_plan plan_id='usage plan id' + + .. versionadded:: Carbon + ''' try: existing = describe_usage_plans(plan_id=plan_id, region=region, key=key, keyid=keyid, profile=profile) @@ -1543,9 +1607,17 @@ def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None, profile=None): ''' - Updates the usage plan identified by plan_id by adding or removing it to each of the stages, specified by apis parameter. + Helper function that updates the usage plan identified by plan_id by adding or removing it to each of the stages, specified by apis parameter. + apis - a list of dictionaries, containing apiId and stage + a list of dictionaries, where each dictionary contains the following: + + apiId + a string, which is the id of the created API in AWS ApiGateway + + stage + a string, which is the stage that the created API is deployed to. + op 'add' or 'remove' ''' @@ -1569,6 +1641,24 @@ def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None def attach_usage_plan_to_apis(plan_id, apis, region=None, key=None, keyid=None, profile=None): ''' Attaches given usage plan to each of the apis provided in a list of apiId and stage values + + apis + a list of dictionaries, where each dictionary contains the following: + + apiId + a string, which is the id of the created API in AWS ApiGateway + + stage + a string, which is the stage that the created API is deployed to. + + CLI Example: + + .. code-block:: bash + + salt myminion boto_apigateway.attach_usage_plan_to_apis plan_id='usage plan id' apis='[{"apiId": "some id 1", "stage": "some stage 1"}]' + + .. versionadded:: Carbon + ''' return _update_usage_plan_apis(plan_id, apis, 'add', region=region, key=key, keyid=keyid, profile=profile) @@ -1576,5 +1666,23 @@ def attach_usage_plan_to_apis(plan_id, apis, region=None, key=None, keyid=None, def detach_usage_plan_from_apis(plan_id, apis, region=None, key=None, keyid=None, profile=None): ''' Detaches given usage plan from each of the apis provided in a list of apiId and stage value + + apis + a list of dictionaries, where each dictionary contains the following: + + apiId + a string, which is the id of the created API in AWS ApiGateway + + stage + a string, which is the stage that the created API is deployed to. + + CLI Example: + + .. code-block:: bash + + salt myminion boto_apigateway.detach_usage_plan_to_apis plan_id='usage plan id' apis='[{"apiId": "some id 1", "stage": "some stage 1"}]' + + .. versionadded:: Carbon + ''' return _update_usage_plan_apis(plan_id, apis, 'remove', region=region, key=key, keyid=keyid, profile=profile) diff --git a/salt/states/boto_apigateway.py b/salt/states/boto_apigateway.py index 79a0e60688..ac81d132ca 100644 --- a/salt/states/boto_apigateway.py +++ b/salt/states/boto_apigateway.py @@ -1623,29 +1623,36 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N name name of the state + plan_name [Required] name of the usage plan + throttle [Optional] throttling parameters expressed as a dictionary. - If provided, at least one of the throttling paramters must be present + If provided, at least one of the throttling parameters must be present + rateLimit rate per second at which capacity bucket is populated + burstLimit maximum rate allowed + quota [Optional] quota on the number of api calls permitted by the plan. If provided, limit and period must be present + limit [Required] number of calls permitted per quota period + offset [Optional] number of calls to be subtracted from the limit at the beginning of the period + period [Required] period to which quota applies. Must be DAY, WEEK or MONTH - Example: + .. code-block:: yaml UsagePlanPresent: boto_apigateway.usage_plan_present: - - name: my_usage_plan - plan_name: my_usage_plan - throttle: rateLimit: 70 @@ -1654,7 +1661,10 @@ def usage_plan_present(name, plan_name, description=None, throttle=None, quota=N limit: 1000 offset: 0 period: DAY - - profile: cfg-aws-profile + - profile: my_profile + + .. versionadded:: Carbon + ''' func_params = locals() @@ -1752,15 +1762,18 @@ def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profil name name of the state + plan_name name of the plan to remove - Example + .. code-block:: yaml usage plan absent: boto_apigateway.usage_plan_absent: - - name: my_usage_plan_state - plan_name: my_usage_plan - - profile: cfg-aws-profile + - profile: my_profile + + .. versionadded:: Carbon + ''' ret = {'name': name, 'result': True, @@ -1814,26 +1827,32 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= name name of the state + plan_name name of the plan to use + apiStages - list of stages + list of dictionaries, where each dictionary consists of the following keys: + apiId apiId of the api to attach usage plan to + stage stage name of the api to attach usage plan to - Example + .. code-block:: yaml UsagePlanAssociationPresent: boto_apigateway.usage_plan_association_present: - - name: usage_plan_association - - plan_name: planB + - plan_name: my_plan - apiStages: - apiId: 9kb0404ec0 - stage: xxx + stage: my_stage - apiId: l9v7o2aj90 - stage: xxx - - profile: cfg-aws-profile + stage: my_stage + - profile: my_profile + + .. versionadded:: Carbon + ''' ret = {'name': name, 'result': True, @@ -1900,26 +1919,32 @@ def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=N name name of the state + plan_name name of the plan to use + apiStages - list of stages + list of dictionaries, where each dictionary consists of the following keys: + apiId apiId of the api to detach usage plan from + stage stage name of the api to detach usage plan from - Example + .. code-block:: yaml UsagePlanAssociationAbsent: boto_apigateway.usage_plan_association_absent: - - name: usage_plan_association - - plan_name: planB + - plan_name: my_plan - apiStages: - apiId: 9kb0404ec0 - stage: xxx + stage: my_stage - apiId: l9v7o2aj90 - stage: xxx - - profile: cfg-aws-profile + stage: my_stage + - profile: my_profile + + .. versionadded:: Carbon + ''' ret = {'name': name, 'result': True, From 74e132cec21fd17f716bbb24ddefb6f93c79684a Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Thu, 15 Sep 2016 11:52:31 -0700 Subject: [PATCH 10/19] changed apiStages to api_stages --- salt/states/boto_apigateway.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/salt/states/boto_apigateway.py b/salt/states/boto_apigateway.py index ac81d132ca..abf67b9bc7 100644 --- a/salt/states/boto_apigateway.py +++ b/salt/states/boto_apigateway.py @@ -1821,9 +1821,9 @@ def usage_plan_absent(name, plan_name, region=None, key=None, keyid=None, profil return ret -def usage_plan_association_present(name, plan_name, apiStages, region=None, key=None, keyid=None, profile=None): +def usage_plan_association_present(name, plan_name, api_stages, region=None, key=None, keyid=None, profile=None): ''' - Ensures usage plan identified by name is added to provided apiStages + Ensures usage plan identified by name is added to provided api_stages name name of the state @@ -1831,7 +1831,7 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= plan_name name of the plan to use - apiStages + api_stages list of dictionaries, where each dictionary consists of the following keys: apiId @@ -1844,7 +1844,7 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= UsagePlanAssociationPresent: boto_apigateway.usage_plan_association_present: - plan_name: my_plan - - apiStages: + - api_stages: - apiId: 9kb0404ec0 stage: my_stage - apiId: l9v7o2aj90 @@ -1886,7 +1886,7 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= plan_stages = plan.get('apiStages', []) stages_to_add = [] - for api in apiStages: + for api in api_stages: if api not in plan_stages: stages_to_add.append(api) @@ -1911,10 +1911,10 @@ def usage_plan_association_present(name, plan_name, apiStages, region=None, key= return ret -def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=None, keyid=None, profile=None): +def usage_plan_association_absent(name, plan_name, api_stages, region=None, key=None, keyid=None, profile=None): ''' - Ensures usage plan identified by name is removed from provided apiStages - If a plan is associated to stages not listed in apiStages parameter, + Ensures usage plan identified by name is removed from provided api_stages + If a plan is associated to stages not listed in api_stages parameter, those associations remain intact name @@ -1923,7 +1923,7 @@ def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=N plan_name name of the plan to use - apiStages + api_stages list of dictionaries, where each dictionary consists of the following keys: apiId @@ -1936,7 +1936,7 @@ def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=N UsagePlanAssociationAbsent: boto_apigateway.usage_plan_association_absent: - plan_name: my_plan - - apiStages: + - api_stages: - apiId: 9kb0404ec0 stage: my_stage - apiId: l9v7o2aj90 @@ -1982,7 +1982,7 @@ def usage_plan_association_absent(name, plan_name, apiStages, region=None, key=N return ret stages_to_remove = [] - for api in apiStages: + for api in api_stages: if api in plan_stages: stages_to_remove.append(api) From 027b90b2bf3fdcb13bf3dfe693577fb089857e8b Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Thu, 15 Sep 2016 13:56:09 -0700 Subject: [PATCH 11/19] added botocore minimum dependency check. --- salt/modules/boto_apigateway.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index d4a5f13121..e01429bd3b 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -98,6 +98,7 @@ try: import boto3 # pylint: enable=unused-import from botocore.exceptions import ClientError + from botocore import __version__ as found_botocore_version logging.getLogger('boto').setLevel(logging.CRITICAL) logging.getLogger('boto3').setLevel(logging.CRITICAL) HAS_BOTO = True @@ -113,6 +114,7 @@ def __virtual__(): ''' required_boto_version = '2.8.0' required_boto3_version = '1.2.1' + required_botocore_version = '1.4.49' # the boto_apigateway execution module relies on the connect_to_region() method # which was added in boto 2.8.0 # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12 @@ -125,6 +127,9 @@ def __virtual__(): elif _LooseVersion(boto3.__version__) < _LooseVersion(required_boto3_version): return (False, 'The boto_apigateway module could not be loaded: ' 'boto3 version {0} or later must be installed.'.format(required_boto3_version)) + elif _LooseVersion(found_botocore_version) < _LooseVersion(required_botocore_version): + return (False, 'The boto_apigateway module could not be loaded: ' + 'botocore version {0} or later must be installed.'.format(required_botocore_version)) else: return True From 9093c8c004bf0dccffc0e24c70f0ac59225fd555 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Fri, 16 Sep 2016 15:56:58 -0700 Subject: [PATCH 12/19] fixed validation for throttle/quota. --- salt/modules/boto_apigateway.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index e01429bd3b..7ba990a1ba 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1437,7 +1437,7 @@ def _validate_throttle(throttle): ''' Helper to verify that throttling parameters are valid ''' - if throttle: + if throttle is not None: if not isinstance(throttle, dict): raise TypeError('throttle must be a dictionary, provided value: {0}'.format(throttle)) @@ -1446,7 +1446,7 @@ def _validate_quota(quota): ''' Helper to verify that quota parameters are valid ''' - if quota: + if quota is not None: if not isinstance(quota, dict): raise TypeError('quota must be a dictionary, provided value: {0}'.format(quota)) periods = ['DAY', 'WEEK', 'MONTH'] From 86d632264c37ecc83e0e927a1a43e7fdaf576f64 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Fri, 16 Sep 2016 16:03:37 -0700 Subject: [PATCH 13/19] return error on delete usage plan if we had problems describing the plan_id. --- salt/modules/boto_apigateway.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index 7ba990a1ba..5f2b1258ea 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1602,6 +1602,9 @@ def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): try: existing = describe_usage_plans(plan_id=plan_id, region=region, key=key, keyid=keyid, profile=profile) # don't attempt to delete the usage plan if it does not exist + if 'error' in existing: + return {'error': existing['error']} + if 'plans' in existing and existing['plans']: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) res = conn.delete_usage_plan(usagePlanId=plan_id) From e120f210218825363b38ac764d37f2fcb80835c1 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 19 Sep 2016 09:38:11 -0700 Subject: [PATCH 14/19] fixed bug in update_usage_plan --- salt/modules/boto_apigateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index 5f2b1258ea..2776e9e94a 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1584,6 +1584,8 @@ def update_usage_plan(plan_id, throttle=None, quota=None, region=None, key=None, except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} + except (TypeError, ValueError) as e: + return {'error': '{0}'.format(e)} def delete_usage_plan(plan_id, region=None, key=None, keyid=None, profile=None): From 37a0f17191686b4c11d95d1186270c224738eeb1 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 19 Sep 2016 09:43:36 -0700 Subject: [PATCH 15/19] unit tests for describe/create usage plan. --- tests/unit/modules/boto_apigateway_test.py | 162 ++++++++++++++++++++- 1 file changed, 158 insertions(+), 4 deletions(-) diff --git a/tests/unit/modules/boto_apigateway_test.py b/tests/unit/modules/boto_apigateway_test.py index 63e3679a47..d72505d6fa 100644 --- a/tests/unit/modules/boto_apigateway_test.py +++ b/tests/unit/modules/boto_apigateway_test.py @@ -24,6 +24,7 @@ from salt.modules import boto_apigateway # pylint: disable=import-error,no-name-in-module try: import boto3 + import botocore from botocore.exceptions import ClientError HAS_BOTO = True except ImportError: @@ -37,6 +38,7 @@ from salt.ext.six.moves import range, zip # which was added in boto 2.8.0 # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12 required_boto3_version = '1.2.1' +required_botocore_version = '1.4.49' region = 'us-east-1' access_key = 'GKTADJGHEIQSXMKKRBJ08H' @@ -91,6 +93,57 @@ api_create_resource_ret = { u'pathPart': u'api3', 'ResponseMetadata': {'HTTPStatusCode': 200, 'RequestId': '2d31072c-9d15-11e5-9977-6d9fcfda9c0a'}} +usage_plan1 = dict( + id='plan1_id', + name='plan1_name', + description='plan1_desc', + apiStages=[], + throttle=dict( + burstLimit=123, + rateLimit=123.0 + ), + quota=dict( + limit=123, + offset=123, + period='DAY' + ) +) +usage_plan2 = dict( + id='plan2_id', + name='plan2_name', + description='plan2_desc', + apiStages=[], + throttle=dict( + burstLimit=123, + rateLimit=123.0 + ), + quota=dict( + limit=123, + offset=123, + period='DAY' + ) +) +usage_plan1b = dict( + id='another_plan1_id', + name='plan1_name', + description='another_plan1_desc', + apiStages=[], + throttle=dict( + burstLimit=123, + rateLimit=123.0 + ), + quota=dict( + limit=123, + offset=123, + period='DAY' + ) +) +usage_plans_ret = dict( + items=[ + usage_plan1, usage_plan2, usage_plan1b + ] +) + log = logging.getLogger(__name__) opts = salt.config.DEFAULT_MINION_OPTS @@ -115,6 +168,18 @@ def _has_required_boto(): return True +def _has_required_botocore(): + ''' + Returns True/False boolean depending on if botocore supports usage plan + ''' + if not HAS_BOTO: + return False + elif LooseVersion(botocore.__version__) < LooseVersion(required_botocore_version): + return False + else: + return True + + class BotoApiGatewayTestCaseBase(TestCase): conn = None @@ -152,11 +217,14 @@ class BotoApiGatewayTestCaseMixin(object): return False -@skipIf(True, 'Skip these tests while investigating failures') +#@skipIf(True, 'Skip these tests while investigating failures') @skipIf(HAS_BOTO is False, 'The boto module must be installed.') -@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' - ' or equal to version {0}' - .format(required_boto3_version)) +@skipIf(_has_required_boto() is False, + 'The boto3 module must be greater than' + ' or equal to version {0}'.format(required_boto3_version)) +@skipIf(_has_required_botocore() is False, + 'The botocore module must be greater than' + ' or equal to version {0}'.format(required_botocore_version)) @skipIf(NO_MOCK, NO_MOCK_REASON) class BotoApiGatewayTestCase(BotoApiGatewayTestCaseBase, BotoApiGatewayTestCaseMixin): ''' @@ -1379,6 +1447,92 @@ class BotoApiGatewayTestCase(BotoApiGatewayTestCaseBase, BotoApiGatewayTestCaseM **conn_parameters) self.assertTrue(result.get('error')) + def test_that_when_describing_usage_plans_and_an_exception_is_thrown_in_get_usage_plans(self): + ''' + Tests True for existence of 'error' + ''' + self.conn.get_usage_plans.side_effect = ClientError(error_content, 'get_usage_plans_exception') + result = boto_apigateway.describe_usage_plans(name='some plan', **conn_parameters) + self.assertEqual(result.get('error').get('message'), error_message.format('get_usage_plans_exception')) + + def test_that_when_describing_usage_plans_and_plan_name_or_id_does_not_exist_that_results_have_empty_plans_list(self): + ''' + Tests for plans equaling empty list + ''' + self.conn.get_usage_plans.return_value = usage_plans_ret + + result = boto_apigateway.describe_usage_plans(name='does not exist', **conn_parameters) + self.assertEqual(result.get('plans'), []) + + result = boto_apigateway.describe_usage_plans(plan_id='does not exist', **conn_parameters) + self.assertEqual(result.get('plans'), []) + + result = boto_apigateway.describe_usage_plans(name='does not exist', plan_id='does not exist', **conn_parameters) + self.assertEqual(result.get('plans'), []) + + result = boto_apigateway.describe_usage_plans(name='plan1_name', plan_id='does not exist', **conn_parameters) + self.assertEqual(result.get('plans'), []) + + result = boto_apigateway.describe_usage_plans(name='does not exist', plan_id='plan1_id', **conn_parameters) + self.assertEqual(result.get('plans'), []) + + def test_that_when_describing_usage_plans_for_plans_that_exist_that_the_function_returns_all_matching_plans(self): + ''' + Tests for plans filtering properly if they exist + ''' + self.conn.get_usage_plans.return_value = usage_plans_ret + + result = boto_apigateway.describe_usage_plans(name=usage_plan1['name'], **conn_parameters) + self.assertEqual(len(result.get('plans')), 2) + for plan in result['plans']: + self.assertTrue(plan in [usage_plan1, usage_plan1b]) + + def test_that_when_creating_or_updating_a_usage_plan_and_throttle_or_quota_failed_to_validate_that_an_error_is_returned(self): + ''' + Tests for TypeError and ValueError in throttle and quota + ''' + for throttle, quota in (([], None), (None, []), ('abc', None), (None, 'def')): + res = boto_apigateway.create_usage_plan('plan1_name', description=None, throttle=throttle, quota=quota, **conn_parameters) + self.assertNotEqual(None, res.get('error')) + res = boto_apigateway.update_usage_plan('plan1_id', throttle=throttle, quota=quota, **conn_parameters) + self.assertNotEqual(None, res.get('error')) + + for quota in ({'limit': 123}, {'period': 123}, {'period': 'DAY'}): + res = boto_apigateway.create_usage_plan('plan1_name', description=None, throttle=None, quota=quota, **conn_parameters) + self.assertNotEqual(None, res.get('error')) + res = boto_apigateway.update_usage_plan('plan1_id', quota=quota, **conn_parameters) + self.assertNotEqual(None, res.get('error')) + + self.conn.get_usage_plans.assert_not_called() + self.conn.create_usage_plan.assert_not_called() + self.conn.update_usage_plan.assert_not_called() + + def test_that_when_creating_a_usage_plan_and_create_usage_plan_throws_an_exception_that_an_error_is_returned(self): + ''' + tests for ClientError + ''' + self.conn.create_usage_plan.side_effect = ClientError(error_content, 'create_usage_plan_exception') + result = boto_apigateway.create_usage_plan(name='some plan', **conn_parameters) + self.assertEqual(result.get('error').get('message'), error_message.format('create_usage_plan_exception')) + + def test_that_create_usage_plan_succeeds(self): + ''' + tests for success user plan creation + ''' + res = 'unit test create_usage_plan succeeded' + self.conn.create_usage_plan.return_value = res + result = boto_apigateway.create_usage_plan(name='some plan', **conn_parameters) + self.assertEqual(result.get('created'), True) + self.assertEqual(result.get('result'), res) + + def test_that_when_udpating_a_usage_plan_and_update_usage_plan_throws_an_exception_that_an_error_is_returned(self): + ''' + tests for ClientError + ''' + self.conn.update_usage_plan.side_effect = ClientError(error_content, 'update_usage_plan_exception') + result = boto_apigateway.update_usage_plan(plan_id='plan1_id', **conn_parameters) + self.assertEqual(result.get('error').get('message'), error_message.format('update_usage_plan_exception')) + if __name__ == '__main__': from integration import run_tests # pylint: disable=import-error run_tests(BotoApiGatewayTestCase, needs_daemon=False) From 802fe3a76c8043050e2a0e660be127b806171da0 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 19 Sep 2016 10:16:48 -0700 Subject: [PATCH 16/19] make sure we don't call update_usage_plan in _update_usage_plan_apis if patchOperations is an empty list. --- salt/modules/boto_apigateway.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index 2776e9e94a..064b45ba56 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1639,10 +1639,10 @@ def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None 'path': '/apiStages', 'value': '{0}:{1}'.format(api['apiId'], api['stage']) }) - - conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) - res = conn.update_usage_plan(usagePlanId=plan_id, - patchOperations=patchOperations) + if patchOperations: + conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) + res = conn.update_usage_plan(usagePlanId=plan_id, + patchOperations=patchOperations) return {'success': True, 'result': res} except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} From 962b0439a522cea78b8ef750e594b02fcdce179a Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 19 Sep 2016 10:54:38 -0700 Subject: [PATCH 17/19] rest of the unit tests for usage plans related functionality in boto_apigateway module. --- salt/modules/boto_apigateway.py | 3 + tests/unit/modules/boto_apigateway_test.py | 115 ++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/salt/modules/boto_apigateway.py b/salt/modules/boto_apigateway.py index 064b45ba56..99d03aae1f 100644 --- a/salt/modules/boto_apigateway.py +++ b/salt/modules/boto_apigateway.py @@ -1639,6 +1639,7 @@ def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None 'path': '/apiStages', 'value': '{0}:{1}'.format(api['apiId'], api['stage']) }) + res = None if patchOperations: conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) res = conn.update_usage_plan(usagePlanId=plan_id, @@ -1646,6 +1647,8 @@ def _update_usage_plan_apis(plan_id, apis, op, region=None, key=None, keyid=None return {'success': True, 'result': res} except ClientError as e: return {'error': salt.utils.boto3.get_error(e)} + except Exception as e: + return {'error': e} def attach_usage_plan_to_apis(plan_id, apis, region=None, key=None, keyid=None, profile=None): diff --git a/tests/unit/modules/boto_apigateway_test.py b/tests/unit/modules/boto_apigateway_test.py index d72505d6fa..ebcfad31c8 100644 --- a/tests/unit/modules/boto_apigateway_test.py +++ b/tests/unit/modules/boto_apigateway_test.py @@ -217,7 +217,7 @@ class BotoApiGatewayTestCaseMixin(object): return False -#@skipIf(True, 'Skip these tests while investigating failures') +@skipIf(True, 'Skip these tests while investigating failures') @skipIf(HAS_BOTO is False, 'The boto module must be installed.') @skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' @@ -1533,6 +1533,119 @@ class BotoApiGatewayTestCase(BotoApiGatewayTestCaseBase, BotoApiGatewayTestCaseM result = boto_apigateway.update_usage_plan(plan_id='plan1_id', **conn_parameters) self.assertEqual(result.get('error').get('message'), error_message.format('update_usage_plan_exception')) + def test_that_when_updating_a_usage_plan_and_if_throttle_and_quota_parameters_are_none_update_usage_plan_removes_throttle_and_quota(self): + ''' + tests for throttle and quota removal + ''' + ret = 'some success status' + self.conn.update_usage_plan.return_value = ret + result = boto_apigateway.update_usage_plan(plan_id='plan1_id', throttle=None, quota=None, **conn_parameters) + self.assertEqual(result.get('updated'), True) + self.assertEqual(result.get('result'), ret) + self.conn.update_usage_plan.assert_called_once() + + def test_that_when_deleting_usage_plan_and_describe_usage_plans_had_error_that_the_same_error_is_returned(self): + ''' + tests for error in describe_usage_plans returns error + ''' + ret = 'get_usage_plans_exception' + self.conn.get_usage_plans.side_effect = ClientError(error_content, ret) + result = boto_apigateway.delete_usage_plan(plan_id='some plan id', **conn_parameters) + self.assertEqual(result.get('error').get('message'), error_message.format(ret)) + self.conn.delete_usage_plan.assert_not_called() + + def test_that_when_deleting_usage_plan_and_plan_exists_that_the_functions_returns_deleted_true(self): + self.conn.get_usage_plans.return_value = usage_plans_ret + ret = 'delete_usage_plan_retval' + self.conn.delete_usage_plan.return_value = ret + result = boto_apigateway.delete_usage_plan(plan_id='plan1_id', **conn_parameters) + self.assertEqual(result.get('deleted'), True) + self.assertEqual(result.get('usagePlanId'), 'plan1_id') + self.conn.delete_usage_plan.assert_called_once() + + def test_that_when_deleting_usage_plan_and_plan_does_not_exist_that_the_functions_returns_deleted_true(self): + ''' + tests for ClientError + ''' + self.conn.get_usage_plans.return_value = dict( + items=[] + ) + ret = 'delete_usage_plan_retval' + self.conn.delete_usage_plan.return_value = ret + result = boto_apigateway.delete_usage_plan(plan_id='plan1_id', **conn_parameters) + self.assertEqual(result.get('deleted'), True) + self.assertEqual(result.get('usagePlanId'), 'plan1_id') + self.conn.delete_usage_plan.assert_not_called() + + def test_that_when_deleting_usage_plan_and_delete_usage_plan_throws_exception_that_an_error_is_returned(self): + ''' + tests for ClientError + ''' + self.conn.get_usage_plans.return_value = usage_plans_ret + error_msg = 'delete_usage_plan_exception' + self.conn.delete_usage_plan.side_effect = ClientError(error_content, error_msg) + result = boto_apigateway.delete_usage_plan(plan_id='plan1_id', **conn_parameters) + self.assertEqual(result.get('error').get('message'), error_message.format(error_msg)) + self.conn.delete_usage_plan.assert_called_once() + + def test_that_attach_or_detach_usage_plan_when_apis_is_empty_that_success_is_returned(self): + ''' + tests for border cases when apis is empty list + ''' + result = boto_apigateway.attach_usage_plan_to_apis(plan_id='plan1_id', apis=[], **conn_parameters) + self.assertEqual(result.get('success'), True) + self.assertEqual(result.get('result', 'no result?'), None) + self.conn.update_usage_plan.assert_not_called() + + result = boto_apigateway.detach_usage_plan_from_apis(plan_id='plan1_id', apis=[], **conn_parameters) + self.assertEqual(result.get('success'), True) + self.assertEqual(result.get('result', 'no result?'), None) + self.conn.update_usage_plan.assert_not_called() + + def test_that_attach_or_detach_usage_plan_when_api_does_not_contain_apiId_or_stage_that_an_error_is_returned(self): + ''' + tests for invalid key in api object + ''' + for api in ({'apiId': 'some Id'}, {'stage': 'some stage'}, {}): + result = boto_apigateway.attach_usage_plan_to_apis(plan_id='plan1_id', apis=[api], **conn_parameters) + self.assertNotEqual(result.get('error'), None) + + result = boto_apigateway.detach_usage_plan_from_apis(plan_id='plan1_id', apis=[api], **conn_parameters) + self.assertNotEqual(result.get('error'), None) + + self.conn.update_usage_plan.assert_not_called() + + def test_that_attach_or_detach_usage_plan_and_update_usage_plan_throws_exception_that_an_error_is_returned(self): + ''' + tests for ClientError + ''' + api = {'apiId': 'some_id', 'stage': 'some_stage'} + error_msg = 'update_usage_plan_exception' + self.conn.update_usage_plan.side_effect = ClientError(error_content, error_msg) + + result = boto_apigateway.attach_usage_plan_to_apis(plan_id='plan1_id', apis=[api], **conn_parameters) + self.assertEqual(result.get('error').get('message'), error_message.format(error_msg)) + + result = boto_apigateway.detach_usage_plan_from_apis(plan_id='plan1_id', apis=[api], **conn_parameters) + self.assertEqual(result.get('error').get('message'), error_message.format(error_msg)) + + def test_that_attach_or_detach_usage_plan_updated_successfully(self): + ''' + tests for update_usage_plan called + ''' + api = {'apiId': 'some_id', 'stage': 'some_stage'} + attach_ret = 'update_usage_plan_add_op_succeeded' + detach_ret = 'update_usage_plan_remove_op_succeeded' + self.conn.update_usage_plan.side_effect = [attach_ret, detach_ret] + + result = boto_apigateway.attach_usage_plan_to_apis(plan_id='plan1_id', apis=[api], **conn_parameters) + self.assertEqual(result.get('success'), True) + self.assertEqual(result.get('result'), attach_ret) + + result = boto_apigateway.detach_usage_plan_from_apis(plan_id='plan1_id', apis=[api], **conn_parameters) + self.assertEqual(result.get('success'), True) + self.assertEqual(result.get('result'), detach_ret) + if __name__ == '__main__': from integration import run_tests # pylint: disable=import-error run_tests(BotoApiGatewayTestCase, needs_daemon=False) From b61dc5abdab9210f86b689d55df46f4871651f2f Mon Sep 17 00:00:00 2001 From: kbelov Date: Mon, 19 Sep 2016 14:37:52 -0700 Subject: [PATCH 18/19] added unit tests for boto_apigateway module for usage plans and usage plan associations --- salt/states/boto_apigateway.py | 2 +- tests/unit/states/boto_apigateway_test.py | 612 ++++++++++++++++++++++ 2 files changed, 613 insertions(+), 1 deletion(-) diff --git a/salt/states/boto_apigateway.py b/salt/states/boto_apigateway.py index abf67b9bc7..8a7c4f2c63 100644 --- a/salt/states/boto_apigateway.py +++ b/salt/states/boto_apigateway.py @@ -1987,7 +1987,7 @@ def usage_plan_association_absent(name, plan_name, api_stages, region=None, key= stages_to_remove.append(api) if not stages_to_remove: - ret['comment'] = 'Usage plan is already not asssociated to any api stages' + ret['comment'] = 'Usage plan is already not asssociated to any api stages' return ret result = __salt__['boto_apigateway.detach_usage_plan_from_apis'](plan_id, stages_to_remove, **common_args) diff --git a/tests/unit/states/boto_apigateway_test.py b/tests/unit/states/boto_apigateway_test.py index 88808b5bbe..9d94ed4253 100644 --- a/tests/unit/states/boto_apigateway_test.py +++ b/tests/unit/states/boto_apigateway_test.py @@ -36,6 +36,13 @@ except ImportError: from salt.ext.six.moves import range +# Import Salt Libs +from salt.states import boto_apigateway + +boto_apigateway.__salt__ = {} +boto_apigateway.__opts__ = {} + + # pylint: enable=import-error,no-name-in-module # the boto_apigateway module relies on the connect_to_region() method @@ -298,6 +305,10 @@ method_ret = dict(apiKeyRequired=False, requestModels={'application/json': 'User'}, requestParameters={}) +throttle_rateLimit = 10.0 +association_stage_1 = {'apiId': 'apiId1', 'stage': 'stage1'} +association_stage_2 = {'apiId': 'apiId1', 'stage': 'stage2'} + log = logging.getLogger(__name__) opts = salt.config.DEFAULT_MINION_OPTS @@ -975,3 +986,604 @@ class BotoApiGatewayTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGatewayTest self.assertIs(result.get('result'), True) self.assertIsNot(result.get('abort'), True) + +@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' + ' or equal to version {0}' + .format(required_boto3_version)) +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGatewayTestCaseMixin): + ''' + TestCase for salt.modules.boto_apigateway state.module, usage_plans portion + ''' + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_present_if_describe_fails(self, *args): + ''' + Tests correct error processing for describe_usage_plan failure + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Failed to describe existing usage plans') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': []})}) + @patch.dict(boto_apigateway.__opts__, {'test': True}) + def test_usage_plan_present_if_there_is_no_such_plan_and_test_option_is_set(self, *args): + ''' + TestCse for salt.modules.boto_apigateway state.module, checking that if __opts__['test'] is set + and usage plan does not exist, correct diagnostic will be returned + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'a new usage plan plan_name would be created') + self.assertIn('result', result) + self.assertEqual(result['result'], None) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': []})}) + @patch.dict(boto_apigateway.__opts__, {'test': False}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.create_usage_plan': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_present_if_create_usage_plan_fails(self, *args): + ''' + Tests behavior for the case when creating a new usage plan fails + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Failed to create a usage plan plan_name, error') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{ + 'id': 'planid', + 'name': 'planname' + }]})}) + @patch.dict(boto_apigateway.__opts__, {'test': False}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.update_usage_plan': MagicMock()}) + def test_usage_plan_present_if_plan_is_there_and_needs_no_updates(self, *args): + ''' + Tests behavior for the case when plan is present and needs no updates + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'usage plan plan_name is already in a correct state') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + boto_apigateway.__salt__['boto_apigateway.update_usage_plan'].assert_not_called() + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{ + 'id': 'planid', + 'name': 'planname', + 'throttle': {'rateLimit': 10.0} + }]})}) + @patch.dict(boto_apigateway.__opts__, {'test': True}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.update_usage_plan': MagicMock()}) + def test_usage_plan_present_if_plan_is_there_and_needs_updates_but_test_is_set(self, *args): + ''' + Tests behavior when usage plan needs to be updated by tests option is set + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) + + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'a new usage plan plan_name would be updated') + self.assertIn('result', result) + self.assertEqual(result['result'], None) + boto_apigateway.__salt__['boto_apigateway.update_usage_plan'].assert_not_called() + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{ + 'id': 'planid', + 'name': 'planname', + 'throttle': {'rateLimit': 10.0} + }]})}) + @patch.dict(boto_apigateway.__opts__, {'test': False}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.update_usage_plan': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_present_if_plan_is_there_and_needs_updates_but_update_fails(self, *args): + ''' + Tests error processing for the case when updating an existing usage plan fails + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Failed to update a usage plan plan_name, error') + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=[{'plans': []}, {'plans': [{'id': 'id'}]}])}) + @patch.dict(boto_apigateway.__opts__, {'test': False}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.create_usage_plan': MagicMock(return_value={'created': True})}) + # @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.update_usage_plan': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_present_if_plan_has_been_created(self, *args): + ''' + Tests successful case for creating a new usage plan + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'A new usage plan plan_name has been created') + self.assertEqual(result['changes']['old'], {'plan': None}) + self.assertEqual(result['changes']['new'], {'plan': {'id': 'id'}}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=[{'plans': [{'id': 'id'}]}, + {'plans': [{'id': 'id', + 'throttle': {'rateLimit': throttle_rateLimit}}]}])}) + @patch.dict(boto_apigateway.__opts__, {'test': False}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.update_usage_plan': MagicMock(return_value={'updated': True})}) + def test_usage_plan_present_if_plan_has_been_updated(self, *args): + ''' + Tests successful case for updating a usage plan + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', throttle={'rateLimit': throttle_rateLimit}, **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'usage plan plan_name has been updated') + self.assertEqual(result['changes']['old'], {'plan': {'id': 'id'}}) + self.assertEqual(result['changes']['new'], {'plan': {'id': 'id', 'throttle': {'rateLimit': throttle_rateLimit}}}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=ValueError('error'))}) + def test_usage_plan_present_if_ValueError_is_raised(self, *args): + ''' + Tests error processing for the case when ValueError is raised when creating a usage plan + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', throttle={'rateLimit': throttle_rateLimit}, **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=IOError('error'))}) + def test_usage_plan_present_if_IOError_is_raised(self, *args): + ''' + Tests error processing for the case when IOError is raised when creating a usage plan + ''' + result = {} + + result = boto_apigateway.usage_plan_present('name', 'plan_name', throttle={'rateLimit': throttle_rateLimit}, **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_absent_if_describe_fails(self, *args): + ''' + Tests correct error processing for describe_usage_plan failure + ''' + result = {} + + result = boto_apigateway.usage_plan_absent('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Failed to describe existing usage plans') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': []})}) + def test_usage_plan_absent_if_plan_is_not_present(self, *args): + ''' + Tests behavior for the case when the plan that needs to be absent does not exist + ''' + result = {} + + result = boto_apigateway.usage_plan_absent('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan plan_name does not exist already') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__opts__, {'test': True}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id':'id'}]})}) + def test_usage_plan_absent_if_plan_is_present_but_test_option_is_set(self, *args): + ''' + Tests behavior for the case when usage plan needs to be deleted by tests option is set + ''' + result = {} + + result = boto_apigateway.usage_plan_absent('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], None) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan plan_name exists and would be deleted') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__opts__, {'test': False}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id':'id'}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.delete_usage_plan': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_absent_if_plan_is_present_but_delete_fails(self, *args): + ''' + Tests correct error processing when deleting a usage plan fails + ''' + result = {} + + result = boto_apigateway.usage_plan_absent('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Failed to delete usage plan plan_name, {\'error\': \'error\'}') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__opts__, {'test': False}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id':'id'}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.delete_usage_plan': MagicMock(return_value={'deleted': True})}) + def test_usage_plan_absent_if_plan_has_been_deleted(self, *args): + ''' + Tests successful case for deleting a usage plan + ''' + result = {} + + result = boto_apigateway.usage_plan_absent('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan plan_name has been deleted') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {'new': {'plan': None}, 'old': {'plan': {'id': 'id'}}}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=ValueError('error'))}) + def test_usage_plan_absent_if_ValueError_is_raised(self, *args): + ''' + Tests correct error processing for the case when ValueError is raised when deleting a usage plan + ''' + result = {} + + result = boto_apigateway.usage_plan_absent('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=IOError('error'))}) + def test_usage_plan_absent_if_IOError_is_raised(self, *args): + ''' + Tests correct error processing for the case when IOError is raised when deleting a usage plan + ''' + result = {} + + result = boto_apigateway.usage_plan_absent('name', 'plan_name', **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + +@skipIf(HAS_BOTO is False, 'The boto module must be installed.') +@skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' + ' or equal to version {0}' + .format(required_boto3_version)) +@skipIf(NO_MOCK, NO_MOCK_REASON) +class BotoApiGatewayUsagePlanAssociationTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGatewayTestCaseMixin): + ''' + TestCase for salt.modules.boto_apigateway state.module, usage_plans_association portion + ''' + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_association_present_if_describe_fails(self, *args): + ''' + Tests correct error processing for describe_usage_plan failure + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Failed to describe existing usage plans') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': []})}) + def test_usage_plan_association_present_if_plan_is_not_present(self, *args): + ''' + Tests correct error processing if a plan for which association has been requested is not present + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan plan_name does not exist') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1'}, + {'id': 'id2'}]})}) + def test_usage_plan_association_present_if_multiple_plans_with_the_same_name_exist(self, *args): + ''' + Tests correct error processing for the case when multiple plans with the same name exist + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'There are multiple usage plans with the same name - it is not supported') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', + 'apiStages': + [association_stage_1]}]})}) + def test_usage_plan_association_present_if_association_already_exists(self, *args): + ''' + Tests the behavior for the case when requested association is already present + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan is already asssociated to all api stages') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', + 'apiStages': + [association_stage_1]}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.attach_usage_plan_to_apis': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_association_present_if_update_fails(self, *args): + ''' + Tests correct error processing for the case when adding associations fails + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [association_stage_2], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertTrue(result['comment'].startswith('Failed to associate a usage plan')) + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', + 'apiStages': + [association_stage_1]}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.attach_usage_plan_to_apis': MagicMock(return_value={'result': {'apiStages': [association_stage_1, + association_stage_2]}})}) + def test_usage_plan_association_present_success(self, *args): + ''' + Tests successful case for adding usage plan associations to a given api stage + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [association_stage_2], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'successfully associated usage plan to apis') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {'new': [association_stage_1, association_stage_2], 'old': [association_stage_1]}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=ValueError('error'))}) + def test_usage_plan_association_present_if_value_error_is_thrown(self, *args): + ''' + Tests correct error processing for the case when IOError is raised while trying to set usage plan associations + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=IOError('error'))}) + def test_usage_plan_association_present_if_io_error_is_thrown(self, *args): + ''' + Tests correct error processing for the case when IOError is raised while trying to set usage plan associations + ''' + result = {} + + result = boto_apigateway.usage_plan_association_present('name', 'plan_name', [], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_association_absent_if_describe_fails(self, *args): + ''' + Tests correct error processing for describe_usage_plan failure + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Failed to describe existing usage plans') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': []})}) + def test_usage_plan_association_absent_if_plan_is_not_present(self, *args): + ''' + Tests error processing for the case when plan for which associations need to be modified is not present + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan plan_name does not exist') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1'}, + {'id': 'id2'}]})}) + def test_usage_plan_association_absent_if_multiple_plans_with_the_same_name_exist(self, *args): + ''' + Tests the case when there are multiple plans with the same name but different Ids + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'There are multiple usage plans with the same name - it is not supported') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', 'apiStages': []}]})}) + def test_usage_plan_association_absent_if_plan_has_no_associations(self, *args): + ''' + Tests the case when the plan has no associations at all + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan plan_name has no associated stages already') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', 'apiStages': [association_stage_1]}]})}) + def test_usage_plan_association_absent_if_plan_has_no_specific_association(self, *args): + ''' + Tests the case when requested association is not present already + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_2], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'Usage plan is already not asssociated to any api stages') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', 'apiStages': [association_stage_1, + association_stage_2]}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.detach_usage_plan_from_apis': MagicMock(return_value={'error': 'error'})}) + def test_usage_plan_association_absent_if_detaching_association_fails(self, *args): + ''' + Tests correct error processing when detaching the usage plan from the api function is called + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_2], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertTrue(result['comment'].startswith('Failed to disassociate a usage plan plan_name from the apis')) + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', 'apiStages': [association_stage_1, + association_stage_2]}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.detach_usage_plan_from_apis': MagicMock(return_value={'result': {'apiStages': [association_stage_1]}})}) + def test_usage_plan_association_absent_success(self, *args): + ''' + Tests successful case of disaccosiation the usage plan from api stages + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_2], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], True) + self.assertIn('comment', result) + self.assertEqual(result['comment'], 'successfully disassociated usage plan from apis') + self.assertIn('changes', result) + self.assertEqual(result['changes'], {'new': [association_stage_1], 'old': [association_stage_1, association_stage_2]}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=ValueError('error'))}) + def test_usage_plan_association_absent_if_ValueError_is_raised(self, *args): + ''' + Tests correct error processing for the case where ValueError is raised while trying to remove plan associations + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=IOError('error'))}) + def test_usage_plan_association_absent_if_IOError_is_raised(self, *args): + ''' + Tests correct error processing for the case where IOError exception is raised while trying to remove plan associations + ''' + result = {} + + result = boto_apigateway.usage_plan_association_absent('name', 'plan_name', [association_stage_1], **conn_parameters) + + self.assertIn('result', result) + self.assertEqual(result['result'], False) + self.assertIn('comment', result) + self.assertEqual(result['comment'], "('error',)") + self.assertIn('changes', result) + self.assertEqual(result['changes'], {}) + From df8648d5e3063bff6c8a7027636f9485463c7925 Mon Sep 17 00:00:00 2001 From: Winston Liu Date: Mon, 19 Sep 2016 14:52:41 -0700 Subject: [PATCH 19/19] lint clean up's. --- tests/unit/states/boto_apigateway_test.py | 43 +++++++++++++++++------ 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/tests/unit/states/boto_apigateway_test.py b/tests/unit/states/boto_apigateway_test.py index 9d94ed4253..2320c6f435 100644 --- a/tests/unit/states/boto_apigateway_test.py +++ b/tests/unit/states/boto_apigateway_test.py @@ -29,6 +29,7 @@ from unit.modules.boto_apigateway_test import BotoApiGatewayTestCaseMixin # Import 3rd-party libs try: import boto3 + import botocore from botocore.exceptions import ClientError HAS_BOTO = True except ImportError: @@ -49,6 +50,7 @@ boto_apigateway.__opts__ = {} # which was added in boto 2.8.0 # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12 required_boto3_version = '1.2.1' +required_botocore_version = '1.4.49' region = 'us-east-1' access_key = 'GKTADJGHEIQSXMKKRBJ08H' @@ -332,6 +334,18 @@ def _has_required_boto(): return True +def _has_required_botocore(): + ''' + Returns True/False boolean depending on if botocore supports usage plan + ''' + if not HAS_BOTO: + return False + elif LooseVersion(botocore.__version__) < LooseVersion(required_botocore_version): + return False + else: + return True + + class TempSwaggerFile(object): _tmp_swagger_dict = {'info': {'version': '0.0.0', 'description': 'salt boto apigateway unit test service', @@ -987,10 +1001,14 @@ class BotoApiGatewayTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGatewayTest self.assertIs(result.get('result'), True) self.assertIsNot(result.get('abort'), True) + @skipIf(HAS_BOTO is False, 'The boto module must be installed.') @skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ' or equal to version {0}' .format(required_boto3_version)) +@skipIf(_has_required_botocore() is False, + 'The botocore module must be greater than' + ' or equal to version {0}'.format(required_botocore_version)) @skipIf(NO_MOCK, NO_MOCK_REASON) class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGatewayTestCaseMixin): ''' @@ -1017,7 +1035,7 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa @patch.dict(boto_apigateway.__opts__, {'test': True}) def test_usage_plan_present_if_there_is_no_such_plan_and_test_option_is_set(self, *args): ''' - TestCse for salt.modules.boto_apigateway state.module, checking that if __opts__['test'] is set + TestCse for salt.modules.boto_apigateway state.module, checking that if __opts__['test'] is set and usage plan does not exist, correct diagnostic will be returned ''' result = {} @@ -1038,7 +1056,7 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa result = {} result = boto_apigateway.usage_plan_present('name', 'plan_name', **conn_parameters) - + self.assertIn('result', result) self.assertEqual(result['result'], False) self.assertIn('comment', result) @@ -1129,8 +1147,8 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa self.assertEqual(result['changes']['old'], {'plan': None}) self.assertEqual(result['changes']['new'], {'plan': {'id': 'id'}}) - @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=[{'plans': [{'id': 'id'}]}, - {'plans': [{'id': 'id', + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=[{'plans': [{'id': 'id'}]}, + {'plans': [{'id': 'id', 'throttle': {'rateLimit': throttle_rateLimit}}]}])}) @patch.dict(boto_apigateway.__opts__, {'test': False}) @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.update_usage_plan': MagicMock(return_value={'updated': True})}) @@ -1210,7 +1228,7 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa self.assertEqual(result['changes'], {}) @patch.dict(boto_apigateway.__opts__, {'test': True}) - @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id':'id'}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id'}]})}) def test_usage_plan_absent_if_plan_is_present_but_test_option_is_set(self, *args): ''' Tests behavior for the case when usage plan needs to be deleted by tests option is set @@ -1227,7 +1245,7 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa self.assertEqual(result['changes'], {}) @patch.dict(boto_apigateway.__opts__, {'test': False}) - @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id':'id'}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id'}]})}) @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.delete_usage_plan': MagicMock(return_value={'error': 'error'})}) def test_usage_plan_absent_if_plan_is_present_but_delete_fails(self, *args): ''' @@ -1245,7 +1263,7 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa self.assertEqual(result['changes'], {}) @patch.dict(boto_apigateway.__opts__, {'test': False}) - @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id':'id'}]})}) + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id'}]})}) @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.delete_usage_plan': MagicMock(return_value={'deleted': True})}) def test_usage_plan_absent_if_plan_has_been_deleted(self, *args): ''' @@ -1275,7 +1293,7 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa self.assertEqual(result['result'], False) self.assertIn('comment', result) self.assertEqual(result['comment'], "('error',)") - + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(side_effect=IOError('error'))}) def test_usage_plan_absent_if_IOError_is_raised(self, *args): ''' @@ -1290,10 +1308,14 @@ class BotoApiGatewayUsagePlanTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGa self.assertIn('comment', result) self.assertEqual(result['comment'], "('error',)") + @skipIf(HAS_BOTO is False, 'The boto module must be installed.') @skipIf(_has_required_boto() is False, 'The boto3 module must be greater than' ' or equal to version {0}' .format(required_boto3_version)) +@skipIf(_has_required_botocore() is False, + 'The botocore module must be greater than' + ' or equal to version {0}'.format(required_botocore_version)) @skipIf(NO_MOCK, NO_MOCK_REASON) class BotoApiGatewayUsagePlanAssociationTestCase(BotoApiGatewayStateTestCaseBase, BotoApiGatewayTestCaseMixin): ''' @@ -1332,7 +1354,7 @@ class BotoApiGatewayUsagePlanAssociationTestCase(BotoApiGatewayStateTestCaseBase self.assertIn('changes', result) self.assertEqual(result['changes'], {}) - @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1'}, + @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1'}, {'id': 'id2'}]})}) def test_usage_plan_association_present_if_multiple_plans_with_the_same_name_exist(self, *args): ''' @@ -1350,7 +1372,7 @@ class BotoApiGatewayUsagePlanAssociationTestCase(BotoApiGatewayStateTestCaseBase self.assertEqual(result['changes'], {}) @patch.dict(boto_apigateway.__salt__, {'boto_apigateway.describe_usage_plans': MagicMock(return_value={'plans': [{'id': 'id1', - 'apiStages': + 'apiStages': [association_stage_1]}]})}) def test_usage_plan_association_present_if_association_already_exists(self, *args): ''' @@ -1586,4 +1608,3 @@ class BotoApiGatewayUsagePlanAssociationTestCase(BotoApiGatewayStateTestCaseBase self.assertEqual(result['comment'], "('error',)") self.assertIn('changes', result) self.assertEqual(result['changes'], {}) -