Merge branch 'dnslib'
commit
999adab4b2
|
@ -12,6 +12,7 @@ build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
|
.eggs
|
||||||
eggs/
|
eggs/
|
||||||
#lib/
|
#lib/
|
||||||
lib64/
|
lib64/
|
||||||
|
|
29
README.md
29
README.md
|
@ -2,9 +2,32 @@
|
||||||
Dyanmic DNS for OpenNebula
|
Dyanmic DNS for OpenNebula
|
||||||
|
|
||||||
## Usage
|
## 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
|
Run the `daemon` command to kick off onedns:
|
||||||
$ onedns --etcd-host=http://localhost:2379 --client-cert /path/to/ssl/cert
|
```
|
||||||
|
$ onedns daemon --dns-port=53
|
||||||
```
|
```
|
||||||
|
|
22
one-dns.yaml
22
one-dns.yaml
|
@ -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
|
|
|
@ -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)
|
|
|
@ -1,52 +1,26 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from onedns import api
|
|
||||||
from onedns import utils
|
from onedns import utils
|
||||||
|
from onedns import server
|
||||||
from onedns import logger
|
from onedns import logger
|
||||||
from onedns import monitor
|
|
||||||
from onedns.clients import skydns
|
|
||||||
|
|
||||||
|
|
||||||
def daemon(args, one_args, etcd_args):
|
def daemon(args, one_args, **kwargs):
|
||||||
mon = monitor.OneMonitor(args.domain, one_kwargs=one_args,
|
testing = kwargs.get('testing', False)
|
||||||
etcd_kwargs=etcd_args)
|
vms = kwargs.get('vms')
|
||||||
mon.run(args.interval)
|
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):
|
def shell(args, one_args, **kwargs):
|
||||||
client = skydns.SkyDNSClient(args.domain, etcd_kwargs=etcd_args)
|
srv = server.OneDNS(args.domain, one_kwargs=one_args)
|
||||||
client.add_host(args.hostname, args.ip)
|
oneclient = srv._one
|
||||||
|
ns = dict(one_dns=srv, oneclient=oneclient, log=logger.log)
|
||||||
|
|
||||||
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)
|
|
||||||
utils.shell(local_ns=ns)
|
utils.shell(local_ns=ns)
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def get_parser():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='OneDNS - Dynamic DNS for OpenNebula')
|
description='OneDNS - Dynamic DNS for OpenNebula')
|
||||||
parser.add_argument('--debug', required=False,
|
parser.add_argument('--debug', required=False,
|
||||||
|
@ -60,53 +34,23 @@ def main(args=None):
|
||||||
help='ONE credentials to use (e.g. user:key)')
|
help='ONE credentials to use (e.g. user:key)')
|
||||||
parser.add_argument('--one-proxy', required=False,
|
parser.add_argument('--one-proxy', required=False,
|
||||||
help='proxy host to use to connect to ONE controller')
|
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()
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
daemon_parser = subparsers.add_parser('daemon')
|
daemon_parser = subparsers.add_parser('daemon')
|
||||||
daemon_parser.set_defaults(func=daemon)
|
daemon_parser.set_defaults(func=daemon)
|
||||||
daemon_parser.add_argument(
|
daemon_parser.add_argument(
|
||||||
'-i', '--interval', required=False, type=int, default=60,
|
'--dns-port', required=False, default=5053, type=int,
|
||||||
help="how often in seconds to poll ONE and update DNS")
|
help="port for DNS server to listen on")
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
shell_parser = subparsers.add_parser('shell')
|
shell_parser = subparsers.add_parser('shell')
|
||||||
shell_parser.set_defaults(func=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)
|
logger.configure_onedns_logging(debug=args.debug)
|
||||||
|
|
||||||
args_dict = vars(args)
|
args_dict = vars(args)
|
||||||
one_args = utils.get_kwargs_from_dict(args_dict, 'one_')
|
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, **kwargs)
|
||||||
|
|
||||||
args.func(args, one_args, etcd_args)
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -26,3 +26,21 @@ class NoNetworksError(OneDnsException):
|
||||||
def __init__(self, vm):
|
def __init__(self, vm):
|
||||||
self.msg = "No networks found for VM {id}: {vm}".format(vm=vm.name,
|
self.msg = "No networks found for VM {id}: {vm}".format(vm=vm.name,
|
||||||
id=vm.id)
|
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)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
|
||||||
|
@ -6,37 +5,20 @@ import logging.handlers
|
||||||
LOG_FORMAT = ("%(asctime)s %(filename)s:%(lineno)d - %(levelname)s - "
|
LOG_FORMAT = ("%(asctime)s %(filename)s:%(lineno)d - %(levelname)s - "
|
||||||
"%(message)s")
|
"%(message)s")
|
||||||
|
|
||||||
|
log = logging.getLogger('onedns')
|
||||||
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()
|
|
||||||
console = logging.StreamHandler()
|
console = logging.StreamHandler()
|
||||||
formatter = logging.Formatter(LOG_FORMAT)
|
formatter = logging.Formatter(LOG_FORMAT)
|
||||||
console.setFormatter(formatter)
|
console.setFormatter(formatter)
|
||||||
|
|
||||||
|
|
||||||
def configure_onedns_logging(use_syslog=False, syslog_device='/dev/log',
|
def configure_onedns_logging(debug=False):
|
||||||
debug=False):
|
|
||||||
"""
|
"""
|
||||||
Configure logging for onedns *application* code
|
Configure logging for onedns *application* code
|
||||||
|
|
||||||
By default onedns's logger has no formatters and a NullHandler so that
|
By default onedns's logger is completely unconfigured so that other
|
||||||
other developers using onedns as a library can configure logging as
|
developers using onedns as a library can configure logging as they see fit.
|
||||||
they see fit. This method is used in onedns's application code (i.e.
|
This method is used in onedns's application code (i.e. the 'onedns'
|
||||||
the 'onedns' command) to toggle onedns's application specific
|
command) to toggle onedns's application specific formatters/handlers
|
||||||
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)
|
|
||||||
"""
|
"""
|
||||||
log.setLevel(logging.DEBUG)
|
log.setLevel(logging.DEBUG)
|
||||||
if debug:
|
if debug:
|
||||||
|
@ -44,8 +26,3 @@ def configure_onedns_logging(use_syslog=False, syslog_device='/dev/log',
|
||||||
else:
|
else:
|
||||||
console.setLevel(logging.INFO)
|
console.setLevel(logging.INFO)
|
||||||
log.addHandler(console)
|
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)
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
||||||
|
)
|
|
@ -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
|
|
@ -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><VM_POOL><VM><ID>0</ID><UID>0</UID><GID>0</GID><UNAME>oneadmin</UNAME><GNAME>oneadmin</GNAME><NAME>test-0</NAME><PERMISSIONS><OWNER_U>1</OWNER_U><OWNER_M>1</OWNER_M><OWNER_A>0</OWNER_A><GROUP_U>0</GROUP_U><GROUP_M>0</GROUP_M><GROUP_A>0</GROUP_A><OTHER_U>0</OTHER_U><OTHER_M>0</OTHER_M><OTHER_A>0</OTHER_A></PERMISSIONS><LAST_POLL>1475867584</LAST_POLL><STATE>3</STATE><LCM_STATE>3</LCM_STATE><PREV_STATE>3</PREV_STATE><PREV_LCM_STATE>3</PREV_LCM_STATE><RESCHED>0</RESCHED><STIME>1475786733</STIME><ETIME>0</ETIME><DEPLOY_ID>one-0</DEPLOY_ID><MONITORING><CPU><![CDATA[14.0]]></CPU><DISK_SIZE><ID><![CDATA[0]]></ID><SIZE><![CDATA[40]]></SIZE></DISK_SIZE><DISK_SIZE><ID><![CDATA[1]]></ID><SIZE><![CDATA[1]]></SIZE></DISK_SIZE><MEMORY><![CDATA[131072]]></MEMORY><NETRX><![CDATA[648]]></NETRX><NETTX><![CDATA[0]]></NETTX><STATE><![CDATA[a]]></STATE></MONITORING><TEMPLATE><AUTOMATIC_DS_REQUIREMENTS><![CDATA[\"\
|
||||||
|
CLUSTERS/ID\" @> 0]]></AUTOMATIC_DS_REQUIREMENTS><AUTOMATIC_REQUIREMENTS><![CDATA[(CLUSTER_ID\
|
||||||
|
\ = 0) & !(PUBLIC_CLOUD = YES)]]></AUTOMATIC_REQUIREMENTS><CONTEXT><DISK_ID><![CDATA[1]]></DISK_ID><ETH0_CONTEXT_FORCE_IPV4><![CDATA[]]></ETH0_CONTEXT_FORCE_IPV4><ETH0_DNS><![CDATA[]]></ETH0_DNS><ETH0_GATEWAY><![CDATA[]]></ETH0_GATEWAY><ETH0_GATEWAY6><![CDATA[]]></ETH0_GATEWAY6><ETH0_IP><![CDATA[172.16.100.2]]></ETH0_IP><ETH0_IP6><![CDATA[]]></ETH0_IP6><ETH0_IP6_ULA><![CDATA[]]></ETH0_IP6_ULA><ETH0_MAC><![CDATA[02:00:ac:10:64:02]]></ETH0_MAC><ETH0_MASK><![CDATA[]]></ETH0_MASK><ETH0_MTU><![CDATA[]]></ETH0_MTU><ETH0_NETWORK><![CDATA[]]></ETH0_NETWORK><ETH0_SEARCH_DOMAIN><![CDATA[]]></ETH0_SEARCH_DOMAIN><ETH0_VLAN_ID><![CDATA[]]></ETH0_VLAN_ID><ETH0_VROUTER_IP><![CDATA[]]></ETH0_VROUTER_IP><ETH0_VROUTER_IP6><![CDATA[]]></ETH0_VROUTER_IP6><ETH0_VROUTER_MANAGEMENT><![CDATA[]]></ETH0_VROUTER_MANAGEMENT><NETWORK><![CDATA[YES]]></NETWORK><SSH_PUBLIC_KEY><![CDATA[]]></SSH_PUBLIC_KEY><TARGET><![CDATA[hdb]]></TARGET></CONTEXT><CPU><![CDATA[0.1]]></CPU><DISK><CLONE><![CDATA[YES]]></CLONE><CLONE_TARGET><![CDATA[SYSTEM]]></CLONE_TARGET><CLUSTER_ID><![CDATA[0]]></CLUSTER_ID><DATASTORE><![CDATA[default]]></DATASTORE><DATASTORE_ID><![CDATA[1]]></DATASTORE_ID><DEV_PREFIX><![CDATA[hd]]></DEV_PREFIX><DISK_ID><![CDATA[0]]></DISK_ID><DISK_SNAPSHOT_TOTAL_SIZE><![CDATA[0]]></DISK_SNAPSHOT_TOTAL_SIZE><DISK_TYPE><![CDATA[FILE]]></DISK_TYPE><IMAGE><![CDATA[ttylinux\
|
||||||
|
\ - kvm]]></IMAGE><IMAGE_ID><![CDATA[0]]></IMAGE_ID><IMAGE_STATE><![CDATA[2]]></IMAGE_STATE><LN_TARGET><![CDATA[SYSTEM]]></LN_TARGET><READONLY><![CDATA[NO]]></READONLY><SAVE><![CDATA[NO]]></SAVE><SIZE><![CDATA[40]]></SIZE><SOURCE><![CDATA[/var/lib/one//datastores/1/203ec12ff6495be37ec95dbbc7cf94ef]]></SOURCE><TARGET><![CDATA[hda]]></TARGET><TM_MAD><![CDATA[ssh]]></TM_MAD><TYPE><![CDATA[FILE]]></TYPE></DISK><GRAPHICS><LISTEN><![CDATA[0.0.0.0]]></LISTEN><PORT><![CDATA[5900]]></PORT><TYPE><![CDATA[VNC]]></TYPE></GRAPHICS><MEMORY><![CDATA[128]]></MEMORY><NIC><AR_ID><![CDATA[0]]></AR_ID><BRIDGE><![CDATA[br0]]></BRIDGE><CLUSTER_ID><![CDATA[0]]></CLUSTER_ID><IP><![CDATA[172.16.100.2]]></IP><MAC><![CDATA[02:00:ac:10:64:02]]></MAC><NETWORK><![CDATA[local]]></NETWORK><NETWORK_ID><![CDATA[0]]></NETWORK_ID><NIC_ID><![CDATA[0]]></NIC_ID><SECURITY_GROUPS><![CDATA[0]]></SECURITY_GROUPS><TARGET><![CDATA[one-0-0]]></TARGET><VN_MAD><![CDATA[dummy]]></VN_MAD></NIC><SECURITY_GROUP_RULE><PROTOCOL><![CDATA[ALL]]></PROTOCOL><RULE_TYPE><![CDATA[OUTBOUND]]></RULE_TYPE><SECURITY_GROUP_ID><![CDATA[0]]></SECURITY_GROUP_ID><SECURITY_GROUP_NAME><![CDATA[default]]></SECURITY_GROUP_NAME></SECURITY_GROUP_RULE><SECURITY_GROUP_RULE><PROTOCOL><![CDATA[ALL]]></PROTOCOL><RULE_TYPE><![CDATA[INBOUND]]></RULE_TYPE><SECURITY_GROUP_ID><![CDATA[0]]></SECURITY_GROUP_ID><SECURITY_GROUP_NAME><![CDATA[default]]></SECURITY_GROUP_NAME></SECURITY_GROUP_RULE><TEMPLATE_ID><![CDATA[0]]></TEMPLATE_ID><VMID><![CDATA[0]]></VMID></TEMPLATE><USER_TEMPLATE><LOGO><![CDATA[images/logos/linux.png]]></LOGO></USER_TEMPLATE><HISTORY_RECORDS><HISTORY><OID>0</OID><SEQ>0</SEQ><HOSTNAME>one-dns-testing</HOSTNAME><HID>0</HID><CID>0</CID><STIME>1475786738</STIME><ETIME>0</ETIME><VM_MAD><![CDATA[kvm]]></VM_MAD><TM_MAD><![CDATA[ssh]]></TM_MAD><DS_ID>0</DS_ID><PSTIME>1475786738</PSTIME><PETIME>1475786739</PETIME><RSTIME>1475786739</RSTIME><RETIME>0</RETIME><ESTIME>0</ESTIME><EETIME>0</EETIME><REASON>0</REASON><ACTION>0</ACTION></HISTORY></HISTORY_RECORDS></VM></VM_POOL></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
File diff suppressed because one or more lines are too long
|
@ -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()
|
|
@ -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()
|
|
@ -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()),
|
||||||
|
)
|
|
@ -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'),
|
||||||
|
)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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.'),
|
||||||
|
)
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -1,10 +1,42 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
import dns.name
|
||||||
|
import dns.reversename
|
||||||
|
|
||||||
|
import dnslib
|
||||||
|
|
||||||
|
from IPy import IP
|
||||||
|
|
||||||
from onedns.logger import log
|
from onedns.logger import log
|
||||||
|
|
||||||
|
|
||||||
def get_kwargs_from_dict(d, prefix):
|
def get_fqdn(name, domain):
|
||||||
kwargs = dict((i.replace(prefix, ''), d[i])
|
domain = dnslib.DNSLabel(domain)
|
||||||
for i in d.keys() if i.startswith(prefix))
|
name = dnslib.DNSLabel(name)
|
||||||
return kwargs
|
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={}):
|
def shell(local_ns={}):
|
||||||
|
|
|
@ -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)
|
|
@ -1,2 +1,5 @@
|
||||||
oca==4.10.0
|
oca==4.10.0
|
||||||
python-etcd==0.4.3
|
IPy==0.83
|
||||||
|
dnslib==0.9.6
|
||||||
|
dnspython==1.14.0
|
||||||
|
wrapt==1.10.8
|
||||||
|
|
|
@ -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/*
|
16
setup.py
16
setup.py
|
@ -20,7 +20,21 @@ setup(
|
||||||
long_description=README,
|
long_description=README,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"oca>=4.10.0",
|
"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']),
|
entry_points=dict(console_scripts=['onedns = onedns.cli:main']),
|
||||||
zip_safe=False
|
zip_safe=False
|
||||||
|
|
Loading…
Reference in New Issue