1220 lines
45 KiB
Python
1220 lines
45 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import copy
|
|
import itertools
|
|
|
|
import mock
|
|
import six
|
|
|
|
from heat.common import exception
|
|
from heat.common import template_format
|
|
from heat.engine.clients.os import glance
|
|
from heat.engine.clients.os import nova
|
|
from heat.engine import resource
|
|
from heat.engine import rsrc_defn
|
|
from heat.engine import scheduler
|
|
from heat.tests import common
|
|
from heat.tests import utils
|
|
|
|
from ..resources import auto_scale # noqa
|
|
|
|
|
|
class FakeScalingGroup(object):
|
|
"""A fake implementation of pyrax's ScalingGroup object."""
|
|
def __init__(self, id, **kwargs):
|
|
self.id = id
|
|
self.kwargs = kwargs
|
|
|
|
|
|
class FakeScalePolicy(object):
|
|
"""A fake implementation of pyrax's AutoScalePolicy object."""
|
|
def __init__(self, id, **kwargs):
|
|
self.id = id
|
|
self.kwargs = kwargs
|
|
|
|
|
|
class FakeWebHook(object):
|
|
"""A fake implementation of pyrax's AutoScaleWebhook object."""
|
|
def __init__(self, id, **kwargs):
|
|
self.id = id
|
|
self.kwargs = kwargs
|
|
self.links = [
|
|
{'rel': 'self', 'href': 'self-url'},
|
|
{'rel': 'capability', 'href': 'capability-url'}]
|
|
|
|
|
|
class FakeAutoScale(object):
|
|
"""A fake implementation of pyrax's autoscale client."""
|
|
|
|
def __init__(self):
|
|
self.groups = {}
|
|
self.policies = {}
|
|
self.webhooks = {}
|
|
self.group_counter = itertools.count()
|
|
self.policy_counter = itertools.count()
|
|
self.webhook_counter = itertools.count()
|
|
|
|
def create(self, **kwargs):
|
|
"""Create a scaling group."""
|
|
new_id = str(next(self.group_counter))
|
|
fsg = FakeScalingGroup(new_id, **kwargs)
|
|
self.groups[new_id] = fsg
|
|
return fsg
|
|
|
|
def _check_args(self, kwargs, allowed):
|
|
for parameter in kwargs:
|
|
if parameter not in allowed:
|
|
raise TypeError("unexpected argument %r" % (parameter,))
|
|
|
|
def _get_group(self, id):
|
|
if id not in self.groups:
|
|
raise auto_scale.NotFound("Group %s not found!" % (id,))
|
|
return self.groups[id]
|
|
|
|
def _get_policy(self, id):
|
|
if id not in self.policies:
|
|
raise auto_scale.NotFound("Policy %s not found!" % (id,))
|
|
return self.policies[id]
|
|
|
|
def _get_webhook(self, webhook_id):
|
|
if webhook_id not in self.webhooks:
|
|
raise auto_scale.NotFound(
|
|
"Webhook %s doesn't exist!" % (webhook_id,))
|
|
return self.webhooks[webhook_id]
|
|
|
|
def replace(self, group_id, **kwargs):
|
|
"""Update the groupConfiguration section of a scaling group."""
|
|
allowed = ['name', 'cooldown',
|
|
'min_entities', 'max_entities', 'metadata']
|
|
self._check_args(kwargs, allowed)
|
|
self._get_group(group_id).kwargs = kwargs
|
|
|
|
def replace_launch_config(self, group_id, **kwargs):
|
|
"""Update the launch configuration on a scaling group."""
|
|
if kwargs.get('launch_config_type') == 'launch_server':
|
|
allowed = ['launch_config_type', 'server_name', 'image', 'flavor',
|
|
'disk_config', 'metadata', 'personality', 'networks',
|
|
'load_balancers', 'key_name', 'user_data',
|
|
'config_drive']
|
|
elif kwargs.get('launch_config_type') == 'launch_stack':
|
|
allowed = ['launch_config_type', 'template', 'template_url',
|
|
'disable_rollback', 'environment', 'files',
|
|
'parameters', 'timeout_mins']
|
|
self._check_args(kwargs, allowed)
|
|
self._get_group(group_id).kwargs = kwargs
|
|
|
|
def delete(self, group_id):
|
|
"""Delete the group, if the min entities and max entities are 0."""
|
|
group = self._get_group(group_id)
|
|
if (group.kwargs['min_entities'] > 0
|
|
or group.kwargs['max_entities'] > 0):
|
|
raise Exception("Can't delete yet!")
|
|
del self.groups[group_id]
|
|
|
|
def add_policy(self, **kwargs):
|
|
"""Create and store a FakeScalePolicy."""
|
|
allowed = [
|
|
'scaling_group', 'name', 'policy_type', 'cooldown', 'change',
|
|
'is_percent', 'desired_capacity', 'args']
|
|
self._check_args(kwargs, allowed)
|
|
policy_id = str(next(self.policy_counter))
|
|
policy = FakeScalePolicy(policy_id, **kwargs)
|
|
self.policies[policy_id] = policy
|
|
return policy
|
|
|
|
def replace_policy(self, scaling_group, policy, **kwargs):
|
|
allowed = [
|
|
'name', 'policy_type', 'cooldown',
|
|
'change', 'is_percent', 'desired_capacity', 'args']
|
|
self._check_args(kwargs, allowed)
|
|
policy = self._get_policy(policy)
|
|
assert policy.kwargs['scaling_group'] == scaling_group
|
|
kwargs['scaling_group'] = scaling_group
|
|
policy.kwargs = kwargs
|
|
|
|
def add_webhook(self, **kwargs):
|
|
"""Create and store a FakeWebHook."""
|
|
allowed = ['scaling_group', 'policy', 'name', 'metadata']
|
|
self._check_args(kwargs, allowed)
|
|
webhook_id = str(next(self.webhook_counter))
|
|
webhook = FakeWebHook(webhook_id, **kwargs)
|
|
self.webhooks[webhook_id] = webhook
|
|
return webhook
|
|
|
|
def delete_policy(self, scaling_group, policy):
|
|
"""Delete a policy, if it exists."""
|
|
if policy not in self.policies:
|
|
raise auto_scale.NotFound("Policy %s doesn't exist!" % (policy,))
|
|
assert self.policies[policy].kwargs['scaling_group'] == scaling_group
|
|
del self.policies[policy]
|
|
|
|
def delete_webhook(self, scaling_group, policy, webhook_id):
|
|
"""Delete a webhook, if it exists."""
|
|
webhook = self._get_webhook(webhook_id)
|
|
assert webhook.kwargs['scaling_group'] == scaling_group
|
|
assert webhook.kwargs['policy'] == policy
|
|
del self.webhooks[webhook_id]
|
|
|
|
def replace_webhook(self, scaling_group, policy, webhook,
|
|
name=None, metadata=None):
|
|
webhook = self._get_webhook(webhook)
|
|
assert webhook.kwargs['scaling_group'] == scaling_group
|
|
assert webhook.kwargs['policy'] == policy
|
|
webhook.kwargs['name'] = name
|
|
webhook.kwargs['metadata'] = metadata
|
|
|
|
|
|
class ScalingGroupTest(common.HeatTestCase):
|
|
|
|
server_template = template_format.parse('''
|
|
HeatTemplateFormatVersion: "2012-12-12"
|
|
Description: "Rackspace Auto Scale"
|
|
Parameters: {}
|
|
Resources:
|
|
my_group:
|
|
Type: Rackspace::AutoScale::Group
|
|
Properties:
|
|
groupConfiguration:
|
|
name: "My Group"
|
|
cooldown: 60
|
|
minEntities: 1
|
|
maxEntities: 25
|
|
metadata:
|
|
group: metadata
|
|
launchConfiguration:
|
|
type: "launch_server"
|
|
args:
|
|
server:
|
|
name: autoscaled-server
|
|
flavorRef: flavor-ref
|
|
imageRef: image-ref
|
|
key_name: my-key
|
|
metadata:
|
|
server: metadata
|
|
personality:
|
|
/tmp/testfile: "dGVzdCBjb250ZW50"
|
|
networks:
|
|
- uuid: "00000000-0000-0000-0000-000000000000"
|
|
- uuid: "11111111-1111-1111-1111-111111111111"
|
|
loadBalancers:
|
|
- loadBalancerId: 234
|
|
port: 80
|
|
|
|
''')
|
|
|
|
stack_template = template_format.parse('''
|
|
HeatTemplateFormatVersion: "2012-12-12"
|
|
Description: "Rackspace Auto Scale"
|
|
Parameters: {}
|
|
Resources:
|
|
my_group:
|
|
Type: Rackspace::AutoScale::Group
|
|
Properties:
|
|
groupConfiguration:
|
|
name: "My Group"
|
|
cooldown: 60
|
|
minEntities: 1
|
|
maxEntities: 25
|
|
metadata:
|
|
group: metadata
|
|
launchConfiguration:
|
|
type: launch_stack
|
|
args:
|
|
stack:
|
|
template: |
|
|
heat_template_version: 2015-10-15
|
|
description: This is a Heat template
|
|
parameters:
|
|
image:
|
|
default: cirros-0.3.4-x86_64-uec
|
|
type: string
|
|
flavor:
|
|
default: m1.tiny
|
|
type: string
|
|
resources:
|
|
rand:
|
|
type: OS::Heat::RandomString
|
|
disable_rollback: False
|
|
environment:
|
|
parameters:
|
|
image: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)
|
|
resource_registry:
|
|
Heat::InstallConfigAgent:
|
|
https://myhost.com/bootconfig.yaml
|
|
files:
|
|
fileA.yaml: Contents of the file
|
|
file:///usr/fileB.template: Contents of the file
|
|
parameters:
|
|
flavor: 4 GB Performance
|
|
timeout_mins: 30
|
|
''')
|
|
|
|
def setUp(self):
|
|
super(ScalingGroupTest, self).setUp()
|
|
for res_name, res_class in auto_scale.resource_mapping().items():
|
|
resource._register_class(res_name, res_class)
|
|
self.fake_auto_scale = FakeAutoScale()
|
|
self.patchobject(auto_scale.Group, 'auto_scale',
|
|
return_value=self.fake_auto_scale)
|
|
# mock nova and glance client methods to satisfy constraints
|
|
mock_im = self.patchobject(glance.GlanceClientPlugin,
|
|
'find_image_by_name_or_id')
|
|
mock_im.return_value = 'image-ref'
|
|
mock_fl = self.patchobject(nova.NovaClientPlugin,
|
|
'find_flavor_by_name_or_id')
|
|
mock_fl.return_value = 'flavor-ref'
|
|
|
|
def _setup_test_stack(self, template=None):
|
|
if template is None:
|
|
template = self.server_template
|
|
self.stack = utils.parse_stack(template)
|
|
self.stack.create()
|
|
self.assertEqual(
|
|
('CREATE', 'COMPLETE'), self.stack.state,
|
|
self.stack.status_reason)
|
|
|
|
def test_group_create_server(self):
|
|
"""Creating a group passes all the correct arguments to pyrax.
|
|
|
|
Also saves the group ID as the resource ID.
|
|
"""
|
|
self._setup_test_stack()
|
|
self.assertEqual(1, len(self.fake_auto_scale.groups))
|
|
self.assertEqual(
|
|
{
|
|
'cooldown': 60,
|
|
'config_drive': False,
|
|
'user_data': None,
|
|
'disk_config': None,
|
|
'flavor': 'flavor-ref',
|
|
'image': 'image-ref',
|
|
'load_balancers': [{
|
|
'loadBalancerId': 234,
|
|
'port': 80,
|
|
}],
|
|
'key_name': "my-key",
|
|
'launch_config_type': u'launch_server',
|
|
'max_entities': 25,
|
|
'group_metadata': {'group': 'metadata'},
|
|
'metadata': {'server': 'metadata'},
|
|
'min_entities': 1,
|
|
'name': 'My Group',
|
|
'networks': [{'uuid': '00000000-0000-0000-0000-000000000000'},
|
|
{'uuid': '11111111-1111-1111-1111-111111111111'}],
|
|
'personality': [{
|
|
'path': u'/tmp/testfile',
|
|
'contents': u'dGVzdCBjb250ZW50'}],
|
|
'server_name': u'autoscaled-server'},
|
|
self.fake_auto_scale.groups['0'].kwargs)
|
|
|
|
resource = self.stack['my_group']
|
|
self.assertEqual('0', resource.FnGetRefId())
|
|
|
|
def test_group_create_stack(self):
|
|
"""Creating a group passes all the correct arguments to pyrax.
|
|
|
|
Also saves the group ID as the resource ID.
|
|
"""
|
|
self._setup_test_stack(self.stack_template)
|
|
self.assertEqual(1, len(self.fake_auto_scale.groups))
|
|
self.assertEqual(
|
|
{
|
|
'cooldown': 60,
|
|
'min_entities': 1,
|
|
'max_entities': 25,
|
|
'group_metadata': {'group': 'metadata'},
|
|
'name': 'My Group',
|
|
'launch_config_type': u'launch_stack',
|
|
'template': (
|
|
'''heat_template_version: 2015-10-15
|
|
description: This is a Heat template
|
|
parameters:
|
|
image:
|
|
default: cirros-0.3.4-x86_64-uec
|
|
type: string
|
|
flavor:
|
|
default: m1.tiny
|
|
type: string
|
|
resources:
|
|
rand:
|
|
type: OS::Heat::RandomString
|
|
'''),
|
|
'template_url': None,
|
|
'disable_rollback': False,
|
|
'environment': {
|
|
'parameters': {
|
|
'image':
|
|
'Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)',
|
|
},
|
|
'resource_registry': {
|
|
'Heat::InstallConfigAgent': ('https://myhost.com/'
|
|
'bootconfig.yaml')
|
|
}
|
|
},
|
|
'files': {
|
|
'fileA.yaml': 'Contents of the file',
|
|
'file:///usr/fileB.template': 'Contents of the file'
|
|
},
|
|
'parameters': {
|
|
'flavor': '4 GB Performance',
|
|
},
|
|
'timeout_mins': 30,
|
|
},
|
|
self.fake_auto_scale.groups['0'].kwargs
|
|
)
|
|
|
|
resource = self.stack['my_group']
|
|
self.assertEqual('0', resource.FnGetRefId())
|
|
|
|
def test_group_create_no_personality(self):
|
|
|
|
template = template_format.parse('''
|
|
HeatTemplateFormatVersion: "2012-12-12"
|
|
Description: "Rackspace Auto Scale"
|
|
Parameters: {}
|
|
Resources:
|
|
my_group:
|
|
Type: Rackspace::AutoScale::Group
|
|
Properties:
|
|
groupConfiguration:
|
|
name: "My Group"
|
|
cooldown: 60
|
|
minEntities: 1
|
|
maxEntities: 25
|
|
metadata:
|
|
group: metadata
|
|
launchConfiguration:
|
|
type: "launch_server"
|
|
args:
|
|
server:
|
|
name: autoscaled-server
|
|
flavorRef: flavor-ref
|
|
imageRef: image-ref
|
|
key_name: my-key
|
|
metadata:
|
|
server: metadata
|
|
networks:
|
|
- uuid: "00000000-0000-0000-0000-000000000000"
|
|
- uuid: "11111111-1111-1111-1111-111111111111"
|
|
''')
|
|
|
|
self.stack = utils.parse_stack(template)
|
|
self.stack.create()
|
|
self.assertEqual(
|
|
('CREATE', 'COMPLETE'), self.stack.state,
|
|
self.stack.status_reason)
|
|
|
|
self.assertEqual(1, len(self.fake_auto_scale.groups))
|
|
self.assertEqual(
|
|
{
|
|
'cooldown': 60,
|
|
'config_drive': False,
|
|
'user_data': None,
|
|
'disk_config': None,
|
|
'flavor': 'flavor-ref',
|
|
'image': 'image-ref',
|
|
'launch_config_type': 'launch_server',
|
|
'load_balancers': [],
|
|
'key_name': "my-key",
|
|
'max_entities': 25,
|
|
'group_metadata': {'group': 'metadata'},
|
|
'metadata': {'server': 'metadata'},
|
|
'min_entities': 1,
|
|
'name': 'My Group',
|
|
'networks': [{'uuid': '00000000-0000-0000-0000-000000000000'},
|
|
{'uuid': '11111111-1111-1111-1111-111111111111'}],
|
|
'personality': None,
|
|
'server_name': u'autoscaled-server'},
|
|
self.fake_auto_scale.groups['0'].kwargs)
|
|
|
|
resource = self.stack['my_group']
|
|
self.assertEqual('0', resource.FnGetRefId())
|
|
|
|
def test_check(self):
|
|
self._setup_test_stack()
|
|
resource = self.stack['my_group']
|
|
mock_get = mock.Mock()
|
|
resource.auto_scale().get = mock_get
|
|
scheduler.TaskRunner(resource.check)()
|
|
self.assertEqual('CHECK', resource.action)
|
|
self.assertEqual('COMPLETE', resource.status)
|
|
|
|
mock_get.side_effect = auto_scale.NotFound('boom')
|
|
exc = self.assertRaises(exception.ResourceFailure,
|
|
scheduler.TaskRunner(resource.check))
|
|
self.assertEqual('CHECK', resource.action)
|
|
self.assertEqual('FAILED', resource.status)
|
|
self.assertIn('boom', str(exc))
|
|
|
|
def test_update_group_config(self):
|
|
"""Updates the groupConfiguration section.
|
|
|
|
Updates the groupConfiguration section in a template results in a
|
|
pyrax call to update the group configuration.
|
|
"""
|
|
self._setup_test_stack()
|
|
|
|
resource = self.stack['my_group']
|
|
uprops = copy.deepcopy(dict(resource.properties.data))
|
|
uprops['groupConfiguration']['minEntities'] = 5
|
|
new_template = rsrc_defn.ResourceDefinition(resource.name,
|
|
resource.type(),
|
|
uprops)
|
|
scheduler.TaskRunner(resource.update, new_template)()
|
|
|
|
self.assertEqual(1, len(self.fake_auto_scale.groups))
|
|
self.assertEqual(
|
|
5, self.fake_auto_scale.groups['0'].kwargs['min_entities'])
|
|
|
|
def test_update_launch_config_server(self):
|
|
"""Updates the launchConfigresults section.
|
|
|
|
Updates the launchConfigresults section in a template results in a
|
|
pyrax call to update the launch configuration.
|
|
"""
|
|
self._setup_test_stack()
|
|
|
|
resource = self.stack['my_group']
|
|
uprops = copy.deepcopy(dict(resource.properties.data))
|
|
lcargs = uprops['launchConfiguration']['args']
|
|
lcargs['loadBalancers'] = [{'loadBalancerId': '1', 'port': 80}]
|
|
new_template = rsrc_defn.ResourceDefinition(resource.name,
|
|
resource.type(),
|
|
uprops)
|
|
|
|
scheduler.TaskRunner(resource.update, new_template)()
|
|
|
|
self.assertEqual(1, len(self.fake_auto_scale.groups))
|
|
self.assertEqual(
|
|
[{'loadBalancerId': 1, 'port': 80}],
|
|
self.fake_auto_scale.groups['0'].kwargs['load_balancers'])
|
|
|
|
def test_update_launch_config_stack(self):
|
|
self._setup_test_stack(self.stack_template)
|
|
|
|
resource = self.stack['my_group']
|
|
uprops = copy.deepcopy(dict(resource.properties.data))
|
|
lcargs = uprops['launchConfiguration']['args']
|
|
lcargs['stack']['timeout_mins'] = 60
|
|
new_template = rsrc_defn.ResourceDefinition(resource.name,
|
|
resource.type(),
|
|
uprops)
|
|
|
|
scheduler.TaskRunner(resource.update, new_template)()
|
|
|
|
self.assertEqual(1, len(self.fake_auto_scale.groups))
|
|
self.assertEqual(
|
|
60,
|
|
self.fake_auto_scale.groups['0'].kwargs['timeout_mins'])
|
|
|
|
def test_delete(self):
|
|
"""Deleting a ScalingGroup resource invokes pyrax API to delete it."""
|
|
self._setup_test_stack()
|
|
resource = self.stack['my_group']
|
|
scheduler.TaskRunner(resource.delete)()
|
|
self.assertEqual({}, self.fake_auto_scale.groups)
|
|
|
|
def test_delete_without_backing_group(self):
|
|
"""Resource deletion succeeds, if no backing scaling group exists."""
|
|
self._setup_test_stack()
|
|
resource = self.stack['my_group']
|
|
del self.fake_auto_scale.groups['0']
|
|
scheduler.TaskRunner(resource.delete)()
|
|
self.assertEqual({}, self.fake_auto_scale.groups)
|
|
|
|
def test_delete_waits_for_server_deletion(self):
|
|
"""Test case for waiting for successful resource deletion.
|
|
|
|
The delete operation may fail until the servers are really gone; the
|
|
resource retries until success.
|
|
"""
|
|
self._setup_test_stack()
|
|
delete_counter = itertools.count()
|
|
|
|
def delete(group_id):
|
|
count = next(delete_counter)
|
|
if count < 3:
|
|
raise auto_scale.Forbidden("Not empty!")
|
|
|
|
self.patchobject(self.fake_auto_scale, 'delete', side_effect=delete)
|
|
resource = self.stack['my_group']
|
|
scheduler.TaskRunner(resource.delete)()
|
|
# It really called delete until it succeeded:
|
|
self.assertEqual(4, next(delete_counter))
|
|
|
|
def test_delete_blows_up_on_other_errors(self):
|
|
"""Test case for correct error handling during deletion.
|
|
|
|
Only the Forbidden (403) error is honored as an indicator of pending
|
|
deletion; other errors cause deletion to fail.
|
|
"""
|
|
self._setup_test_stack()
|
|
|
|
def delete(group_id):
|
|
1 / 0
|
|
|
|
self.patchobject(self.fake_auto_scale, 'delete', side_effect=delete)
|
|
resource = self.stack['my_group']
|
|
err = self.assertRaises(
|
|
exception.ResourceFailure, scheduler.TaskRunner(resource.delete))
|
|
self.assertIsInstance(err.exc, ZeroDivisionError)
|
|
|
|
|
|
class PolicyTest(common.HeatTestCase):
|
|
policy_template = template_format.parse('''
|
|
HeatTemplateFormatVersion: "2012-12-12"
|
|
Description: "Rackspace Auto Scale"
|
|
Parameters: {}
|
|
Resources:
|
|
my_policy:
|
|
Type: Rackspace::AutoScale::ScalingPolicy
|
|
Properties:
|
|
group: "my-group-id"
|
|
name: "+10 on webhook"
|
|
change: 10
|
|
cooldown: 0
|
|
type: "webhook"
|
|
''')
|
|
|
|
def setUp(self):
|
|
super(PolicyTest, self).setUp()
|
|
for res_name, res_class in auto_scale.resource_mapping().items():
|
|
resource._register_class(res_name, res_class)
|
|
self.fake_auto_scale = FakeAutoScale()
|
|
self.patchobject(auto_scale.ScalingPolicy, 'auto_scale',
|
|
return_value=self.fake_auto_scale)
|
|
|
|
def _setup_test_stack(self, template):
|
|
self.stack = utils.parse_stack(template)
|
|
self.stack.create()
|
|
self.assertEqual(
|
|
('CREATE', 'COMPLETE'), self.stack.state,
|
|
self.stack.status_reason)
|
|
|
|
def test_create_webhook_change(self):
|
|
"""Creating the resource creates the scaling policy with pyrax.
|
|
|
|
Also sets the resource's ID to {group_id}:{policy_id}.
|
|
"""
|
|
self._setup_test_stack(self.policy_template)
|
|
resource = self.stack['my_policy']
|
|
self.assertEqual('my-group-id:0', resource.FnGetRefId())
|
|
self.assertEqual(
|
|
{
|
|
'name': '+10 on webhook',
|
|
'scaling_group': 'my-group-id',
|
|
'change': 10,
|
|
'cooldown': 0,
|
|
'policy_type': 'webhook'},
|
|
self.fake_auto_scale.policies['0'].kwargs)
|
|
|
|
def test_webhook_change_percent(self):
|
|
"""Test case for specified changePercent.
|
|
|
|
When changePercent is specified, it translates to pyrax arguments
|
|
'change' and 'is_percent'.
|
|
"""
|
|
template = copy.deepcopy(self.policy_template)
|
|
template['Resources']['my_policy']['Properties']['changePercent'] = 10
|
|
del template['Resources']['my_policy']['Properties']['change']
|
|
self._setup_test_stack(template)
|
|
self.assertEqual(
|
|
{
|
|
'name': '+10 on webhook',
|
|
'scaling_group': 'my-group-id',
|
|
'change': 10,
|
|
'is_percent': True,
|
|
'cooldown': 0,
|
|
'policy_type': 'webhook'},
|
|
self.fake_auto_scale.policies['0'].kwargs)
|
|
|
|
def test_webhook_desired_capacity(self):
|
|
"""Test case for desiredCapacity property.
|
|
|
|
The desiredCapacity property translates to the desired_capacity pyrax
|
|
argument.
|
|
"""
|
|
template = copy.deepcopy(self.policy_template)
|
|
template['Resources']['my_policy']['Properties']['desiredCapacity'] = 1
|
|
del template['Resources']['my_policy']['Properties']['change']
|
|
self._setup_test_stack(template)
|
|
self.assertEqual(
|
|
{
|
|
'name': '+10 on webhook',
|
|
'scaling_group': 'my-group-id',
|
|
'desired_capacity': 1,
|
|
'cooldown': 0,
|
|
'policy_type': 'webhook'},
|
|
self.fake_auto_scale.policies['0'].kwargs)
|
|
|
|
def test_schedule(self):
|
|
"""We can specify schedule-type policies with args."""
|
|
template = copy.deepcopy(self.policy_template)
|
|
props = template['Resources']['my_policy']['Properties']
|
|
props['type'] = 'schedule'
|
|
props['args'] = {'cron': '0 0 0 * *'}
|
|
self._setup_test_stack(template)
|
|
self.assertEqual(
|
|
{
|
|
'name': '+10 on webhook',
|
|
'scaling_group': 'my-group-id',
|
|
'change': 10,
|
|
'cooldown': 0,
|
|
'policy_type': 'schedule',
|
|
'args': {'cron': '0 0 0 * *'}},
|
|
self.fake_auto_scale.policies['0'].kwargs)
|
|
|
|
def test_update(self):
|
|
"""Updating the resource calls appropriate update method with pyrax."""
|
|
self._setup_test_stack(self.policy_template)
|
|
resource = self.stack['my_policy']
|
|
uprops = copy.deepcopy(dict(resource.properties.data))
|
|
uprops['changePercent'] = 50
|
|
del uprops['change']
|
|
template = rsrc_defn.ResourceDefinition(resource.name,
|
|
resource.type(),
|
|
uprops)
|
|
|
|
scheduler.TaskRunner(resource.update, template)()
|
|
self.assertEqual(
|
|
{
|
|
'name': '+10 on webhook',
|
|
'scaling_group': 'my-group-id',
|
|
'change': 50,
|
|
'is_percent': True,
|
|
'cooldown': 0,
|
|
'policy_type': 'webhook'},
|
|
self.fake_auto_scale.policies['0'].kwargs)
|
|
|
|
def test_delete(self):
|
|
"""Deleting the resource deletes the policy with pyrax."""
|
|
self._setup_test_stack(self.policy_template)
|
|
resource = self.stack['my_policy']
|
|
scheduler.TaskRunner(resource.delete)()
|
|
self.assertEqual({}, self.fake_auto_scale.policies)
|
|
|
|
def test_delete_policy_non_existent(self):
|
|
"""Test case for deleting resource without backing policy.
|
|
|
|
Deleting a resource for which there is no backing policy succeeds
|
|
silently.
|
|
"""
|
|
self._setup_test_stack(self.policy_template)
|
|
resource = self.stack['my_policy']
|
|
del self.fake_auto_scale.policies['0']
|
|
scheduler.TaskRunner(resource.delete)()
|
|
self.assertEqual({}, self.fake_auto_scale.policies)
|
|
|
|
|
|
class WebHookTest(common.HeatTestCase):
|
|
webhook_template = template_format.parse('''
|
|
HeatTemplateFormatVersion: "2012-12-12"
|
|
Description: "Rackspace Auto Scale"
|
|
Parameters: {}
|
|
Resources:
|
|
my_webhook:
|
|
Type: Rackspace::AutoScale::WebHook
|
|
Properties:
|
|
policy: my-group-id:my-policy-id
|
|
name: "exec my policy"
|
|
metadata:
|
|
a: b
|
|
''')
|
|
|
|
def setUp(self):
|
|
super(WebHookTest, self).setUp()
|
|
for res_name, res_class in auto_scale.resource_mapping().items():
|
|
resource._register_class(res_name, res_class)
|
|
self.fake_auto_scale = FakeAutoScale()
|
|
self.patchobject(auto_scale.WebHook, 'auto_scale',
|
|
return_value=self.fake_auto_scale)
|
|
|
|
def _setup_test_stack(self, template):
|
|
self.stack = utils.parse_stack(template)
|
|
self.stack.create()
|
|
self.assertEqual(
|
|
('CREATE', 'COMPLETE'), self.stack.state,
|
|
self.stack.status_reason)
|
|
|
|
def test_create(self):
|
|
"""Creates a webhook with pyrax and makes attributes available."""
|
|
self._setup_test_stack(self.webhook_template)
|
|
resource = self.stack['my_webhook']
|
|
self.assertEqual(
|
|
{
|
|
'name': 'exec my policy',
|
|
'scaling_group': 'my-group-id',
|
|
'policy': 'my-policy-id',
|
|
'metadata': {'a': 'b'}},
|
|
self.fake_auto_scale.webhooks['0'].kwargs)
|
|
self.assertEqual("self-url", resource.FnGetAtt("executeUrl"))
|
|
self.assertEqual("capability-url", resource.FnGetAtt("capabilityUrl"))
|
|
|
|
def test_failed_create(self):
|
|
"""When a create fails, getting the attributes returns None."""
|
|
template = copy.deepcopy(self.webhook_template)
|
|
template['Resources']['my_webhook']['Properties']['policy'] = 'foobar'
|
|
self.stack = utils.parse_stack(template)
|
|
self.stack.create()
|
|
resource = self.stack['my_webhook']
|
|
self.assertIsNone(resource.FnGetAtt('capabilityUrl'))
|
|
|
|
def test_update(self):
|
|
self._setup_test_stack(self.webhook_template)
|
|
resource = self.stack['my_webhook']
|
|
uprops = copy.deepcopy(dict(resource.properties.data))
|
|
uprops['metadata']['a'] = 'different!'
|
|
uprops['name'] = 'newhook'
|
|
template = rsrc_defn.ResourceDefinition(resource.name,
|
|
resource.type(),
|
|
uprops)
|
|
|
|
scheduler.TaskRunner(resource.update, template)()
|
|
self.assertEqual(
|
|
{
|
|
'name': 'newhook',
|
|
'scaling_group': 'my-group-id',
|
|
'policy': 'my-policy-id',
|
|
'metadata': {'a': 'different!'}},
|
|
self.fake_auto_scale.webhooks['0'].kwargs)
|
|
|
|
def test_delete(self):
|
|
"""Deleting the resource deletes the webhook with pyrax."""
|
|
self._setup_test_stack(self.webhook_template)
|
|
resource = self.stack['my_webhook']
|
|
scheduler.TaskRunner(resource.delete)()
|
|
self.assertEqual({}, self.fake_auto_scale.webhooks)
|
|
|
|
def test_delete_without_backing_webhook(self):
|
|
"""Test case for deleting resource without backing webhook.
|
|
|
|
Deleting a resource for which there is no backing webhook succeeds
|
|
silently.
|
|
"""
|
|
self._setup_test_stack(self.webhook_template)
|
|
resource = self.stack['my_webhook']
|
|
del self.fake_auto_scale.webhooks['0']
|
|
scheduler.TaskRunner(resource.delete)()
|
|
self.assertEqual({}, self.fake_auto_scale.webhooks)
|
|
|
|
|
|
@mock.patch.object(resource.Resource, "client_plugin")
|
|
@mock.patch.object(resource.Resource, "client")
|
|
class AutoScaleGroupValidationTests(common.HeatTestCase):
|
|
def setUp(self):
|
|
super(AutoScaleGroupValidationTests, self).setUp()
|
|
self.mockstack = mock.Mock()
|
|
self.mockstack.has_cache_data.return_value = False
|
|
self.mockstack.db_resource_get.return_value = None
|
|
|
|
def test_validate_no_rcv3_pool(self, mock_client, mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_server",
|
|
"args": {
|
|
"loadBalancers": [{
|
|
"loadBalancerId": 'not integer!',
|
|
}],
|
|
"server": {
|
|
"name": "sdfsdf",
|
|
"flavorRef": "ffdgdf",
|
|
"imageRef": "image-ref",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
mock_client().list_load_balancer_pools.return_value = []
|
|
error = self.assertRaises(
|
|
exception.StackValidationFailed, asg.validate)
|
|
self.assertEqual(
|
|
'Could not find RackConnectV3 pool with id not integer!: ',
|
|
six.text_type(error))
|
|
|
|
def test_validate_rcv3_pool_found(self, mock_client, mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_server",
|
|
"args": {
|
|
"loadBalancers": [{
|
|
"loadBalancerId": 'pool_exists',
|
|
}],
|
|
"server": {
|
|
"name": "sdfsdf",
|
|
"flavorRef": "ffdgdf",
|
|
"imageRef": "image-ref",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
mock_client().list_load_balancer_pools.return_value = [
|
|
mock.Mock(id='pool_exists'),
|
|
]
|
|
self.assertIsNone(asg.validate())
|
|
|
|
def test_validate_no_lb_specified(self, mock_client, mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_server",
|
|
"args": {
|
|
"server": {
|
|
"name": "sdfsdf",
|
|
"flavorRef": "ffdgdf",
|
|
"imageRef": "image-ref",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
self.assertIsNone(asg.validate())
|
|
|
|
def test_validate_launch_stack(self, mock_client, mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_stack",
|
|
"args": {
|
|
"stack": {
|
|
'template': (
|
|
'''heat_template_version: 2015-10-15
|
|
description: This is a Heat template
|
|
parameters:
|
|
image:
|
|
default: cirros-0.3.4-x86_64-uec
|
|
type: string
|
|
flavor:
|
|
default: m1.tiny
|
|
type: string
|
|
resources:
|
|
rand:
|
|
type: OS::Heat::RandomString
|
|
'''),
|
|
'template_url': None,
|
|
'disable_rollback': False,
|
|
'environment': {
|
|
'parameters': {
|
|
'image':
|
|
'Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)',
|
|
},
|
|
'resource_registry': {
|
|
'Heat::InstallConfigAgent': (
|
|
'https://myhost.com/bootconfig.yaml')
|
|
}
|
|
},
|
|
'files': {
|
|
'fileA.yaml': 'Contents of the file',
|
|
'file:///usr/fileB.yaml': 'Contents of the file'
|
|
},
|
|
'parameters': {
|
|
'flavor': '4 GB Performance',
|
|
},
|
|
'timeout_mins': 30,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
self.assertIsNone(asg.validate())
|
|
|
|
def test_validate_launch_server_and_stack(self, mock_client, mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_server",
|
|
"args": {
|
|
"server": {
|
|
"name": "sdfsdf",
|
|
"flavorRef": "ffdgdf",
|
|
"imageRef": "image-ref",
|
|
},
|
|
"stack": {
|
|
'template': (
|
|
'''heat_template_version: 2015-10-15
|
|
description: This is a Heat template
|
|
parameters:
|
|
image:
|
|
default: cirros-0.3.4-x86_64-uec
|
|
type: string
|
|
flavor:
|
|
default: m1.tiny
|
|
type: string
|
|
resources:
|
|
rand:
|
|
type: OS::Heat::RandomString
|
|
'''),
|
|
'template_url': None,
|
|
'disable_rollback': False,
|
|
'environment': {
|
|
'parameters': {
|
|
'image':
|
|
'Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)',
|
|
},
|
|
'resource_registry': {
|
|
'Heat::InstallConfigAgent': (
|
|
'https://myhost.com/bootconfig.yaml')
|
|
}
|
|
},
|
|
'files': {
|
|
'fileA.yaml': 'Contents of the file',
|
|
'file:///usr/fileB.yaml': 'Contents of the file'
|
|
},
|
|
'parameters': {
|
|
'flavor': '4 GB Performance',
|
|
},
|
|
'timeout_mins': 30,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
error = self.assertRaises(
|
|
exception.StackValidationFailed, asg.validate)
|
|
self.assertIn(
|
|
'Must provide one of server or stack in launchConfiguration',
|
|
six.text_type(error))
|
|
|
|
def test_validate_no_launch_server_or_stack(self, mock_client,
|
|
mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_server",
|
|
"args": {}
|
|
}
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
error = self.assertRaises(
|
|
exception.StackValidationFailed, asg.validate)
|
|
self.assertIn(
|
|
'Must provide one of server or stack in launchConfiguration',
|
|
six.text_type(error))
|
|
|
|
def test_validate_stack_template_and_template_url(self, mock_client,
|
|
mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_server",
|
|
"args": {
|
|
"stack": {
|
|
'template': (
|
|
'''heat_template_version: 2015-10-15
|
|
description: This is a Heat template
|
|
parameters:
|
|
image:
|
|
default: cirros-0.3.4-x86_64-uec
|
|
type: string
|
|
flavor:
|
|
default: m1.tiny
|
|
type: string
|
|
resources:
|
|
rand:
|
|
type: OS::Heat::RandomString
|
|
'''),
|
|
'template_url': 'https://myhost.com/template.yaml',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
error = self.assertRaises(
|
|
exception.StackValidationFailed, asg.validate)
|
|
self.assertIn(
|
|
'Must provide one of template or template_url',
|
|
six.text_type(error))
|
|
|
|
def test_validate_stack_no_template_or_template_url(self, mock_client,
|
|
mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_server",
|
|
"args": {
|
|
"stack": {
|
|
'disable_rollback': False,
|
|
'environment': {
|
|
'parameters': {
|
|
'image':
|
|
'Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)',
|
|
},
|
|
'resource_registry': {
|
|
'Heat::InstallConfigAgent': (
|
|
'https://myhost.com/bootconfig.yaml')
|
|
}
|
|
},
|
|
'files': {
|
|
'fileA.yaml': 'Contents of the file',
|
|
'file:///usr/fileB.yaml': 'Contents of the file'
|
|
},
|
|
'parameters': {
|
|
'flavor': '4 GB Performance',
|
|
},
|
|
'timeout_mins': 30,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
error = self.assertRaises(
|
|
exception.StackValidationFailed, asg.validate)
|
|
self.assertIn(
|
|
'Must provide one of template or template_url',
|
|
six.text_type(error))
|
|
|
|
def test_validate_invalid_template(self, mock_client, mock_plugin):
|
|
asg_properties = {
|
|
"groupConfiguration": {
|
|
"name": "My Group",
|
|
"cooldown": 60,
|
|
"minEntities": 1,
|
|
"maxEntities": 25,
|
|
"metadata": {
|
|
"group": "metadata",
|
|
},
|
|
},
|
|
"launchConfiguration": {
|
|
"type": "launch_stack",
|
|
"args": {
|
|
"stack": {
|
|
'template': (
|
|
'''SJDADKJAJKLSheat_template_version: 2015-10-15
|
|
description: This is a Heat template
|
|
parameters:
|
|
image:
|
|
default: cirros-0.3.4-x86_64-uec
|
|
type: string
|
|
flavor:
|
|
default: m1.tiny
|
|
type: string
|
|
resources:
|
|
rand:
|
|
type: OS::Heat::RandomString
|
|
'''),
|
|
'template_url': None,
|
|
'disable_rollback': False,
|
|
'environment': {'Foo': 'Bar'},
|
|
'files': {
|
|
'fileA.yaml': 'Contents of the file',
|
|
'file:///usr/fileB.yaml': 'Contents of the file'
|
|
},
|
|
'parameters': {
|
|
'flavor': '4 GB Performance',
|
|
},
|
|
'timeout_mins': 30,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rsrcdef = rsrc_defn.ResourceDefinition(
|
|
"test", auto_scale.Group, properties=asg_properties)
|
|
asg = auto_scale.Group("test", rsrcdef, self.mockstack)
|
|
|
|
error = self.assertRaises(
|
|
exception.StackValidationFailed, asg.validate)
|
|
self.assertIn(
|
|
'Encountered error while loading template:',
|
|
six.text_type(error))
|