Merge branch 'dnslib'

master
Justin Riley 2016-10-07 15:32:47 -04:00
commit 999adab4b2
32 changed files with 1429 additions and 267 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ build/
develop-eggs/
dist/
downloads/
.eggs
eggs/
#lib/
lib64/

View File

@ -2,9 +2,32 @@
Dyanmic DNS for OpenNebula
## Usage
Global options:
```
$ onedns --help
usage: onedns [-h] [--debug] [-d DOMAIN] [--one-address ONE_ADDRESS]
[--one-secret ONE_SECRET] [--one-proxy ONE_PROXY]
{daemon,shell} ...
OneDNS - Dynamic DNS for OpenNebula
positional arguments:
{daemon,shell}
optional arguments:
-h, --help show this help message and exit
--debug ONE controller host address
-d DOMAIN, --domain DOMAIN
DNS domain to use
--one-address ONE_ADDRESS
ONE controller host address
--one-secret ONE_SECRET
ONE credentials to use (e.g. user:key)
--one-proxy ONE_PROXY
proxy host to use to connect to ONE controller
```
$ docker-compose -f one-dns.yaml up
$ python setup.py install
$ onedns --etcd-host=http://localhost:2379 --client-cert /path/to/ssl/cert
Run the `daemon` command to kick off onedns:
```
$ onedns daemon --dns-port=53
```

View File

@ -1,22 +0,0 @@
version: '2'
services:
etcd:
image: quay.io/coreos/etcd:v2.3.2
restart: always
ports:
- "${ETCD_PORT}:${ETCD_PORT}"
volumes:
- "/tmp/data:/data"
command: --listen-client-urls http://0.0.0.0:${ETCD_PORT} --advertise-client-urls http://0.0.0.0:${ETCD_PORT} --data-dir /data
skydns:
image: skynetservices/skydns:2.5.3a
restart: always
ports:
- "${DNS_PORT}:${DNS_PORT}"
- "${DNS_PORT}:${DNS_PORT}/udp"
links:
- etcd
depends_on:
- etcd
command: -machines http://etcd:${ETCD_PORT} -addr 0.0.0.0:${DNS_PORT} -no-rec

View File

@ -1,59 +0,0 @@
from onedns import exception
from onedns.clients import one
from onedns.clients import skydns
from onedns.logger import log
class OneDNS(object):
"""
This class bridges the gap between OpenNebula and SkyDNS APIs. It primarily
provides convenience methods for adding/removing VMs to SkyDNS.
"""
def __init__(self, domain, one_kwargs={}, etcd_kwargs={}):
self._one = one.OneClient(**one_kwargs)
self._skydns = skydns.SkyDNSClient(domain, etcd_kwargs=etcd_kwargs)
def _check_for_networks(self, vm):
if not hasattr(vm.template, 'nics'):
raise exception.NoNetworksError(vm)
def _get_vm_dns_entries(self, vm):
entries = {}
hostname = vm.name
primary_ip = vm.template.nics[0].ip
entries[hostname] = primary_ip
for nic in vm.template.nics[1:]:
nicname = "{hostname}-{id}".format(hostname=hostname,
id=nic.nic_id)
entries[nicname] = nic.ip
return entries
def add_vm(self, vm):
self._check_for_networks(vm)
dns_entries = self._get_vm_dns_entries(vm)
log.info("Adding VM {id}: {vm}".format(id=vm.id, vm=vm.name))
for name, ip in dns_entries.items():
self._skydns.add_host(name, ip)
def remove_vm(self, vm):
self._check_for_networks(vm)
dns_entries = self._get_vm_dns_entries(vm)
log.info("Removing VM {id}: {vm}".format(id=vm.id, vm=vm.name))
for name, ip in dns_entries.items():
self._skydns.remove_host(name, ip)
def add_vm_by_id(self, vm_id):
vm = self._one.get_vm_by_id(vm_id)
return self.add_vm(vm)
def remove_vm_by_id(self, vm_id):
vm = self._one.get_vm_by_id(vm_id)
return self.remove_vm(vm)
def sync(self):
for vm in self._one.vms():
try:
self.add_vm(vm)
except exception.NoNetworksError as e:
e.log(warn=True)

View File

@ -1,52 +1,26 @@
import argparse
from onedns import api
from onedns import utils
from onedns import server
from onedns import logger
from onedns import monitor
from onedns.clients import skydns
def daemon(args, one_args, etcd_args):
mon = monitor.OneMonitor(args.domain, one_kwargs=one_args,
etcd_kwargs=etcd_args)
mon.run(args.interval)
def daemon(args, one_args, **kwargs):
testing = kwargs.get('testing', False)
vms = kwargs.get('vms')
srv = server.OneDNS(args.domain, one_kwargs=one_args)
srv.sync(vms=vms)
srv.daemon(dns_port=args.dns_port, testing=testing)
def add_host(args, one_args, etcd_args):
client = skydns.SkyDNSClient(args.domain, etcd_kwargs=etcd_args)
client.add_host(args.hostname, args.ip)
def remove_host(args, one_args, etcd_args):
client = skydns.SkyDNSClient(args.domain, etcd_kwargs=etcd_args)
client.remove_host(args.hostname, args.ip)
def add_vm(args, one_args, etcd_args):
client = api.OneDNS(args.domain, one_kwargs=one_args,
etcd_kwargs=etcd_args)
client.add_vm_by_id(args.id)
def remove_vm(args, one_args, etcd_args):
client = api.OneDNS(args.domain, one_kwargs=one_args,
etcd_kwargs=etcd_args)
client.remove_vm_by_id(args.id)
def shell(args, one_args, etcd_args):
onemon = monitor.OneMonitor(args.domain, one_kwargs=one_args,
etcd_kwargs=etcd_args)
oneclient = onemon._one
skyclient = onemon._skydns
etcdclient = skyclient._etcd
ns = dict(onemon=onemon, skyclient=skyclient, oneclient=oneclient,
etcdclient=etcdclient, log=logger.log)
def shell(args, one_args, **kwargs):
srv = server.OneDNS(args.domain, one_kwargs=one_args)
oneclient = srv._one
ns = dict(one_dns=srv, oneclient=oneclient, log=logger.log)
utils.shell(local_ns=ns)
def main(args=None):
def get_parser():
parser = argparse.ArgumentParser(
description='OneDNS - Dynamic DNS for OpenNebula')
parser.add_argument('--debug', required=False,
@ -60,53 +34,23 @@ def main(args=None):
help='ONE credentials to use (e.g. user:key)')
parser.add_argument('--one-proxy', required=False,
help='proxy host to use to connect to ONE controller')
parser.add_argument('--etcd-host', required=False,
help='etcd host to connect to')
parser.add_argument('--etcd-port', required=False, type=int, default=4001,
help='etcd port to connect to')
parser.add_argument('--etcd-cert', required=False, type=int,
help='path to etcd client ssl cert')
subparsers = parser.add_subparsers()
daemon_parser = subparsers.add_parser('daemon')
daemon_parser.set_defaults(func=daemon)
daemon_parser.add_argument(
'-i', '--interval', required=False, type=int, default=60,
help="how often in seconds to poll ONE and update DNS")
add_parser = subparsers.add_parser('add')
add_subparser = add_parser.add_subparsers()
add_vm_parser = add_subparser.add_parser('vm')
add_vm_parser.set_defaults(func=add_vm)
add_vm_parser.add_argument('id', type=int, help='id of the vm to add')
add_host_parser = add_subparser.add_parser('host')
add_host_parser.set_defaults(func=add_host)
add_host_parser.add_argument('hostname', help='name of host to add')
add_host_parser.add_argument('ip', help='ip of host to add')
rm_parser = subparsers.add_parser('remove')
rm_subparser = rm_parser.add_subparsers()
rm_vm_parser = rm_subparser.add_parser('vm')
rm_vm_parser.set_defaults(func=remove_vm)
rm_vm_parser.add_argument('id', type=int, help='id of the vm to add')
rm_host_parser = rm_subparser.add_parser('host')
rm_host_parser.set_defaults(func=remove_host)
rm_host_parser.add_argument('hostname', help='name of host to remove')
rm_host_parser.add_argument('ip', help='ip of host to remove')
'--dns-port', required=False, default=5053, type=int,
help="port for DNS server to listen on")
shell_parser = subparsers.add_parser('shell')
shell_parser.set_defaults(func=shell)
return parser
args = parser.parse_args(args=args)
def main(**kwargs):
parser = get_parser()
args = parser.parse_args(args=kwargs.pop('args', None))
logger.configure_onedns_logging(debug=args.debug)
args_dict = vars(args)
one_args = utils.get_kwargs_from_dict(args_dict, 'one_')
etcd_args = utils.get_kwargs_from_dict(args_dict, 'etcd_')
args.func(args, one_args, etcd_args)
args.func(args, one_args, **kwargs)

View File

@ -1,60 +0,0 @@
import re
import json
import etcd
from onedns.logger import log
RE_VALIDNAME = re.compile('[^\w\d.-]')
class SkyDNSClient(object):
def __init__(self, domain, etcd_kwargs={}):
self.domain = domain
self._reverse_domain_parts = domain.split('.')
self._reverse_domain_parts.reverse()
self._etcd = etcd.Client(**etcd_kwargs)
def _sanitize_name(self, name):
return RE_VALIDNAME.sub('', name).rstrip('.')
def _skydns_ns(self, parts):
return '/'.join(['skydns'] + parts)
def _get_forward_ns(self, hostname):
return self._skydns_ns(self._reverse_domain_parts + [hostname])
def _get_reverse_ns(self, ip):
ip_parts = ip.split('.')
return self._skydns_ns(['arpa/in-addr'] + ip_parts)
def add_forward(self, hostname, ip):
forward = self._get_forward_ns(hostname)
log.debug("adding forward: {path}".format(path=forward))
self._etcd.write(forward, json.dumps(dict(host=ip)))
def remove_forward(self, hostname):
forward = self._get_forward_ns(hostname)
log.debug("removing forward: {path}".format(path=forward))
self._etcd.delete(forward)
def add_reverse(self, ip, hostname):
reverse = self._get_reverse_ns(ip)
fqdn = '.'.join([hostname, self.domain])
log.debug("adding reverse: {path}".format(path=reverse))
self._etcd.write(reverse, json.dumps(dict(host=fqdn)))
def remove_reverse(self, ip):
reverse = self._get_reverse_ns(ip)
log.debug("removing reverse: {path}".format(path=reverse))
self._etcd.delete(reverse)
def add_host(self, hostname, ip):
hostname = self._sanitize_name(hostname)
self.add_forward(hostname, ip)
self.add_reverse(ip, hostname)
def remove_host(self, hostname, ip):
hostname = self._sanitize_name(hostname)
self.remove_forward(hostname)
self.remove_reverse(ip)

View File

@ -26,3 +26,21 @@ class NoNetworksError(OneDnsException):
def __init__(self, vm):
self.msg = "No networks found for VM {id}: {vm}".format(vm=vm.name,
id=vm.id)
class RecordDoesNotExist(OneDnsException):
"""
Raised when a zone record does not exist
"""
def __init__(self, key, val=None):
self.msg = "Record Does Not Exist: {}".format(key)
if val is not None:
self.msg += " -> {}".format(val)
class DuplicateVMError(OneDnsException):
"""
Raised when two or more VMs share a name or IP
"""
def __init__(self, vmid, key, val):
self.msg = "VM one-{} has a duplicate: {} -> {}".format(vmid, key, val)

View File

@ -1,4 +1,3 @@
import os
import logging
import logging.handlers
@ -6,37 +5,20 @@ import logging.handlers
LOG_FORMAT = ("%(asctime)s %(filename)s:%(lineno)d - %(levelname)s - "
"%(message)s")
class NullHandler(logging.Handler):
def emit(self, record):
pass
def get_onedns_logger():
log = logging.getLogger('onedns')
log.addHandler(NullHandler())
return log
log = get_onedns_logger()
log = logging.getLogger('onedns')
console = logging.StreamHandler()
formatter = logging.Formatter(LOG_FORMAT)
console.setFormatter(formatter)
def configure_onedns_logging(use_syslog=False, syslog_device='/dev/log',
debug=False):
def configure_onedns_logging(debug=False):
"""
Configure logging for onedns *application* code
By default onedns's logger has no formatters and a NullHandler so that
other developers using onedns as a library can configure logging as
they see fit. This method is used in onedns's application code (i.e.
the 'onedns' command) to toggle onedns's application specific
formatters/handlers
use_syslog - enable logging all messages to syslog. currently only works if
/dev/log exists on the system (standard for most Linux distros)
By default onedns's logger is completely unconfigured so that other
developers using onedns as a library can configure logging as they see fit.
This method is used in onedns's application code (i.e. the 'onedns'
command) to toggle onedns's application specific formatters/handlers
"""
log.setLevel(logging.DEBUG)
if debug:
@ -44,8 +26,3 @@ def configure_onedns_logging(use_syslog=False, syslog_device='/dev/log',
else:
console.setLevel(logging.INFO)
log.addHandler(console)
if use_syslog and os.path.exists(syslog_device):
log.debug("Logging to %s" % syslog_device)
syslog_handler = logging.handlers.SysLogHandler(address=syslog_device)
syslog_handler.setLevel(logging.DEBUG)
log.addHandler(syslog_handler)

View File

@ -1,13 +0,0 @@
import time
from onedns import api
class OneMonitor(api.OneDNS):
"""
Daemon that syncs OpenNebula VMs with SkyDNS
"""
def run(self, interval=60):
while True:
self.sync()
time.sleep(interval)

108
onedns/resolver.py Normal file
View File

@ -0,0 +1,108 @@
import time
import threading
import dnslib
from dnslib import server
from wrapt import synchronized
from onedns import zone
from onedns import utils
from onedns import exception
from onedns.logger import log
class DynamicResolver(server.BaseResolver):
"""
Dynamic In-Memory DNS Resolver
"""
_lock = threading.RLock()
def __init__(self, domain):
"""
Initialise resolver from zone list
Stores RRs as a list of (label, type, rr) tuples
"""
self.domain = domain
self.zone = zone.Zone(domain)
self._tcp_server = None
self._udp_server = None
@synchronized(_lock)
def resolve(self, request, handler):
"""
Respond to DNS request - parameters are request packet & handler.
Method is expected to return DNS response
"""
reply = request.reply()
qname = request.q.qname
qtype = request.q.qtype
try:
if qtype in (dnslib.QTYPE.A, dnslib.QTYPE.AAAA):
forward = self.zone.get_forward(qname)
reply.add_answer(forward)
elif qtype == dnslib.QTYPE.PTR:
reverse = self.zone.get_reverse(
utils.reverse_to_ip(qname.idna()))
reply.add_answer(reverse)
forward = self.zone.get_forward(str(reverse.rdata))
if forward:
reply.add_ar(forward)
except exception.RecordDoesNotExist:
reply.header.rcode = dnslib.RCODE.NXDOMAIN
return reply
@synchronized(_lock)
def clear(self):
self.zone.clear()
@synchronized(_lock)
def load(self, zone):
self.zone = zone
@synchronized(_lock)
def add_host(self, name, ip, zone=None):
z = zone or self.zone
z.add_host(name, ip)
@synchronized(_lock)
def remove_host(self, name, ip, zone=None):
z = zone or self.zone
z.remove_host(name, ip)
def start(self, dns_address='0.0.0.0', dns_port=53,
api_address='127.0.0.1', api_port=8000, tcp=False, udplen=0,
log_components="request,reply,truncated,error",
log_prefix=False):
logger = server.DNSLogger(log_components, log_prefix)
log.info("Starting OneDNS (%s:%d) [%s]" %
(dns_address or "*", dns_port, "UDP/TCP" if tcp else "UDP"))
server.DNSHandler.udplen = udplen
self._udp_server = server.DNSServer(self, port=dns_port,
address=dns_address, logger=logger)
self._udp_server.start_thread()
if tcp:
self._tcp_server = server.DNSServer(self, port=dns_port,
address=dns_address, tcp=True,
logger=logger)
self._tcp_server.start_thread()
def close(self):
for srv in [self._tcp_server, self._udp_server]:
if srv:
srv.stop()
srv.server.socket.close()
def daemon(self, *args, **kwargs):
testing = kwargs.pop('testing', False)
if self._udp_server is None or not self._udp_server.isAlive():
self.start(*args, **kwargs)
while self._udp_server.isAlive():
time.sleep(1)
if testing:
break

90
onedns/server.py Normal file
View File

@ -0,0 +1,90 @@
import re
from onedns import zone
from onedns import resolver
from onedns import exception
from onedns.clients import one
from onedns.logger import log
_BAD_CHARS = re.compile('[^-a-zA-Z0-9]')
class OneDNS(resolver.DynamicResolver):
"""
This class provides convenience methods for adding/removing VMs to the
DynamicResolver.
"""
def __init__(self, domain, one_kwargs={}):
super(OneDNS, self).__init__(domain)
self._one = one.OneClient(**one_kwargs)
def _check_for_networks(self, vm):
if not hasattr(vm.template, 'nics'):
raise exception.NoNetworksError(vm)
def _sanitize_name(self, name):
name = _BAD_CHARS.sub('-', name)
return name.strip('-')
def _get_vm_dns_entries(self, vm):
self._check_for_networks(vm)
entries = {}
hostname = self._sanitize_name(vm.name)
primary_ip = vm.template.nics[0].ip
entries[hostname] = primary_ip
for nic in vm.template.nics[1:]:
nicname = self._sanitize_name("{hostname}-{id}".format(
id=nic.nic_id,
hostname=hostname
))
entries[nicname] = nic.ip
return entries
def _check_for_duplicates(self, vm_id, name, ip, zone=None):
z = zone or self.zone
try:
f = z.get_forward(name)
raise exception.DuplicateVMError(vm_id, f, ip)
except exception.RecordDoesNotExist:
pass
try:
r = z.get_reverse(ip)
raise exception.DuplicateVMError(vm_id, ip, r)
except exception.RecordDoesNotExist:
pass
def add_vm(self, vm, zone=None):
dns_entries = self._get_vm_dns_entries(vm)
log.info("Adding VM {id}: {vm}".format(id=vm.id, vm=vm.name))
for name, ip in dns_entries.items():
self._check_for_duplicates(vm.id, name, ip, zone=zone)
self.add_host(name, ip, zone=zone)
def remove_vm(self, vm, zone=None):
dns_entries = self._get_vm_dns_entries(vm)
log.info("Removing VM {id}: {vm}".format(id=vm.id, vm=vm.name))
for name, ip in dns_entries.items():
self.remove_host(name, ip, zone=zone)
def add_vm_by_id(self, vm_id):
vm = self._one.get_vm_by_id(vm_id)
return self.add_vm(vm)
def remove_vm_by_id(self, vm_id):
vm = self._one.get_vm_by_id(vm_id)
return self.remove_vm(vm)
def sync(self, vms=None):
z = zone.Zone(self.domain)
vms = vms or self._one.vms()
vms.sort(key=lambda x: x.id)
for vm in vms:
try:
self.add_vm(vm, zone=z)
except exception.NoNetworksError as e:
e.log(warn=True)
except exception.DuplicateVMError as e:
e.log(warn=True)
self.load(z)

30
onedns/tests/__init__.py Normal file
View File

@ -0,0 +1,30 @@
import os
import StringIO
import xml.etree.ElementTree as ET
from vcr import VCR
def scrub_auth(request):
xml = StringIO.StringIO(request.body)
tree = ET.parse(xml)
root = tree.getroot()
auth_param = root.findall('./params/param/value/string')[0]
auth_param.text = 'someuser:sometoken'
scrubbed = StringIO.StringIO()
tree.write(scrubbed)
scrubbed.seek(0)
request.body = scrubbed.read()
return request
vcr = VCR(
serializer='yaml',
cassette_library_dir=os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'fixtures/oneclient'
),
record_mode='once',
decode_compressed_response=True,
path_transformer=VCR.ensure_suffix('.yaml'),
before_record=scrub_auth,
)

79
onedns/tests/conftest.py Normal file
View File

@ -0,0 +1,79 @@
import pytest
import dnslib
from IPy import IP
from onedns import zone
from onedns import server
from onedns import resolver
from onedns.tests import vcr
from onedns.clients import one
DOMAIN = 'onedns.test'
INTERFACE = '127.0.0.1'
PORT = 9053
HOST_SHORT = 'testhost'
HOST = '.'.join([HOST_SHORT, DOMAIN])
HOST_IP = '10.242.118.112'
TEST_LOOKUP_DATA = [
(HOST, dnslib.QTYPE.A, HOST_IP),
(IP(HOST_IP).reverseName(), dnslib.QTYPE.PTR, HOST + '.')
]
TEST_GET_FQDN_DATA = [
('hostwithnodot', '192.168.1.23'),
('hostwithdot.', '192.168.1.19'),
]
TEST_GET_FORWARD_DATA = [
HOST_SHORT,
HOST_SHORT + '.',
HOST,
HOST + '.',
]
TEST_FQDN_DATA = [
(HOST_SHORT, DOMAIN),
(HOST_SHORT + '.', DOMAIN),
(HOST_SHORT, DOMAIN + '.'),
(HOST_SHORT + '.', DOMAIN + '.'),
(HOST, DOMAIN),
]
TEST_FQDN_RESULT = HOST + '.'
@pytest.fixture(scope="function")
def dns(request):
dns = resolver.DynamicResolver(domain=DOMAIN)
dns.start(dns_address=INTERFACE, dns_port=PORT, tcp=True)
request.addfinalizer(dns.close)
return dns
@pytest.fixture(scope="function")
def one_dns(request, oneclient):
dns = server.OneDNS(domain=DOMAIN)
dns._one = oneclient
dns.start(dns_address=INTERFACE, dns_port=PORT, tcp=True)
request.addfinalizer(dns.close)
return dns
@pytest.fixture(scope="function")
def oneclient():
"""
NOTE: All fixtures must be function scope to work with VCRPY cassettes
"""
return one.OneClient()
@pytest.fixture(scope="function")
@vcr.use_cassette()
def vms(oneclient):
return oneclient.vms()
@pytest.fixture(scope="function")
def onezone():
z = zone.Zone(DOMAIN)
z.add_host(HOST_SHORT, HOST_IP)
for name, ip in TEST_GET_FQDN_DATA:
z.add_host(name, ip)
return z

View File

@ -0,0 +1,66 @@
interactions:
- request:
body: !!python/unicode '<methodCall>
<methodName>one.vmpool.info</methodName>
<params>
<param>
<value><string>someuser:sometoken</string></value>
</param>
<param>
<value><int>-1</int></value>
</param>
<param>
<value><int>0</int></value>
</param>
<param>
<value><int>0</int></value>
</param>
<param>
<value><int>-2</int></value>
</param>
</params>
</methodCall>'
headers:
Accept-Encoding: [gzip]
Content-Length: ['382']
Content-Type: [text/xml]
User-Agent: [xmlrpclib.py/1.0.1 (by www.pythonware.com)]
method: POST
uri: http://localhost:2633/RPC2
response:
body: {string: !!python/unicode "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\
<methodResponse>\r\n<params>\r\n<param><value><array><data>\r\n<value><boolean>1</boolean></value>\r\
\n<value><string>&lt;VM_POOL&gt;&lt;VM&gt;&lt;ID&gt;0&lt;/ID&gt;&lt;UID&gt;0&lt;/UID&gt;&lt;GID&gt;0&lt;/GID&gt;&lt;UNAME&gt;oneadmin&lt;/UNAME&gt;&lt;GNAME&gt;oneadmin&lt;/GNAME&gt;&lt;NAME&gt;test-0&lt;/NAME&gt;&lt;PERMISSIONS&gt;&lt;OWNER_U&gt;1&lt;/OWNER_U&gt;&lt;OWNER_M&gt;1&lt;/OWNER_M&gt;&lt;OWNER_A&gt;0&lt;/OWNER_A&gt;&lt;GROUP_U&gt;0&lt;/GROUP_U&gt;&lt;GROUP_M&gt;0&lt;/GROUP_M&gt;&lt;GROUP_A&gt;0&lt;/GROUP_A&gt;&lt;OTHER_U&gt;0&lt;/OTHER_U&gt;&lt;OTHER_M&gt;0&lt;/OTHER_M&gt;&lt;OTHER_A&gt;0&lt;/OTHER_A&gt;&lt;/PERMISSIONS&gt;&lt;LAST_POLL&gt;1475867584&lt;/LAST_POLL&gt;&lt;STATE&gt;3&lt;/STATE&gt;&lt;LCM_STATE&gt;3&lt;/LCM_STATE&gt;&lt;PREV_STATE&gt;3&lt;/PREV_STATE&gt;&lt;PREV_LCM_STATE&gt;3&lt;/PREV_LCM_STATE&gt;&lt;RESCHED&gt;0&lt;/RESCHED&gt;&lt;STIME&gt;1475786733&lt;/STIME&gt;&lt;ETIME&gt;0&lt;/ETIME&gt;&lt;DEPLOY_ID&gt;one-0&lt;/DEPLOY_ID&gt;&lt;MONITORING&gt;&lt;CPU&gt;&lt;![CDATA[14.0]]&gt;&lt;/CPU&gt;&lt;DISK_SIZE&gt;&lt;ID&gt;&lt;![CDATA[0]]&gt;&lt;/ID&gt;&lt;SIZE&gt;&lt;![CDATA[40]]&gt;&lt;/SIZE&gt;&lt;/DISK_SIZE&gt;&lt;DISK_SIZE&gt;&lt;ID&gt;&lt;![CDATA[1]]&gt;&lt;/ID&gt;&lt;SIZE&gt;&lt;![CDATA[1]]&gt;&lt;/SIZE&gt;&lt;/DISK_SIZE&gt;&lt;MEMORY&gt;&lt;![CDATA[131072]]&gt;&lt;/MEMORY&gt;&lt;NETRX&gt;&lt;![CDATA[648]]&gt;&lt;/NETRX&gt;&lt;NETTX&gt;&lt;![CDATA[0]]&gt;&lt;/NETTX&gt;&lt;STATE&gt;&lt;![CDATA[a]]&gt;&lt;/STATE&gt;&lt;/MONITORING&gt;&lt;TEMPLATE&gt;&lt;AUTOMATIC_DS_REQUIREMENTS&gt;&lt;![CDATA[\"\
CLUSTERS/ID\" @&gt; 0]]&gt;&lt;/AUTOMATIC_DS_REQUIREMENTS&gt;&lt;AUTOMATIC_REQUIREMENTS&gt;&lt;![CDATA[(CLUSTER_ID\
\ = 0) &amp; !(PUBLIC_CLOUD = YES)]]&gt;&lt;/AUTOMATIC_REQUIREMENTS&gt;&lt;CONTEXT&gt;&lt;DISK_ID&gt;&lt;![CDATA[1]]&gt;&lt;/DISK_ID&gt;&lt;ETH0_CONTEXT_FORCE_IPV4&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_CONTEXT_FORCE_IPV4&gt;&lt;ETH0_DNS&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_DNS&gt;&lt;ETH0_GATEWAY&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_GATEWAY&gt;&lt;ETH0_GATEWAY6&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_GATEWAY6&gt;&lt;ETH0_IP&gt;&lt;![CDATA[172.16.100.2]]&gt;&lt;/ETH0_IP&gt;&lt;ETH0_IP6&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_IP6&gt;&lt;ETH0_IP6_ULA&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_IP6_ULA&gt;&lt;ETH0_MAC&gt;&lt;![CDATA[02:00:ac:10:64:02]]&gt;&lt;/ETH0_MAC&gt;&lt;ETH0_MASK&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_MASK&gt;&lt;ETH0_MTU&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_MTU&gt;&lt;ETH0_NETWORK&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_NETWORK&gt;&lt;ETH0_SEARCH_DOMAIN&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_SEARCH_DOMAIN&gt;&lt;ETH0_VLAN_ID&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_VLAN_ID&gt;&lt;ETH0_VROUTER_IP&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_VROUTER_IP&gt;&lt;ETH0_VROUTER_IP6&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_VROUTER_IP6&gt;&lt;ETH0_VROUTER_MANAGEMENT&gt;&lt;![CDATA[]]&gt;&lt;/ETH0_VROUTER_MANAGEMENT&gt;&lt;NETWORK&gt;&lt;![CDATA[YES]]&gt;&lt;/NETWORK&gt;&lt;SSH_PUBLIC_KEY&gt;&lt;![CDATA[]]&gt;&lt;/SSH_PUBLIC_KEY&gt;&lt;TARGET&gt;&lt;![CDATA[hdb]]&gt;&lt;/TARGET&gt;&lt;/CONTEXT&gt;&lt;CPU&gt;&lt;![CDATA[0.1]]&gt;&lt;/CPU&gt;&lt;DISK&gt;&lt;CLONE&gt;&lt;![CDATA[YES]]&gt;&lt;/CLONE&gt;&lt;CLONE_TARGET&gt;&lt;![CDATA[SYSTEM]]&gt;&lt;/CLONE_TARGET&gt;&lt;CLUSTER_ID&gt;&lt;![CDATA[0]]&gt;&lt;/CLUSTER_ID&gt;&lt;DATASTORE&gt;&lt;![CDATA[default]]&gt;&lt;/DATASTORE&gt;&lt;DATASTORE_ID&gt;&lt;![CDATA[1]]&gt;&lt;/DATASTORE_ID&gt;&lt;DEV_PREFIX&gt;&lt;![CDATA[hd]]&gt;&lt;/DEV_PREFIX&gt;&lt;DISK_ID&gt;&lt;![CDATA[0]]&gt;&lt;/DISK_ID&gt;&lt;DISK_SNAPSHOT_TOTAL_SIZE&gt;&lt;![CDATA[0]]&gt;&lt;/DISK_SNAPSHOT_TOTAL_SIZE&gt;&lt;DISK_TYPE&gt;&lt;![CDATA[FILE]]&gt;&lt;/DISK_TYPE&gt;&lt;IMAGE&gt;&lt;![CDATA[ttylinux\
\ - kvm]]&gt;&lt;/IMAGE&gt;&lt;IMAGE_ID&gt;&lt;![CDATA[0]]&gt;&lt;/IMAGE_ID&gt;&lt;IMAGE_STATE&gt;&lt;![CDATA[2]]&gt;&lt;/IMAGE_STATE&gt;&lt;LN_TARGET&gt;&lt;![CDATA[SYSTEM]]&gt;&lt;/LN_TARGET&gt;&lt;READONLY&gt;&lt;![CDATA[NO]]&gt;&lt;/READONLY&gt;&lt;SAVE&gt;&lt;![CDATA[NO]]&gt;&lt;/SAVE&gt;&lt;SIZE&gt;&lt;![CDATA[40]]&gt;&lt;/SIZE&gt;&lt;SOURCE&gt;&lt;![CDATA[/var/lib/one//datastores/1/203ec12ff6495be37ec95dbbc7cf94ef]]&gt;&lt;/SOURCE&gt;&lt;TARGET&gt;&lt;![CDATA[hda]]&gt;&lt;/TARGET&gt;&lt;TM_MAD&gt;&lt;![CDATA[ssh]]&gt;&lt;/TM_MAD&gt;&lt;TYPE&gt;&lt;![CDATA[FILE]]&gt;&lt;/TYPE&gt;&lt;/DISK&gt;&lt;GRAPHICS&gt;&lt;LISTEN&gt;&lt;![CDATA[0.0.0.0]]&gt;&lt;/LISTEN&gt;&lt;PORT&gt;&lt;![CDATA[5900]]&gt;&lt;/PORT&gt;&lt;TYPE&gt;&lt;![CDATA[VNC]]&gt;&lt;/TYPE&gt;&lt;/GRAPHICS&gt;&lt;MEMORY&gt;&lt;![CDATA[128]]&gt;&lt;/MEMORY&gt;&lt;NIC&gt;&lt;AR_ID&gt;&lt;![CDATA[0]]&gt;&lt;/AR_ID&gt;&lt;BRIDGE&gt;&lt;![CDATA[br0]]&gt;&lt;/BRIDGE&gt;&lt;CLUSTER_ID&gt;&lt;![CDATA[0]]&gt;&lt;/CLUSTER_ID&gt;&lt;IP&gt;&lt;![CDATA[172.16.100.2]]&gt;&lt;/IP&gt;&lt;MAC&gt;&lt;![CDATA[02:00:ac:10:64:02]]&gt;&lt;/MAC&gt;&lt;NETWORK&gt;&lt;![CDATA[local]]&gt;&lt;/NETWORK&gt;&lt;NETWORK_ID&gt;&lt;![CDATA[0]]&gt;&lt;/NETWORK_ID&gt;&lt;NIC_ID&gt;&lt;![CDATA[0]]&gt;&lt;/NIC_ID&gt;&lt;SECURITY_GROUPS&gt;&lt;![CDATA[0]]&gt;&lt;/SECURITY_GROUPS&gt;&lt;TARGET&gt;&lt;![CDATA[one-0-0]]&gt;&lt;/TARGET&gt;&lt;VN_MAD&gt;&lt;![CDATA[dummy]]&gt;&lt;/VN_MAD&gt;&lt;/NIC&gt;&lt;SECURITY_GROUP_RULE&gt;&lt;PROTOCOL&gt;&lt;![CDATA[ALL]]&gt;&lt;/PROTOCOL&gt;&lt;RULE_TYPE&gt;&lt;![CDATA[OUTBOUND]]&gt;&lt;/RULE_TYPE&gt;&lt;SECURITY_GROUP_ID&gt;&lt;![CDATA[0]]&gt;&lt;/SECURITY_GROUP_ID&gt;&lt;SECURITY_GROUP_NAME&gt;&lt;![CDATA[default]]&gt;&lt;/SECURITY_GROUP_NAME&gt;&lt;/SECURITY_GROUP_RULE&gt;&lt;SECURITY_GROUP_RULE&gt;&lt;PROTOCOL&gt;&lt;![CDATA[ALL]]&gt;&lt;/PROTOCOL&gt;&lt;RULE_TYPE&gt;&lt;![CDATA[INBOUND]]&gt;&lt;/RULE_TYPE&gt;&lt;SECURITY_GROUP_ID&gt;&lt;![CDATA[0]]&gt;&lt;/SECURITY_GROUP_ID&gt;&lt;SECURITY_GROUP_NAME&gt;&lt;![CDATA[default]]&gt;&lt;/SECURITY_GROUP_NAME&gt;&lt;/SECURITY_GROUP_RULE&gt;&lt;TEMPLATE_ID&gt;&lt;![CDATA[0]]&gt;&lt;/TEMPLATE_ID&gt;&lt;VMID&gt;&lt;![CDATA[0]]&gt;&lt;/VMID&gt;&lt;/TEMPLATE&gt;&lt;USER_TEMPLATE&gt;&lt;LOGO&gt;&lt;![CDATA[images/logos/linux.png]]&gt;&lt;/LOGO&gt;&lt;/USER_TEMPLATE&gt;&lt;HISTORY_RECORDS&gt;&lt;HISTORY&gt;&lt;OID&gt;0&lt;/OID&gt;&lt;SEQ&gt;0&lt;/SEQ&gt;&lt;HOSTNAME&gt;one-dns-testing&lt;/HOSTNAME&gt;&lt;HID&gt;0&lt;/HID&gt;&lt;CID&gt;0&lt;/CID&gt;&lt;STIME&gt;1475786738&lt;/STIME&gt;&lt;ETIME&gt;0&lt;/ETIME&gt;&lt;VM_MAD&gt;&lt;![CDATA[kvm]]&gt;&lt;/VM_MAD&gt;&lt;TM_MAD&gt;&lt;![CDATA[ssh]]&gt;&lt;/TM_MAD&gt;&lt;DS_ID&gt;0&lt;/DS_ID&gt;&lt;PSTIME&gt;1475786738&lt;/PSTIME&gt;&lt;PETIME&gt;1475786739&lt;/PETIME&gt;&lt;RSTIME&gt;1475786739&lt;/RSTIME&gt;&lt;RETIME&gt;0&lt;/RETIME&gt;&lt;ESTIME&gt;0&lt;/ESTIME&gt;&lt;EETIME&gt;0&lt;/EETIME&gt;&lt;REASON&gt;0&lt;/REASON&gt;&lt;ACTION&gt;0&lt;/ACTION&gt;&lt;/HISTORY&gt;&lt;/HISTORY_RECORDS&gt;&lt;/VM&gt;&lt;/VM_POOL&gt;</string></value>\r\
\n<value><i4>0</i4></value>\r\n</data></array></value></param>\r\n</params>\r\
\n</methodResponse>\r\n"}
headers:
connection: [Keep-Alive]
content-length: ['6762']
content-type: [text/xml; charset=utf-8]
date: ['Fri, 07 Oct 2016 19:13:15 UTC']
keep-alive: ['timeout=15, max=30']
server: [Xmlrpc-c_Abyss/1.40.0]
status: {code: 200, message: OK}
version: 1

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

109
onedns/tests/fixtures/oneclient/vms.yaml vendored Normal file

File diff suppressed because one or more lines are too long

24
onedns/tests/test_cli.py Normal file
View File

@ -0,0 +1,24 @@
import pytest
import mock
from onedns import cli
from onedns.tests import test_shell
def test_cli_help():
with pytest.raises(SystemExit):
cli.main(args=['--help'])
@mock.patch.object(cli, 'logger', mock.MagicMock())
def test_cli_subcmd_daemon(vms):
cli.main(args=['daemon'], testing=True, vms=vms)
@mock.patch.dict('sys.modules', test_shell.IPY_MODULES)
@mock.patch.object(cli, 'logger', mock.MagicMock())
def test_cli_subcmd_shell():
test_shell.IPY.embed.reset_mock()
cli.main(args=['shell'], testing=True)
test_shell.IPY.embed.assert_called_once()

View File

@ -0,0 +1,67 @@
import pytest
import dnslib
from IPy import IP
from onedns import zone
from onedns.tests import utils
from onedns.tests import conftest
def _clear_and_add_test_host(dns, name, ip):
dns.clear()
dns.add_host(name, ip)
@pytest.mark.parametrize("qname,qtype,output", conftest.TEST_LOOKUP_DATA)
def test_lookup(dns, qname, qtype, output):
_clear_and_add_test_host(dns, conftest.HOST, conftest.HOST_IP)
try:
a = utils.dnsquery(qname, qtype)
assert a.short() == output
finally:
dns.close()
def test_nxdomain(dns):
dns.clear()
try:
a = utils.dnsquery('unknownhost', dnslib.QTYPE.A)
assert dnslib.RCODE.get(a.header.rcode) == 'NXDOMAIN'
finally:
dns.close()
def test_daemon(dns):
dns.close()
dns.daemon(dns_address=conftest.INTERFACE, dns_port=conftest.PORT,
tcp=True, testing=True)
@pytest.mark.parametrize("qname,qtype,output", conftest.TEST_LOOKUP_DATA)
def test_remove_host(dns, qname, qtype, output):
_clear_and_add_test_host(dns, conftest.HOST, conftest.HOST_IP)
try:
a = utils.dnsquery(qname, qtype)
assert a.short() == output
dns.remove_host(conftest.HOST, conftest.HOST_IP)
a = utils.dnsquery(qname, qtype)
assert dnslib.RCODE.get(a.header.rcode) == 'NXDOMAIN'
finally:
dns.close()
def test_load_zone(dns):
new_ip = IP(IP(conftest.HOST_IP).int() + 1)
new_zone = zone.Zone(conftest.DOMAIN)
new_zone.add_host(conftest.HOST, new_ip)
_clear_and_add_test_host(dns, conftest.HOST, conftest.HOST_IP)
try:
a = utils.dnsquery(conftest.HOST, dnslib.QTYPE.A)
assert a.short() == conftest.HOST_IP
dns.load(new_zone)
a = utils.dnsquery(conftest.HOST, dnslib.QTYPE.A)
assert a.short() == str(new_ip)
finally:
dns.close()

View File

@ -0,0 +1,19 @@
from onedns import exception
from testfixtures import LogCapture
def test_onedns_exception():
test_msg = "test message"
e = exception.OneDnsException("test message")
assert e.msg == test_msg
assert e.args == (test_msg,)
assert str(e) == test_msg
assert e.explain() == 'OneDnsException: {}'.format(test_msg)
with LogCapture() as log_capture:
e.log()
e.log(warn=True)
log_capture.check(
('onedns', 'ERROR', e.explain()),
('onedns', 'WARNING', e.explain()),
)

View File

@ -0,0 +1,21 @@
import logging
from testfixtures import LogCapture
from onedns import logger
def test_onedns_logger():
assert not logger.log.handlers
with LogCapture() as log_capture:
logger.configure_onedns_logging()
assert logger.log.handlers
assert logger.console.level == logging.INFO
logger.log.info('test')
logger.configure_onedns_logging(debug=True)
assert logger.console.level == logging.DEBUG
logger.log.debug('test')
log_capture.check(
('onedns', 'INFO', 'test'),
('onedns', 'DEBUG', 'test'),
)

View File

@ -0,0 +1,19 @@
from onedns.tests import vcr
import pytest
import oca
from oca import vm
def test_get_vms(vms):
assert isinstance(vms, vm.VirtualMachinePool)
assert len(vms) > 0
@vcr.use_cassette()
def test_get_vm_by_id(oneclient):
with pytest.raises(TypeError):
oneclient.get_vm_by_id('asdf')
vm = oneclient.get_vm_by_id(0)
assert isinstance(vm, oca.VirtualMachine)

View File

@ -0,0 +1,78 @@
import pytest
from onedns import exception
from onedns.tests import vcr
from onedns.tests import utils
def _get_vm_with_nics(one_dns, vms):
for vm in vms:
try:
one_dns._check_for_networks(vm)
return vm
except exception.NoNetworksError:
continue
def _add_and_verify(one_dns, vm, by_id=False, dns_entries=None):
if by_id:
one_dns.add_vm_by_id(vm)
else:
one_dns.add_vm(vm)
dns_entries = dns_entries or one_dns._get_vm_dns_entries(vm)
utils.verify_vm_dns(dns_entries)
def test_onedns_sync(one_dns, vms):
one_dns.sync(vms=vms)
uniq_names = []
uniq_ips = []
for vm in vms:
try:
dns_entries = one_dns._get_vm_dns_entries(vm)
except exception.NoNetworksError:
continue
if vm.name not in uniq_names:
for name, ip in dns_entries.items():
dns_entry = {name: ip}
if ip not in uniq_ips:
uniq_ips.append(ip)
utils.verify_vm_dns(dns_entry)
else:
with pytest.raises(AssertionError):
utils.verify_vm_dns(dns_entry)
uniq_names.append(vm.name)
else:
with pytest.raises(AssertionError):
utils.verify_vm_dns(dns_entries)
def test_onedns_add_vm(one_dns, vms):
vm = _get_vm_with_nics(one_dns, vms)
_add_and_verify(one_dns, vm)
def test_onedns_remove_vm(one_dns, vms):
vm = _get_vm_with_nics(one_dns, vms)
dns_entries = one_dns._get_vm_dns_entries(vm)
_add_and_verify(one_dns, vm)
one_dns.remove_vm(vm)
utils.verify_vm_dns_absent(dns_entries)
@vcr.use_cassette()
def test_onedns_add_vm_by_id(oneclient, one_dns):
vms = oneclient.vms()
vm = _get_vm_with_nics(one_dns, vms)
dns_entries = one_dns._get_vm_dns_entries(vm)
_add_and_verify(one_dns, vm.id, by_id=True, dns_entries=dns_entries)
@vcr.use_cassette()
def test_onedns_remove_vm_by_id(oneclient, one_dns):
vms = oneclient.vms()
vm = _get_vm_with_nics(one_dns, vms)
dns_entries = one_dns._get_vm_dns_entries(vm)
_add_and_verify(one_dns, vm)
one_dns.remove_vm_by_id(vm.id)
utils.verify_vm_dns_absent(dns_entries)

View File

@ -0,0 +1,34 @@
import sys
import mock
from testfixtures import LogCapture
IPY = mock.MagicMock()
IPY.embed = mock.MagicMock()
IPY_MODULES = {
'IPython': IPY,
}
@mock.patch.dict('sys.modules', IPY_MODULES)
def test_with_ipython():
IPY.embed.reset_mock()
from onedns import utils
ns = dict(test=True)
utils.shell(local_ns=ns)
IPY.embed.assert_called_once_with(user_ns=ns)
@mock.patch.object(sys, 'path', [])
def test_without_ipython():
from onedns import utils
with LogCapture() as log:
utils.shell()
log.check(
('onedns', 'ERROR',
'Unable to load IPython:\n\nNo module named IPython\n'),
('onedns', 'ERROR',
'Please check that IPython is installed and working.'),
)

View File

@ -0,0 +1,32 @@
import os
import mock
import pytest
from onedns import utils
from onedns.tests import conftest
ONE_XMLRPC = 'https://controller:2633/RPC2'
def test_get_kwargs_from_dict():
d = dict(ONE_XMLRPC=ONE_XMLRPC)
kwargs = utils.get_kwargs_from_dict(d, prefix='ONE_')
assert kwargs['XMLRPC'] == ONE_XMLRPC
kwargs = utils.get_kwargs_from_dict(d, prefix='ONE_', lower=True)
assert kwargs['xmlrpc'] == ONE_XMLRPC
@mock.patch.dict(os.environ, {'ONE_XMLRPC': ONE_XMLRPC})
def test_get_kwargs_from_env():
kwargs = utils.get_kwargs_from_env(prefix='ONE_')
assert kwargs['XMLRPC'] == ONE_XMLRPC
kwargs = utils.get_kwargs_from_env(prefix='ONE_', lower=True)
assert kwargs['xmlrpc'] == ONE_XMLRPC
@pytest.mark.parametrize("name,domain", conftest.TEST_FQDN_DATA)
def test_get_fqdn(name, domain):
fqdn = utils.get_fqdn(name, domain)
assert fqdn == conftest.TEST_FQDN_RESULT

61
onedns/tests/test_zone.py Normal file
View File

@ -0,0 +1,61 @@
import pytest
import dnslib
from IPy import IP
from onedns import exception
from onedns.tests import conftest
def test_zone_clear(onezone):
assert onezone._forward
assert onezone._reverse
onezone.clear()
assert not onezone._forward
assert not onezone._reverse
def test_add_host(onezone):
onezone.clear()
with pytest.raises(exception.RecordDoesNotExist):
onezone._get_forward(conftest.HOST_SHORT, conftest.HOST_IP)
with pytest.raises(exception.RecordDoesNotExist):
onezone._get_reverse(conftest.HOST_IP, conftest.HOST_SHORT)
onezone.add_host(conftest.HOST_SHORT, conftest.HOST_IP)
assert onezone._get_forward(conftest.HOST_SHORT, conftest.HOST_IP)
assert onezone._get_reverse(conftest.HOST_IP, conftest.HOST_SHORT)
def test_remove_host(onezone):
onezone.clear()
onezone.add_host(conftest.HOST_SHORT, conftest.HOST_IP)
assert onezone._get_forward(conftest.HOST_SHORT, conftest.HOST_IP)
assert onezone._get_reverse(conftest.HOST_IP, conftest.HOST_SHORT)
onezone.remove_host(conftest.HOST_SHORT, conftest.HOST_IP)
with pytest.raises(exception.RecordDoesNotExist):
onezone._get_forward(conftest.HOST_SHORT, conftest.HOST_IP)
with pytest.raises(exception.RecordDoesNotExist):
onezone._get_reverse(conftest.HOST_IP, conftest.HOST_SHORT)
@pytest.mark.parametrize("host", conftest.TEST_GET_FORWARD_DATA)
def test_get_forward(onezone, host):
forward = onezone.get_forward(host)
fqdn = conftest.HOST + '.'
assert isinstance(forward, dnslib.RR)
assert forward.rname == fqdn
assert forward.rtype == dnslib.QTYPE.A
assert forward.rclass == dnslib.CLASS.IN
assert str(forward.rdata) == conftest.HOST_IP
def test_get_reverse(onezone):
reverse = onezone.get_reverse(conftest.HOST_IP)
fqdn = conftest.HOST + '.'
revip = IP(conftest.HOST_IP).reverseName()
assert isinstance(reverse, dnslib.RR)
assert reverse.rname == revip
assert reverse.rtype == dnslib.QTYPE.PTR
assert reverse.rclass == dnslib.CLASS.IN
assert str(reverse.rdata) == fqdn

34
onedns/tests/utils.py Normal file
View File

@ -0,0 +1,34 @@
import dnslib
from IPy import IP
from onedns import utils
from onedns.tests import conftest
def dnsquery(qname, qtype, server=None, port=None, tcp=False):
server = server or conftest.INTERFACE
port = port or conftest.PORT
q = dnslib.DNSRecord(q=dnslib.DNSQuestion(qname, qtype))
a_pkt = q.send(server, port, tcp=tcp)
return dnslib.DNSRecord.parse(a_pkt)
def verify_vm_dns(dns_entries, domain=None):
domain = domain or conftest.DOMAIN
for name, ip in dns_entries.items():
fqdn = utils.get_fqdn(name, domain)
reverse = IP(ip).reverseName()
assert dnsquery(fqdn, dnslib.QTYPE.A).short() == ip
assert dnsquery(reverse, dnslib.QTYPE.PTR).short() == fqdn
def verify_vm_dns_absent(dns_entries, domain=None):
domain = domain or conftest.DOMAIN
for name, ip in dns_entries.items():
fqdn = utils.get_fqdn(name, domain)
reverse = IP(ip).reverseName()
q = dnsquery(fqdn, dnslib.QTYPE.A)
assert dnslib.RCODE.get(q.header.rcode) == 'NXDOMAIN'
q = dnsquery(reverse, dnslib.QTYPE.PTR)
assert dnslib.RCODE.get(q.header.rcode) == 'NXDOMAIN'

View File

@ -1,10 +1,42 @@
import os
import dns.name
import dns.reversename
import dnslib
from IPy import IP
from onedns.logger import log
def get_kwargs_from_dict(d, prefix):
kwargs = dict((i.replace(prefix, ''), d[i])
for i in d.keys() if i.startswith(prefix))
return kwargs
def get_fqdn(name, domain):
domain = dnslib.DNSLabel(domain)
name = dnslib.DNSLabel(name)
if name.label[-1 * len(domain.label):] != domain.label:
return dnslib.DNSLabel(name.label + domain.label).idna()
else:
return name.idna()
def reverse_to_ip(reverse):
rname = dns.name.Name(reverse.split('.'))
return IP(dns.reversename.to_address(rname))
def get_kwargs_from_dict(d, prefix, lower=False):
tups_list = []
for i in d:
if i.startswith(prefix):
arg = i.replace(prefix, '')
if lower:
arg = arg.lower()
tups_list.append((arg, d[i]))
return dict(tups_list)
def get_kwargs_from_env(prefix, lower=False):
return get_kwargs_from_dict(os.environ, prefix, lower=lower)
def shell(local_ns={}):

73
onedns/zone.py Normal file
View File

@ -0,0 +1,73 @@
import dnslib
from dnslib import dns
from IPy import IP
from onedns import utils
from onedns import exception
class Zone(object):
def __init__(self, domain):
self.domain = domain
self._forward = {}
self._reverse = {}
def clear(self):
self._forward = {}
self._reverse = {}
def _get_fqdn(self, name):
return utils.get_fqdn(name, self.domain)
def _get_rr(self, rname, rtype, rdata):
return dnslib.RR(rname=dnslib.DNSLabel(rname),
rtype=dnslib.QTYPE.reverse[rtype],
rclass=dnslib.CLASS.reverse['IN'],
rdata=getattr(dns, rtype)(rdata))
def _add_forward(self, name, ip):
self._forward[self._get_fqdn(name)] = IP(ip)
def _get_forward(self, name, ip=None):
fqdn = self._get_fqdn(name)
fip = self._forward.get(fqdn)
if not fip or (ip and fip != IP(ip)):
raise exception.RecordDoesNotExist(name, ip)
return fip
def _remove_forward(self, name, ip=None):
self._get_forward(name, ip)
del self._forward[self._get_fqdn(name)]
def _add_reverse(self, ip, name):
self._reverse[IP(ip)] = self._get_fqdn(name)
def _get_reverse(self, ip, name=None):
reverse = self._reverse.get(IP(ip))
fqdn = self._get_fqdn(name) if name else None
if not reverse or (name and fqdn != reverse):
raise exception.RecordDoesNotExist(ip, fqdn)
return reverse
def _remove_reverse(self, ip, name=None):
self._get_reverse(ip, name)
del self._reverse[IP(ip)]
def add_host(self, name, ip):
self._add_forward(name, ip)
self._add_reverse(ip, name)
def remove_host(self, name, ip):
self._remove_forward(name, ip)
self._remove_reverse(ip, name)
def get_forward(self, name):
fqdn = self._get_fqdn(name)
forward = self._get_forward(fqdn)
return self._get_rr(fqdn, 'A', str(forward))
def get_reverse(self, ip):
ip = IP(ip)
reverse = self._get_reverse(ip)
return self._get_rr(ip.reverseName(), 'PTR', reverse)

View File

@ -1,2 +1,5 @@
oca==4.10.0
python-etcd==0.4.3
IPy==0.83
dnslib==0.9.6
dnspython==1.14.0
wrapt==1.10.8

14
setup.cfg Normal file
View File

@ -0,0 +1,14 @@
[aliases]
test=pytest
[flake8]
exclude =
.git,
build,
dist
[tool:pytest]
addopts = -v --cov=onedns --cov-report term-missing --flake8
[coverage:run]
omit = onedns/tests/*

View File

@ -20,7 +20,21 @@ setup(
long_description=README,
install_requires=[
"oca>=4.10.0",
"python-etcd>=0.4.3",
"IPy>=0.83",
"dnslib>=0.9.6",
"dnspython>=1.14.0",
"wrapt>=1.10.8",
],
setup_requires=[
'pytest-runner>=2.9'
],
tests_require=[
"pytest>=2.9.2",
"pytest-cov>=2.3.0",
"pytest-flake8>=0.6",
"testfixtures>=4.10.0",
"vcrpy>=1.9.0",
"mock>=2.0.0",
],
entry_points=dict(console_scripts=['onedns = onedns.cli:main']),
zip_safe=False