Skip to content

Commit

Permalink
issue #432 - handle RIs with vCPU limits
Browse files Browse the repository at this point in the history
  • Loading branch information
jantman committed Oct 30, 2019
1 parent 1f2a871 commit eeaab1e
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 25 deletions.
44 changes: 31 additions & 13 deletions awslimitchecker/services/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,20 @@ def _find_usage_instances_nonvcpu(self):
)

def _find_usage_instances_vcpu(self):
return {}
res_usage = self._get_reserved_instance_count()
logger.debug('Reserved instance count: %s', res_usage)
usage = self._instance_usage_vcpu(res_usage)
limit_values = defaultdict(int)
for i_family, count in usage.items():
limname = self.instance_family_to_limit_name.get(
i_family, self.default_limit_name
)
limit_values[limname] += count
for limname, count in limit_values.items():
self.limits[limname]._add_current_usage(
count,
aws_type='AWS::EC2::Instance',
)

def _find_usage_spot_instances(self):
"""calculate spot instance request usage and update Limits"""
Expand Down Expand Up @@ -278,16 +291,18 @@ def _instance_usage(self):
"counting", inst.instance_type)
return az_to_inst

def _instance_usage_vcpu(self):
def _instance_usage_vcpu(self, ris):
"""
Find counts of currently-running EC2 Instance vCPUs
(On-Demand or Reserved) by placement (Availability
Zone) and instance family. Return as a nested dict
of AZ name to dict of instance family to count.
(On-Demand or Reserved) by instance family. Return as a dict of
instance family letter to count.
:param ris: nested dict of reserved instances, as returned by
:py:meth:`~._get_reserved_instance_count`
:type ris: dict
:rtype: dict
"""
az_to_inst = {}
inst_counts = defaultdict(int)
logger.debug("Getting usage for on-demand instances (vCPU limit)")
for inst in self.resource_conn.instances.all():
if inst.spot_instance_request_id:
Expand All @@ -298,17 +313,20 @@ def _instance_usage_vcpu(self):
logger.debug("Ignoring instance %s in state %s", inst.id,
inst.state['Name'])
continue
if inst.placement['AvailabilityZone'] not in az_to_inst:
az_to_inst[
inst.placement['AvailabilityZone']] = defaultdict(int)
az_to_inst[
inst.placement['AvailabilityZone']
][inst.instance_type[0]] += (
az = inst.placement['AvailabilityZone']
itype = inst.instance_type
if ris.get(az, {}).get(itype, 0) > 0:
logger.debug(
'Using RI for %s: %s in %s', inst.id, itype, az
)
ris[az][itype] -= 1
continue
inst_counts[inst.instance_type[0]] += (
inst.cpu_options['CoreCount'] * inst.cpu_options[
'ThreadsPerCore'
]
)
return az_to_inst
return inst_counts

@property
def _use_vcpu_limits(self):
Expand Down
29 changes: 28 additions & 1 deletion awslimitchecker/tests/services/result_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1947,7 +1947,7 @@ def test_instance_usage_vcpu(self):
type(mock_inst2C).cpu_options = {'CoreCount': 16, 'ThreadsPerCore': 2}

mock_instStopped = Mock(spec_set=Instance)
type(mock_instStopped).id = '2C'
type(mock_instStopped).id = 'instStopped'
type(mock_instStopped).instance_type = 'm4.8xlarge'
type(mock_instStopped).spot_instance_request_id = None
type(mock_instStopped).placement = {'AvailabilityZone': 'az1a'}
Expand Down Expand Up @@ -1996,6 +1996,22 @@ def test_instance_usage_vcpu(self):
type(mock_inst3A).state = {'Code': 16, 'Name': 'running'}
type(mock_inst3A).cpu_options = {'CoreCount': 32, 'ThreadsPerCore': 2}

mock_inst3F = Mock(spec_set=Instance)
type(mock_inst3F).id = '3F'
type(mock_inst3F).instance_type = 'p2.8xlarge'
type(mock_inst3F).spot_instance_request_id = None
type(mock_inst3F).placement = {'AvailabilityZone': 'az1c'}
type(mock_inst3F).state = {'Code': 16, 'Name': 'running'}
type(mock_inst3F).cpu_options = {'CoreCount': 16, 'ThreadsPerCore': 2}

mock_inst3G = Mock(spec_set=Instance)
type(mock_inst3G).id = '3G'
type(mock_inst3G).instance_type = 'p2.8xlarge'
type(mock_inst3G).spot_instance_request_id = None
type(mock_inst3G).placement = {'AvailabilityZone': 'az1c'}
type(mock_inst3G).state = {'Code': 16, 'Name': 'running'}
type(mock_inst3G).cpu_options = {'CoreCount': 16, 'ThreadsPerCore': 2}

mock_inst3B = Mock(spec_set=Instance)
type(mock_inst3B).id = '3B'
type(mock_inst3B).instance_type = 'r3.2xlarge'
Expand All @@ -2020,6 +2036,14 @@ def test_instance_usage_vcpu(self):
type(mock_inst3D).state = {'Code': 16, 'Name': 'running'}
type(mock_inst3D).cpu_options = {'CoreCount': 32, 'ThreadsPerCore': 4}

mock_inst3E = Mock(spec_set=Instance)
type(mock_inst3E).id = '3E'
type(mock_inst3E).instance_type = 'x1e.32xlarge'
type(mock_inst3E).spot_instance_request_id = None
type(mock_inst3E).placement = {'AvailabilityZone': 'az1c'}
type(mock_inst3E).state = {'Code': 16, 'Name': 'running'}
type(mock_inst3E).cpu_options = {'CoreCount': 32, 'ThreadsPerCore': 4}

return_value = [
mock_inst1A,
mock_inst1B,
Expand All @@ -2035,6 +2059,9 @@ def test_instance_usage_vcpu(self):
mock_inst3B,
mock_inst3C,
mock_inst3D,
mock_inst3E,
mock_inst3F,
mock_inst3G,
]
return return_value

Expand Down
108 changes: 97 additions & 11 deletions awslimitchecker/tests/services/test_ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,28 +445,55 @@ def test_key_error(self):

class TestInstanceUsageVcpu(object):

def test_simple(self):
def test_no_RIs(self):
cls = _Ec2Service(21, 43)
mock_conn = Mock()
retval = fixtures.test_instance_usage_vcpu
mock_conn.instances.all.return_value = retval
cls.resource_conn = mock_conn

res = cls._instance_usage_vcpu()
res = cls._instance_usage_vcpu({})
assert res == {
'c': 16,
'f': 72,
'g': 48,
'm': 32,
'r': 16,
't': 2,
'p': 128,
'x': 256,
}
assert mock_conn.mock_calls == [
call.instances.all()
]

def test_with_RIs(self):
cls = _Ec2Service(21, 43)
mock_conn = Mock()
retval = fixtures.test_instance_usage_vcpu
mock_conn.instances.all.return_value = retval
cls.resource_conn = mock_conn

res = cls._instance_usage_vcpu({
'az1a': {
'c': 16,
'f': 72,
'g': 48,
'm': 32,
'r': 8,
't': 2,
'f1.2xlarge': 10,
'c4.4xlarge': 8,
'c4.2xlarge': 16,
},
'az1c': {
'r': 8,
'p': 64,
'x': 128,
'x1e.32xlarge': 1,
'p2.8xlarge': 1,
'p2.16xlarge': 1
}
})
assert res == {
'f': 64,
'g': 48,
'm': 32,
'r': 16,
't': 2,
'p': 32,
'x': 128,
}
assert mock_conn.mock_calls == [
call.instances.all()
Expand Down Expand Up @@ -589,6 +616,65 @@ def test_simple(self):
assert mock_conn.mock_calls == []


class TestFindUsageInstancesVcpu(object):

def test_happy_path(self):
usage = {
'c': 16,
'f': 72,
'g': 48,
'm': 32,
'r': 16,
't': 2,
'p': 128,
'x': 256,
'a': 512,
'k': 3
}

mock_f = Mock(spec_set=AwsLimit)
mock_g = Mock(spec_set=AwsLimit)
mock_p = Mock(spec_set=AwsLimit)
mock_x = Mock(spec_set=AwsLimit)
mock_std = Mock(spec_set=AwsLimit)
limits = {
'Running On-Demand All F instances': mock_f,
'Running On-Demand All G instances': mock_g,
'Running On-Demand All P instances': mock_p,
'Running On-Demand All X instances': mock_x,
'Running On-Demand All Standard '
'(A, C, D, H, I, M, R, T, Z) instances': mock_std
}

cls = _Ec2Service(21, 43)
cls.limits = limits

with patch(
'%s._get_reserved_instance_count' % pb, autospec=True
) as m_gric:
with patch('%s._instance_usage_vcpu' % pb, autospec=True) as m_iuv:
m_gric.return_value = {'res': 'inst'}
m_iuv.return_value = usage
cls._find_usage_instances_vcpu()
assert m_gric.mock_calls == [call(cls)]
assert m_iuv.mock_calls == [call(cls, {'res': 'inst'})]
assert mock_f.mock_calls == [
call._add_current_usage(72, aws_type='AWS::EC2::Instance')
]
assert mock_g.mock_calls == [
call._add_current_usage(48, aws_type='AWS::EC2::Instance')
]
assert mock_p.mock_calls == [
call._add_current_usage(128, aws_type='AWS::EC2::Instance')
]
assert mock_x.mock_calls == [
call._add_current_usage(256, aws_type='AWS::EC2::Instance')
]
assert mock_std.mock_calls == [
call._add_current_usage(581, aws_type='AWS::EC2::Instance')
]


class TestRequiredIamPermissions(object):

def test_simple(self):
Expand Down

0 comments on commit eeaab1e

Please sign in to comment.