# Copyright 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools import re from designateclient import exceptions as designate_exceptions from django.core.exceptions import ValidationError # noqa from django.core import validators from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy as _ # noqa from horizon import forms from horizon import messages from designatedashboard import api from designatedashboard.dashboards.project.dns_domains.utils\ import limit_records_to_fips from oslo_log import log as logging LOG = logging.getLogger(__name__) MAX_TTL = 2147483647 # These regexes were given to me by Kiall Mac Innes here: # https://gerrit.hpcloud.net/#/c/25300/2/ DOMAIN_NAME_REGEX = r'^(?!.{255,})(?:(?!\-)[A-Za-z0-9_\-]{1,63}(?= 500: msg += " (Request ID: %(request_id)s" data["request_id"] = ex.request_id form.api_error(_(msg) % data) # noqa return False except Exception: messages.error(request, form.exc_message) return True return wrapped class DomainForm(forms.SelfHandlingForm): '''Base class for DomainCreate and DomainUpdate forms. Sets-up all of the common form fields. ''' name = forms.RegexField( label=_("Domain Name"), regex=DOMAIN_NAME_REGEX, error_messages={'invalid': _('Enter a valid domain name.')}, ) email = forms.EmailField( label=_("Email"), max_length=255, ) ttl = forms.IntegerField( label=_("TTL (seconds)"), min_value=1, max_value=MAX_TTL, required=False, ) description = forms.CharField( label=_("Description"), required=False, max_length=160, widget=forms.Textarea(), ) class DomainCreate(DomainForm): '''Form for creating new domain records. Name and email address are required. ''' exc_message = _("Unable to create domain.") @handle_exc def handle(self, request, data): domain = api.designate.domain_create( request, name=data['name'], email=data['email'], ttl=data['ttl'], description=data['description']) messages.success(request, _('Domain %(name)s created.') % {"name": domain.name}) return domain class DomainUpdate(DomainForm): '''Form for displaying domain record details and updating them.''' exc_message = _('Unable to update domain.') id = forms.CharField( required=False, widget=forms.HiddenInput() ) serial = forms.CharField( label=_("Serial"), required=False, widget=forms.TextInput(attrs={'readonly': 'readonly'}), ) created_at = forms.CharField( label=_("Created At"), required=False, widget=forms.TextInput(attrs={'readonly': 'readonly'}), ) updated_at = forms.CharField( label=_("Updated At"), required=False, widget=forms.TextInput(attrs={'readonly': 'readonly'}), ) def __init__(self, request, *args, **kwargs): super(DomainUpdate, self).__init__(request, *args, **kwargs) # Mark name as read-only self.fields['name'].required = False self.fields['name'].widget.attrs['readonly'] = 'readonly' self.fields['ttl'].required = True # Customize display order for fields self.fields.keyOrder = [ 'id', 'name', 'serial', 'email', 'ttl', 'description', 'created_at', 'updated_at', ] @handle_exc def handle(self, request, data): domain = api.designate.domain_update( request, domain_id=data['id'], email=data['email'], ttl=data['ttl'], description=data['description']) messages.success(request, _('Domain %(name)s updated.') % {"name": domain.name}) return domain class PrefixWidget(forms.TextInput): def render(self, name, value, attrs=None): template_name = 'project/dns_domains/prefix_html_widget.html' result = super(PrefixWidget, self).render(name, value, attrs) view_data = {'input': result, 'suffix': getattr(self, "suffix", '')} return render_to_string(template_name, view_data) class RecordForm(forms.SelfHandlingForm): '''Base class for RecordCreate and RecordUpdate forms. Sets-up all of the form fields and implements the complex validation logic. ''' domain_id = forms.CharField( widget=forms.HiddenInput()) domain_name = forms.CharField( widget=forms.HiddenInput()) type = forms.ChoiceField( label=_("Record Type"), required=False, choices=[ ('a', _('A - Address record')), ('aaaa', _('AAAA - IPv6 address record')), ('cname', _('CNAME - Canonical name record')), ('mx', _('MX - Mail exchange record')), ('ptr', _('PTR - Pointer record')), ('spf', _('SPF - Sender Policy Framework')), ('srv', _('SRV - Service locator')), ('sshfp', _('SSHFP - SSH Public Key Fingerprint')), ('txt', _('TXT - Text record')), ], widget=forms.Select(attrs={ 'class': 'switchable', 'data-slug': 'record_type', }), ) name = forms.CharField( required=False, widget=PrefixWidget(attrs={ 'class': 'switched', 'data-switch-on': 'record_type', 'data-record_type-a': _('Name'), 'data-record_type-aaaa': _('Name'), 'data-record_type-cname': _('Name'), 'data-record_type-mx': _('Name'), 'data-record_type-ns': _('Name'), 'data-record_type-ptr': _('Name'), 'data-record_type-soa': _('Name'), 'data-record_type-spf': _('Name'), 'data-record_type-srv': _('Name'), 'data-record_type-sshfp': _('Name'), 'data-record_type-txt': _('Name'), }), ) data = forms.CharField( required=False, widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'record_type', 'data-record_type-a': _('IP Address'), 'data-record_type-aaaa': _('IP Address'), 'data-record_type-cname': _('Canonical Name'), 'data-record_type-ns': _('Name Server'), 'data-record_type-mx': _('Mail Server'), 'data-record_type-ptr': _('PTR Domain Name'), 'data-record_type-soa': _('Value'), 'data-record_type-srv': _('Value'), }), ) ip_addr = forms.ChoiceField( required=False, widget=forms.Select(attrs={ 'class': 'switched', 'data-switch-on': 'record_type', 'data-record_type-a': _('IP Address'), 'data-record_type-aaaa': _('IP Address'), }), ) txt = forms.CharField( label=_('TXT'), required=False, widget=forms.Textarea(attrs={ 'class': 'switched', 'data-switch-on': 'record_type', 'data-record_type-txt': _('Text'), 'data-record_type-spf': _('Text'), 'data-record_type-sshfp': _('Text'), }), ) priority = forms.IntegerField( min_value=0, max_value=65535, required=False, widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'record_type', 'data-record_type-mx': _('Priority'), 'data-record_type-srv': _('Priority'), }), ) ttl = forms.IntegerField( label=_('TTL'), min_value=1, max_value=MAX_TTL, required=False, widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'record_type', 'data-record_type-a': _('TTL'), 'data-record_type-aaaa': _('TTL'), 'data-record_type-cname': _('TTL'), 'data-record_type-mx': _('TTL'), 'data-record_type-ptr': _('TTL'), 'data-record_type-soa': _('TTL'), 'data-record_type-spf': _('TTL'), 'data-record_type-srv': _('TTL'), 'data-record_type-sshfp': _('TTL'), 'data-record_type-txt': _('TTL'), }), ) description = forms.CharField( label=_("Description"), required=False, max_length=160, widget=forms.Textarea(), ) def __init__(self, request, *args, **kwargs): super(RecordForm, self).__init__(request, *args, **kwargs) initial = kwargs.get('initial', {}) domain_suffix = "." + initial['domain_name'] self.fields['name'].widget.suffix = domain_suffix self.fields['name'].max_length = min(NAME_MAX_LENGTH, 255 - len(domain_suffix)) if limit_records_to_fips(): del self.fields['data'].widget.attrs['data-record_type-a'] del self.fields['data'].widget.attrs['data-record_type-aaaa'] self.fields['ip_addr'].choices = \ self.populate_ip_addr_choices(request, initial) else: del self.fields['ip_addr'] def _generate_fip_list(self, fips, instances): instance_dict = {instance.id: instance for instance in instances} for fip in fips: instance_name = _("Unknown instance name") if getattr(fip, "instance_id", "None") in instance_dict: instance_name = instance_dict[getattr(fip, "instance_id")].name yield (fip.ip, "%s (%s)" % (fip.ip, instance_name)) def populate_ip_addr_choices(self, request, initial): results = [(None, _('Select an IP')), ] if (initial.get('ip_addr') and initial['ip_addr'] not in [fip.ip for fip in initial['fips']]): """The record is currently using an ip not in the list of fips - this can happen when instance goes away or in multi region setups """ results.append((initial['ip_addr'], initial['ip_addr'])) results.extend(self._generate_fip_list(initial['fips'], initial['instances'])) if len(results) == 1: messages.warning(request, _("There are no floating IP addresses " "currently in use to select from.")) return results def clean_type(self): '''Type value needs to be uppercased before it is sent to the API.''' return self.cleaned_data['type'].upper() def clean(self): '''Handles the validation logic for the domain record form. Validation gets pretty complicated due to the fact that the different record types (A, AAAA, MX, etc) have different requirements for each of the fields. ''' cleaned_data = super(RecordForm, self).clean() record_type = cleaned_data['type'] domain_name = cleaned_data['domain_name'] if limit_records_to_fips(): ip_addr = cleaned_data.pop('ip_addr') if (record_type in ['AAAA', 'A'] and limit_records_to_fips()): cleaned_data['data'] = ip_addr # Name field if self._is_field_blank(cleaned_data, 'name'): if record_type in ['CNAME', 'SRV']: self._add_required_field_error('name') elif record_type in ['MX', 'A', 'AAAA', 'TXT', 'PTR']: cleaned_data['name'] = domain_name else: if record_type == 'SRV': if not re.match(SRV_NAME_REGEX, cleaned_data['name']): self._add_field_error('name', _('Enter a valid SRV name')) else: cleaned_data['name'] += domain_name else: cleaned_data['name'] += "." + domain_name if not re.match(WILDCARD_DOMAIN_NAME_REGEX, cleaned_data['name']): self._add_field_error('name', _('Enter a valid hostname. The ' 'hostname should contain letters ' 'and numbers, and be no more than ' '63 characters.')) # Data field if self._is_field_blank(cleaned_data, 'data'): if record_type in ['A', 'AAAA', 'CNAME', 'MX', 'SRV']: self._add_required_field_error('data') else: if record_type == 'A': try: validators.validate_ipv4_address(cleaned_data['data']) except ValidationError: self._add_field_error('data', _('Enter a valid IPv4 address')) elif record_type == 'AAAA': try: validators.validate_ipv6_address(cleaned_data['data']) except ValidationError: self._add_field_error('data', _('Enter a valid IPv6 address')) elif record_type in ['CNAME', 'MX', 'PTR']: if not re.match(DOMAIN_NAME_REGEX, cleaned_data['data']): self._add_field_error('data', _('Enter a valid hostname')) elif record_type == 'SRV': if not re.match(SRV_DATA_REGEX, cleaned_data['data']): self._add_field_error('data', _('Enter a valid SRV record')) # Txt field if self._is_field_blank(cleaned_data, 'txt'): if record_type == 'TXT': self._add_required_field_error('txt') else: if record_type == 'TXT': cleaned_data['data'] = cleaned_data['txt'] if record_type == 'SSHFP': if not re.match(SSHFP_DATA_REGEX, cleaned_data['txt']): self._add_field_error('txt', _('Enter a valid SSHFP record')) cleaned_data['data'] = cleaned_data['txt'] cleaned_data.pop('txt') # Priority field # Check against '' instead of using _is_field_blank because we need to # allow a valud of 0. if ('priority' not in cleaned_data or cleaned_data['priority'] == '' or cleaned_data['priority'] is None): if record_type in ['MX', 'SRV']: self._add_required_field_error('priority') # Rename 'id' to 'record_id' if 'id' in cleaned_data: cleaned_data['record_id'] = cleaned_data.pop('id') # Remove domain_name cleaned_data.pop('domain_name') return cleaned_data def _add_required_field_error(self, field): '''Set a required field error on the specified field.''' self._add_field_error(field, _('This field is required')) def _add_field_error(self, field, msg): '''Set the specified msg as an error on the field.''' self._errors[field] = self.error_class([msg]) def _is_field_blank(self, cleaned_data, field): '''Returns a flag indicating whether the specified field is blank.''' return field in cleaned_data and not cleaned_data[field] class RecordCreate(RecordForm): '''Form for creating a new domain record.''' exc_message = _('Unable to create record.') @handle_exc def handle(self, request, data): record = api.designate.record_create(request, **data) messages.success(request, _('Domain record %(name)s created.') % {"name": record.name}) return record class RecordUpdate(RecordForm): '''Form for editing a domain record.''' exc_message = _('Unable to create record.') id = forms.CharField(widget=forms.HiddenInput()) def __init__(self, request, *args, **kwargs): super(RecordUpdate, self).__init__(request, *args, **kwargs) # Force the type field to be read-only self.fields['type'].widget.attrs['readonly'] = 'readonly' if self['type'].value() in ('soa', 'ns'): self.fields['type'].choices.append(('ns', _('NS'))) self.fields['type'].choices.append(('soa', _('SOA'))) self.fields['name'].widget.attrs['readonly'] = 'readonly' self.fields['data'].widget.attrs['readonly'] = 'readonly' self.fields['description'].widget.attrs['readonly'] = 'readonly' self.fields['ttl'].widget.attrs['readonly'] = 'readonly' # Filter the choice list so that it only contains the type for # the current record. Ideally, we would just disable the select # field, but that has the unfortunate side-effect of breaking # the 'selectable' javascript code. self.fields['type'].choices = ( [choice for choice in self.fields['type'].choices if choice[0] == self.initial['type']]) @handle_exc def handle(self, request, data): if data['type'] in ('SOA', 'NS'): return True record = api.designate.record_update(request, **data) messages.success(request, _('Domain record %(name)s updated.') % {"name": record.name}) return record