Merge "Add DeployedServer resource"
This commit is contained in:
commit
d3cf51a10a
118
heat/engine/resources/openstack/heat/deployed_server.py
Normal file
118
heat/engine/resources/openstack/heat/deployed_server.py
Normal file
@ -0,0 +1,118 @@
|
||||
#
|
||||
# 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 oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from heat.common.i18n import _
|
||||
from heat.engine import attributes
|
||||
from heat.engine import constraints
|
||||
from heat.engine import properties
|
||||
from heat.engine.resources import server_base
|
||||
|
||||
cfg.CONF.import_opt('default_software_config_transport', 'heat.common.config')
|
||||
cfg.CONF.import_opt('default_user_data_format', 'heat.common.config')
|
||||
cfg.CONF.import_opt('max_server_name_length', 'heat.common.config')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeployedServer(server_base.BaseServer):
|
||||
"""A resource for managing servers that are already deployed.
|
||||
|
||||
A DeployedServer resource manages resources for servers that have been
|
||||
deployed externally from OpenStack. These servers can be associated with
|
||||
SoftwareDeployments for further orchestration via Heat.
|
||||
"""
|
||||
|
||||
PROPERTIES = (
|
||||
NAME, METADATA, SOFTWARE_CONFIG_TRANSPORT
|
||||
) = (
|
||||
'name', 'metadata', 'software_config_transport'
|
||||
)
|
||||
|
||||
_SOFTWARE_CONFIG_TRANSPORTS = (
|
||||
POLL_SERVER_CFN, POLL_SERVER_HEAT, POLL_TEMP_URL, ZAQAR_MESSAGE
|
||||
) = (
|
||||
'POLL_SERVER_CFN', 'POLL_SERVER_HEAT', 'POLL_TEMP_URL', 'ZAQAR_MESSAGE'
|
||||
)
|
||||
|
||||
properties_schema = {
|
||||
NAME: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('Server name.'),
|
||||
update_allowed=True
|
||||
),
|
||||
METADATA: properties.Schema(
|
||||
properties.Schema.MAP,
|
||||
_('Arbitrary key/value metadata to store for this server. Both '
|
||||
'keys and values must be 255 characters or less. Non-string '
|
||||
'values will be serialized to JSON (and the serialized '
|
||||
'string must be 255 characters or less).'),
|
||||
update_allowed=True
|
||||
),
|
||||
SOFTWARE_CONFIG_TRANSPORT: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('How the server should receive the metadata required for '
|
||||
'software configuration. POLL_SERVER_CFN will allow calls to '
|
||||
'the cfn API action DescribeStackResource authenticated with '
|
||||
'the provided keypair. POLL_SERVER_HEAT will allow calls to '
|
||||
'the Heat API resource-show using the provided keystone '
|
||||
'credentials. POLL_TEMP_URL will create and populate a '
|
||||
'Swift TempURL with metadata for polling. ZAQAR_MESSAGE will '
|
||||
'create a dedicated zaqar queue and post the metadata '
|
||||
'for polling.'),
|
||||
default=cfg.CONF.default_software_config_transport,
|
||||
update_allowed=True,
|
||||
constraints=[
|
||||
constraints.AllowedValues(_SOFTWARE_CONFIG_TRANSPORTS),
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
ATTRIBUTES = (
|
||||
NAME_ATTR
|
||||
) = (
|
||||
'name'
|
||||
)
|
||||
|
||||
attributes_schema = {
|
||||
NAME_ATTR: attributes.Schema(
|
||||
_('Name of the server.'),
|
||||
type=attributes.Schema.STRING
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, name, json_snippet, stack):
|
||||
super(DeployedServer, self).__init__(name, json_snippet, stack)
|
||||
self._register_access_key()
|
||||
|
||||
def handle_create(self):
|
||||
metadata = self.metadata_get(True) or {}
|
||||
self.resource_id_set(self.uuid)
|
||||
|
||||
self._create_transport_credentials(self.properties)
|
||||
self._populate_deployments_metadata(metadata, self.properties)
|
||||
|
||||
return self.resource_id
|
||||
|
||||
def _delete(self):
|
||||
self._delete_queue()
|
||||
self._delete_user()
|
||||
self._delete_temp_url()
|
||||
|
||||
|
||||
def resource_mapping():
|
||||
return {
|
||||
'OS::Heat::DeployedServer': DeployedServer,
|
||||
}
|
@ -543,8 +543,9 @@ class SoftwareDeployment(signal_responder.SignalResponder):
|
||||
if server:
|
||||
res = self.stack.resource_by_refid(server)
|
||||
if res:
|
||||
if not (res.properties.get('user_data_format') ==
|
||||
'SOFTWARE_CONFIG'):
|
||||
user_data_format = res.properties.get('user_data_format')
|
||||
if user_data_format and \
|
||||
not (user_data_format == 'SOFTWARE_CONFIG'):
|
||||
raise exception.StackValidationFailed(message=_(
|
||||
"Resource %s's property user_data_format should be "
|
||||
"set to SOFTWARE_CONFIG since there are software "
|
||||
|
351
heat/tests/openstack/heat/test_deployed_server.py
Normal file
351
heat/tests/openstack/heat/test_deployed_server.py
Normal file
@ -0,0 +1,351 @@
|
||||
#
|
||||
# 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 oslo_utils import uuidutils
|
||||
from six.moves.urllib import parse as urlparse
|
||||
|
||||
from heat.common import template_format
|
||||
from heat.engine.clients.os import heat_plugin
|
||||
from heat.engine.clients.os import swift
|
||||
from heat.engine.clients.os import zaqar
|
||||
from heat.engine import environment
|
||||
from heat.engine.resources.openstack.heat import deployed_server
|
||||
from heat.engine import scheduler
|
||||
from heat.engine import stack as parser
|
||||
from heat.engine import template
|
||||
from heat.tests import common
|
||||
from heat.tests import utils
|
||||
|
||||
ds_tmpl = """
|
||||
heat_template_version: 2015-10-15
|
||||
resources:
|
||||
server:
|
||||
type: OS::Heat::DeployedServer
|
||||
properties:
|
||||
software_config_transport: POLL_TEMP_URL
|
||||
"""
|
||||
|
||||
server_sc_tmpl = """
|
||||
heat_template_version: 2015-10-15
|
||||
resources:
|
||||
server:
|
||||
type: OS::Heat::DeployedServer
|
||||
properties:
|
||||
software_config_transport: POLL_SERVER_CFN
|
||||
"""
|
||||
|
||||
server_heat_tmpl = """
|
||||
heat_template_version: 2015-10-15
|
||||
resources:
|
||||
server:
|
||||
type: OS::Heat::DeployedServer
|
||||
properties:
|
||||
software_config_transport: POLL_SERVER_HEAT
|
||||
"""
|
||||
|
||||
server_zaqar_tmpl = """
|
||||
heat_template_version: 2015-10-15
|
||||
resources:
|
||||
server:
|
||||
type: OS::Heat::DeployedServer
|
||||
properties:
|
||||
software_config_transport: ZAQAR_MESSAGE
|
||||
"""
|
||||
|
||||
|
||||
class DeployedServersTest(common.HeatTestCase):
|
||||
def setUp(self):
|
||||
super(DeployedServersTest, self).setUp()
|
||||
|
||||
def _create_test_server(self, name, override_name=False):
|
||||
server = self._setup_test_server(name, override_name)
|
||||
scheduler.TaskRunner(server.create)()
|
||||
return server
|
||||
|
||||
def _setup_test_stack(self, stack_name, test_templ=ds_tmpl):
|
||||
t = template_format.parse(test_templ)
|
||||
tmpl = template.Template(t, env=environment.Environment())
|
||||
stack = parser.Stack(utils.dummy_context(), stack_name, tmpl,
|
||||
stack_id=uuidutils.generate_uuid(),
|
||||
stack_user_project_id='8888')
|
||||
return (tmpl, stack)
|
||||
|
||||
def _server_create_software_config_poll_temp_url(self,
|
||||
server_name='server'):
|
||||
stack_name = '%s_s' % server_name
|
||||
(tmpl, stack) = self._setup_test_stack(stack_name)
|
||||
|
||||
props = tmpl.t['resources']['server']['properties']
|
||||
props['software_config_transport'] = 'POLL_TEMP_URL'
|
||||
self.server_props = props
|
||||
|
||||
resource_defns = tmpl.resource_definitions(stack)
|
||||
server = deployed_server.DeployedServer(
|
||||
server_name, resource_defns[server_name], stack)
|
||||
|
||||
sc = mock.Mock()
|
||||
sc.head_account.return_value = {
|
||||
'x-account-meta-temp-url-key': 'secrit'
|
||||
}
|
||||
sc.url = 'http://192.0.2.2'
|
||||
|
||||
self.patchobject(swift.SwiftClientPlugin, '_create',
|
||||
return_value=sc)
|
||||
scheduler.TaskRunner(server.create)()
|
||||
# self._create_test_server(server_name)
|
||||
metadata_put_url = server.data().get('metadata_put_url')
|
||||
md = server.metadata_get()
|
||||
metadata_url = md['os-collect-config']['request']['metadata_url']
|
||||
self.assertNotEqual(metadata_url, metadata_put_url)
|
||||
|
||||
container_name = server.physical_resource_name()
|
||||
object_name = server.data().get('metadata_object_name')
|
||||
self.assertTrue(uuidutils.is_uuid_like(object_name))
|
||||
test_path = '/v1/AUTH_test_tenant_id/%s/%s' % (
|
||||
server.physical_resource_name(), object_name)
|
||||
self.assertEqual(test_path, urlparse.urlparse(metadata_put_url).path)
|
||||
self.assertEqual(test_path, urlparse.urlparse(metadata_url).path)
|
||||
sc.put_object.assert_called_once_with(
|
||||
container_name, object_name, jsonutils.dumps(md))
|
||||
|
||||
sc.head_container.return_value = {'x-container-object-count': '0'}
|
||||
server._delete_temp_url()
|
||||
sc.delete_object.assert_called_once_with(container_name, object_name)
|
||||
sc.head_container.assert_called_once_with(container_name)
|
||||
sc.delete_container.assert_called_once_with(container_name)
|
||||
return metadata_url, server
|
||||
|
||||
def test_server_create_software_config_poll_temp_url(self):
|
||||
metadata_url, server = (
|
||||
self._server_create_software_config_poll_temp_url())
|
||||
|
||||
self.assertEqual({
|
||||
'os-collect-config': {
|
||||
'request': {
|
||||
'metadata_url': metadata_url
|
||||
},
|
||||
'collectors': ['ec2', 'request', 'local']
|
||||
},
|
||||
'deployments': []
|
||||
}, server.metadata_get())
|
||||
|
||||
def _server_create_software_config(self,
|
||||
server_name='server_sc',
|
||||
md=None,
|
||||
ret_tmpl=False):
|
||||
stack_name = '%s_s' % server_name
|
||||
(tmpl, stack) = self._setup_test_stack(stack_name, server_sc_tmpl)
|
||||
self.stack = stack
|
||||
self.server_props = tmpl.t['resources']['server']['properties']
|
||||
if md is not None:
|
||||
tmpl.t['resources']['server']['metadata'] = md
|
||||
|
||||
stack.stack_user_project_id = '8888'
|
||||
resource_defns = tmpl.resource_definitions(stack)
|
||||
server = deployed_server.DeployedServer(
|
||||
'server', resource_defns['server'], stack)
|
||||
self.patchobject(server, 'heat')
|
||||
scheduler.TaskRunner(server.create)()
|
||||
|
||||
self.assertEqual('4567', server.access_key)
|
||||
self.assertEqual('8901', server.secret_key)
|
||||
self.assertEqual('1234', server._get_user_id())
|
||||
self.assertEqual('POLL_SERVER_CFN',
|
||||
server.properties.get('software_config_transport'))
|
||||
|
||||
self.assertTrue(stack.access_allowed('4567', 'server'))
|
||||
self.assertFalse(stack.access_allowed('45678', 'server'))
|
||||
self.assertFalse(stack.access_allowed('4567', 'wserver'))
|
||||
if ret_tmpl:
|
||||
return server, tmpl
|
||||
else:
|
||||
return server
|
||||
|
||||
@mock.patch.object(heat_plugin.HeatClientPlugin, 'url_for')
|
||||
def test_server_create_software_config(self, fake_url):
|
||||
fake_url.return_value = 'the-cfn-url'
|
||||
server = self._server_create_software_config()
|
||||
|
||||
self.assertEqual({
|
||||
'os-collect-config': {
|
||||
'cfn': {
|
||||
'access_key_id': '4567',
|
||||
'metadata_url': 'the-cfn-url/v1/',
|
||||
'path': 'server.Metadata',
|
||||
'secret_access_key': '8901',
|
||||
'stack_name': 'server_sc_s'
|
||||
},
|
||||
'collectors': ['ec2', 'cfn', 'local']
|
||||
},
|
||||
'deployments': []
|
||||
}, server.metadata_get())
|
||||
|
||||
@mock.patch.object(heat_plugin.HeatClientPlugin, 'url_for')
|
||||
def test_server_create_software_config_metadata(self, fake_url):
|
||||
md = {'os-collect-config': {'polling_interval': 10}}
|
||||
fake_url.return_value = 'the-cfn-url'
|
||||
server = self._server_create_software_config(md=md)
|
||||
|
||||
self.assertEqual({
|
||||
'os-collect-config': {
|
||||
'cfn': {
|
||||
'access_key_id': '4567',
|
||||
'metadata_url': 'the-cfn-url/v1/',
|
||||
'path': 'server.Metadata',
|
||||
'secret_access_key': '8901',
|
||||
'stack_name': 'server_sc_s'
|
||||
},
|
||||
'collectors': ['ec2', 'cfn', 'local'],
|
||||
'polling_interval': 10
|
||||
},
|
||||
'deployments': []
|
||||
}, server.metadata_get())
|
||||
|
||||
def _server_create_software_config_poll_heat(self,
|
||||
server_name='server_heat',
|
||||
md=None):
|
||||
stack_name = '%s_s' % server_name
|
||||
(tmpl, stack) = self._setup_test_stack(stack_name, server_heat_tmpl)
|
||||
self.stack = stack
|
||||
props = tmpl.t['resources']['server']['properties']
|
||||
props['software_config_transport'] = 'POLL_SERVER_HEAT'
|
||||
if md is not None:
|
||||
tmpl.t['resources']['server']['metadata'] = md
|
||||
self.server_props = props
|
||||
|
||||
resource_defns = tmpl.resource_definitions(stack)
|
||||
server = deployed_server.DeployedServer(
|
||||
'server', resource_defns['server'], stack)
|
||||
|
||||
scheduler.TaskRunner(server.create)()
|
||||
self.assertEqual('1234', server._get_user_id())
|
||||
|
||||
self.assertTrue(stack.access_allowed('1234', 'server'))
|
||||
self.assertFalse(stack.access_allowed('45678', 'server'))
|
||||
self.assertFalse(stack.access_allowed('4567', 'wserver'))
|
||||
return stack, server
|
||||
|
||||
def test_server_create_software_config_poll_heat(self):
|
||||
stack, server = self._server_create_software_config_poll_heat()
|
||||
|
||||
self.assertEqual({
|
||||
'os-collect-config': {
|
||||
'heat': {
|
||||
'auth_url': 'http://server.test:5000/v2.0',
|
||||
'password': server.password,
|
||||
'project_id': '8888',
|
||||
'resource_name': 'server',
|
||||
'stack_id': 'server_heat_s/%s' % stack.id,
|
||||
'user_id': '1234'
|
||||
},
|
||||
'collectors': ['ec2', 'heat', 'local']
|
||||
},
|
||||
'deployments': []
|
||||
}, server.metadata_get())
|
||||
|
||||
def test_server_create_software_config_poll_heat_metadata(self):
|
||||
md = {'os-collect-config': {'polling_interval': 10}}
|
||||
stack, server = self._server_create_software_config_poll_heat(md=md)
|
||||
|
||||
self.assertEqual({
|
||||
'os-collect-config': {
|
||||
'heat': {
|
||||
'auth_url': 'http://server.test:5000/v2.0',
|
||||
'password': server.password,
|
||||
'project_id': '8888',
|
||||
'resource_name': 'server',
|
||||
'stack_id': 'server_heat_s/%s' % stack.id,
|
||||
'user_id': '1234'
|
||||
},
|
||||
'collectors': ['ec2', 'heat', 'local'],
|
||||
'polling_interval': 10
|
||||
},
|
||||
'deployments': []
|
||||
}, server.metadata_get())
|
||||
|
||||
def _server_create_software_config_zaqar(self,
|
||||
server_name='server_zaqar',
|
||||
md=None):
|
||||
stack_name = '%s_s' % server_name
|
||||
(tmpl, stack) = self._setup_test_stack(stack_name, server_zaqar_tmpl)
|
||||
self.stack = stack
|
||||
props = tmpl.t['resources']['server']['properties']
|
||||
props['software_config_transport'] = 'ZAQAR_MESSAGE'
|
||||
if md is not None:
|
||||
tmpl.t['resources']['server']['metadata'] = md
|
||||
self.server_props = props
|
||||
|
||||
resource_defns = tmpl.resource_definitions(stack)
|
||||
server = deployed_server.DeployedServer(
|
||||
'server', resource_defns['server'], stack)
|
||||
|
||||
zcc = self.patchobject(zaqar.ZaqarClientPlugin, 'create_for_tenant')
|
||||
zc = mock.Mock()
|
||||
zcc.return_value = zc
|
||||
queue = mock.Mock()
|
||||
zc.queue.return_value = queue
|
||||
scheduler.TaskRunner(server.create)()
|
||||
|
||||
metadata_queue_id = server.data().get('metadata_queue_id')
|
||||
md = server.metadata_get()
|
||||
queue_id = md['os-collect-config']['zaqar']['queue_id']
|
||||
self.assertEqual(queue_id, metadata_queue_id)
|
||||
|
||||
zc.queue.assert_called_once_with(queue_id)
|
||||
queue.post.assert_called_once_with(
|
||||
{'body': server.metadata_get(), 'ttl': 3600})
|
||||
|
||||
zc.queue.reset_mock()
|
||||
|
||||
server._delete_queue()
|
||||
|
||||
zc.queue.assert_called_once_with(queue_id)
|
||||
zc.queue(queue_id).delete.assert_called_once_with()
|
||||
return queue_id, server
|
||||
|
||||
def test_server_create_software_config_zaqar(self):
|
||||
queue_id, server = self._server_create_software_config_zaqar()
|
||||
self.assertEqual({
|
||||
'os-collect-config': {
|
||||
'zaqar': {
|
||||
'user_id': '1234',
|
||||
'password': server.password,
|
||||
'auth_url': 'http://server.test:5000/v2.0',
|
||||
'project_id': '8888',
|
||||
'queue_id': queue_id
|
||||
},
|
||||
'collectors': ['ec2', 'zaqar', 'local']
|
||||
},
|
||||
'deployments': []
|
||||
}, server.metadata_get())
|
||||
|
||||
def test_server_create_software_config_zaqar_metadata(self):
|
||||
md = {'os-collect-config': {'polling_interval': 10}}
|
||||
queue_id, server = self._server_create_software_config_zaqar(md=md)
|
||||
self.assertEqual({
|
||||
'os-collect-config': {
|
||||
'zaqar': {
|
||||
'user_id': '1234',
|
||||
'password': server.password,
|
||||
'auth_url': 'http://server.test:5000/v2.0',
|
||||
'project_id': '8888',
|
||||
'queue_id': queue_id
|
||||
},
|
||||
'collectors': ['ec2', 'zaqar', 'local'],
|
||||
'polling_interval': 10
|
||||
},
|
||||
'deployments': []
|
||||
}, server.metadata_get())
|
Loading…
Reference in New Issue
Block a user