Add support for Designate zones

This is the first commit to add initial support for Designate.
Starting with zones objects, more to come.

Depends-On: Ieaddeb4a0b317f85a2161e67bc5c202cc1b01464
Change-Id: I1109f89075ed663620ecb11d18507e8a5d7351b4
This commit is contained in:
Ricardo Carrillo Cruz 2016-04-06 15:42:19 +02:00
parent 677f656dbd
commit 40a50918bd
7 changed files with 339 additions and 0 deletions

View File

@ -0,0 +1,4 @@
---
features:
- Add support for Designate zones resources, with the
usual methods (search/list/get/create/update/delete).

View File

@ -19,5 +19,6 @@ python-troveclient>=1.2.0
python-ironicclient>=0.10.0
python-swiftclient>=2.5.0
python-heatclient>=0.3.0
python-designateclient>=2.1.0
dogpile.cache>=0.5.3

View File

@ -772,3 +772,23 @@ class StackDelete(task_manager.Task):
class StackGet(task_manager.Task):
def main(self, client):
return client.heat_client.stacks.get(**self.args)
class ZoneList(task_manager.Task):
def main(self, client):
return client.designate_client.zones.list()
class ZoneCreate(task_manager.Task):
def main(self, client):
return client.designate_client.zones.create(**self.args)
class ZoneUpdate(task_manager.Task):
def main(self, client):
return client.designate_client.zones.update(**self.args)
class ZoneDelete(task_manager.Task):
def main(self, client):
return client.designate_client.zones.delete(**self.args)

View File

@ -40,6 +40,7 @@ import swiftclient.client
import swiftclient.service
import swiftclient.exceptions as swift_exceptions
import troveclient.client
import designateclient.client
from shade.exc import * # noqa
from shade import _log
@ -250,6 +251,7 @@ class OpenStackCloud(object):
self._swift_client_lock = threading.Lock()
self._swift_service_lock = threading.Lock()
self._trove_client = None
self._designate_client = None
self._raw_clients = {}
@ -820,6 +822,13 @@ class OpenStackCloud(object):
'network', neutronclient.neutron.client.Client)
return self._neutron_client
@property
def designate_client(self):
if self._designate_client is None:
self._designate_client = self._get_client(
'dns', designateclient.client.Client)
return self._designate_client
def create_stack(
self, name,
template_file=None, template_url=None,
@ -4821,3 +4830,113 @@ class OpenStackCloud(object):
raise OpenStackCloudUnavailableFeature(
"Unavailable feature: security groups"
)
def list_zones(self):
"""List all available zones.
:returns: A list of zones dicts.
"""
with _utils.shade_exceptions("Error fetching zones list"):
return self.manager.submitTask(_tasks.ZoneList())
def get_zone(self, name_or_id, filters=None):
"""Get a zone by name or ID.
:param name_or_id: Name or ID of the zone
:param dict filters:
A dictionary of meta data to use for further filtering
:returns: A zone dict or None if no matching zone is
found.
"""
return _utils._get_entity(self.search_zones, name_or_id, filters)
def search_zones(self, name_or_id=None, filters=None):
zones = self.list_zones()
return _utils._filter_list(zones, name_or_id, filters)
def create_zone(self, name, zone_type=None, email=None, description=None,
ttl=None, masters=None):
"""Create a new zone.
:param name: Name of the zone being created.
:param zone_type: Type of the zone (primary/secondary)
:param email: Email of the zone owner (only
applies if zone_type is primary)
:param description: Description of the zone
:param ttl: TTL (Time to live) value in seconds
:param masters: Master nameservers (only applies
if zone_type is secondary)
:returns: a dict representing the created zone.
:raises: OpenStackCloudException on operation error.
"""
# We capitalize in case the user passes time in lowercase, as
# designate call expects PRIMARY/SECONDARY
if zone_type is not None:
zone_type = zone_type.upper()
if zone_type not in ('PRIMARY', 'SECONDARY'):
raise OpenStackCloudException(
"Invalid type %s, valid choices are PRIMARY or SECONDARY" %
zone_type)
with _utils.shade_exceptions("Unable to create zone {name}".format(
name=name)):
return self.manager.submitTask(_tasks.ZoneCreate(
name=name, type_=zone_type, email=email,
description=description, ttl=ttl, masters=masters))
@_utils.valid_kwargs('email', 'description', 'ttl', 'masters')
def update_zone(self, name_or_id, **kwargs):
"""Update a zone.
:param name_or_id: Name or ID of the zone being updated.
:param email: Email of the zone owner (only
applies if zone_type is primary)
:param description: Description of the zone
:param ttl: TTL (Time to live) value in seconds
:param masters: Master nameservers (only applies
if zone_type is secondary)
:returns: a dict representing the updated zone.
:raises: OpenStackCloudException on operation error.
"""
zone = self.get_zone(name_or_id)
if not zone:
raise OpenStackCloudException(
"Zone %s not found." % name_or_id)
with _utils.shade_exceptions(
"Error updating zone {0}".format(name_or_id)):
new_zone = self.manager.submitTask(
_tasks.ZoneUpdate(
zone=zone['id'], values=kwargs))
return new_zone
def delete_zone(self, name_or_id):
"""Delete a zone.
:param name_or_id: Name or ID of the zone being deleted.
:returns: True if delete succeeded, False otherwise.
:raises: OpenStackCloudException on operation error.
"""
zone = self.get_zone(name_or_id)
if zone is None:
self.log.debug("Zone %s not found for deleting" % name_or_id)
return False
with _utils.shade_exceptions(
"Error deleting zone {0}".format(name_or_id)):
self.manager.submitTask(
_tasks.ZoneDelete(zone=zone['id']))
return True

View File

@ -246,3 +246,15 @@ class FakeStack(object):
self.stack_name = name
self.stack_description = description
self.stack_status = status
class FakeZone(object):
def __init__(self, id, name, type_, email, description,
ttl, masters):
self.id = id
self.name = name
self.type_ = type_
self.email = email
self.description = description
self.ttl = ttl
self.masters = masters

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_zone
----------------------------------
Functional tests for `shade` zone methods.
"""
from testtools import content
from shade.tests.functional import base
class TestZone(base.BaseFunctionalTestCase):
def setUp(self):
super(TestZone, self).setUp()
if not self.demo_cloud.has_service('dns'):
self.skipTest('dns service not supported by cloud')
def test_zones(self):
'''Test DNS zones functionality'''
name = 'example.net.'
zone_type = 'primary'
email = 'test@example.net'
description = 'Test zone'
ttl = 3600
masters = None
self.addDetail('zone', content.text_content(name))
self.addCleanup(self.cleanup, name)
# Test we can create a zone and we get it returned
zone = self.demo_cloud.create_zone(
name=name, zone_type=zone_type, email=email,
description=description, ttl=ttl,
masters=masters)
self.assertEquals(zone['name'], name)
self.assertEquals(zone['type'], zone_type.upper())
self.assertEquals(zone['email'], email)
self.assertEquals(zone['description'], description)
self.assertEquals(zone['ttl'], ttl)
self.assertEquals(zone['masters'], [])
# Test that we can list zones
zones = self.demo_cloud.list_zones()
self.assertIsNotNone(zones)
# Test we get the same zone with the get_zone method
zone_get = self.demo_cloud.get_zone(zone['id'])
self.assertEquals(zone_get['id'], zone['id'])
# Test the get method also works by name
zone_get = self.demo_cloud.get_zone(name)
self.assertEquals(zone_get['name'], zone['name'])
# Test we can update a field on the zone and only that field
# is updated
zone_update = self.demo_cloud.update_zone(zone['id'], ttl=7200)
self.assertEquals(zone_update['id'], zone['id'])
self.assertEquals(zone_update['name'], zone['name'])
self.assertEquals(zone_update['type'], zone['type'])
self.assertEquals(zone_update['email'], zone['email'])
self.assertEquals(zone_update['description'], zone['description'])
self.assertEquals(zone_update['ttl'], 7200)
self.assertEquals(zone_update['masters'], zone['masters'])
# Test we can delete and get True returned
zone_delete = self.demo_cloud.delete_zone(zone['id'])
self.assertTrue(zone_delete)
def cleanup(self, name):
self.demo_cloud.delete_zone(name)

View File

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# 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
import testtools
import shade
from shade.tests.unit import base
from shade.tests import fakes
zone_obj = fakes.FakeZone(
id='1',
name='example.net.',
type_='PRIMARY',
email='test@example.net',
description='Example zone',
ttl=3600,
masters=None
)
class TestZone(base.TestCase):
def setUp(self):
super(TestZone, self).setUp()
self.cloud = shade.openstack_cloud(validate=False)
@mock.patch.object(shade.OpenStackCloud, 'designate_client')
def test_create_zone(self, mock_designate):
self.cloud.create_zone(name=zone_obj.name, zone_type=zone_obj.type_,
email=zone_obj.email,
description=zone_obj.description,
ttl=zone_obj.ttl, masters=zone_obj.masters)
mock_designate.zones.create.assert_called_once_with(
name=zone_obj.name, type_=zone_obj.type_.upper(),
email=zone_obj.email, description=zone_obj.description,
ttl=zone_obj.ttl, masters=zone_obj.masters
)
@mock.patch.object(shade.OpenStackCloud, 'designate_client')
def test_create_zone_exception(self, mock_designate):
mock_designate.zones.create.side_effect = Exception()
with testtools.ExpectedException(
shade.OpenStackCloudException,
"Unable to create zone example.net."
):
self.cloud.create_zone('example.net.')
@mock.patch.object(shade.OpenStackCloud, 'designate_client')
def test_update_zone(self, mock_designate):
new_ttl = 7200
mock_designate.zones.list.return_value = [zone_obj]
self.cloud.update_zone('1', ttl=new_ttl)
mock_designate.zones.update.assert_called_once_with(
zone='1', values={'ttl': new_ttl}
)
@mock.patch.object(shade.OpenStackCloud, 'designate_client')
def test_delete_zone(self, mock_designate):
mock_designate.zones.list.return_value = [zone_obj]
self.cloud.delete_zone('1')
mock_designate.zones.delete.assert_called_once_with(
zone='1'
)
@mock.patch.object(shade.OpenStackCloud, 'designate_client')
def test_get_zone_by_id(self, mock_designate):
mock_designate.zones.list.return_value = [zone_obj]
zone = self.cloud.get_zone('1')
self.assertTrue(mock_designate.zones.list.called)
self.assertEqual(zone['id'], '1')
@mock.patch.object(shade.OpenStackCloud, 'designate_client')
def test_get_zone_by_name(self, mock_designate):
mock_designate.zones.list.return_value = [zone_obj]
zone = self.cloud.get_zone('example.net.')
self.assertTrue(mock_designate.zones.list.called)
self.assertEqual(zone['name'], 'example.net.')
@mock.patch.object(shade.OpenStackCloud, 'designate_client')
def test_get_zone_not_found_returns_false(self, mock_designate):
mock_designate.zones.list.return_value = []
zone = self.cloud.get_zone('nonexistingzone.net.')
self.assertFalse(zone)