Browse Source
Adds resource plugin for the designate Record. implements blueprint heat-designate-resource Change-Id: Ic7bbfca7b5adea2879f43fea76b89a6025584b88changes/98/193898/14
4 changed files with 515 additions and 0 deletions
@ -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 |
||||
} |
@ -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) |
Loading…
Reference in new issue