# # 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 re import mock import six from testtools import matchers from heat.common import exception from heat.common import template_format from heat.engine import node_data from heat.engine import stack as parser from heat.engine import template from heat.tests import common from heat.tests import utils class TestRandomString(common.HeatTestCase): template_random_string = ''' HeatTemplateFormatVersion: '2012-12-12' Resources: secret1: Type: OS::Heat::RandomString secret2: Type: OS::Heat::RandomString Properties: length: 10 secret3: Type: OS::Heat::RandomString Properties: length: 32 character_classes: - class: digits min: 1 - class: uppercase min: 1 - class: lowercase min: 20 character_sequences: - sequence: (),[]{} min: 1 - sequence: $_ min: 2 - sequence: '@' min: 5 secret4: Type: OS::Heat::RandomString Properties: length: 25 character_classes: - class: digits min: 1 - class: uppercase min: 1 - class: lowercase min: 20 secret5: Type: OS::Heat::RandomString Properties: length: 10 character_sequences: - sequence: (),[]{} min: 1 - sequence: $_ min: 2 - sequence: '@' min: 5 ''' def create_stack(self, templ): self.stack = self.parse_stack(template_format.parse(templ)) self.assertIsNone(self.stack.create()) return self.stack def parse_stack(self, t): stack_name = 'test_stack' tmpl = template.Template(t) stack = parser.Stack(utils.dummy_context(), stack_name, tmpl) stack.validate() stack.store() return stack def assert_min(self, pattern, string, minimum): self.assertGreaterEqual(len(re.findall(pattern, string)), minimum) def test_random_string(self): stack = self.create_stack(self.template_random_string) secret1 = stack['secret1'] random_string = secret1.FnGetAtt('value') self.assert_min('[a-zA-Z0-9]', random_string, 32) self.assertRaises(exception.InvalidTemplateAttribute, secret1.FnGetAtt, 'foo') self.assertEqual(secret1.FnGetRefId(), random_string) secret2 = stack['secret2'] random_string = secret2.FnGetAtt('value') self.assert_min('[a-zA-Z0-9]', random_string, 10) self.assertEqual(secret2.FnGetRefId(), random_string) secret3 = stack['secret3'] random_string = secret3.FnGetAtt('value') self.assertEqual(32, len(random_string)) self.assert_min('[0-9]', random_string, 1) self.assert_min('[A-Z]', random_string, 1) self.assert_min('[a-z]', random_string, 20) self.assert_min(r'[(),\[\]{}]', random_string, 1) self.assert_min('[$_]', random_string, 2) self.assert_min('@', random_string, 5) self.assertEqual(secret3.FnGetRefId(), random_string) secret4 = stack['secret4'] random_string = secret4.FnGetAtt('value') self.assertEqual(25, len(random_string)) self.assert_min('[0-9]', random_string, 1) self.assert_min('[A-Z]', random_string, 1) self.assert_min('[a-z]', random_string, 20) self.assertEqual(secret4.FnGetRefId(), random_string) secret5 = stack['secret5'] random_string = secret5.FnGetAtt('value') self.assertEqual(10, len(random_string)) self.assert_min(r'[(),\[\]{}]', random_string, 1) self.assert_min('[$_]', random_string, 2) self.assert_min('@', random_string, 5) self.assertEqual(secret5.FnGetRefId(), random_string) # Prove the name is returned before create sets the ID secret5.resource_id = None self.assertEqual('secret5', secret5.FnGetRefId()) def test_hidden_sequence_property(self): hidden_prop_templ = ''' HeatTemplateFormatVersion: '2012-12-12' Resources: secret: Type: OS::Heat::RandomString Properties: length: 100 sequence: octdigits ''' stack = self.create_stack(hidden_prop_templ) secret = stack['secret'] random_string = secret.FnGetAtt('value') self.assert_min('[0-7]', random_string, 100) self.assertEqual(secret.FnGetRefId(), random_string) # check, that property was translated according to the TranslationRule self.assertIsNone(secret.properties['sequence']) expected = [{'class': u'octdigits', 'min': 1}] self.assertEqual(expected, secret.properties['character_classes']) def test_random_string_refid_convergence_cache_data(self): t = template_format.parse(self.template_random_string) cache_data = {'secret1': node_data.NodeData.from_dict({ 'uuid': mock.ANY, 'id': mock.ANY, 'action': 'CREATE', 'status': 'COMPLETE', 'reference_id': 'xyz' })} stack = utils.parse_stack(t, cache_data=cache_data) rsrc = stack.defn['secret1'] self.assertEqual('xyz', rsrc.FnGetRefId()) def test_invalid_length(self): template_random_string = ''' HeatTemplateFormatVersion: '2012-12-12' Resources: secret: Type: OS::Heat::RandomString Properties: length: 5 character_classes: - class: digits min: 5 character_sequences: - sequence: (),[]{} min: 1 ''' exc = self.assertRaises(exception.StackValidationFailed, self.create_stack, template_random_string) self.assertEqual("Length property cannot be smaller than combined " "character class and character sequence minimums", six.text_type(exc)) def test_max_length(self): template_random_string = ''' HeatTemplateFormatVersion: '2012-12-12' Resources: secret: Type: OS::Heat::RandomString Properties: length: 512 ''' stack = self.create_stack(template_random_string) secret = stack['secret'] random_string = secret.FnGetAtt('value') self.assertEqual(512, len(random_string)) self.assertEqual(secret.FnGetRefId(), random_string) def test_exceeds_max_length(self): template_random_string = ''' HeatTemplateFormatVersion: '2012-12-12' Resources: secret: Type: OS::Heat::RandomString Properties: length: 513 ''' exc = self.assertRaises(exception.StackValidationFailed, self.create_stack, template_random_string) self.assertIn('513 is out of range (min: 1, max: 512)', six.text_type(exc)) class TestGenerateRandomString(common.HeatTestCase): scenarios = [ ('lettersdigits', dict( length=1, seq='lettersdigits', pattern='[a-zA-Z0-9]')), ('letters', dict( length=10, seq='letters', pattern='[a-zA-Z]')), ('lowercase', dict( length=100, seq='lowercase', pattern='[a-z]')), ('uppercase', dict( length=50, seq='uppercase', pattern='[A-Z]')), ('digits', dict( length=512, seq='digits', pattern='[0-9]')), ('hexdigits', dict( length=16, seq='hexdigits', pattern='[A-F0-9]')), ('octdigits', dict( length=32, seq='octdigits', pattern='[0-7]')) ] template_rs = ''' HeatTemplateFormatVersion: '2012-12-12' Resources: secret: Type: OS::Heat::RandomString ''' def parse_stack(self, t): stack_name = 'test_stack' tmpl = template.Template(t) stack = parser.Stack(utils.dummy_context(), stack_name, tmpl) stack.validate() stack.store() return stack # test was saved to test backward compatibility with old behavior def test_generate_random_string_backward_compatiable(self): stack = self.parse_stack(template_format.parse(self.template_rs)) secret = stack['secret'] char_classes = secret.properties['character_classes'] for char_cl in char_classes: char_cl['class'] = self.seq # run each test multiple times to confirm random generator # doesn't generate a matching pattern by chance for i in range(1, 32): r = secret._generate_random_string(None, char_classes, self.length) self.assertThat(r, matchers.HasLength(self.length)) regex = '%s{%s}' % (self.pattern, self.length) self.assertThat(r, matchers.MatchesRegex(regex))