implement "update other services", with tests, no ui yet

(can be used when adding the records via django admin)
This commit is contained in:
Thomas Waldmann 2013-11-26 08:10:05 +01:00
parent 7566e6a5e3
commit aa610e9c3a
8 changed files with 287 additions and 12 deletions

View File

@ -22,6 +22,11 @@ USERNAME = 'test'
USERNAME2 = 'test2' USERNAME2 = 'test2'
PASSWORD = 'pass' PASSWORD = 'pass'
HOSTNAME = 'nsupdate-ddns-client-unittest.' + BASEDOMAIN
_PASSWORD = 'yUTvxjRwNu' # no problem, is only used for this unit test
SERVER = 'ipv4.' + BASEDOMAIN
SECURE = False # SSL/SNI support on python 2.x sucks :(
from django.utils.translation import activate from django.utils.translation import activate
@ -32,7 +37,7 @@ def db_init(db): # note: db is a predefined fixture and required here to have t
Init the database contents for testing, so we have a service domain, ... Init the database contents for testing, so we have a service domain, ...
""" """
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from nsupdate.main.models import Host, Domain from nsupdate.main.models import Host, Domain, ServiceUpdater, ServiceUpdaterHostConfig
user_model = get_user_model() user_model = get_user_model()
# create a fresh test user # create a fresh test user
u = user_model.objects.create_user(USERNAME, settings.DEFAULT_FROM_EMAIL, PASSWORD) u = user_model.objects.create_user(USERNAME, settings.DEFAULT_FROM_EMAIL, PASSWORD)
@ -60,8 +65,24 @@ def db_init(db): # note: db is a predefined fixture and required here to have t
# a Host for api / session update tests # a Host for api / session update tests
h = Host(subdomain='test', domain=d, created_by=u) h = Host(subdomain='test', domain=d, created_by=u)
h.generate_secret(secret=TEST_SECRET) h.generate_secret(secret=TEST_SECRET)
h = Host(subdomain='test2', domain=d, created_by=u2) h2 = Host(subdomain='test2', domain=d, created_by=u2)
h.generate_secret(secret=TEST_SECRET2) h2.generate_secret(secret=TEST_SECRET2)
# "update other service" ddns_client feature
s = ServiceUpdater.objects.create(
name='nsupdate',
server=SERVER,
secure=SECURE,
created_by=u,
)
ServiceUpdaterHostConfig.objects.create(
hostname=None, # not needed for nsupdate.info, see below
name=HOSTNAME,
password=_PASSWORD,
service=s,
host=h,
created_by=u,
)
def pytest_runtest_setup(item): def pytest_runtest_setup(item):

View File

@ -6,6 +6,8 @@ import pytest
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from nsupdate.main.dnstools import query_ns
TEST_HOST = "test.nsupdate.info" TEST_HOST = "test.nsupdate.info"
TEST_HOST2 = "test2.nsupdate.info" TEST_HOST2 = "test2.nsupdate.info"
@ -14,6 +16,9 @@ TEST_SECRET = "secret"
USERNAME = 'test' USERNAME = 'test'
PASSWORD = 'pass' PASSWORD = 'pass'
BASEDOMAIN = "nsupdate.info"
HOSTNAME = 'nsupdate-ddns-client-unittest.' + BASEDOMAIN
def test_myip(client): def test_myip(client):
response = client.get(reverse('myip')) response = client.get(reverse('myip'))
@ -95,6 +100,28 @@ def test_nic_update_authorized_myip(client):
assert response.content == 'nochg 1.2.3.4' assert response.content == 'nochg 1.2.3.4'
def test_nic_update_authorized_update_other_services(client):
response = client.get(reverse('nic_update') + '?myip=4.3.2.1',
HTTP_AUTHORIZATION=make_basic_auth_header(TEST_HOST, TEST_SECRET))
assert response.status_code == 200
# we don't care whether it is nochg or good, but should be the ip from myip=...:
assert response.content in ['good 4.3.2.1', 'nochg 4.3.2.1']
response = client.get(reverse('nic_update') + '?myip=1.2.3.4',
HTTP_AUTHORIZATION=make_basic_auth_header(TEST_HOST, TEST_SECRET))
assert response.status_code == 200
# must be good (was different IP)
assert response.content == 'good 1.2.3.4'
# now check if it updated the other service also:
assert query_ns(HOSTNAME, 'A') == '1.2.3.4'
response = client.get(reverse('nic_update') + '?myip=2.3.4.5',
HTTP_AUTHORIZATION=make_basic_auth_header(TEST_HOST, TEST_SECRET))
assert response.status_code == 200
# must be good (was different IP)
assert response.content == 'good 2.3.4.5'
# now check if it updated the other service also:
assert query_ns(HOSTNAME, 'A') == '2.3.4.5'
def test_nic_update_authorized_badagent(client, settings): def test_nic_update_authorized_badagent(client, settings):
settings.BAD_AGENTS = ['foo', 'bad_agent', 'bar', ] settings.BAD_AGENTS = ['foo', 'bad_agent', 'bar', ]
response = client.get(reverse('nic_update'), response = client.get(reverse('nic_update'),

View File

@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.sessions.backends.db import SessionStore from django.contrib.sessions.backends.db import SessionStore
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from ..utils import log from ..utils import log, ddns_client
from ..main.models import Host from ..main.models import Host
from ..main.dnstools import update, SameIpError, DnsUpdateError, NameServerNotAvailable, check_ip, put_ip_into_session from ..main.dnstools import update, SameIpError, DnsUpdateError, NameServerNotAvailable, check_ip, put_ip_into_session
@ -261,6 +261,19 @@ def _update(host, hostname, ipaddr, agent='unknown', ssl=False, logger=None):
try: try:
update(hostname, ipaddr) update(hostname, ipaddr)
logger.info('%s - received good update -> ip: %s ssl: %r' % (hostname, ipaddr, ssl)) logger.info('%s - received good update -> ip: %s ssl: %r' % (hostname, ipaddr, ssl))
# now check if there are other services we shall relay updates to:
for uc in host.serviceupdaterhostconfigs.all():
kwargs = dict(
name=uc.name, password=uc.password,
hostname=uc.hostname, myip=ipaddr,
server=uc.service.server, path=uc.service.path, secure=uc.service.secure,
)
try:
ddns_client.dyndns2_update(**kwargs)
except Exception:
# we never want to crash here
kwargs.pop('password')
logger.exception("the dyndns2 updater raised an exception [%r]" % kwargs)
return Response('good %s' % ipaddr) return Response('good %s' % ipaddr)
except SameIpError: except SameIpError:
logger.warning('%s - received no-change update, ip: %s ssl: %r' % (hostname, ipaddr, ssl)) logger.warning('%s - received no-change update, ip: %s ssl: %r' % (hostname, ipaddr, ssl))

View File

@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import Host, Domain, BlacklistedDomain from .models import Host, Domain, BlacklistedDomain, ServiceUpdater, ServiceUpdaterHostConfig
admin.site.register(BlacklistedDomain) admin.site.register(BlacklistedDomain)
admin.site.register(Domain) admin.site.register(Domain)
admin.site.register(Host) admin.site.register(Host)
admin.site.register(ServiceUpdater)
admin.site.register(ServiceUpdaterHostConfig)

View File

@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'ServiceUpdater'
db.create_table(u'main_serviceupdater', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=32)),
('comment', self.gf('django.db.models.fields.CharField')(default='', max_length=255, null=True, blank=True)),
('server', self.gf('django.db.models.fields.CharField')(max_length=255)),
('path', self.gf('django.db.models.fields.CharField')(default='/nic/update', max_length=255)),
('secure', self.gf('django.db.models.fields.BooleanField')(default=True)),
('last_update', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='serviceupdater', to=orm['auth.User'])),
))
db.send_create_signal(u'main', ['ServiceUpdater'])
# Adding model 'ServiceUpdaterHostConfig'
db.create_table(u'main_serviceupdaterhostconfig', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('service', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.ServiceUpdater'])),
('hostname', self.gf('django.db.models.fields.CharField')(default='', max_length=255, null=True, blank=True)),
('comment', self.gf('django.db.models.fields.CharField')(default='', max_length=255, null=True, blank=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('password', self.gf('django.db.models.fields.CharField')(max_length=255)),
('host', self.gf('django.db.models.fields.related.ForeignKey')(related_name='serviceupdaterhostconfigs', to=orm['main.Host'])),
('last_update', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('created_by', self.gf('django.db.models.fields.related.ForeignKey')(related_name='serviceupdaterhostconfigs', to=orm['auth.User'])),
))
db.send_create_signal(u'main', ['ServiceUpdaterHostConfig'])
def backwards(self, orm):
# Deleting model 'ServiceUpdater'
db.delete_table(u'main_serviceupdater')
# Deleting model 'ServiceUpdaterHostConfig'
db.delete_table(u'main_serviceupdaterhostconfig')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'main.blacklisteddomain': {
'Meta': {'object_name': 'BlacklistedDomain'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'blacklisted_domains'", 'to': u"orm['auth.User']"}),
'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
u'main.domain': {
'Meta': {'object_name': 'Domain'},
'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'domains'", 'to': u"orm['auth.User']"}),
'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'nameserver_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
'nameserver_update_algorithm': ('django.db.models.fields.CharField', [], {'default': "'HMAC_SHA512'", 'max_length': '16'}),
'nameserver_update_secret': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '88'}),
'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
u'main.host': {
'Meta': {'unique_together': "(('subdomain', 'domain'),)", 'object_name': 'Host'},
'client_faults': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': u"orm['auth.User']"}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['main.Domain']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'last_update_ipv4': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'last_update_ipv6': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'server_faults': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'ssl_update_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'ssl_update_ipv6': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'subdomain': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'update_secret': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
u'main.serviceupdater': {
'Meta': {'object_name': 'ServiceUpdater'},
'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'serviceupdater'", 'to': u"orm['auth.User']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'path': ('django.db.models.fields.CharField', [], {'default': "'/nic/update'", 'max_length': '255'}),
'secure': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'server': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'main.serviceupdaterhostconfig': {
'Meta': {'object_name': 'ServiceUpdaterHostConfig'},
'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'serviceupdaterhostconfigs'", 'to': u"orm['auth.User']"}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'serviceupdaterhostconfigs'", 'to': u"orm['main.Host']"}),
'hostname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'service': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['main.ServiceUpdater']"})
}
}
complete_apps = ['main']

View File

@ -227,3 +227,62 @@ def pre_delete_host(sender, **kwargs):
pass pass
pre_delete.connect(pre_delete_host, sender=Host) pre_delete.connect(pre_delete_host, sender=Host)
class ServiceUpdater(models.Model):
name = models.CharField(
max_length=32,
help_text="Service name")
comment = models.CharField(
max_length=255, # should be enough
default='', blank=True, null=True,
help_text="Some arbitrary comment about the service")
server = models.CharField(
max_length=255, # should be enough
help_text="Update Server [name or IP] of this service")
path = models.CharField(
max_length=255, # should be enough
default='/nic/update',
help_text="Update Server URL path of this service")
secure = models.BooleanField(
default=True,
help_text="Use https / SSL to contact the Update Server?")
last_update = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='serviceupdater')
def __unicode__(self):
return u"%s service updater" % (
self.name)
class ServiceUpdaterHostConfig(models.Model):
service = models.ForeignKey(ServiceUpdater, on_delete=models.CASCADE)
hostname = models.CharField(
max_length=255, # should be enough
default='', blank=True, null=True,
help_text="The hostname for that service (used in query string)")
comment = models.CharField(
max_length=255, # should be enough
default='', blank=True, null=True,
help_text="Some arbitrary comment about your host on that service")
# credentials for http basic auth for THAT service (not for us),
# we need to store the password in plain text, we can't hash it
name = models.CharField(
max_length=255, # should be enough
help_text="The name/id for that service (used for http basic auth)")
password = models.CharField(
max_length=255, # should be enough
help_text="The password/secret for that service (used for http basic auth)")
host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='serviceupdaterhostconfigs')
last_update = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='serviceupdaterhostconfigs')
def __unicode__(self):
return u"%s service updater data for %s" % (
self.service.name, unicode(self.host))

View File

@ -4,9 +4,7 @@ Tests for ddns_client module.
import pytest import pytest
from requests import Timeout, ConnectionError from ..ddns_client import dyndns2_update, Timeout, ConnectionError
from ..ddns_client import dyndns2_update
# see also conftest.py # see also conftest.py
BASEDOMAIN = 'nsupdate.info' BASEDOMAIN = 'nsupdate.info'

View File

@ -6,17 +6,19 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import requests import requests
from requests import Timeout, ConnectionError # keep, is imported from here
TIMEOUT = 30.0 # timeout for http request response [s] TIMEOUT = 30.0 # timeout for http request response [s]
def dyndns2_update(userid, password, def dyndns2_update(name, password,
server, hostname=None, myip=None, server, hostname=None, myip=None,
path='/nic/update', secure=True, timeout=TIMEOUT): path='/nic/update', secure=True, timeout=TIMEOUT):
""" """
send a dyndns2-compatible update request send a dyndns2-compatible update request
:param userid: for http basic auth :param name: for http basic auth
:param password: for http basic auth :param password: for http basic auth
:param server: server to send the update to :param server: server to send the update to
:param hostname: hostname we want to update :param hostname: hostname we want to update
@ -34,7 +36,8 @@ def dyndns2_update(userid, password,
if myip is not None: if myip is not None:
params['myip'] = myip params['myip'] = myip
url = "%s://%s%s" % ('https' if secure else 'http', server, path) url = "%s://%s%s" % ('https' if secure else 'http', server, path)
r = requests.get(url, params=params, auth=(userid, password), timeout=timeout) logger.debug("update request: %s %r" % (url, params, ))
r = requests.get(url, params=params, auth=(name, password), timeout=timeout)
r.close() r.close()
logger.debug("update response: %d %s" % (r.status_code, r.text, )) logger.debug("update response: %d %s" % (r.status_code, r.text, ))
return r.status_code, r.text return r.status_code, r.text.strip()