Resource type for software configuration

Implementation of the SoftwareConfig resource. Some notes on the implementation:
    * This is a simple wrapper over the REST API, and is essentially
      just for defining data which gets stored.
    * SoftwareConfig will always be UpdateReplace, and the REST entity
      is immutable.
    * OS::Heat::SoftwareConfig will sometimes be used in a template directly
      and sometimes inside a resource provider template which
      defines CM-tool specific properties and aggregates the result into
      the OS::Heat::SoftwareConfig config property.

Implements: blueprint hot-software-config

Change-Id: I7350c31ec59d152751c6aa7d811a91e1df62e89d
This commit is contained in:
Steve Baker 2014-01-18 17:08:33 +13:00 committed by JUN JIE NAN
parent 6cef9c42e9
commit 21f60b155e
4 changed files with 291 additions and 0 deletions

View File

@ -327,3 +327,7 @@ class StackResourceLimitExceeded(HeatException):
class ActionInProgress(HeatException):
msg_fmt = _("Stack %(stack_name)s already has an action (%(action)s) "
"in progress.")
class SoftwareConfigMissing(HeatException):
msg_fmt = _("The config (%(software_config_id)s) could not be found.")

View File

@ -0,0 +1,178 @@
# 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 heat.common import exception
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.openstack.common.gettextutils import _
from heat.openstack.common import log as logging
import heatclient.exc as heat_exp
logger = logging.getLogger(__name__)
class SoftwareConfig(resource.Resource):
PROPERTIES = (
GROUP, CONFIG, OPTIONS, INPUTS, OUTPUTS
) = (
'group', 'config', 'options', 'inputs', 'outputs'
)
IO_PROPERTIES = (
NAME, DESCRIPTION, TYPE, DEFAULT, ERROR_OUTPUT
) = (
'name', 'description', 'type', 'default', 'error_output'
)
input_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the input.'),
required=True
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description of the input.')
),
TYPE: properties.Schema(
properties.Schema.STRING,
_('Type of the value of the input.'),
default='String',
constraints=[constraints.AllowedValues((
'String', 'Number', 'CommaDelimitedList', 'Json'))]
),
DEFAULT: properties.Schema(
properties.Schema.STRING,
_('Default value for the input if none is specified.'),
),
}
output_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the output.'),
required=True
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description of the output.')
),
TYPE: properties.Schema(
properties.Schema.STRING,
_('Type of the value of the output.'),
default='String',
constraints=[constraints.AllowedValues((
'String', 'Number', 'CommaDelimitedList', 'Json'))]
),
ERROR_OUTPUT: properties.Schema(
properties.Schema.BOOLEAN,
_('Denotes that the deployment is in an error state if this '
'output has a value.'),
default=False
)
}
properties_schema = {
GROUP: properties.Schema(
properties.Schema.STRING,
_('Namespace to group this software config by when delivered to '
'a server. This may imply what configuration tool is going to '
'perform the configuration.'),
default='Heat::Ungrouped',
required=True
),
CONFIG: properties.Schema(
properties.Schema.STRING,
_('Configuration script or manifest which specifies what actual '
'configuration is performed.'),
),
OPTIONS: properties.Schema(
properties.Schema.MAP,
_('Map containing options specific to the configuration '
'management tool used by this.'),
),
INPUTS: properties.Schema(
properties.Schema.LIST,
_('Schema representing the inputs that this software config is '
'expecting.'),
schema=properties.Schema(properties.Schema.MAP,
schema=input_schema)
),
OUTPUTS: properties.Schema(
properties.Schema.LIST,
_('Schema representing the outputs that this software config '
'will produce.'),
schema=properties.Schema(properties.Schema.MAP,
schema=output_schema)
),
}
attributes_schema = {
"config": _("The config value of the software config.")
}
def handle_create(self):
props = dict(self.properties)
props[self.NAME] = self.physical_resource_name()
sc = self.heat().software_configs.create(**props)
self.resource_id_set(sc.id)
def handle_delete(self):
if self.resource_id is None:
return
try:
self.heat().software_configs.delete(self.resource_id)
except heat_exp.HTTPNotFound:
logger.debug(
_('Software config %s is not found.') % self.resource_id)
def _resolve_attribute(self, name):
'''
"config" returns the config value of the software config. If the
software config does not exist, returns an empty string.
'''
if name == self.CONFIG and self.resource_id:
try:
return self.get_software_config(self.heat(), self.resource_id)
except exception.SoftwareConfigMissing:
return ''
@staticmethod
def get_software_config(heat_client, software_config_id):
'''
Get the software config specified by :software_config_id:
:param heat_client: the heat client to use
:param software_config_id: the ID of the config to look for
:returns: the config script string for :software_config_id:
:raises: exception.NotFound
'''
try:
return heat_client.software_configs.get(software_config_id).config
except heat_exp.HTTPNotFound:
raise exception.SoftwareConfigMissing(
software_config_id=software_config_id)
def resource_mapping():
return {
'OS::Heat::SoftwareConfig': SoftwareConfig,
}

View File

@ -0,0 +1,109 @@
#
# 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 heat.common import exception
from heat.engine import parser
from heat.engine import template
import heat.engine.resources.software_config.software_config as sc
from heatclient.exc import HTTPNotFound
from heat.tests.common import HeatTestCase
from heat.tests import utils
class SoftwareConfigTest(HeatTestCase):
def setUp(self):
super(SoftwareConfigTest, self).setUp()
utils.setup_dummy_db()
self.ctx = utils.dummy_context()
self.properties = {
'group': 'Heat::Shell',
'inputs': [],
'outputs': [],
'options': {},
'config': '#!/bin/bash'
}
self.stack = parser.Stack(
self.ctx, 'software_config_test_stack',
template.Template({
'Resources': {
'config_mysql': {
'Type': 'OS::Heat::SoftwareConfig',
'Properties': self.properties
}}}))
self.config = self.stack['config_mysql']
heat = mock.MagicMock()
self.heatclient = mock.MagicMock()
self.config.heat = heat
heat.return_value = self.heatclient
self.software_configs = self.heatclient.software_configs
def test_resource_mapping(self):
mapping = sc.resource_mapping()
self.assertEqual(1, len(mapping))
self.assertEqual(sc.SoftwareConfig,
mapping['OS::Heat::SoftwareConfig'])
self.assertIsInstance(self.config, sc.SoftwareConfig)
def test_handle_create(self):
value = mock.MagicMock()
config_id = 'c8a19429-7fde-47ea-a42f-40045488226c'
value.id = config_id
self.software_configs.create.return_value = value
self.config.handle_create()
self.assertEqual(config_id, self.config.resource_id)
def test_handle_delete(self):
self.resource_id = None
self.assertIsNone(self.config.handle_delete())
config_id = 'c8a19429-7fde-47ea-a42f-40045488226c'
self.config.resource_id = config_id
self.software_configs.delete.return_value = None
self.assertIsNone(self.config.handle_delete())
self.software_configs.delete.side_effect = HTTPNotFound()
self.assertIsNone(self.config.handle_delete())
def test_get_software_config(self):
config_id = 'c8a19429-7fde-47ea-a42f-40045488226c'
value = mock.MagicMock()
value.config = '#!/bin/bash'
self.software_configs.get.return_value = value
heatclient = self.heatclient
config = sc.SoftwareConfig.get_software_config(heatclient, config_id)
self.assertEqual('#!/bin/bash', config)
self.software_configs.get.side_effect = HTTPNotFound()
err = self.assertRaises(
exception.SoftwareConfigMissing,
self.config.get_software_config,
heatclient, config_id)
self.assertEqual(
('The config (c8a19429-7fde-47ea-a42f-40045488226c) '
'could not be found.'), str(err))
def test_resolve_attribute(self):
self.assertIsNone(self.config._resolve_attribute('others'))
self.config.resource_id = None
self.assertIsNone(self.config._resolve_attribute('config'))
self.config.resource_id = 'c8a19429-7fde-47ea-a42f-40045488226c'
value = mock.MagicMock()
value.config = '#!/bin/bash'
self.software_configs.get.return_value = value
self.assertEqual(
'#!/bin/bash', self.config._resolve_attribute('config'))
self.software_configs.get.side_effect = HTTPNotFound()
self.assertEqual('', self.config._resolve_attribute('config'))