Heat autoscaling scenario test
This test starts with a single server and scales up to three servers triggered by a script that consumes memory. Seven minutes after stack creation, memory consumption script will quit and the scale down alarms will scale back down to a single server. Due to the nature of this test, it takes about 10 minutes to run locally. The scenario test has been put in package orchestration for the following reasons: - this will be the first of many heat scenario tests - this will allow a tox filter to run this test for the slow heat gating job Change-Id: I53ed12369d12b902108b9b8fa7885df34f6ab51f
This commit is contained in:
@@ -13,6 +13,7 @@ python-keystoneclient>=0.2.0
|
|||||||
python-novaclient>=2.10.0
|
python-novaclient>=2.10.0
|
||||||
python-neutronclient>=2.2.3,<3.0.0
|
python-neutronclient>=2.2.3,<3.0.0
|
||||||
python-cinderclient>=1.0.4
|
python-cinderclient>=1.0.4
|
||||||
|
python-heatclient>=0.2.3
|
||||||
testresources
|
testresources
|
||||||
keyring
|
keyring
|
||||||
testrepository
|
testrepository
|
||||||
|
|||||||
@@ -16,11 +16,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
# Default client libs
|
# Default client libs
|
||||||
import cinderclient.client
|
import cinderclient.client
|
||||||
import glanceclient
|
import glanceclient
|
||||||
|
import heatclient.client
|
||||||
import keystoneclient.v2_0.client
|
import keystoneclient.v2_0.client
|
||||||
import netaddr
|
import netaddr
|
||||||
from neutronclient.common import exceptions as exc
|
from neutronclient.common import exceptions as exc
|
||||||
@@ -48,6 +50,7 @@ class OfficialClientManager(tempest.manager.Manager):
|
|||||||
|
|
||||||
NOVACLIENT_VERSION = '2'
|
NOVACLIENT_VERSION = '2'
|
||||||
CINDERCLIENT_VERSION = '1'
|
CINDERCLIENT_VERSION = '1'
|
||||||
|
HEATCLIENT_VERSION = '1'
|
||||||
|
|
||||||
def __init__(self, username, password, tenant_name):
|
def __init__(self, username, password, tenant_name):
|
||||||
super(OfficialClientManager, self).__init__()
|
super(OfficialClientManager, self).__init__()
|
||||||
@@ -62,6 +65,10 @@ class OfficialClientManager(tempest.manager.Manager):
|
|||||||
self.volume_client = self._get_volume_client(username,
|
self.volume_client = self._get_volume_client(username,
|
||||||
password,
|
password,
|
||||||
tenant_name)
|
tenant_name)
|
||||||
|
self.orchestration_client = self._get_orchestration_client(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
tenant_name)
|
||||||
|
|
||||||
def _get_compute_client(self, username, password, tenant_name):
|
def _get_compute_client(self, username, password, tenant_name):
|
||||||
# Novaclient will not execute operations for anyone but the
|
# Novaclient will not execute operations for anyone but the
|
||||||
@@ -98,6 +105,32 @@ class OfficialClientManager(tempest.manager.Manager):
|
|||||||
tenant_name,
|
tenant_name,
|
||||||
auth_url)
|
auth_url)
|
||||||
|
|
||||||
|
def _get_orchestration_client(self, username=None, password=None,
|
||||||
|
tenant_name=None):
|
||||||
|
if not username:
|
||||||
|
username = self.config.identity.admin_username
|
||||||
|
if not password:
|
||||||
|
password = self.config.identity.admin_password
|
||||||
|
if not tenant_name:
|
||||||
|
tenant_name = self.config.identity.tenant_name
|
||||||
|
|
||||||
|
self._validate_credentials(username, password, tenant_name)
|
||||||
|
|
||||||
|
keystone = self._get_identity_client(username, password, tenant_name)
|
||||||
|
token = keystone.auth_token
|
||||||
|
try:
|
||||||
|
endpoint = keystone.service_catalog.url_for(
|
||||||
|
service_type='orchestration',
|
||||||
|
endpoint_type='publicURL')
|
||||||
|
except keystoneclient.exceptions.EndpointNotFound:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return heatclient.client.Client(self.HEATCLIENT_VERSION,
|
||||||
|
endpoint,
|
||||||
|
token=token,
|
||||||
|
username=username,
|
||||||
|
password=password)
|
||||||
|
|
||||||
def _get_identity_client(self, username, password, tenant_name):
|
def _get_identity_client(self, username, password, tenant_name):
|
||||||
# This identity client is not intended to check the security
|
# This identity client is not intended to check the security
|
||||||
# of the identity service, so use admin credentials by default.
|
# of the identity service, so use admin credentials by default.
|
||||||
@@ -153,13 +186,8 @@ class OfficialClientTest(tempest.test.BaseTestCase):
|
|||||||
super(OfficialClientTest, cls).setUpClass()
|
super(OfficialClientTest, cls).setUpClass()
|
||||||
cls.isolated_creds = isolated_creds.IsolatedCreds(
|
cls.isolated_creds = isolated_creds.IsolatedCreds(
|
||||||
__name__, tempest_client=False)
|
__name__, tempest_client=False)
|
||||||
if cls.config.compute.allow_tenant_isolation:
|
|
||||||
creds = cls.isolated_creds.get_primary_creds()
|
username, tenant_name, password = cls.credentials()
|
||||||
username, tenant_name, password = creds
|
|
||||||
else:
|
|
||||||
username = cls.config.identity.username
|
|
||||||
password = cls.config.identity.password
|
|
||||||
tenant_name = cls.config.identity.tenant_name
|
|
||||||
|
|
||||||
cls.manager = OfficialClientManager(username, password, tenant_name)
|
cls.manager = OfficialClientManager(username, password, tenant_name)
|
||||||
cls.compute_client = cls.manager.compute_client
|
cls.compute_client = cls.manager.compute_client
|
||||||
@@ -167,9 +195,20 @@ class OfficialClientTest(tempest.test.BaseTestCase):
|
|||||||
cls.identity_client = cls.manager.identity_client
|
cls.identity_client = cls.manager.identity_client
|
||||||
cls.network_client = cls.manager.network_client
|
cls.network_client = cls.manager.network_client
|
||||||
cls.volume_client = cls.manager.volume_client
|
cls.volume_client = cls.manager.volume_client
|
||||||
|
cls.orchestration_client = cls.manager.orchestration_client
|
||||||
cls.resource_keys = {}
|
cls.resource_keys = {}
|
||||||
cls.os_resources = []
|
cls.os_resources = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def credentials(cls):
|
||||||
|
if cls.config.compute.allow_tenant_isolation:
|
||||||
|
return cls.isolated_creds.get_primary_creds()
|
||||||
|
|
||||||
|
username = cls.config.identity.username
|
||||||
|
password = cls.config.identity.password
|
||||||
|
tenant_name = cls.config.identity.tenant_name
|
||||||
|
return username, tenant_name, password
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
# NOTE(jaypipes): Because scenario tests are typically run in a
|
# NOTE(jaypipes): Because scenario tests are typically run in a
|
||||||
@@ -498,3 +537,30 @@ class NetworkScenarioTest(OfficialClientTest):
|
|||||||
timeout=self.config.compute.ssh_timeout),
|
timeout=self.config.compute.ssh_timeout),
|
||||||
'Auth failure in connecting to %s@%s via ssh' %
|
'Auth failure in connecting to %s@%s via ssh' %
|
||||||
(username, ip_address))
|
(username, ip_address))
|
||||||
|
|
||||||
|
|
||||||
|
class OrchestrationScenarioTest(OfficialClientTest):
|
||||||
|
"""
|
||||||
|
Base class for orchestration scenario tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def credentials(cls):
|
||||||
|
username = cls.config.identity.admin_username
|
||||||
|
password = cls.config.identity.admin_password
|
||||||
|
tenant_name = cls.config.identity.tenant_name
|
||||||
|
return username, tenant_name, password
|
||||||
|
|
||||||
|
def _load_template(self, base_file, file_name):
|
||||||
|
filepath = os.path.join(os.path.dirname(os.path.realpath(base_file)),
|
||||||
|
file_name)
|
||||||
|
with open(filepath) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _stack_rand_name(cls):
|
||||||
|
return rand_name(cls.__name__ + '-')
|
||||||
|
|
||||||
|
def _create_keypair(self):
|
||||||
|
kp_name = rand_name('keypair-smoke')
|
||||||
|
return self.compute_client.keypairs.create(kp_name)
|
||||||
|
|||||||
0
tempest/scenario/orchestration/__init__.py
Normal file
0
tempest/scenario/orchestration/__init__.py
Normal file
108
tempest/scenario/orchestration/test_autoscaling.py
Normal file
108
tempest/scenario/orchestration/test_autoscaling.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from tempest.openstack.common import log as logging
|
||||||
|
from tempest.scenario import manager
|
||||||
|
from tempest.test import attr
|
||||||
|
from tempest.test import call_until_true
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoScalingTest(manager.OrchestrationScenarioTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(AutoScalingTest, self).setUp()
|
||||||
|
if not self.config.orchestration.image_ref:
|
||||||
|
raise self.skipException("No image available to test")
|
||||||
|
self.client = self.orchestration_client
|
||||||
|
|
||||||
|
def assign_keypair(self):
|
||||||
|
self.stack_name = self._stack_rand_name()
|
||||||
|
if self.config.orchestration.keypair_name:
|
||||||
|
self.keypair_name = self.config.orchestration.keypair_name
|
||||||
|
else:
|
||||||
|
self.keypair = self._create_keypair()
|
||||||
|
self.keypair_name = self.keypair.id
|
||||||
|
self.set_resource('keypair', self.keypair)
|
||||||
|
|
||||||
|
def launch_stack(self):
|
||||||
|
self.parameters = {
|
||||||
|
'KeyName': self.keypair_name,
|
||||||
|
'InstanceType': self.config.orchestration.instance_type,
|
||||||
|
'ImageId': self.config.orchestration.image_ref,
|
||||||
|
'StackStart': str(time.time())
|
||||||
|
}
|
||||||
|
|
||||||
|
# create the stack
|
||||||
|
self.template = self._load_template(__file__, 'test_autoscaling.yaml')
|
||||||
|
self.client.stacks.create(
|
||||||
|
stack_name=self.stack_name,
|
||||||
|
template=self.template,
|
||||||
|
parameters=self.parameters)
|
||||||
|
|
||||||
|
self.stack = self.client.stacks.get(self.stack_name)
|
||||||
|
self.stack_identifier = '%s/%s' % (self.stack_name, self.stack.id)
|
||||||
|
|
||||||
|
# if a keypair was set, do not delete the stack on exit to allow
|
||||||
|
# for manual post-mortums
|
||||||
|
if not self.config.orchestration.keypair_name:
|
||||||
|
self.set_resource('stack', self.stack)
|
||||||
|
|
||||||
|
@attr(type='slow')
|
||||||
|
def test_scale_up_then_down(self):
|
||||||
|
|
||||||
|
self.assign_keypair()
|
||||||
|
self.launch_stack()
|
||||||
|
|
||||||
|
sid = self.stack_identifier
|
||||||
|
timeout = self.config.orchestration.build_timeout
|
||||||
|
interval = 10
|
||||||
|
|
||||||
|
self.assertEqual('CREATE', self.stack.action)
|
||||||
|
# wait for create to complete.
|
||||||
|
self.status_timeout(self.client.stacks, sid, 'COMPLETE')
|
||||||
|
|
||||||
|
self.stack.get()
|
||||||
|
self.assertEqual('CREATE_COMPLETE', self.stack.stack_status)
|
||||||
|
|
||||||
|
# the resource SmokeServerGroup is implemented as a nested
|
||||||
|
# stack, so servers can be counted by counting the resources
|
||||||
|
# inside that nested stack
|
||||||
|
resource = self.client.resources.get(sid, 'SmokeServerGroup')
|
||||||
|
nested_stack_id = resource.physical_resource_id
|
||||||
|
|
||||||
|
def server_count():
|
||||||
|
# the number of servers is the number of resources
|
||||||
|
# in the nexted stack
|
||||||
|
self.server_count = len(
|
||||||
|
self.client.resources.list(nested_stack_id))
|
||||||
|
return self.server_count
|
||||||
|
|
||||||
|
def assertScale(from_servers, to_servers):
|
||||||
|
call_until_true(lambda: server_count() == to_servers,
|
||||||
|
timeout, interval)
|
||||||
|
self.assertEqual(to_servers, self.server_count,
|
||||||
|
'Failed scaling from %d to %d servers' % (
|
||||||
|
from_servers, to_servers))
|
||||||
|
|
||||||
|
# he marched them up to the top of the hill
|
||||||
|
assertScale(1, 2)
|
||||||
|
assertScale(2, 3)
|
||||||
|
|
||||||
|
# and he marched them down again
|
||||||
|
assertScale(3, 2)
|
||||||
|
assertScale(2, 1)
|
||||||
182
tempest/scenario/orchestration/test_autoscaling.yaml
Normal file
182
tempest/scenario/orchestration/test_autoscaling.yaml
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
HeatTemplateFormatVersion: '2012-12-12'
|
||||||
|
Description: |
|
||||||
|
Template which tests autoscaling and load balancing
|
||||||
|
Parameters:
|
||||||
|
KeyName:
|
||||||
|
Type: String
|
||||||
|
InstanceType:
|
||||||
|
Type: String
|
||||||
|
ImageId:
|
||||||
|
Type: String
|
||||||
|
StackStart:
|
||||||
|
Description: Epoch seconds when the stack was launched
|
||||||
|
Type: Number
|
||||||
|
ConsumeStartSeconds:
|
||||||
|
Description: Seconds after invocation when memory should be consumed
|
||||||
|
Type: Number
|
||||||
|
Default: '60'
|
||||||
|
ConsumeStopSeconds:
|
||||||
|
Description: Seconds after StackStart when memory should be released
|
||||||
|
Type: Number
|
||||||
|
Default: '420'
|
||||||
|
ScaleUpThreshold:
|
||||||
|
Description: Memory percentage threshold to scale up on
|
||||||
|
Type: Number
|
||||||
|
Default: '70'
|
||||||
|
ScaleDownThreshold:
|
||||||
|
Description: Memory percentage threshold to scale down on
|
||||||
|
Type: Number
|
||||||
|
Default: '60'
|
||||||
|
ConsumeMemoryLimit:
|
||||||
|
Description: Memory percentage threshold to consume
|
||||||
|
Type: Number
|
||||||
|
Default: '71'
|
||||||
|
Resources:
|
||||||
|
SmokeServerGroup:
|
||||||
|
Type: AWS::AutoScaling::AutoScalingGroup
|
||||||
|
Properties:
|
||||||
|
AvailabilityZones: {'Fn::GetAZs': ''}
|
||||||
|
LaunchConfigurationName: {Ref: LaunchConfig}
|
||||||
|
MinSize: '1'
|
||||||
|
MaxSize: '3'
|
||||||
|
SmokeServerScaleUpPolicy:
|
||||||
|
Type: AWS::AutoScaling::ScalingPolicy
|
||||||
|
Properties:
|
||||||
|
AdjustmentType: ChangeInCapacity
|
||||||
|
AutoScalingGroupName: {Ref: SmokeServerGroup}
|
||||||
|
Cooldown: '60'
|
||||||
|
ScalingAdjustment: '1'
|
||||||
|
SmokeServerScaleDownPolicy:
|
||||||
|
Type: AWS::AutoScaling::ScalingPolicy
|
||||||
|
Properties:
|
||||||
|
AdjustmentType: ChangeInCapacity
|
||||||
|
AutoScalingGroupName: {Ref: SmokeServerGroup}
|
||||||
|
Cooldown: '60'
|
||||||
|
ScalingAdjustment: '-1'
|
||||||
|
MEMAlarmHigh:
|
||||||
|
Type: AWS::CloudWatch::Alarm
|
||||||
|
Properties:
|
||||||
|
AlarmDescription: Scale-up if MEM > ScaleUpThreshold% for 10 seconds
|
||||||
|
MetricName: MemoryUtilization
|
||||||
|
Namespace: system/linux
|
||||||
|
Statistic: Average
|
||||||
|
Period: '10'
|
||||||
|
EvaluationPeriods: '1'
|
||||||
|
Threshold: {Ref: ScaleUpThreshold}
|
||||||
|
AlarmActions: [{Ref: SmokeServerScaleUpPolicy}]
|
||||||
|
Dimensions:
|
||||||
|
- Name: AutoScalingGroupName
|
||||||
|
Value: {Ref: SmokeServerGroup}
|
||||||
|
ComparisonOperator: GreaterThanThreshold
|
||||||
|
MEMAlarmLow:
|
||||||
|
Type: AWS::CloudWatch::Alarm
|
||||||
|
Properties:
|
||||||
|
AlarmDescription: Scale-down if MEM < ScaleDownThreshold% for 10 seconds
|
||||||
|
MetricName: MemoryUtilization
|
||||||
|
Namespace: system/linux
|
||||||
|
Statistic: Average
|
||||||
|
Period: '10'
|
||||||
|
EvaluationPeriods: '1'
|
||||||
|
Threshold: {Ref: ScaleDownThreshold}
|
||||||
|
AlarmActions: [{Ref: SmokeServerScaleDownPolicy}]
|
||||||
|
Dimensions:
|
||||||
|
- Name: AutoScalingGroupName
|
||||||
|
Value: {Ref: SmokeServerGroup}
|
||||||
|
ComparisonOperator: LessThanThreshold
|
||||||
|
CfnUser:
|
||||||
|
Type: AWS::IAM::User
|
||||||
|
SmokeKeys:
|
||||||
|
Type: AWS::IAM::AccessKey
|
||||||
|
Properties:
|
||||||
|
UserName: {Ref: CfnUser}
|
||||||
|
SmokeSecurityGroup:
|
||||||
|
Type: AWS::EC2::SecurityGroup
|
||||||
|
Properties:
|
||||||
|
GroupDescription: Standard firewall rules
|
||||||
|
SecurityGroupIngress:
|
||||||
|
- {IpProtocol: tcp, FromPort: '22', ToPort: '22', CidrIp: 0.0.0.0/0}
|
||||||
|
- {IpProtocol: tcp, FromPort: '80', ToPort: '80', CidrIp: 0.0.0.0/0}
|
||||||
|
LaunchConfig:
|
||||||
|
Type: AWS::AutoScaling::LaunchConfiguration
|
||||||
|
Metadata:
|
||||||
|
AWS::CloudFormation::Init:
|
||||||
|
config:
|
||||||
|
files:
|
||||||
|
/etc/cfn/cfn-credentials:
|
||||||
|
content:
|
||||||
|
Fn::Replace:
|
||||||
|
- $AWSAccessKeyId: {Ref: SmokeKeys}
|
||||||
|
$AWSSecretKey: {'Fn::GetAtt': [SmokeKeys, SecretAccessKey]}
|
||||||
|
- |
|
||||||
|
AWSAccessKeyId=$AWSAccessKeyId
|
||||||
|
AWSSecretKey=$AWSSecretKey
|
||||||
|
mode: '000400'
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
/root/watch_loop:
|
||||||
|
content:
|
||||||
|
Fn::Replace:
|
||||||
|
- _hi_: {Ref: MEMAlarmHigh}
|
||||||
|
_lo_: {Ref: MEMAlarmLow}
|
||||||
|
- |
|
||||||
|
#!/bin/bash
|
||||||
|
while :
|
||||||
|
do
|
||||||
|
/opt/aws/bin/cfn-push-stats --watch _hi_ --mem-util
|
||||||
|
/opt/aws/bin/cfn-push-stats --watch _lo_ --mem-util
|
||||||
|
sleep 4
|
||||||
|
done
|
||||||
|
mode: '000700'
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
/root/consume_memory:
|
||||||
|
content:
|
||||||
|
Fn::Replace:
|
||||||
|
- StackStart: {Ref: StackStart}
|
||||||
|
ConsumeStopSeconds: {Ref: ConsumeStopSeconds}
|
||||||
|
ConsumeStartSeconds: {Ref: ConsumeStartSeconds}
|
||||||
|
ConsumeMemoryLimit: {Ref: ConsumeMemoryLimit}
|
||||||
|
- |
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import psutil
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
a = []
|
||||||
|
sleep_until_consume = ConsumeStartSeconds
|
||||||
|
stack_start = StackStart
|
||||||
|
consume_stop_time = stack_start + ConsumeStopSeconds
|
||||||
|
memory_limit = ConsumeMemoryLimit
|
||||||
|
if sleep_until_consume > 0:
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(sleep_until_consume)
|
||||||
|
while psutil.virtual_memory().percent < memory_limit:
|
||||||
|
sys.stdout.flush()
|
||||||
|
a.append(' ' * 10**5)
|
||||||
|
time.sleep(0.1)
|
||||||
|
sleep_until_exit = consume_stop_time - time.time()
|
||||||
|
if sleep_until_exit > 0:
|
||||||
|
time.sleep(sleep_until_exit)
|
||||||
|
mode: '000700'
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
Properties:
|
||||||
|
ImageId: {Ref: ImageId}
|
||||||
|
InstanceType: {Ref: InstanceType}
|
||||||
|
KeyName: {Ref: KeyName}
|
||||||
|
SecurityGroups: [{Ref: SmokeSecurityGroup}]
|
||||||
|
UserData:
|
||||||
|
Fn::Base64:
|
||||||
|
Fn::Replace:
|
||||||
|
- ConsumeStopSeconds: {Ref: ConsumeStopSeconds}
|
||||||
|
ConsumeStartSeconds: {Ref: ConsumeStartSeconds}
|
||||||
|
ConsumeMemoryLimit: {Ref: ConsumeMemoryLimit}
|
||||||
|
- |
|
||||||
|
#!/bin/bash -v
|
||||||
|
/opt/aws/bin/cfn-init
|
||||||
|
# report on memory consumption every 4 seconds
|
||||||
|
/root/watch_loop &
|
||||||
|
# wait ConsumeStartSeconds then ramp up memory consumption
|
||||||
|
# until it is over ConsumeMemoryLimit%
|
||||||
|
# then exits ConsumeStopSeconds seconds after stack launch
|
||||||
|
/root/consume_memory > /root/consume_memory.log &
|
||||||
Reference in New Issue
Block a user