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_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')
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)
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)
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)
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()
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)