mirror of
https://github.com/valitydev/salt.git
synced 2024-11-08 01:18:58 +00:00
Merge pull request #34919 from tonybaloney/libcloud_dns_states
Add a new state module for managing DNS records and zones through Libcloud
This commit is contained in:
commit
55bb3d09ab
229
salt/states/libcloud_dns.py
Normal file
229
salt/states/libcloud_dns.py
Normal file
@ -0,0 +1,229 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Manage DNS records and zones using libcloud
|
||||
|
||||
:codeauthor: :email:`Anthony Shaw <anthonyshaw@apache.org>`
|
||||
|
||||
.. versionadded:: Carbon
|
||||
|
||||
Create and delete DNS records or zones through Libcloud. Libcloud's DNS system supports over 20 DNS
|
||||
providers including Amazon, Google, GoDaddy, Softlayer
|
||||
|
||||
This module uses ``libcloud``, which can be installed via package, or pip.
|
||||
|
||||
:configuration:
|
||||
This module uses a configuration profile for one or multiple DNS providers
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
libcloud_dns:
|
||||
profile1:
|
||||
driver: godaddy
|
||||
key: 2orgk34kgk34g
|
||||
profile2:
|
||||
driver: route53
|
||||
key: blah
|
||||
secret: blah
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
webserver:
|
||||
libcloud_dns.zone_present:
|
||||
name: mywebsite.com
|
||||
profile: profile1
|
||||
libcloud_dns.record_present:
|
||||
name: www
|
||||
zone: mywebsite.com
|
||||
type: A
|
||||
data: 12.34.32.3
|
||||
profile: profile1
|
||||
|
||||
|
||||
:depends: apache-libcloud
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
|
||||
from distutils.version import LooseVersion as _LooseVersion # pylint: disable=import-error,no-name-in-module
|
||||
|
||||
import salt.modules.libcloud_dns as libcloud_dns_module
|
||||
|
||||
# Import salt libs
|
||||
import salt.utils
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Import third party libs
|
||||
REQUIRED_LIBCLOUD_VERSION = '1.0.0'
|
||||
try:
|
||||
#pylint: disable=unused-import
|
||||
import libcloud
|
||||
from libcloud.dns.providers import get_driver
|
||||
#pylint: enable=unused-import
|
||||
if _LooseVersion(libcloud.__version__) < _LooseVersion(REQUIRED_LIBCLOUD_VERSION):
|
||||
raise ImportError()
|
||||
logging.getLogger('libcloud').setLevel(logging.CRITICAL)
|
||||
HAS_LIBCLOUD = True
|
||||
except ImportError:
|
||||
HAS_LIBCLOUD = False
|
||||
|
||||
|
||||
def __virtual__():
|
||||
'''
|
||||
Only load if libcloud libraries exist.
|
||||
'''
|
||||
if not HAS_LIBCLOUD:
|
||||
msg = ('A apache-libcloud library with version at least {0} was not '
|
||||
'found').format(REQUIRED_LIBCLOUD_VERSION)
|
||||
return (False, msg)
|
||||
return True
|
||||
|
||||
|
||||
def __init__(opts):
|
||||
salt.utils.compat.pack_dunder(__name__)
|
||||
|
||||
|
||||
def _get_driver(profile):
|
||||
config = __salt__['config.option']('libcloud_dns')[profile]
|
||||
cls = get_driver(config['driver'])
|
||||
key = config.get('key')
|
||||
secret = config.get('secret', None)
|
||||
secure = config.get('secure', True)
|
||||
host = config.get('host', None)
|
||||
port = config.get('port', None)
|
||||
return cls(key, secret, secure, host, port)
|
||||
|
||||
|
||||
def state_result(result, message):
|
||||
return {'result': result, 'comment': message}
|
||||
|
||||
|
||||
def zone_present(domain, type, profile):
|
||||
'''
|
||||
Ensures a record is present.
|
||||
|
||||
:param domain: Zone name, i.e. the domain name
|
||||
:type domain: ``str``
|
||||
|
||||
:param type: Zone type (master / slave), defaults to master
|
||||
:type type: ``str``
|
||||
|
||||
:param profile: The profile key
|
||||
:type profile: ``str``
|
||||
'''
|
||||
zones = libcloud_dns_module.list_zones(profile)
|
||||
if not type:
|
||||
type = 'master'
|
||||
matching_zone = [z for z in zones if z.domain == domain]
|
||||
if len(matching_zone) > 0:
|
||||
return state_result(True, "Zone already exists")
|
||||
else:
|
||||
result = libcloud_dns_module.create_zone(domain, profile, type)
|
||||
return state_result(result, "Created new zone")
|
||||
|
||||
|
||||
def zone_absent(domain, profile):
|
||||
'''
|
||||
Ensures a record is absent.
|
||||
|
||||
:param domain: Zone name, i.e. the domain name
|
||||
:type domain: ``str``
|
||||
|
||||
:param profile: The profile key
|
||||
:type profile: ``str``
|
||||
'''
|
||||
zones = libcloud_dns_module.list_zones(profile)
|
||||
matching_zone = [z for z in zones if z.domain == domain]
|
||||
if len(matching_zone) == 0:
|
||||
return state_result(True, "Zone already absent")
|
||||
else:
|
||||
result = libcloud_dns_module.delete_zone(matching_zone[0].id, profile)
|
||||
return state_result(result, "Deleted zone")
|
||||
|
||||
|
||||
def record_present(name, zone, type, data, profile):
|
||||
'''
|
||||
Ensures a record is present.
|
||||
|
||||
:param name: Record name without the domain name (e.g. www).
|
||||
Note: If you want to create a record for a base domain
|
||||
name, you should specify empty string ('') for this
|
||||
argument.
|
||||
:type name: ``str``
|
||||
|
||||
:param zone: Zone where the requested record is created, the domain name
|
||||
:type zone: ``str``
|
||||
|
||||
:param type: DNS record type (A, AAAA, ...).
|
||||
:type type: ``str``
|
||||
|
||||
:param data: Data for the record (depends on the record type).
|
||||
:type data: ``str``
|
||||
|
||||
:param profile: The profile key
|
||||
:type profile: ``str``
|
||||
'''
|
||||
zones = libcloud_dns_module.list_zones(profile)
|
||||
try:
|
||||
matching_zone = [z for z in zones if z.domain == zone][0]
|
||||
except IndexError:
|
||||
return state_result(False, "Could not locate zone")
|
||||
records = libcloud_dns_module.list_records(matching_zone.id, profile)
|
||||
matching_records = [record for record in records
|
||||
if record.name == name and
|
||||
record.type == type and
|
||||
record.data == data]
|
||||
if len(matching_records) == 0:
|
||||
result = libcloud_dns_module.create_record(
|
||||
name, matching_zone.id,
|
||||
type, data, profile)
|
||||
return state_result(result, "Created new record")
|
||||
else:
|
||||
return state_result(True, "Record already exists")
|
||||
|
||||
|
||||
def record_absent(name, zone, type, data, profile):
|
||||
'''
|
||||
Ensures a record is absent.
|
||||
|
||||
:param name: Record name without the domain name (e.g. www).
|
||||
Note: If you want to create a record for a base domain
|
||||
name, you should specify empty string ('') for this
|
||||
argument.
|
||||
:type name: ``str``
|
||||
|
||||
:param zone: Zone where the requested record is created, the domain name
|
||||
:type zone: ``str``
|
||||
|
||||
:param type: DNS record type (A, AAAA, ...).
|
||||
:type type: ``str``
|
||||
|
||||
:param data: Data for the record (depends on the record type).
|
||||
:type data: ``str``
|
||||
|
||||
:param profile: The profile key
|
||||
:type profile: ``str``
|
||||
'''
|
||||
zones = libcloud_dns_module.list_zones(profile)
|
||||
try:
|
||||
matching_zone = [z for z in zones if z.domain == zone][0]
|
||||
except IndexError:
|
||||
return state_result(False, "Zone could not be found")
|
||||
records = libcloud_dns_module.list_records(matching_zone.id, profile)
|
||||
matching_records = [record for record in records
|
||||
if record.name == name and
|
||||
record.type == type and
|
||||
record.data == data]
|
||||
if len(matching_records) > 0:
|
||||
result = []
|
||||
for record in matching_records:
|
||||
result.append(libcloud_dns_module.delete_record(
|
||||
matching_zone.id,
|
||||
record.id,
|
||||
profile))
|
||||
return state_result(all(result), "Removed {0} records".format(len(result)))
|
||||
else:
|
||||
return state_result(True, "Records already absent")
|
198
tests/unit/states/libcloud_dns_test.py
Normal file
198
tests/unit/states/libcloud_dns_test.py
Normal file
@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
:codeauthor: :email:`Anthony Shaw <anthonyshaw@apache.org>`
|
||||
'''
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Import Salt Testing Libs
|
||||
from salttesting import skipIf
|
||||
from tests.unit import ModuleTestCase, hasDependency
|
||||
from salttesting.mock import (
|
||||
patch,
|
||||
MagicMock,
|
||||
NO_MOCK,
|
||||
NO_MOCK_REASON
|
||||
)
|
||||
from salttesting.helpers import ensure_in_syspath
|
||||
from salt.states import libcloud_dns
|
||||
|
||||
ensure_in_syspath('../../')
|
||||
|
||||
SERVICE_NAME = 'libcloud_dns'
|
||||
libcloud_dns.__salt__ = {}
|
||||
libcloud_dns.__utils__ = {}
|
||||
|
||||
|
||||
class TestZone(object):
|
||||
def __init__(self, id, domain):
|
||||
self.id = id
|
||||
self.domain = domain
|
||||
|
||||
|
||||
class TestRecord(object):
|
||||
def __init__(self, id, name, type, data):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.data = data
|
||||
|
||||
|
||||
class MockDNSDriver(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_mock_driver():
|
||||
return MockDNSDriver()
|
||||
|
||||
|
||||
class MockDnsModule(object):
|
||||
test_records = {
|
||||
"zone1": [TestRecord(0, "www", "A", "127.0.0.1")]
|
||||
}
|
||||
|
||||
def list_zones(self, profile):
|
||||
return [TestZone("zone1", "test.com")]
|
||||
|
||||
def list_records(self, zone_id, profile):
|
||||
return MockDnsModule.test_records[zone_id]
|
||||
|
||||
def create_record(self, *args):
|
||||
return True
|
||||
|
||||
def delete_record(self, *args):
|
||||
return True
|
||||
|
||||
def create_zone(self, *args):
|
||||
return True
|
||||
|
||||
def delete_zone(self, *args):
|
||||
return True
|
||||
|
||||
|
||||
@skipIf(NO_MOCK, NO_MOCK_REASON)
|
||||
@patch('salt.states.libcloud_dns._get_driver',
|
||||
MagicMock(return_value=MockDNSDriver()))
|
||||
class LibcloudDnsModuleTestCase(ModuleTestCase):
|
||||
def setUp(self):
|
||||
hasDependency('libcloud')
|
||||
|
||||
def get_config(service):
|
||||
if service == SERVICE_NAME:
|
||||
return {
|
||||
'test': {
|
||||
'driver': 'test',
|
||||
'key': '2orgk34kgk34g'
|
||||
}
|
||||
}
|
||||
else:
|
||||
raise KeyError("service name invalid")
|
||||
|
||||
self.setup_loader()
|
||||
self.loader.set_result(libcloud_dns, 'config.option', get_config)
|
||||
libcloud_dns.libcloud_dns_module = MockDnsModule()
|
||||
|
||||
def test_module_creation(self, *args):
|
||||
client = libcloud_dns._get_driver('test')
|
||||
self.assertFalse(client is None)
|
||||
|
||||
def test_init(self):
|
||||
with patch('salt.utils.compat.pack_dunder', return_value=False) as dunder:
|
||||
libcloud_dns.__init__(None)
|
||||
dunder.assert_called_with('salt.states.libcloud_dns')
|
||||
|
||||
def test_present_record_exists(self):
|
||||
"""
|
||||
Try and create a record that already exists
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'create_record', MagicMock(return_value=True)) as create_patch:
|
||||
result = libcloud_dns.record_present("www", "test.com", "A", "127.0.0.1", "test")
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(create_patch.called)
|
||||
|
||||
def test_present_record_does_not_exist(self):
|
||||
"""
|
||||
Try and create a record that already exists
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'create_record') as create_patch:
|
||||
result = libcloud_dns.record_present("mail", "test.com", "A", "127.0.0.1", "test")
|
||||
self.assertTrue(result)
|
||||
create_patch.assert_called_with('mail', "zone1", "A", "127.0.0.1", "test")
|
||||
|
||||
def test_absent_record_exists(self):
|
||||
"""
|
||||
Try and deny a record that already exists
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'delete_record', MagicMock(return_value=True)) as create_patch:
|
||||
result = libcloud_dns.record_absent("www", "test.com", "A", "127.0.0.1", "test")
|
||||
self.assertTrue(result)
|
||||
create_patch.assert_called_with('zone1', 0, 'test')
|
||||
|
||||
def test_absent_record_does_not_exist(self):
|
||||
"""
|
||||
Try and deny a record that already exists
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'delete_record') as create_patch:
|
||||
result = libcloud_dns.record_absent("mail", "test.com", "A", "127.0.0.1", "test")
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(create_patch.called)
|
||||
|
||||
def test_present_zone_not_found(self):
|
||||
"""
|
||||
Assert that when you try and ensure present state for a record to a zone that doesn't exist
|
||||
it fails gracefully
|
||||
"""
|
||||
result = libcloud_dns.record_present("mail", "notatest.com", "A", "127.0.0.1", "test")
|
||||
self.assertFalse(result['result'])
|
||||
|
||||
def test_absent_zone_not_found(self):
|
||||
"""
|
||||
Assert that when you try and ensure absent state for a record to a zone that doesn't exist
|
||||
it fails gracefully
|
||||
"""
|
||||
result = libcloud_dns.record_absent("mail", "notatest.com", "A", "127.0.0.1", "test")
|
||||
self.assertFalse(result['result'])
|
||||
|
||||
def test_zone_present(self):
|
||||
"""
|
||||
Assert that a zone is present (that did not exist)
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'create_zone') as create_patch:
|
||||
result = libcloud_dns.zone_present('testing.com', 'master', 'test1')
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(create_patch.called)
|
||||
create_patch.assert_called_with('testing.com', 'test1', 'master')
|
||||
|
||||
def test_zone_already_present(self):
|
||||
"""
|
||||
Assert that a zone is present (that did exist)
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'create_zone') as create_patch:
|
||||
result = libcloud_dns.zone_present('test.com', 'master', 'test1')
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(create_patch.called)
|
||||
|
||||
def test_zone_absent(self):
|
||||
"""
|
||||
Assert that a zone that did exist is absent
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'delete_zone') as create_patch:
|
||||
result = libcloud_dns.zone_absent('test.com', 'test1')
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(create_patch.called)
|
||||
create_patch.assert_called_with('zone1', 'test1')
|
||||
|
||||
def test_zone_already_absent(self):
|
||||
"""
|
||||
Assert that a zone that did not exist is absent
|
||||
"""
|
||||
with patch.object(MockDnsModule, 'delete_zone') as create_patch:
|
||||
result = libcloud_dns.zone_absent('testing.com', 'test1')
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(create_patch.called)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from unit import run_tests
|
||||
run_tests(LibcloudDnsModuleTestCase)
|
Loading…
Reference in New Issue
Block a user