heat/heat/tests/test_metadata_refresh.py

356 lines
12 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 mock
from oslo_serialization import jsonutils
from heat.common import identifier
from heat.common import template_format
from heat.engine.clients.os import glance
from heat.engine.clients.os import heat_plugin
from heat.engine.clients.os import nova
from heat.engine import environment
from heat.engine.resources.aws.cfn.wait_condition_handle import (
WaitConditionHandle)
from heat.engine.resources.aws.ec2 import instance
from heat.engine.resources.openstack.nova import server
from heat.engine import scheduler
from heat.engine import service
from heat.engine import stack as stk
from heat.engine import stk_defn
from heat.engine import template as tmpl
from heat.tests import common
from heat.tests import utils
TEST_TEMPLATE_METADATA = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "",
"Parameters" : {
"KeyName" : {"Type" : "String", "Default": "mine" },
},
"Resources" : {
"S1": {
"Type": "AWS::EC2::Instance",
"Metadata" : {
"AWS::CloudFormation::Init" : {
"config" : {
"files" : {
"/tmp/random_file" : {
"content" : { "Fn::Join" : ["", [
"s2-ip=", {"Fn::GetAtt": ["S2", "PublicIp"]}
]]},
"mode" : "000400",
"owner" : "root",
"group" : "root"
}
}
}
}
},
"Properties": {
"ImageId" : "a",
"InstanceType" : "m1.large",
"KeyName" : { "Ref" : "KeyName" },
"UserData" : "#!/bin/bash -v\n"
}
},
"S2": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId" : "a",
"InstanceType" : "m1.large",
"KeyName" : { "Ref" : "KeyName" },
"UserData" : "#!/bin/bash -v\n"
}
}
}
}
'''
TEST_TEMPLATE_WAIT_CONDITION = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Just a WaitCondition.",
"Parameters" : {
"KeyName" : {"Type" : "String", "Default": "mine" },
},
"Resources" : {
"WH" : {
"Type" : "AWS::CloudFormation::WaitConditionHandle"
},
"S1": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId" : "a",
"InstanceType" : "m1.large",
"KeyName" : { "Ref" : "KeyName" },
"UserData" : { "Fn::Join" : [ "", [ "#!/bin/bash -v\n",
"echo ",
{ "Ref" : "WH" },
"\n" ] ] }
}
},
"WC" : {
"Type" : "AWS::CloudFormation::WaitCondition",
"DependsOn": "S1",
"Properties" : {
"Handle" : {"Ref" : "WH"},
"Timeout" : "5"
}
},
"S2": {
"Type": "AWS::EC2::Instance",
"Metadata" : {
"test" : {"Fn::GetAtt": ["WC", "Data"]}
},
"Properties": {
"ImageId" : "a",
"InstanceType" : "m1.large",
"KeyName" : { "Ref" : "KeyName" },
"UserData" : "#!/bin/bash -v\n"
}
}
}
}
'''
TEST_TEMPLATE_SERVER = '''
heat_template_version: 2013-05-23
resources:
instance1:
type: OS::Nova::Server
metadata: {"template_data": {get_attr: [instance2, networks]}}
properties:
image: cirros-0.3.2-x86_64-disk
flavor: m1.small
key_name: stack_key
instance2:
type: OS::Nova::Server
metadata: {'apples': 'pears'}
properties:
image: cirros-0.3.2-x86_64-disk
flavor: m1.small
key_name: stack_key
'''
class MetadataRefreshTest(common.HeatTestCase):
@mock.patch.object(nova.NovaClientPlugin, 'find_flavor_by_name_or_id')
@mock.patch.object(glance.GlanceClientPlugin, 'find_image_by_name_or_id')
@mock.patch.object(instance.Instance, 'handle_create')
@mock.patch.object(instance.Instance, 'check_create_complete')
@mock.patch.object(instance.Instance, '_resolve_attribute')
def test_FnGetAtt_metadata_updated(self, mock_get, mock_check,
mock_handle, *args):
"""Tests that metadata gets updated when FnGetAtt return changes."""
# Setup
temp = template_format.parse(TEST_TEMPLATE_METADATA)
template = tmpl.Template(temp,
env=environment.Environment({}))
ctx = utils.dummy_context()
stack = stk.Stack(ctx, 'test_stack', template, disable_rollback=True)
stack.store()
self.stub_KeypairConstraint_validate()
# Configure FnGetAtt to return different values on subsequent calls
mock_get.side_effect = [
'10.0.0.1',
'10.0.0.2',
]
# Initial resolution of the metadata
stack.create()
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
# Sanity check on S2
s2 = stack['S2']
self.assertEqual((s2.CREATE, s2.COMPLETE), s2.state)
# Verify S1 is using the initial value from S2
s1 = stack['S1']
content = self._get_metadata_content(s1.metadata_get())
self.assertEqual('s2-ip=10.0.0.1', content)
# This is not a terribly realistic test - the metadata updates below
# happen in run_alarm_action() in service_stack_watch, and actually
# operate on a freshly loaded stack so there's no cached attributes.
# Clear the attributes cache here to keep it passing.
s2.attributes.reset_resolved_values()
# Run metadata update to pick up the new value from S2
# (simulating run_alarm_action() in service_stack_watch)
s2.metadata_update()
stk_defn.update_resource_data(stack.defn, s2.name, s2.node_data())
s1.metadata_update()
stk_defn.update_resource_data(stack.defn, s1.name, s1.node_data())
# Verify the updated value is correct in S1
content = self._get_metadata_content(s1.metadata_get())
self.assertEqual('s2-ip=10.0.0.2', content)
# Verify outgoing calls
mock_get.assert_has_calls([
mock.call('PublicIp'),
mock.call('PublicIp')])
self.assertEqual(2, mock_handle.call_count)
self.assertEqual(2, mock_check.call_count)
@staticmethod
def _get_metadata_content(m):
tmp = m['AWS::CloudFormation::Init']['config']['files']
return tmp['/tmp/random_file']['content']
class WaitConditionMetadataUpdateTest(common.HeatTestCase):
def setUp(self):
super(WaitConditionMetadataUpdateTest, self).setUp()
self.man = service.EngineService('a-host', 'a-topic')
@mock.patch.object(nova.NovaClientPlugin, 'find_flavor_by_name_or_id')
@mock.patch.object(glance.GlanceClientPlugin, 'find_image_by_name_or_id')
@mock.patch.object(heat_plugin.HeatClientPlugin, 'get_heat_cfn_url')
@mock.patch.object(instance.Instance, 'handle_create')
@mock.patch.object(instance.Instance, 'check_create_complete')
@mock.patch.object(scheduler.TaskRunner, '_sleep')
@mock.patch.object(WaitConditionHandle, 'identifier')
def test_wait_metadata(self, mock_identifier, mock_sleep,
mock_check, mock_handle, mock_get, *args):
"""Tests a wait condition metadata update after a signal call."""
mock_get.return_value = 'http://server.test:8000/v1'
# Setup Stack
temp = template_format.parse(TEST_TEMPLATE_WAIT_CONDITION)
template = tmpl.Template(temp)
ctx = utils.dummy_context()
stack = stk.Stack(ctx, 'test-stack', template, disable_rollback=True)
stack.store()
self.stub_KeypairConstraint_validate()
res_id = identifier.ResourceIdentifier('test_tenant_id', stack.name,
stack.id, '', 'WH')
mock_identifier.return_value = res_id
watch = stack['WC']
inst = stack['S2']
# Setup Sleep Behavior
self.run_empty = True
def check_empty(sleep_time):
self.assertEqual('{}', watch.FnGetAtt('Data'))
self.assertIsNone(inst.metadata_get()['test'])
def update_metadata(unique_id, data, reason):
self.man.resource_signal(ctx,
dict(stack.identifier()),
'WH',
{'Data': data, 'Reason': reason,
'Status': 'SUCCESS',
'UniqueId': unique_id},
sync_call=True)
def post_success(sleep_time):
update_metadata('123', 'foo', 'bar')
def side_effect_popper(sleep_time):
wh = stack['WH']
if wh.status == wh.IN_PROGRESS:
return
elif self.run_empty:
self.run_empty = False
check_empty(sleep_time)
else:
post_success(sleep_time)
mock_sleep.side_effect = side_effect_popper
# Test Initial Creation
stack.create()
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
self.assertEqual('{"123": "foo"}', watch.FnGetAtt('Data'))
self.assertEqual('{"123": "foo"}', inst.metadata_get()['test'])
# Test Update
update_metadata('456', 'blarg', 'wibble')
self.assertEqual({'123': 'foo', '456': 'blarg'},
jsonutils.loads(watch.FnGetAtt('Data')))
self.assertEqual('{"123": "foo"}',
inst.metadata_get()['test'])
self.assertEqual(
{'123': 'foo', '456': 'blarg'},
jsonutils.loads(inst.metadata_get(refresh=True)['test']))
# Verify outgoing calls
self.assertEqual(2, mock_handle.call_count)
self.assertEqual(2, mock_check.call_count)
class MetadataRefreshServerTest(common.HeatTestCase):
@mock.patch.object(nova.NovaClientPlugin, 'find_flavor_by_name_or_id',
return_value=1)
@mock.patch.object(glance.GlanceClientPlugin, 'find_image_by_name_or_id',
return_value=1)
@mock.patch.object(server.Server, 'handle_create')
@mock.patch.object(server.Server, 'check_create_complete')
@mock.patch.object(server.Server, 'get_attribute', new_callable=mock.Mock)
def test_FnGetAtt_metadata_update(self, mock_get, mock_check,
mock_handle, *args):
temp = template_format.parse(TEST_TEMPLATE_SERVER)
template = tmpl.Template(temp,
env=environment.Environment({}))
ctx = utils.dummy_context()
stack = stk.Stack(ctx, 'test-stack', template, disable_rollback=True)
stack.store()
self.stub_KeypairConstraint_validate()
# Note dummy addresses are from TEST-NET-1 ref rfc5737
mock_get.side_effect = ['192.0.2.1', '192.0.2.2']
# Test
stack.create()
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
s1 = stack['instance1']
s2 = stack['instance2']
md = s1.metadata_get()
self.assertEqual({u'template_data': '192.0.2.1'}, md)
# Now set some metadata via the resource, like is done by
# _populate_deployments_metadata. This should be persisted over
# calls to metadata_update()
new_md = {u'template_data': '192.0.2.2', 'set_by_rsrc': 'orange'}
s1.metadata_set(new_md)
md = s1.metadata_get(refresh=True)
self.assertEqual(new_md, md)
s2.attributes.reset_resolved_values()
stk_defn.update_resource_data(stack.defn, s2.name, s2.node_data())
s1.metadata_update()
md = s1.metadata_get(refresh=True)
self.assertEqual(new_md, md)
# Verify outgoing calls
mock_get.assert_has_calls([
mock.call('networks'),
mock.call('networks')])
self.assertEqual(2, mock_handle.call_count)
self.assertEqual(2, mock_check.call_count)