diff --git a/heat/engine/resources/server.py b/heat/engine/resources/server.py index 52fb93caad..e778fcd4b6 100644 --- a/heat/engine/resources/server.py +++ b/heat/engine/resources/server.py @@ -38,13 +38,13 @@ class Server(resource.Resource): FLAVOR_UPDATE_POLICY, IMAGE_UPDATE_POLICY, KEY_NAME, ADMIN_USER, AVAILABILITY_ZONE, SECURITY_GROUPS, NETWORKS, SCHEDULER_HINTS, METADATA, USER_DATA_FORMAT, USER_DATA, - RESERVATION_ID, CONFIG_DRIVE, DISK_CONFIG, + RESERVATION_ID, CONFIG_DRIVE, DISK_CONFIG, PERSONALITY, ) = ( 'name', 'image', 'block_device_mapping', 'flavor', 'flavor_update_policy', 'image_update_policy', 'key_name', 'admin_user', 'availability_zone', 'security_groups', 'networks', 'scheduler_hints', 'metadata', 'user_data_format', 'user_data', - 'reservation_id', 'config_drive', 'diskConfig', + 'reservation_id', 'config_drive', 'diskConfig', 'personality', ) _BLOCK_DEVICE_MAPPING_KEYS = ( @@ -231,6 +231,12 @@ class Server(resource.Resource): constraints.AllowedValues(['AUTO', 'MANUAL']), ] ), + PERSONALITY: properties.Schema( + properties.Schema.MAP, + _('A map of files to create/overwrite on the server upon boot. ' + 'Keys are file names and values are the file contents.'), + default={} + ) } attributes_schema = { @@ -268,6 +274,10 @@ class Server(resource.Resource): return super(Server, self).physical_resource_name() + def _personality(self): + # This method is overridden by the derived CloudServer resource + return self.properties.get(self.PERSONALITY) + def handle_create(self): security_groups = self.properties.get(self.SECURITY_GROUPS) @@ -320,7 +330,8 @@ class Server(resource.Resource): block_device_mapping=block_device_mapping, reservation_id=reservation_id, config_drive=config_drive, - disk_config=disk_config) + disk_config=disk_config, + files=self._personality()) finally: # Avoid a race condition where the thread could be cancelled # before the ID is stored @@ -570,18 +581,39 @@ class Server(resource.Resource): network=network[self.NETWORK_ID], server=self.name)) + # retrieve provider's absolute limits if it will be needed + metadata = self.properties.get(self.METADATA) + personality = self._personality() + if metadata is not None or personality is not None: + limits = nova_utils.absolute_limits(self.nova()) + # verify that the number of metadata entries is not greater # than the maximum number allowed in the provider's absolute # limits - metadata = self.properties.get('metadata') if metadata is not None: - limits = nova_utils.absolute_limits(self.nova()) if len(metadata) > limits['maxServerMeta']: msg = _('Instance metadata must not contain greater than %s ' 'entries. This is the maximum number allowed by your ' 'service provider') % limits['maxServerMeta'] raise exception.StackValidationFailed(message=msg) + # verify the number of personality files and the size of each + # personality file against the provider's absolute limits + if personality is not None: + if len(personality) > limits['maxPersonality']: + msg = _("The personality property may not contain " + "greater than %s entries.") % limits['maxPersonality'] + raise exception.StackValidationFailed(message=msg) + + for path, contents in personality.items(): + if len(bytes(contents)) > limits['maxPersonalitySize']: + msg = (_("The contents of personality file \"%(path)s\" " + "is larger than the maximum allowed personality " + "file size (%(max_size)s bytes).") % + {'path': path, + 'max_size': limits['maxPersonalitySize']}) + raise exception.StackValidationFailed(message=msg) + def handle_delete(self): ''' Delete a server, blocking until it is disposed by OpenStack diff --git a/heat/tests/test_server.py b/heat/tests/test_server.py index dda3eab800..fbe3bc0be1 100644 --- a/heat/tests/test_server.py +++ b/heat/tests/test_server.py @@ -64,6 +64,22 @@ class ServersTest(HeatTestCase): super(ServersTest, self).setUp() self.fc = fakes.FakeClient() utils.setup_dummy_db() + self.limits = self.m.CreateMockAnything() + self.limits.absolute = self._limits_absolute() + + def _limits_absolute(self): + max_personality = self.m.CreateMockAnything() + max_personality.name = 'maxPersonality' + max_personality.value = 5 + max_personality_size = self.m.CreateMockAnything() + max_personality_size.name = 'maxPersonalitySize' + max_personality_size.value = 10240 + max_server_meta = self.m.CreateMockAnything() + max_server_meta.name = 'maxServerMeta' + max_server_meta.value = 3 + yield max_personality + yield max_personality_size + yield max_server_meta def _setup_test_stack(self, stack_name): t = template_format.parse(wp_template) @@ -106,7 +122,7 @@ class ServersTest(HeatTestCase): userdata=mox.IgnoreArg(), scheduler_hints=None, meta=None, nics=None, availability_zone=None, block_device_mapping=None, config_drive=None, - disk_config=None, reservation_id=None).AndReturn( + disk_config=None, reservation_id=None, files={}).AndReturn( return_server) return server @@ -224,7 +240,7 @@ class ServersTest(HeatTestCase): userdata=mox.IgnoreArg(), scheduler_hints=None, meta=instance_meta, nics=None, availability_zone=None, block_device_mapping=None, config_drive=None, - disk_config=None, reservation_id=None).AndReturn( + disk_config=None, reservation_id=None, files={}).AndReturn( return_server) self.m.StubOutWithMock(server, 'nova') @@ -380,7 +396,7 @@ class ServersTest(HeatTestCase): userdata='wordpress', scheduler_hints=None, meta=None, nics=None, availability_zone=None, block_device_mapping=None, config_drive=None, - disk_config=None, reservation_id=None).AndReturn( + disk_config=None, reservation_id=None, files={}).AndReturn( return_server) self.m.ReplayAll() @@ -1300,13 +1316,8 @@ class ServersTest(HeatTestCase): server = servers.Server('server_create_image_err', t['Resources']['WebServer'], stack) - limits = self.m.CreateMockAnything() - max_server_meta = self.m.CreateMockAnything() - max_server_meta.name = 'maxServerMeta' - max_server_meta.value = 3 - limits.absolute = [max_server_meta] self.m.StubOutWithMock(self.fc.limits, 'get') - self.fc.limits.get().AndReturn(limits) + self.fc.limits.get().MultipleTimes().AndReturn(self.limits) self.m.StubOutWithMock(server, 'nova') server.nova().MultipleTimes().AndReturn(self.fc) @@ -1328,13 +1339,57 @@ class ServersTest(HeatTestCase): server = servers.Server('server_create_image_err', t['Resources']['WebServer'], stack) - limits = self.m.CreateMockAnything() - max_server_meta = self.m.CreateMockAnything() - max_server_meta.name = 'maxServerMeta' - max_server_meta.value = 3 - limits.absolute = [max_server_meta] self.m.StubOutWithMock(self.fc.limits, 'get') - self.fc.limits.get().AndReturn(limits) + self.fc.limits.get().MultipleTimes().AndReturn(self.limits) + + self.m.StubOutWithMock(server, 'nova') + server.nova().MultipleTimes().AndReturn(self.fc) + self.m.ReplayAll() + self.assertIsNone(server.validate()) + self.m.VerifyAll() + + def test_server_validate_too_many_personality(self): + stack_name = 'srv_val' + (t, stack) = self._setup_test_stack(stack_name) + + t['Resources']['WebServer']['Properties']['personality'] = \ + {"/fake/path1": "fake contents1", + "/fake/path2": "fake_contents2", + "/fake/path3": "fake_contents3", + "/fake/path4": "fake_contents4", + "/fake/path5": "fake_contents5", + "/fake/path6": "fake_contents6"} + server = servers.Server('server_create_image_err', + t['Resources']['WebServer'], stack) + + self.m.StubOutWithMock(self.fc.limits, 'get') + self.fc.limits.get().MultipleTimes().AndReturn(self.limits) + + self.m.StubOutWithMock(server, 'nova') + server.nova().MultipleTimes().AndReturn(self.fc) + self.m.ReplayAll() + + exc = self.assertRaises(exception.StackValidationFailed, + server.validate) + self.assertEqual("The personality property may not contain " + "greater than 5 entries.", str(exc)) + self.m.VerifyAll() + + def test_server_validate_personality_okay(self): + stack_name = 'srv_val' + (t, stack) = self._setup_test_stack(stack_name) + + t['Resources']['WebServer']['Properties']['personality'] = \ + {"/fake/path1": "fake contents1", + "/fake/path2": "fake_contents2", + "/fake/path3": "fake_contents3", + "/fake/path4": "fake_contents4", + "/fake/path5": "fake_contents5"} + server = servers.Server('server_create_image_err', + t['Resources']['WebServer'], stack) + + self.m.StubOutWithMock(self.fc.limits, 'get') + self.fc.limits.get().MultipleTimes().AndReturn(self.limits) self.m.StubOutWithMock(server, 'nova') server.nova().MultipleTimes().AndReturn(self.fc) @@ -1342,3 +1397,45 @@ class ServersTest(HeatTestCase): self.assertIsNone(server.validate()) self.m.VerifyAll() + + def test_server_validate_personality_file_size_okay(self): + stack_name = 'srv_val' + (t, stack) = self._setup_test_stack(stack_name) + + t['Resources']['WebServer']['Properties']['personality'] = \ + {"/fake/path1": "a" * 10240} + server = servers.Server('server_create_image_err', + t['Resources']['WebServer'], stack) + + self.m.StubOutWithMock(self.fc.limits, 'get') + self.fc.limits.get().MultipleTimes().AndReturn(self.limits) + + self.m.StubOutWithMock(server, 'nova') + server.nova().MultipleTimes().AndReturn(self.fc) + self.m.ReplayAll() + + self.assertIsNone(server.validate()) + self.m.VerifyAll() + + def test_server_validate_personality_file_size_too_big(self): + stack_name = 'srv_val' + (t, stack) = self._setup_test_stack(stack_name) + + t['Resources']['WebServer']['Properties']['personality'] = \ + {"/fake/path1": "a" * 10241} + server = servers.Server('server_create_image_err', + t['Resources']['WebServer'], stack) + + self.m.StubOutWithMock(self.fc.limits, 'get') + self.fc.limits.get().MultipleTimes().AndReturn(self.limits) + + self.m.StubOutWithMock(server, 'nova') + server.nova().MultipleTimes().AndReturn(self.fc) + self.m.ReplayAll() + + exc = self.assertRaises(exception.StackValidationFailed, + server.validate) + self.assertEqual("The contents of personality file \"/fake/path1\" " + "is larger than the maximum allowed personality " + "file size (10240 bytes).", str(exc)) + self.m.VerifyAll() diff --git a/heat/tests/v1_1/fakes.py b/heat/tests/v1_1/fakes.py index a07ec0c005..5167c079dd 100644 --- a/heat/tests/v1_1/fakes.py +++ b/heat/tests/v1_1/fakes.py @@ -373,4 +373,6 @@ class FakeHTTPClient(base_client.HTTPClient): # Limits # def get_limits(self, *kw): - return (200, {'limits': {'absolute': {'maxServerMeta': 3}}}) + return (200, {'limits': {'absolute': {'maxServerMeta': 3, + 'maxPersonalitySize': 10240, + 'maxPersonality': 5}}})