Merge "Designate Record resource"

This commit is contained in:
Jenkins 2015-07-15 04:26:38 +00:00 committed by Gerrit Code Review
commit 9e352b3e93
4 changed files with 515 additions and 0 deletions

View File

@ -14,6 +14,7 @@
from designateclient import exceptions from designateclient import exceptions
from designateclient import v1 as client from designateclient import v1 as client
from designateclient.v1 import domains from designateclient.v1 import domains
from designateclient.v1 import records
from heat.common import exception as heat_exception from heat.common import exception as heat_exception
from heat.engine.clients import client_plugin from heat.engine.clients import client_plugin
@ -64,6 +65,26 @@ class DesignateClientPlugin(client_plugin.ClientPlugin):
return self.client().domains.update(domain) return self.client().domains.update(domain)
def record_create(self, **kwargs):
domain_id = self.get_domain_id(kwargs.pop('domain'))
record = records.Record(**kwargs)
return self.client().records.create(domain_id, record)
def record_update(self, **kwargs):
# Designate mandates to pass the Record object with updated properties
domain_id = self.get_domain_id(kwargs.pop('domain'))
record = self.client().records.get(domain_id, kwargs['id'])
for key in kwargs.keys():
setattr(record, key, kwargs[key])
return self.client().records.update(record.domain_id, record)
def record_delete(self, **kwargs):
domain_id = self.get_domain_id(kwargs.pop('domain'))
return self.client().records.delete(domain_id,
kwargs.pop('id'))
class DesignateDomainConstraint(constraints.BaseCustomConstraint): class DesignateDomainConstraint(constraints.BaseCustomConstraint):

View File

@ -0,0 +1,163 @@
#
# 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.i18n import _
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine import support
class DesignateRecord(resource.Resource):
"""Heat Template Resource for Designate Record."""
support_status = support.SupportStatus(
version='5.0.0')
PROPERTIES = (
NAME, TTL, DESCRIPTION, TYPE, DATA, PRIORITY, DOMAIN
) = (
'name', 'ttl', 'description', 'type', 'data', 'priority', 'domain'
)
_ALLOWED_TYPES = (
A, AAAA, CNAME, MX, SRV, TXT, SPF,
NS, PTR, SSHFP, SOA
) = (
'A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF',
'NS', 'PTR', 'SSHFP', 'SOA'
)
properties_schema = {
# Based on RFC 1035, length of name is set to max of 255
NAME: properties.Schema(
properties.Schema.STRING,
_('Record name.'),
required=True,
constraints=[constraints.Length(max=255)]
),
# Based on RFC 1035, range for ttl is set to 0 to signed 32 bit number
TTL: properties.Schema(
properties.Schema.INTEGER,
_('Time To Live (Seconds).'),
update_allowed=True,
constraints=[constraints.Range(min=0,
max=2147483647)]
),
# designate mandates to the max length of 160 for description
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description of record.'),
update_allowed=True,
constraints=[constraints.Length(max=160)]
),
TYPE: properties.Schema(
properties.Schema.STRING,
_('DNS Record type.'),
update_allowed=True,
required=True,
constraints=[constraints.AllowedValues(
_ALLOWED_TYPES
)]
),
DATA: properties.Schema(
properties.Schema.STRING,
_('DNS record data, varies based on the type of record. For more '
'details, please refer rfc 1035.'),
update_allowed=True,
required=True
),
# Based on RFC 1035, range for priority is set to 0 to signed 16 bit
# number
PRIORITY: properties.Schema(
properties.Schema.INTEGER,
_('DNS record priority. It is considered only for MX and SRV '
'types, otherwise, it is ignored.'),
update_allowed=True,
constraints=[constraints.Range(min=0,
max=65536)]
),
DOMAIN: properties.Schema(
properties.Schema.STRING,
_('DNS Domain id or name.'),
required=True,
constraints=[constraints.CustomConstraint('designate.domain')]
),
}
default_client_name = 'designate'
def handle_create(self):
args = dict(
name=self.properties[self.NAME],
type=self.properties[self.TYPE],
description=self.properties[self.DESCRIPTION],
ttl=self.properties[self.TTL],
data=self.properties[self.DATA],
# priority is considered only for MX and SRV record.
priority=(self.properties[self.PRIORITY]
if self.properties[self.TYPE] in (self.MX, self.SRV)
else None),
domain=self.properties[self.DOMAIN]
)
domain = self.client_plugin().record_create(**args)
self.resource_id_set(domain.id)
def handle_update(self,
json_snippet=None,
tmpl_diff=None,
prop_diff=None):
args = dict()
if prop_diff.get(self.TTL):
args['ttl'] = prop_diff.get(self.TTL)
if prop_diff.get(self.DESCRIPTION):
args['description'] = prop_diff.get(self.DESCRIPTION)
if prop_diff.get(self.TYPE):
args['type'] = prop_diff.get(self.TYPE)
# priority is considered only for MX and SRV record.
if prop_diff.get(self.PRIORITY):
args['priority'] = (prop_diff.get(self.PRIORITY)
if (prop_diff.get(self.TYPE) or
self.properties[self.TYPE]) in
(self.MX, self.SRV)
else None)
if prop_diff.get(self.DATA):
args['data'] = prop_diff.get(self.DATA)
if len(args.keys()) > 0:
args['id'] = self.resource_id
args['domain'] = self.properties[self.DOMAIN]
self.client_plugin().record_update(**args)
def handle_delete(self):
if self.resource_id is not None:
try:
self.client_plugin().record_delete(
id=self.resource_id,
domain=self.properties[self.DOMAIN]
)
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
def resource_mapping():
return {
'OS::Designate::Record': DesignateRecord
}

View File

@ -203,3 +203,102 @@ class DesignateClientPluginDomainTest(common.HeatTestCase):
self._client.domains.update.assert_called_once_with( self._client.domains.update.assert_called_once_with(
mock_domain) mock_domain)
class DesignateClientPluginRecordTest(common.HeatTestCase):
sample_uuid = '477e8273-60a7-4c41-b683-fdb0bc7cd152'
sample_domain_id = '477e8273-60a7-4c41-b683-fdb0bc7cd153'
def _get_mock_record(self):
record = mock.MagicMock()
record.id = self.sample_uuid
record.domain_id = self.sample_domain_id
return record
def setUp(self):
super(DesignateClientPluginRecordTest, self).setUp()
self._client = mock.MagicMock()
self.client_plugin = client.DesignateClientPlugin(
context=mock.MagicMock()
)
self.client_plugin.get_domain_id = mock.Mock(
return_value=self.sample_domain_id)
@mock.patch.object(client.DesignateClientPlugin, 'client')
@mock.patch('designateclient.v1.records.Record')
def test_record_create(self, mock_record, client_designate):
self._client.records.create.return_value = None
client_designate.return_value = self._client
record = dict(
name='test-record.com',
description='updated description',
ttl=4200,
type='',
priority=1,
data='1.1.1.1',
domain=self.sample_domain_id
)
mock_sample_record = mock.Mock()
mock_record.return_value = mock_sample_record
self.client_plugin.record_create(**record)
# Make sure record entity is created with right arguments
domain_id = record.pop('domain')
mock_record.assert_called_once_with(**record)
self._client.records.create.assert_called_once_with(
domain_id,
mock_sample_record)
@mock.patch.object(client.DesignateClientPlugin, 'client')
@mock.patch('designateclient.v1.records.Record')
def test_record_update(self, mock_record, client_designate):
self._client.records.update.return_value = None
mock_record = self._get_mock_record()
self._client.records.get.return_value = mock_record
client_designate.return_value = self._client
record = dict(
id=self.sample_uuid,
name='test-record.com',
description='updated description',
ttl=4200,
type='',
priority=1,
data='1.1.1.1',
domain=self.sample_domain_id
)
self.client_plugin.record_update(**record)
self._client.records.get.assert_called_once_with(
self.sample_domain_id,
self.sample_uuid)
for key in record.keys():
setattr(mock_record, key, record[key])
self._client.records.update.assert_called_once_with(
self.sample_domain_id,
mock_record)
@mock.patch.object(client.DesignateClientPlugin, 'client')
@mock.patch('designateclient.v1.records.Record')
def test_record_delete(self, mock_record, client_designate):
self._client.records.delete.return_value = None
client_designate.return_value = self._client
record = dict(
id=self.sample_uuid,
domain=self.sample_domain_id
)
self.client_plugin.record_delete(**record)
self._client.records.delete.assert_called_once_with(
self.sample_domain_id,
self.sample_uuid)

View File

@ -0,0 +1,232 @@
#
# 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 designateclient import exceptions as designate_exception
from heat.engine.resources.openstack.designate import record
from heat.engine import stack
from heat.engine import template
from heat.tests import common
from heat.tests import utils
sample_template = {
'heat_template_version': '2015-04-30',
'resources': {
'test_resource': {
'type': 'OS::Designate::Record',
'properties': {
'name': 'test-record.com',
'description': 'Test record',
'ttl': 3600,
'type': 'MX',
'priority': 1,
'data': '1.1.1.1',
'domain': '1234567'
}
}
}
}
RESOURCE_TYPE = 'OS::Designate::Record'
class DesignateRecordTest(common.HeatTestCase):
def setUp(self):
super(DesignateRecordTest, self).setUp()
self.ctx = utils.dummy_context()
self.stack = stack.Stack(
self.ctx, 'test_stack',
template.Template(sample_template)
)
self.test_resource = self.stack['test_resource']
# Mock client plugin
self.test_client_plugin = mock.MagicMock()
self.test_resource.client_plugin = mock.MagicMock(
return_value=self.test_client_plugin)
# Mock client
self.test_client = mock.MagicMock()
self.test_resource.client = mock.MagicMock(
return_value=self.test_client)
def _get_mock_resource(self):
value = mock.MagicMock()
value.id = '477e8273-60a7-4c41-b683-fdb0bc7cd152'
return value
def test_resource_validate_properties(self):
mock_record_create = self.test_client_plugin.record_create
mock_resource = self._get_mock_resource()
mock_record_create.return_value = mock_resource
# validate the properties
self.assertEqual(
'test-record.com',
self.test_resource.properties.get(record.DesignateRecord.NAME))
self.assertEqual(
'Test record',
self.test_resource.properties.get(
record.DesignateRecord.DESCRIPTION))
self.assertEqual(
3600,
self.test_resource.properties.get(record.DesignateRecord.TTL))
self.assertEqual(
'MX',
self.test_resource.properties.get(record.DesignateRecord.TYPE))
self.assertEqual(
1,
self.test_resource.properties.get(record.DesignateRecord.PRIORITY))
self.assertEqual(
'1.1.1.1',
self.test_resource.properties.get(record.DesignateRecord.DATA))
self.assertEqual(
'1234567',
self.test_resource.properties.get(
record.DesignateRecord.DOMAIN))
def test_resource_handle_create_non_mx_or_srv(self):
mock_record_create = self.test_client_plugin.record_create
mock_resource = self._get_mock_resource()
mock_record_create.return_value = mock_resource
for type in (set(self.test_resource._ALLOWED_TYPES) -
set([self.test_resource.MX,
self.test_resource.SRV])):
self.test_resource.properties = args = dict(
name='test-record.com',
description='Test record',
ttl=3600,
type=type,
priority=1,
data='1.1.1.1',
domain='1234567'
)
self.test_resource.handle_create()
# Make sure priority is set to None for non mx or srv records
args['priority'] = None
mock_record_create.assert_called_with(
**args
)
# validate physical resource id
self.assertEqual(mock_resource.id, self.test_resource.resource_id)
def test_resource_handle_create_mx_or_srv(self):
mock_record_create = self.test_client_plugin.record_create
mock_resource = self._get_mock_resource()
mock_record_create.return_value = mock_resource
for type in [self.test_resource.MX, self.test_resource.SRV]:
self.test_resource.properties = args = dict(
name='test-record.com',
description='Test record',
ttl=3600,
type=type,
priority=1,
data='1.1.1.1',
domain='1234567'
)
self.test_resource.handle_create()
mock_record_create.assert_called_with(
**args
)
# validate physical resource id
self.assertEqual(mock_resource.id, self.test_resource.resource_id)
def test_resource_handle_update_non_mx_or_srv(self):
mock_record_update = self.test_client_plugin.record_update
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
for type in (set(self.test_resource._ALLOWED_TYPES) -
set([self.test_resource.MX,
self.test_resource.SRV])):
prop_diff = args = {
record.DesignateRecord.DESCRIPTION: 'updated description',
record.DesignateRecord.TTL: 4200,
record.DesignateRecord.TYPE: type,
record.DesignateRecord.DATA: '2.2.2.2',
record.DesignateRecord.PRIORITY: 1}
self.test_resource.handle_update(json_snippet=None,
tmpl_diff=None,
prop_diff=prop_diff)
# priority is not considered for records other than mx or srv
args.update(dict(
id=self.test_resource.resource_id,
priority=None,
domain='1234567',
))
mock_record_update.assert_called_with(**args)
def test_resource_handle_update_mx_or_srv(self):
mock_record_update = self.test_client_plugin.record_update
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
for type in [self.test_resource.MX, self.test_resource.SRV]:
prop_diff = args = {
record.DesignateRecord.DESCRIPTION: 'updated description',
record.DesignateRecord.TTL: 4200,
record.DesignateRecord.TYPE: type,
record.DesignateRecord.DATA: '2.2.2.2',
record.DesignateRecord.PRIORITY: 1}
self.test_resource.handle_update(json_snippet=None,
tmpl_diff=None,
prop_diff=prop_diff)
args.update(dict(
id=self.test_resource.resource_id,
domain='1234567',
))
mock_record_update.assert_called_with(**args)
def test_resource_handle_delete(self):
mock_record_delete = self.test_client_plugin.record_delete
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
mock_record_delete.return_value = None
self.assertIsNone(self.test_resource.handle_delete())
mock_record_delete.assert_called_once_with(
domain='1234567',
id=self.test_resource.resource_id
)
def test_resource_handle_delete_resource_id_is_none(self):
self.test_resource.resource_id = None
self.assertIsNone(self.test_resource.handle_delete())
def test_resource_handle_delete_not_found(self):
mock_record_delete = self.test_client_plugin.record_delete
mock_record_delete.side_effect = designate_exception.NotFound
self.assertIsNone(self.test_resource.handle_delete())
def test_resource_mapping(self):
mapping = record.resource_mapping()
self.assertEqual(1, len(mapping))
self.assertEqual(record.DesignateRecord, mapping[RESOURCE_TYPE])
self.assertIsInstance(self.test_resource, record.DesignateRecord)