예제 #1
0
 def setUp(self):
     yml = {
         'cpu': '>=60.5',
         'check_every_seconds': 60,
         'periods': 5
     }
     self.alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml, scaling_policy_arn='my_arn')
예제 #2
0
 def test_ComparisonOperatorLessThan(self):
     yml = {
         'cpu': '<60.5',
         'check_every_seconds': 60,
         'periods': 5
     }
     alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml, scaling_policy_arn='my_arn')
     self.assertEqual(alarm._render_create()['ComparisonOperator'], 'LessThanThreshold')
예제 #3
0
class TestECSServiceCPUAlarm_load_yaml(unittest.TestCase):

    def setUp(self):
        yml = {
            'cpu': '>=60.5',
            'check_every_seconds': 60,
            'periods': 5
        }
        self.alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml, scaling_policy_arn='my_arn')

    def test_exists(self):
        self.assertEqual(self.alarm.exists(), False)

    def test_arn(self):
        self.assertEqual(self.alarm.arn, None)

    def test_name(self):
        self.assertEqual(self.alarm.name, 'my_cluster-my_service-high')

    def test_cpu(self):
        self.assertEqual(self.alarm.cpu, ">=60.5")

    def test_check_every_seconds(self):
        self.assertEqual(self.alarm.check_every_seconds, 60)

    def test_check_every_periods(self):
        self.assertEqual(self.alarm.periods, 5)
예제 #4
0
    def __init__(self, serviceName, clusterName, yml=None, aws=None):
        """
        ``yml`` is dict parsed from one of the scaling policy subsection of the
        ``application-scaling`` section from ``deployfish.yml``.  Example:

            {
                'cpu': '>=60',
                'check_every_seconds': 60,
                'periods': 5,
                'cooldown': 60,
                'scale_by': 1
            }

        ``aws`` is an entry from the ``ScalingPolicies`` list in the response from
        ``boto3.client('application-autoscaling').describe_scaling_policies()`

        :param serviceName: the name of an ECS service in cluster ``clusterName``
        :type serviceName: string

        :param clusterName: the name of an ECS cluster
        :type clusterName: string

        :param yml: scaling policy from ``deployfish.yml`` as described above
        :type yml: dict

        :param aws: scaling policy AWS dict as described above
        :type aws: dict
        """
        if not yml:
            yml = {}
        if not aws:
            aws = {}
        self.scaling = get_boto3_session().client('application-autoscaling')
        self.serviceName = serviceName
        self.clusterName = clusterName
        self.alarm = None
        self.__defaults()
        self.from_yaml(yml)
        self.from_aws(aws)
        self.alarm = None
        if yml:
            self.alarm = ECSServiceCPUAlarm(self.serviceName,
                                            self.clusterName,
                                            scaling_policy_arn=self.arn,
                                            yml=yml)
예제 #5
0
 def setUp(self):
     yml = {
         'cpu': '>=60',
         'check_every_seconds': 60,
         'periods': 5
     }
     aws_data = {
         'MetricAlarms': [
             {
                 'AlarmName': 'my_cluster-my_service-high',
                 'AlarmArn': 'actual_aws_alarm_arn',
                 'AlarmDescription': 'Scale up ECS service my_service in cluster my_cluster if Service Average CPU is >=60 for 300 seconds',
                 'AlarmConfigurationUpdatedTimestamp': datetime(2015, 1, 1),
                 'ActionsEnabled': True,
                 'OKActions': [],
                 'AlarmActions': [
                     'arn:aws:something:something',
                 ],
                 'InsufficientDataActions': [],
                 'StateValue': 'OK',
                 'StateReason': 'string',
                 'StateReasonData': 'string',
                 'StateUpdatedTimestamp': datetime(2015, 1, 1),
                 'MetricName': 'CPUUtilization',
                 'Namespace': 'AWS/ECS',
                 'Statistic': 'Average',
                 'Dimensions': [
                     {
                         'Name': 'ClusterName',
                         'Value': 'my_cluster'
                     },
                     {
                         'Name': 'ServiceName',
                         'Value': 'my_service'
                     },
                 ],
                 'Period': 60,
                 'Unit': 'Seconds',
                 'EvaluationPeriods': 5,
                 'Threshold': 60.0,
                 'ComparisonOperator': 'GreaterThanOrEqualToThreshold'
             }
         ]
     }
     cloudwatch_client = Mock()
     cloudwatch_client.describe_alarms = Mock()
     cloudwatch_client.describe_alarms.return_value = aws_data
     cloudwatch_client.list_metrics = Mock()
     cloudwatch_client.list_metrics.return_value = {'Metrics': []}
     client = Mock()
     client.return_value = cloudwatch_client
     with Replacer() as r:
         r('boto3.client', client)
         self.alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml)
예제 #6
0
class ScalingPolicy(object):
    """
    A class which allows us to manage Application AutoScaling ScalingPolicies.
    """
    def __init__(self, serviceName, clusterName, yml={}, aws={}):
        """
        ``yml`` is dict parsed from one of the scaling policy subsection of the
        ``application-scaling`` section from ``deployfish.yml``.  Example:

            {
                'cpu': '>=60',
                'check_every_seconds': 60,
                'periods': 5,
                'cooldown': 60,
                'scale_by': 1
            }

        ``aws`` is an entry from the ``ScalingPolicies`` list in the response from
        ``boto3.client('application-autoscaling').describe_scaling_policies()`

        :param serviceName: the name of an ECS service in cluster ``clusterName``
        :type service: string

        :param clusterName: the name of an ECS cluster
        :type cluster: string

        :param yml: scaling policy from ``deployfish.yml`` as described above
        :type yml: dict

        :param aws: scaling policy AWS dict as described above
        :type aws: dict
        """
        self.scaling = boto3.client('application-autoscaling')
        self.serviceName = serviceName
        self.clusterName = clusterName
        self.alarm = None
        self.__defaults()
        self.from_yaml(yml)
        self.from_aws(aws)
        self.alarm = None
        if yml:
            self.alarm = ECSServiceCPUAlarm(self.serviceName,
                                            self.clusterName,
                                            scaling_policy_arn=self.arn,
                                            yml=yml)

    def __defaults(self):
        self._name = None
        self._cooldown = 0
        self._scale_by = 0
        self._MetricIntervalLowerBound = None
        self._MetricIntervalUpperBound = None
        self._cpu = ''

    def __getattr__(self, attr):
        try:
            return self.__getattribute__(attr)
        except AttributeError:
            if attr in [
                    'MetricIntervalLowerBound', 'MetricIntervalUpperBound'
            ]:
                if not getattr(self, "_" + attr):
                    if self.cpu:
                        if (('>' in self.cpu
                             and attr == 'MetricIntervalLowerBound')
                                or ('<' in self.cpu
                                    and attr == 'MetricIntervalUpperBound')):
                            setattr(self, "_" + attr, 0)
                    elif self.__aws_scaling_policy:
                        adj = self.__aws_scaling_policy[
                            'StepScalingPolicyConfiguration'][
                                'StepAdjustments'][0]
                        if attr in adj:
                            setattr(self, "_" + attr, adj[attr])
                return getattr(self, "_" + attr)
            else:
                raise AttributeError

    @property
    def cpu(self):
        """
        We're keeping track of cpu here in the scaling policy so we can set the
        StepAdustment ``MetricIntervalLowerBound`` and
        ``MetricIntervalUpperBound`` parameters appropriately.
        """
        if not self._cpu:
            if self.alarm:
                self._cpu = self.alarm.cpu
        return self._cpu

    @cpu.setter
    def cpu(self, value):
        self._cpu = value

    @property
    def arn(self):
        """
        The ARN for the policy.  We'll only have this if the policy exists in AWS.
        """
        if self.exists():
            return self.__aws_scaling_policy['PolicyARN']
        return None

    @property
    def name(self):
        """
        The name the scaling policy should have in AWS.
        """
        if self.exists():
            self.name = self.__aws_scaling_policy['PolicyName']
        else:
            if self._scale_by < 0:
                direction = 'scale-down'
            else:
                direction = 'scale-up'
            self.name = '{}-{}-{}'.format(self.clusterName, self.serviceName,
                                          direction)
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    @property
    def scale_by(self):
        """
        The number of tasks to scale by when this policy is activated.  If
        positive, scale up; if negative, scale down.
        """
        # we always want to prefer what was set via yaml here.  yaml loads
        # come before aws loads, so _scale_by should be set already by the
        # time we get here
        if not self._scale_by and self.exists():
            self._scale_by = self.__aws_scaling_policy[
                'StepScalingPolicyConfiguration']['StepAdjustments'][0][
                    'ScalingAdjustment']
        return self._scale_by

    @scale_by.setter
    def scale_by(self, scale_by):
        self._scale_by = scale_by

    @property
    def cooldown(self):
        """
        The amount of time, in seconds, after a scaling activity completes where
        previous trigger-related scaling activities can influence future scaling
        events.  Look at the documentation for PutScalingPolicy.  The actual cooldown
        meaning is more complicated than this.
        """
        if not self._cooldown and self.exists():
            self._cooldown = self.__aws_scaling_policy[
                'StepScalingPolicyConfiguration']['Cooldown']
        return self._cooldown

    @cooldown.setter
    def cooldown(self, cooldown):
        self._cooldown = cooldown

    def from_aws(self, aws={}):
        self.__aws_scaling_policy = {}
        if aws:
            self.__aws_scaling_policy = aws
        else:
            response = self.scaling.describe_scaling_policies(
                PolicyNames=[self.name],
                ServiceNamespace='ecs',
                ResourceId='service/{}/{}'.format(self.clusterName,
                                                  self.serviceName),
                ScalableDimension='ecs:service:DesiredCount')
            if response['ScalingPolicies']:
                self.__aws_scaling_policy = response['ScalingPolicies'][0]

    def from_yaml(self, yml):
        """
        Load our configuration from the config read from ``deployfish.yml``.

        :param yml: a scaling policy level entry from the ``deployfish.yml`` file
        :type yml: dict
        """
        if yml:
            self.cooldown = yml['cooldown']
            self.scale_by = yml['scale_by']
            self.cpu = yml['cpu']

    def exists(self):
        """
        Return ``True`` if application autoscaling has been set up for the
        service named ``self.serviceName`` in a cluster named
        ``self.clusterName`` exists, and data related to that is loaded into
        this object.

        :rtype: boolean
        """
        if self.__aws_scaling_policy:
            return True
        return False

    def _render_create(self):
        """
        Return the argument list that we'll pass to ``put_scaling_policy()``.

        :rtype: dict
        """
        r = {}
        r['PolicyName'] = self.name
        r['ServiceNamespace'] = 'ecs'
        r['ResourceId'] = 'service/{}/{}'.format(self.clusterName,
                                                 self.serviceName)
        r['ScalableDimension'] = 'ecs:service:DesiredCount'
        r['PolicyType'] = 'StepScaling'
        r['StepScalingPolicyConfiguration'] = {}
        r['StepScalingPolicyConfiguration'][
            'AdjustmentType'] = 'ChangeInCapacity'
        r['StepScalingPolicyConfiguration']['StepAdjustments'] = []
        adjustment = {}
        adjustment['ScalingAdjustment'] = self.scale_by
        if self.MetricIntervalLowerBound is not None:
            adjustment[
                'MetricIntervalLowerBound'] = self.MetricIntervalLowerBound
        if self.MetricIntervalUpperBound is not None:
            adjustment[
                'MetricIntervalUpperBound'] = self.MetricIntervalUpperBound
        r['StepScalingPolicyConfiguration']['StepAdjustments'].append(
            adjustment)
        r['StepScalingPolicyConfiguration']['Cooldown'] = self.cooldown
        r['StepScalingPolicyConfiguration'][
            'MetricAggregationType'] = 'Average'
        return r

    def _render_delete(self):
        """
        Return the argument list that we'll pass to ``delete_scaling_policy()``.

        :rtype: dict
        """
        r = {}
        r['PolicyName'] = self.name
        r['ServiceNamespace'] = 'ecs'
        r['ResourceId'] = 'service/{}/{}'.format(self.clusterName,
                                                 self.serviceName)
        r['ScalableDimension'] = 'ecs:service:DesiredCount'
        return r

    def __eq__(self, other):
        if (self.MetricIntervalLowerBound == other.MetricIntervalLowerBound and
                self.MetricIntervalUpperBound == other.MetricIntervalUpperBound
                and self.cooldown == other.cooldown
                and self.scale_by == other.scale_by
                and self.clusterName == other.clusterName
                and self.serviceName == other.serviceName):  # NOQA
            return True
        return False

    def __ne__(self, other):
        return not self == other

    def create(self):
        """
        Create the scaling policy and its associated CloudWhach alarm.
        """
        self.scaling.put_scaling_policy(**self._render_create())
        self.from_aws()
        self.alarm.scaling_policy_arn = self.arn
        self.alarm.create()

    def delete(self):
        """
        Delete the scaling policy and its associated CloudWhach alarm.
        """
        if self.exists():
            try:
                self.scaling.delete_scaling_policy(**self._render_delete())
            except botocore.exceptions.ClientError:
                pass
            self.alarm.delete()
        self.__aws_scaling_policy = {}

    def needs_update(self):
        """
        If our desired scaling policy or associated CloudWatch alarm is
        different than what actually exists in AWS, return ``True``, else return
        ``False``.

        :rtype: boolean
        """
        if self == ScalingPolicy(self.serviceName,
                                 self.clusterName,
                                 aws=self.__aws_scaling_policy):
            if self.alarm.needs_update():
                return True
            return False
        return True

    def update(self):
        """
        If our desired scaling policy or associated CloudWatch alarm is
        different than what actually exists in AWS, delete them and recreate
        them with the config we want.
        """
        if self != ScalingPolicy(self.serviceName,
                                 self.clusterName,
                                 aws=self.__aws_scaling_policy):
            # The scaling policy itself needs updating
            self.delete()
            self.create()
        else:
            # The scaling policy doesn't need updating, but maybe the alarm does
            self.alarm.update()
예제 #7
0
class TestECSServiceCPUAlarm__render_create(unittest.TestCase):

    def setUp(self):
        yml = {
            'cpu': '>=60.5',
            'check_every_seconds': 60,
            'periods': 5
        }
        self.alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml, scaling_policy_arn='my_arn')

    def test_AlarmName(self):
        self.assertEqual(self.alarm._render_create()['AlarmName'], 'my_cluster-my_service-high')

    def test_AlarmActions(self):
        compare(self.alarm._render_create()['AlarmActions'], ['my_arn'])

    def test_AlarmDescription(self):
        self.assertEqual(self.alarm._render_create()['AlarmDescription'], 'Scale up ECS service my_service in cluster my_cluster if service Average CPU is >=60.5 for 300 seconds')

    def test_MetricName(self):
        self.assertEqual(self.alarm._render_create()['MetricName'], 'CPUUtilization')

    def test_Namespace(self):
        self.assertEqual(self.alarm._render_create()['Namespace'], 'AWS/ECS')

    def test_Statistic(self):
        self.assertEqual(self.alarm._render_create()['Statistic'], 'Average')

    def test_Dimensions(self):
        compare(self.alarm._render_create()['Dimensions'], [{'Name': 'ClusterName', 'Value': 'my_cluster'}, {'Name': 'ServiceName', 'Value': 'my_service'}])

    def test_Period(self):
        self.assertEqual(self.alarm._render_create()['Period'], 60)

    def test_Unit(self):
        self.assertEqual(self.alarm._render_create()['Unit'], "Seconds")

    def test_EvaluationPeriods(self):
        self.assertEqual(self.alarm._render_create()['EvaluationPeriods'], 5)

    def test_ComparisonOperator(self):
        self.assertEqual(self.alarm._render_create()['ComparisonOperator'], 'GreaterThanOrEqualToThreshold')

    def test_ComparisonOperatorGreaterThan(self):
        yml = {
            'cpu': '>60.5',
            'check_every_seconds': 60,
            'periods': 5
        }
        alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml, scaling_policy_arn='my_arn')
        self.assertEqual(alarm._render_create()['ComparisonOperator'], 'GreaterThanThreshold')

    def test_ComparisonOperatorLessThanOrEqualTo(self):
        yml = {
            'cpu': '<=60.5',
            'check_every_seconds': 60,
            'periods': 5
        }
        alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml, scaling_policy_arn='my_arn')
        self.assertEqual(alarm._render_create()['ComparisonOperator'], 'LessThanOrEqualToThreshold')

    def test_ComparisonOperatorLessThan(self):
        yml = {
            'cpu': '<60.5',
            'check_every_seconds': 60,
            'periods': 5
        }
        alarm = ECSServiceCPUAlarm('my_service', 'my_cluster', yml, scaling_policy_arn='my_arn')
        self.assertEqual(alarm._render_create()['ComparisonOperator'], 'LessThanThreshold')

    def test_Threshold(self):
        self.assertEqual(self.alarm._render_create()['Threshold'], 60.5)