Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2289 from allegro/feature/dns
DNS records from DNSAAS.
- Loading branch information
Showing
12 changed files
with
450 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# DNS | ||
|
||
## Introduction | ||
|
||
Ralph integrated with DNSAAS [PowerDNS](https://github.com/allegro/django-powerdns-dnssec) | ||
|
||
|
||
## Configuration | ||
|
||
- ``ENABLE_DNSAAS_INTEGRATION`` - set to True if you want to enable DNSaaS integration | ||
- ``DNSAAS_URL`` - Url to DNSAAS | ||
- ``DNSAAS_TOKEN`` - API Token to DNSAAS | ||
- ``DNSAAS_AUTO_PTR_ALWAYS`` - DNSAAS auto_ptr value, default is 2 | ||
- ``DNSAAS_AUTO_PTR_NEVER`` - DNSAAS auto_ptr value, default is 1 | ||
|
||
On the edit page of DataCenterAsset will appear a new tab DNS Edit. | ||
DNS records are matched using DataCenterAssets IP |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
# -*- coding: utf-8 -*- | ||
import logging | ||
from urllib.parse import urlencode, urljoin | ||
|
||
import requests | ||
from django.conf import settings | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
from ralph.dns.forms import RecordType | ||
from ralph.helpers import cache | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class DNSaaS: | ||
|
||
def __init__(self, headers=None): | ||
self.session = requests.Session() | ||
_headers = { | ||
'Authorization': 'Token {}'.format(settings.DNSAAS_TOKEN) | ||
} | ||
if headers is not None: | ||
_headers.update(headers) | ||
self.session.headers.update(_headers) | ||
|
||
def get_api_result(self, url): | ||
""" | ||
Returns 'results' from DNSAAS API. | ||
Args: | ||
url: Url to API | ||
Returns: | ||
list of records | ||
""" | ||
request = self.session.get(url) | ||
json_data = request.json() | ||
api_results = json_data.get('results', []) | ||
if json_data.get('next', None): | ||
_api_results = self.get_api_result(json_data['next']) | ||
api_results.extend(_api_results) | ||
return api_results | ||
|
||
def get_dns_records(self, ipaddresses): | ||
"""Gets DNS Records for `ipaddresses` by API call""" | ||
dns_records = [] | ||
ipaddresses = [('ip', i) for i in ipaddresses] | ||
url = urljoin( | ||
settings.DNSAAS_URL, | ||
'api/records/?{}'.format( | ||
urlencode([ | ||
('limit', 100), | ||
('offset', 0) | ||
] + ipaddresses) | ||
) | ||
) | ||
api_results = self.get_api_result(url) | ||
ptrs = set([i['content'] for i in api_results if i['type'] == 'PTR']) | ||
|
||
for item in api_results: | ||
if item['type'] in {'A', 'CNAME', 'TXT'}: | ||
dns_records.append({ | ||
'pk': item['id'], | ||
'name': item['name'], | ||
'type': RecordType.from_name(item['type'].lower()).id, | ||
'content': item['content'], | ||
'ptr': item['name'] in ptrs and item['type'] == 'A', | ||
'owner': settings.DNSAAS_OWNER | ||
}) | ||
return sorted(dns_records, key=lambda x: x['type']) | ||
|
||
def update_dns_record(self, record): | ||
""" | ||
Update DNS Record in DNSAAS | ||
Args: | ||
record: record cleaned data | ||
Returns: | ||
Validation error from API or None if update correct | ||
""" | ||
url = urljoin( | ||
settings.DNSAAS_URL, 'api/records/{}/'.format(record['pk']) | ||
) | ||
data = { | ||
'name': record['name'], | ||
'type': RecordType.raw_from_id(int(record['type'])), | ||
'content': record['content'], | ||
'auto_ptr': ( | ||
settings.DNSAAS_AUTO_PTR_ALWAYS if record['ptr'] and | ||
record['type'] == str(RecordType.a.id) | ||
else settings.DNSAAS_AUTO_PTR_NEVER | ||
), | ||
'owner': settings.DNSAAS_OWNER | ||
} | ||
request = self.session.patch(url, data=data) | ||
if request.status_code != 200: | ||
return request.json() | ||
|
||
@cache(skip_first=True) | ||
def get_domain(self, domain_name): | ||
""" | ||
Return domain URL base on record name. | ||
Args: | ||
domain_name: Domain name | ||
Return: | ||
Domain URL from API or False if not exists | ||
""" | ||
url = urljoin( | ||
settings.DNSAAS_URL, 'api/domains/?'.format( | ||
urlencode([('name', domain_name)]) | ||
) | ||
) | ||
result = self.get_api_result(url) | ||
if result: | ||
return result[0]['url'] | ||
|
||
def create_dns_record(self, record): | ||
""" | ||
Create new DNS record. | ||
Args: | ||
records: Record cleaned data | ||
Returns: | ||
Validation error from API or None if create correct | ||
""" | ||
|
||
url = urljoin(settings.DNSAAS_URL, 'api/records/') | ||
domain_name = record['name'].split('.', 1) | ||
domain = self.get_domain(domain_name[-1]) | ||
if not domain: | ||
logger.error( | ||
'Domain not found for record {}'.format(record) | ||
) | ||
return {'name': [_('Domain not found.')]} | ||
|
||
data = { | ||
'name': record['name'], | ||
'type': RecordType.raw_from_id(int(record['type'])), | ||
'content': record['content'], | ||
'auto_ptr': ( | ||
settings.DNSAAS_AUTO_PTR_ALWAYS if record['ptr'] and | ||
record['type'] == RecordType.a.id | ||
else settings.DNSAAS_AUTO_PTR_NEVER | ||
), | ||
'domain': domain, | ||
'owner': settings.DNSAAS_OWNER | ||
} | ||
request = self.session.post(url, data=data) | ||
if request.status_code != 201: | ||
return request.json() | ||
|
||
def delete_dns_record(self, record_id): | ||
""" | ||
Delete record in DNSAAS | ||
Args: | ||
record_ids: ID's to delete | ||
Returns: | ||
Validation error from API or None if delete correct | ||
""" | ||
url = urljoin( | ||
settings.DNSAAS_URL, 'api/records/{}/'.format(record_id) | ||
) | ||
request = self.session.delete(url) | ||
if request.status_code != 204: | ||
return request.json() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# -*- coding: utf-8 -*- | ||
from dj.choices import Choices | ||
from django import forms | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
|
||
class RecordType(Choices): | ||
_ = Choices.Choice | ||
|
||
a = _('A') | ||
txt = _('TXT') | ||
cname = _('CNAME') | ||
|
||
|
||
class DNSRecordForm(forms.Form): | ||
|
||
pk = forms.IntegerField( | ||
label='', | ||
widget=forms.HiddenInput(), | ||
required=False | ||
) | ||
name = forms.CharField( | ||
label=_('Name'), | ||
max_length=255, | ||
help_text=_( | ||
'Actual name of a record. Must not end in a \'.\' and be' | ||
' fully qualified - it is not relative to the name of the' | ||
' domain!' | ||
), | ||
) | ||
type = forms.ChoiceField( | ||
label=_("Record type"), | ||
choices=RecordType(), | ||
) | ||
content = forms.CharField( | ||
label=_('Content'), | ||
max_length=255, | ||
help_text=_( | ||
'The \'right hand side\' of a DNS record. For an A' | ||
' record, this is the IP address' | ||
), | ||
) | ||
ptr = forms.BooleanField( | ||
label=_('PTR'), | ||
initial=False, | ||
required=False | ||
) | ||
|
||
def clean(self): | ||
cleaned_data = super().clean() | ||
if ( | ||
cleaned_data.get('ptr', False) and | ||
cleaned_data.get('type', None) != str(RecordType.a.id) | ||
): | ||
raise forms.ValidationError(_('Only A type record can be PTR')) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{% extends BASE_TEMPLATE %} | ||
{% load i18n %} | ||
|
||
{% block view_content %} | ||
<div class="row"> | ||
<div class="large-12 columns"> | ||
<h1>{% trans 'DNS edit' %}</h1> | ||
<table> | ||
<thead> | ||
{% for name, field in forms.0.fields.items %} | ||
<th>{{ field.label }}</th> | ||
{% endfor %} | ||
<th></th> | ||
<th></th> | ||
</thead> | ||
{% for form in forms %} | ||
{% if form.non_field_errors %} | ||
<tr class="field-row error"> | ||
<td colspan="{{ forms.0.fields.items|length|add:2 }}" class="error"> | ||
<ul class="errorlist"> | ||
{% for error in form.non_field_errors %} | ||
<li>{{ error }}</li> | ||
{% endfor %} | ||
</ul> | ||
</td> | ||
</tr> | ||
{% endif %} | ||
<tr> | ||
<form method="post" action=""> | ||
{% csrf_token %} | ||
{% for field in form %} | ||
<td>{{ field }} {{ field.errors }}</td> | ||
{% endfor %} | ||
<td> | ||
<input type="submit" class="button" value="{% trans 'Save' %}" /> | ||
</td> | ||
<td> | ||
{% if form.pk.value %} | ||
<input type="submit" name="delete" class="button small alert" value="{% trans 'Delete' %}" onclick="return confirm('{% trans 'Are you sure to delete?' %}');" /> | ||
{% endif %} | ||
</td> | ||
</form> | ||
</tr> | ||
{% endfor %} | ||
</table> | ||
<br /> | ||
</div> | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# -*- coding: utf-8 -*- | ||
from unittest.mock import patch | ||
|
||
from django.test import override_settings, TestCase | ||
|
||
from ralph.dns.dnsaas import DNSaaS | ||
from ralph.dns.forms import RecordType | ||
from ralph.dns.views import DNSaaSIntegrationNotEnabledError, DNSView | ||
|
||
|
||
class TestGetDnsRecords(TestCase): | ||
|
||
def setUp(self): | ||
self.dnsaas = DNSaaS() | ||
|
||
@patch.object(DNSaaS, 'get_api_result') | ||
def test_return_empty_when_api_returns_empty(self, mocked): | ||
mocked.return_value = [] | ||
found_dns = self.dnsaas.get_dns_records(['192.168.0.1']) | ||
self.assertEqual(found_dns, []) | ||
|
||
@patch.object(DNSaaS, 'get_api_result') | ||
def test_return_dns_records_when_api_returns_records(self, mocked): | ||
data = { | ||
'content': '127.0.0.3', | ||
'name': '1.test.pl', | ||
'type': 'A', | ||
'id': 1 | ||
} | ||
mocked.return_value = [data] | ||
found_dns = self.dnsaas.get_dns_records(['192.168.0.1']) | ||
self.assertEqual(len(found_dns), 1) | ||
self.assertEqual(found_dns[0]['content'], data['content']) | ||
self.assertEqual(found_dns[0]['name'], data['name']) | ||
self.assertEqual(found_dns[0]['type'], RecordType.a) | ||
|
||
|
||
class TestDNSView(TestCase): | ||
@override_settings(ENABLE_DNSAAS_INTEGRATION=False) | ||
def test_dnsaasintegration_disabled(self): | ||
with self.assertRaises(DNSaaSIntegrationNotEnabledError): | ||
DNSView() | ||
|
||
@override_settings(ENABLE_DNSAAS_INTEGRATION=True) | ||
def test_dnsaasintegration_enabled(self): | ||
# should not raise exception | ||
DNSView() |
Oops, something went wrong.