fields.py 11.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE.  If not, see <http://www.gnu.org/licenses/>.

18
from string import ascii_letters
19 20 21 22
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.ipv6 import is_valid_ipv6_address
23
from django import forms
24
from netaddr import (IPAddress, IPNetwork, AddrFormatError, ZEROFILL,
25
                     EUI, mac_unix, AddrConversionError)
26 27
import re

28

29
alfanum_re = re.compile(r'^[A-Za-z0-9_-]+$')
30 31
domain_re = re.compile(r'^([A-Za-z0-9_/-]\.?)+$')
domain_wildcard_re = re.compile(r'^(\*\.)?([A-Za-z0-9_/-]\.?)+$')
32
ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$')
33
reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9./-])+$')
34

35

36 37 38 39
class mac_custom(mac_unix):
    word_fmt = '%.2X'


40
class MACAddressFormField(forms.Field):
41
    default_error_messages = {
42
        'invalid': _(u'Enter a valid MAC address. %s'),
43 44
    }

45 46
    def validate(self, value):
        try:
47 48
            return MACAddressField.to_python.im_func(None, value)
        except (AddrFormatError, TypeError, ValidationError) as e:
49 50
            raise ValidationError(self.default_error_messages['invalid']
                                  % unicode(e))
51

52

53
class MACAddressField(models.Field):
54 55
    description = _('MAC Address object')

56 57 58 59
    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 17
        super(MACAddressField, self).__init__(*args, **kwargs)

60 61 62 63 64 65 66 67
    def deconstruct(self):
        name, path, args, kwargs = super(MACAddressField, self).deconstruct()
        del kwargs['max_length']
        return name, path, args, kwargs

    def from_db_value(self, value, expression, connection, context):
        return self.to_python(value)

68 69
    def to_python(self, value):
        if not value:
70 71 72 73 74
            return None

        if isinstance(value, EUI):
            return value

75
        return EUI(value, dialect=mac_custom)
76

77 78 79
    def get_internal_type(self):
        return 'CharField'

80 81
    def get_prep_value(self, value, prepared=False):
        if not value:
82 83 84 85 86
            return None

        if isinstance(value, EUI):
            return str(value)

87
        return value
88

89 90 91 92
    def formfield(self, **kwargs):
        defaults = {'form_class': MACAddressFormField}
        defaults.update(kwargs)
        return super(MACAddressField, self).formfield(**defaults)
93

94

95 96 97 98 99 100 101
class IPAddressFormField(forms.Field):
    default_error_messages = {
        'invalid': _(u'Enter a valid IP address. %s'),
    }

    def validate(self, value):
        try:
102 103
            IPAddressField(version=self.version).to_python(value)
        except (AddrFormatError, TypeError, ValueError) as e:
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
            raise ValidationError(self.default_error_messages['invalid']
                                  % unicode(e))

    def __init__(self, *args, **kwargs):
        self.version = kwargs['version']
        del kwargs['version']
        super(IPAddressFormField, self).__init__(*args, **kwargs)


class IPAddressField(models.Field):
    description = _('IP Network object')

    def __init__(self, version=4, serialize=True, *args, **kwargs):
        kwargs['max_length'] = 100
        self.version = version
        super(IPAddressField, self).__init__(*args, **kwargs)

121 122 123 124 125 126 127
    def deconstruct(self):
        name, path, args, kwargs = super(IPAddressField, self).deconstruct()
        del kwargs['max_length']
        if self.version != 4:
            kwargs['version'] = self.version
        return name, path, args, kwargs

128 129 130
    def get_internal_type(self):
        return "CharField"

131 132 133
    def from_db_value(self, value, expression, connection, context):
        return self.to_python(value)

134 135
    def to_python(self, value):
        if not value:
136 137 138 139 140
            return None

        if isinstance(value, IPAddress):
            return value

141
        return IPAddress(value.split('/')[0], version=self.version,
142
                         flags=ZEROFILL)
143

144 145
    def get_prep_value(self, value, prepared=False):
        if not value:
146 147 148 149
            return None

        if isinstance(value, IPAddress):
            if self.version == 4:
150
                return '.'.join("%03d" % x for x in value.words)
151
            else:
152
                return ':'.join("%04X" % x for x in value.words)
153 154 155 156 157 158 159 160 161
        return value

    def formfield(self, **kwargs):
        defaults = {'form_class': IPAddressFormField}
        defaults['version'] = self.version
        defaults.update(kwargs)
        return super(IPAddressField, self).formfield(**defaults)


162 163 164 165 166 167 168
class IPNetworkFormField(forms.Field):
    default_error_messages = {
        'invalid': _(u'Enter a valid IP network. %s'),
    }

    def validate(self, value):
        try:
169 170
            return IPNetworkField(version=self.version).to_python(value)
        except (AddrFormatError, TypeError) as e:
171 172 173 174 175 176 177 178 179 180 181 182
            raise ValidationError(self.default_error_messages['invalid']
                                  % unicode(e))

    def __init__(self, *args, **kwargs):
        self.version = kwargs['version']
        del kwargs['version']
        super(IPNetworkFormField, self).__init__(*args, **kwargs)


class IPNetworkField(models.Field):
    description = _('IP Network object')

183
    def __init__(self, version=4, serialize=True, *args, **kwargs):
184 185 186 187
        kwargs['max_length'] = 100
        self.version = version
        super(IPNetworkField, self).__init__(*args, **kwargs)

188 189 190 191 192 193 194 195 196 197
    def deconstruct(self):
        name, path, args, kwargs = super(IPNetworkField, self).deconstruct()
        del kwargs['max_length']
        if self.version != 4:
            kwargs['version'] = self.version
        return name, path, args, kwargs

    def from_db_value(self, value, expression, connection, context):
        return self.to_python(value)

198 199
    def to_python(self, value):
        if not value:
200 201 202 203 204
            return None

        if isinstance(value, IPNetwork):
            return value

205
        return IPNetwork(value, version=self.version)
206 207 208 209

    def get_internal_type(self):
        return "CharField"

210 211
    def get_prep_value(self, value, prepared=False):
        if not value:
212 213 214 215
            return None

        if isinstance(value, IPNetwork):
            if self.version == 4:
216 217
                return ('.'.join("%03d" % x for x in value.ip.words) +
                        '/%02d' % value.prefixlen)
218
            else:
219 220
                return (':'.join("%04X" % x for x in value.ip.words) +
                        '/%03d' % value.prefixlen)
221 222 223 224 225 226 227 228
        return value

    def formfield(self, **kwargs):
        defaults = {'form_class': IPNetworkFormField}
        defaults['version'] = self.version
        defaults.update(kwargs)
        return super(IPNetworkField, self).formfield(**defaults)

229

230 231 232 233
def val_alfanum(value):
    """Validate whether the parameter is a valid alphanumeric value."""
    if not alfanum_re.match(value):
        raise ValidationError(_(u'%s - only letters, numbers, underscores '
234 235
                                'and hyphens are allowed!') % value)

236 237 238 239 240

def is_valid_domain(value):
    """Check whether the parameter is a valid domain name."""
    return domain_re.match(value) is not None

241

242 243 244 245 246
def is_valid_domain_wildcard(value):
    """Check whether the parameter is a valid domain name."""
    return domain_wildcard_re.match(value) is not None


247 248 249 250
def val_domain(value):
    """Validate whether the parameter is a valid domin name."""
    if not is_valid_domain(value):
        raise ValidationError(_(u'%s - invalid domain name') % value)
251 252 253 254 255 256


def val_domain_wildcard(value):
    """Validate whether the parameter is a valid domin name."""
    if not is_valid_domain_wildcard(value):
        raise ValidationError(_(u'%s - invalid domain name') % value)
257

258

259 260 261 262
def is_valid_reverse_domain(value):
    """Check whether the parameter is a valid reverse domain name."""
    return reverse_domain_re.match(value) is not None

263

264 265 266 267 268
def val_reverse_domain(value):
    """Validate whether the parameter is a valid reverse domain name."""
    if not is_valid_reverse_domain(value):
        raise ValidationError(u'%s - invalid reverse domain name' % value)

269

270
def val_ipv6_template(value):
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
    """Validate whether the parameter is a valid ipv6 template.

    Normal use:
    >>> val_ipv6_template("123::%(a)d:%(b)d:%(c)d:%(d)d")
    >>> val_ipv6_template("::%(a)x:%(b)x:%(c)d:%(d)d")

    Don't have to use all bytes from the left (no a):
    >>> val_ipv6_template("::%(b)x:%(c)d:%(d)d")

    But have to use all ones to the right (a, but no b):
    >>> val_ipv6_template("::%(a)x:%(c)d:%(d)d")
    Traceback (most recent call last):
        ...
    ValidationError: [u"template doesn't use parameter b"]

    Detects valid templates building invalid ips:
    >>> val_ipv6_template("xxx::%(a)d:%(b)d:%(c)d:%(d)d")
    Traceback (most recent call last):
        ...
    ValidationError: [u'template renders invalid IPv6 address']

    Also IPv4-compatible addresses are invalid:
    >>> val_ipv6_template("::%(a)02x%(b)02x:%(c)d:%(d)d")
    Traceback (most recent call last):
        ...
    ValidationError: [u'template results in IPv4 address']
    """
    tpl = {ascii_letters[i]: 255 for i in range(4)}
    try:
        v6 = value % tpl
    except:
        raise ValidationError(_('%s: invalid template') % value)

    used = False
    for i in ascii_letters[:4]:
        try:
            value % {k: tpl[k] for k in tpl if k != i}
        except KeyError:
            used = True  # ok, it misses this key
        else:
            if used:
                raise ValidationError(
                    _("template doesn't use parameter %s") % i)
    try:
        v6 = IPAddress(v6, 6)
    except:
        raise ValidationError(_('template renders invalid IPv6 address'))
    try:
        v6.ipv4()
    except (AddrConversionError, AddrFormatError):
        pass  # can't converted to ipv4 == it's real ipv6
    else:
        raise ValidationError(_('template results in IPv4 address'))
324 325


326 327 328 329
def is_valid_ipv4_address(value):
    """Check whether the parameter is a valid IPv4 address."""
    return ipv4_re.match(value) is not None

330

331 332 333 334 335
def val_ipv4(value):
    """Validate whether the parameter is a valid IPv4 address."""
    if not is_valid_ipv4_address(value):
        raise ValidationError(_(u'%s - not an IPv4 address') % value)

336

337 338 339 340 341
def val_ipv6(value):
    """Validate whether the parameter is a valid IPv6 address."""
    if not is_valid_ipv6_address(value):
        raise ValidationError(_(u'%s - not an IPv6 address') % value)

342 343 344 345 346 347 348 349 350 351 352

def val_mx(value):
    """Validate whether the parameter is a valid MX address definition.

    Expected form is <priority>:<hostname>.
    """
    mx = value.split(':', 1)
    if not (len(mx) == 2 and mx[0].isdigit() and
            domain_re.match(mx[1])):
        raise ValidationError(_("Bad MX address format. "
                                "Should be: <priority>:<hostname>"))