Merge "Implement 'InstanceId' for autoscaling group"
This commit is contained in:
@@ -23,9 +23,12 @@ from heat.common.i18n import _LE
|
|||||||
from heat.common.i18n import _LI
|
from heat.common.i18n import _LI
|
||||||
from heat.engine import attributes
|
from heat.engine import attributes
|
||||||
from heat.engine import constraints
|
from heat.engine import constraints
|
||||||
|
from heat.engine import function
|
||||||
from heat.engine.notification import autoscaling as notification
|
from heat.engine.notification import autoscaling as notification
|
||||||
from heat.engine import properties
|
from heat.engine import properties
|
||||||
|
from heat.engine import resource
|
||||||
from heat.engine.resources import instance_group as instgrp
|
from heat.engine.resources import instance_group as instgrp
|
||||||
|
from heat.engine import rsrc_defn
|
||||||
from heat.engine import support
|
from heat.engine import support
|
||||||
from heat.openstack.common import log as logging
|
from heat.openstack.common import log as logging
|
||||||
from heat.scaling import cooldown
|
from heat.scaling import cooldown
|
||||||
@@ -78,10 +81,12 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin):
|
|||||||
AVAILABILITY_ZONES, LAUNCH_CONFIGURATION_NAME, MAX_SIZE, MIN_SIZE,
|
AVAILABILITY_ZONES, LAUNCH_CONFIGURATION_NAME, MAX_SIZE, MIN_SIZE,
|
||||||
COOLDOWN, DESIRED_CAPACITY, HEALTH_CHECK_GRACE_PERIOD,
|
COOLDOWN, DESIRED_CAPACITY, HEALTH_CHECK_GRACE_PERIOD,
|
||||||
HEALTH_CHECK_TYPE, LOAD_BALANCER_NAMES, VPCZONE_IDENTIFIER, TAGS,
|
HEALTH_CHECK_TYPE, LOAD_BALANCER_NAMES, VPCZONE_IDENTIFIER, TAGS,
|
||||||
|
INSTANCE_ID,
|
||||||
) = (
|
) = (
|
||||||
'AvailabilityZones', 'LaunchConfigurationName', 'MaxSize', 'MinSize',
|
'AvailabilityZones', 'LaunchConfigurationName', 'MaxSize', 'MinSize',
|
||||||
'Cooldown', 'DesiredCapacity', 'HealthCheckGracePeriod',
|
'Cooldown', 'DesiredCapacity', 'HealthCheckGracePeriod',
|
||||||
'HealthCheckType', 'LoadBalancerNames', 'VPCZoneIdentifier', 'Tags',
|
'HealthCheckType', 'LoadBalancerNames', 'VPCZoneIdentifier', 'Tags',
|
||||||
|
'InstanceId',
|
||||||
)
|
)
|
||||||
|
|
||||||
_TAG_KEYS = (
|
_TAG_KEYS = (
|
||||||
@@ -117,9 +122,18 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin):
|
|||||||
LAUNCH_CONFIGURATION_NAME: properties.Schema(
|
LAUNCH_CONFIGURATION_NAME: properties.Schema(
|
||||||
properties.Schema.STRING,
|
properties.Schema.STRING,
|
||||||
_('The reference to a LaunchConfiguration resource.'),
|
_('The reference to a LaunchConfiguration resource.'),
|
||||||
required=True,
|
|
||||||
update_allowed=True
|
update_allowed=True
|
||||||
),
|
),
|
||||||
|
INSTANCE_ID: properties.Schema(
|
||||||
|
properties.Schema.STRING,
|
||||||
|
_('The ID of an existing instance to use to '
|
||||||
|
'create the Auto Scaling group. If specify this property, '
|
||||||
|
'will create the group use an existing instance instead of '
|
||||||
|
'a launch configuration.'),
|
||||||
|
constraints=[
|
||||||
|
constraints.CustomConstraint("nova.server")
|
||||||
|
]
|
||||||
|
),
|
||||||
MAX_SIZE: properties.Schema(
|
MAX_SIZE: properties.Schema(
|
||||||
properties.Schema.INTEGER,
|
properties.Schema.INTEGER,
|
||||||
_('Maximum number of instances in the group.'),
|
_('Maximum number of instances in the group.'),
|
||||||
@@ -215,9 +229,32 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin):
|
|||||||
self.validate_launchconfig()
|
self.validate_launchconfig()
|
||||||
return self.create_with_template(self.child_template())
|
return self.create_with_template(self.child_template())
|
||||||
|
|
||||||
|
def _make_launch_config_resource(self, name, props):
|
||||||
|
lc_res_type = 'AWS::AutoScaling::LaunchConfiguration'
|
||||||
|
lc_res_def = rsrc_defn.ResourceDefinition(name,
|
||||||
|
lc_res_type,
|
||||||
|
props)
|
||||||
|
lc_res = resource.Resource(name, lc_res_def, self.stack)
|
||||||
|
return lc_res
|
||||||
|
|
||||||
def _get_conf_properties(self):
|
def _get_conf_properties(self):
|
||||||
conf, props = super(AutoScalingGroup, self)._get_conf_properties()
|
instance_id = self.properties.get(self.INSTANCE_ID)
|
||||||
vpc_zone_ids = self.properties.get(AutoScalingGroup.VPCZONE_IDENTIFIER)
|
if instance_id:
|
||||||
|
server = self.client_plugin('nova').get_server(instance_id)
|
||||||
|
instance_props = {
|
||||||
|
'ImageId': server.image['id'],
|
||||||
|
'InstanceType': server.flavor['id'],
|
||||||
|
'KeyName': server.key_name,
|
||||||
|
'SecurityGroups': [sg['name']
|
||||||
|
for sg in server.security_groups]
|
||||||
|
}
|
||||||
|
conf = self._make_launch_config_resource(self.name,
|
||||||
|
instance_props)
|
||||||
|
props = function.resolve(conf.properties.data)
|
||||||
|
else:
|
||||||
|
conf, props = super(AutoScalingGroup, self)._get_conf_properties()
|
||||||
|
|
||||||
|
vpc_zone_ids = self.properties.get(self.VPCZONE_IDENTIFIER)
|
||||||
if vpc_zone_ids:
|
if vpc_zone_ids:
|
||||||
props['SubnetId'] = vpc_zone_ids[0]
|
props['SubnetId'] = vpc_zone_ids[0]
|
||||||
|
|
||||||
@@ -325,10 +362,6 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin):
|
|||||||
return super(AutoScalingGroup, self)._tags() + autoscaling_tag
|
return super(AutoScalingGroup, self)._tags() + autoscaling_tag
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
res = super(AutoScalingGroup, self).validate()
|
|
||||||
if res:
|
|
||||||
return res
|
|
||||||
|
|
||||||
# check validity of group size
|
# check validity of group size
|
||||||
min_size = self.properties[self.MIN_SIZE]
|
min_size = self.properties[self.MIN_SIZE]
|
||||||
max_size = self.properties[self.MAX_SIZE]
|
max_size = self.properties[self.MAX_SIZE]
|
||||||
@@ -355,6 +388,19 @@ class AutoScalingGroup(instgrp.InstanceGroup, cooldown.CooldownMixin):
|
|||||||
len(self.properties[self.VPCZONE_IDENTIFIER]) != 1):
|
len(self.properties[self.VPCZONE_IDENTIFIER]) != 1):
|
||||||
raise exception.NotSupported(feature=_("Anything other than one "
|
raise exception.NotSupported(feature=_("Anything other than one "
|
||||||
"VPCZoneIdentifier"))
|
"VPCZoneIdentifier"))
|
||||||
|
# validate properties InstanceId and LaunchConfigurationName
|
||||||
|
# for aws auto scaling group.
|
||||||
|
# should provide just only one of
|
||||||
|
if self.type() == 'AWS::AutoScaling::AutoScalingGroup':
|
||||||
|
instanceId = self.properties.get(self.INSTANCE_ID)
|
||||||
|
launch_config = self.properties.get(
|
||||||
|
self.LAUNCH_CONFIGURATION_NAME)
|
||||||
|
if bool(instanceId) == bool(launch_config):
|
||||||
|
msg = _("Either 'InstanceId' or 'LaunchConfigurationName' "
|
||||||
|
"must be provided.")
|
||||||
|
raise exception.StackValidationFailed(message=msg)
|
||||||
|
|
||||||
|
super(AutoScalingGroup, self).validate()
|
||||||
|
|
||||||
def _resolve_attribute(self, name):
|
def _resolve_attribute(self, name):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from heat.common import exception
|
|||||||
from heat.common import grouputils
|
from heat.common import grouputils
|
||||||
from heat.common import short_id
|
from heat.common import short_id
|
||||||
from heat.common import template_format
|
from heat.common import template_format
|
||||||
|
from heat.engine.clients.os import nova
|
||||||
from heat.engine.notification import autoscaling as notification
|
from heat.engine.notification import autoscaling as notification
|
||||||
from heat.engine import parser
|
from heat.engine import parser
|
||||||
from heat.engine import resource
|
from heat.engine import resource
|
||||||
@@ -111,12 +112,14 @@ class AutoScalingTest(common.HeatTestCase):
|
|||||||
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
|
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
|
||||||
return rsrc
|
return rsrc
|
||||||
|
|
||||||
def _stub_create(self, num, with_error=None):
|
def _stub_create(self, num, with_error=None, with_lcn=True):
|
||||||
self.m.StubOutWithMock(instance.Instance, 'handle_create')
|
self.m.StubOutWithMock(instance.Instance, 'handle_create')
|
||||||
self.m.StubOutWithMock(instance.Instance, 'check_create_complete')
|
self.m.StubOutWithMock(instance.Instance, 'check_create_complete')
|
||||||
self.stub_ImageConstraint_validate()
|
self.stub_ImageConstraint_validate()
|
||||||
self.stub_FlavorConstraint_validate()
|
self.stub_FlavorConstraint_validate()
|
||||||
self.stub_SnapshotConstraint_validate()
|
# create with launch config name, need to stub snapshot constraint
|
||||||
|
if with_lcn:
|
||||||
|
self.stub_SnapshotConstraint_validate()
|
||||||
if with_error:
|
if with_error:
|
||||||
instance.Instance.handle_create().AndRaise(
|
instance.Instance.handle_create().AndRaise(
|
||||||
exception.Error(with_error))
|
exception.Error(with_error))
|
||||||
@@ -701,3 +704,84 @@ class AutoScalingTest(common.HeatTestCase):
|
|||||||
|
|
||||||
rsrc.delete()
|
rsrc.delete()
|
||||||
self.m.VerifyAll()
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def _stub_nova_server_get(self, not_found=False):
|
||||||
|
mock_server = mock.MagicMock()
|
||||||
|
mock_server.image = {'id': 'dd619705-468a-4f7d-8a06-b84794b3561a'}
|
||||||
|
mock_server.flavor = {'id': '1'}
|
||||||
|
mock_server.key_name = 'test'
|
||||||
|
mock_server.security_groups = [{u'name': u'hth_test'}]
|
||||||
|
if not_found:
|
||||||
|
self.patchobject(nova.NovaClientPlugin, 'get_server',
|
||||||
|
side_effect=exception.ServerNotFound(
|
||||||
|
server='5678'))
|
||||||
|
else:
|
||||||
|
self.patchobject(nova.NovaClientPlugin, 'get_server',
|
||||||
|
return_value=mock_server)
|
||||||
|
|
||||||
|
def test_validate_without_InstanceId_and_LaunchConfigurationName(self):
|
||||||
|
t = template_format.parse(as_template)
|
||||||
|
agp = t['Resources']['WebServerGroup']['Properties']
|
||||||
|
agp.pop('LaunchConfigurationName')
|
||||||
|
agp.pop('LoadBalancerNames')
|
||||||
|
stack = utils.parse_stack(t, params=self.params)
|
||||||
|
rsrc = stack['WebServerGroup']
|
||||||
|
error_msg = ("Either 'InstanceId' or 'LaunchConfigurationName' "
|
||||||
|
"must be provided.")
|
||||||
|
exc = self.assertRaises(exception.StackValidationFailed,
|
||||||
|
rsrc.validate)
|
||||||
|
self.assertIn(error_msg, six.text_type(exc))
|
||||||
|
|
||||||
|
def test_validate_with_InstanceId_and_LaunchConfigurationName(self):
|
||||||
|
t = template_format.parse(as_template)
|
||||||
|
agp = t['Resources']['WebServerGroup']['Properties']
|
||||||
|
agp['InstanceId'] = '5678'
|
||||||
|
stack = utils.parse_stack(t, params=self.params)
|
||||||
|
rsrc = stack['WebServerGroup']
|
||||||
|
error_msg = ("Either 'InstanceId' or 'LaunchConfigurationName' "
|
||||||
|
"must be provided.")
|
||||||
|
exc = self.assertRaises(exception.StackValidationFailed,
|
||||||
|
rsrc.validate)
|
||||||
|
self.assertIn(error_msg, six.text_type(exc))
|
||||||
|
|
||||||
|
def test_scaling_group_create_with_instanceid(self):
|
||||||
|
t = template_format.parse(as_template)
|
||||||
|
agp = t['Resources']['WebServerGroup']['Properties']
|
||||||
|
agp['InstanceId'] = '5678'
|
||||||
|
agp.pop('LaunchConfigurationName')
|
||||||
|
agp.pop('LoadBalancerNames')
|
||||||
|
stack = utils.parse_stack(t, params=self.params)
|
||||||
|
rsrc = stack['WebServerGroup']
|
||||||
|
self.stub_KeypairConstraint_validate()
|
||||||
|
self._stub_nova_server_get()
|
||||||
|
self._stub_create(1, with_lcn=False)
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
scheduler.TaskRunner(rsrc.create)()
|
||||||
|
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
|
||||||
|
instance_definition = rsrc._get_instance_definition()
|
||||||
|
ins_props = instance_definition['Properties']
|
||||||
|
self.assertEqual('dd619705-468a-4f7d-8a06-b84794b3561a',
|
||||||
|
ins_props['ImageId'])
|
||||||
|
self.assertEqual('test', ins_props['KeyName'])
|
||||||
|
self.assertEqual(['hth_test'], ins_props['SecurityGroups'])
|
||||||
|
self.assertEqual('1', ins_props['InstanceType'])
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_scaling_group_create_with_instanceid_not_found(self):
|
||||||
|
t = template_format.parse(as_template)
|
||||||
|
agp = t['Resources']['WebServerGroup']['Properties']
|
||||||
|
agp.pop('LaunchConfigurationName')
|
||||||
|
agp['InstanceId'] = '5678'
|
||||||
|
stack = utils.parse_stack(t, params=self.params)
|
||||||
|
rsrc = stack['WebServerGroup']
|
||||||
|
self._stub_nova_server_get(not_found=True)
|
||||||
|
self.m.ReplayAll()
|
||||||
|
msg = ("Property error : WebServerGroup: InstanceId Error validating "
|
||||||
|
"value '5678': The server (5678) could not be found")
|
||||||
|
exc = self.assertRaises(exception.StackValidationFailed,
|
||||||
|
rsrc.validate)
|
||||||
|
self.assertIn(msg, six.text_type(exc))
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|||||||
Reference in New Issue
Block a user