Skip to content

Commit

Permalink
Merge pull request #2289 from allegro/feature/dns
Browse files Browse the repository at this point in the history
DNS records from DNSAAS.
  • Loading branch information
ar4s committed Apr 1, 2016
2 parents 44e8bf8 + 6255380 commit cea9eae
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 3 deletions.
17 changes: 17 additions & 0 deletions docs/user/dns.md
@@ -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
1 change: 1 addition & 0 deletions requirements/base.txt
Expand Up @@ -14,6 +14,7 @@ mysqlclient==1.3.6
python-dateutil==2.4.2
pytz==2015.4
redis==2.10.3
requests==2.9.1
rq==0.5.6
six>=1.9.0
sqlparse==0.1.16
Expand Down
1 change: 0 additions & 1 deletion requirements/openstack.txt
Expand Up @@ -19,7 +19,6 @@ oslo.utils==3.7.0
pbr==1.8.1
positional==1.0.1
prettytable==0.7.2
requests==2.9.1
simplejson==3.8.2
stevedore==1.12.0
wrapt==1.10.6
5 changes: 5 additions & 0 deletions src/ralph/data_center/admin.py
Expand Up @@ -42,6 +42,9 @@
from ralph.operations.views import OperationViewReadOnlyForExisiting
from ralph.supports.models import BaseObjectsSupport

if settings.ENABLE_DNSAAS_INTEGRATION:
from ralph.dns.views import DNSView


@register(Accessory)
class AccessoryAdmin(RalphAdmin):
Expand Down Expand Up @@ -138,6 +141,8 @@ class DataCenterAssetAdmin(
DataCenterAssetOperation,
NetworkView,
]
if settings.ENABLE_DNSAAS_INTEGRATION:
change_views += [DNSView]
show_transition_history = True
resource_class = resources.DataCenterAssetResource
list_display = [
Expand Down
Empty file added src/ralph/dns/__init__.py
Empty file.
171 changes: 171 additions & 0 deletions src/ralph/dns/dnsaas.py
@@ -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()
55 changes: 55 additions & 0 deletions src/ralph/dns/forms.py
@@ -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'))
49 changes: 49 additions & 0 deletions src/ralph/dns/templates/dns/dns_edit.html
@@ -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 %}
47 changes: 47 additions & 0 deletions src/ralph/dns/tests.py
@@ -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()

0 comments on commit cea9eae

Please sign in to comment.