tiramisu/tiramisu/option/option.py

637 lines
24 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"option types and option description"
2017-07-08 15:59:56 +02:00
# Copyright (C) 2012-2017 Team tiramisu (see AUTHORS for all contributors)
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# This program 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# The original `Config` design model is unproudly borrowed from
# the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
# the whole pypy projet is under MIT licence
# ____________________________________________________________
import re
import sys
2017-02-02 10:11:26 +01:00
import datetime
from IPy import IP
from types import FunctionType
from ..setting import undefined
from ..error import ConfigError
from ..i18n import _
2016-09-11 20:41:36 +02:00
from .baseoption import Option, validate_callback, display_list
from ..autolib import carry_out_calculation
class ChoiceOption(Option):
"""represents a choice out of several objects.
The option can also have the value ``None``
"""
2016-10-10 21:41:22 +02:00
__slots__ = tuple()
_display_name = _('choice')
def __init__(self, name, doc, values, default=None,
values_params=None, default_multi=None, requires=None,
multi=False, callback=None, callback_params=None,
validator=None, validator_params=None,
properties=None, warnings_only=False):
"""
:param values: is a list of values the option can possibly take
"""
if isinstance(values, FunctionType):
2017-07-08 15:59:56 +02:00
validate_callback(values, values_params, 'values', self)
2014-07-06 15:31:57 +02:00
else:
if values_params is not None:
raise ValueError(_('values is not a function, so values_params must be None'))
2017-02-04 10:21:44 +01:00
if not isinstance(values, tuple):
2014-07-06 15:31:57 +02:00
raise TypeError(_('values must be a tuple or a function for {0}'
).format(name))
2017-07-22 16:26:06 +02:00
self._choice_values = values
if values_params is not None:
self._choice_values_params = values_params
super(ChoiceOption, self).__init__(name, doc, default=default,
default_multi=default_multi,
callback=callback,
callback_params=callback_params,
requires=requires,
multi=multi,
validator=validator,
validator_params=validator_params,
properties=properties,
2017-07-22 16:26:06 +02:00
warnings_only=warnings_only)
2016-10-14 22:20:14 +02:00
def impl_get_values(self, context, current_opt=undefined):
if current_opt is undefined:
current_opt = self
#FIXME cache? but in context...
2016-10-10 21:41:22 +02:00
values = self._choice_values
if isinstance(values, FunctionType):
if context is None:
values = []
else:
values = carry_out_calculation(current_opt, context=context,
callback=values,
2017-07-22 16:26:06 +02:00
callback_params=getattr(self, '_choice_values_params', {}))
if isinstance(values, Exception):
return values
2017-02-04 10:21:44 +01:00
if values is not undefined and not isinstance(values, list):
raise ConfigError(_('calculated values for {0} is not a list'
'').format(self.impl_getname()))
return values
2016-09-30 22:45:33 +02:00
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
values = self.impl_get_values(context, current_opt=current_opt)
if isinstance(values, Exception):
return values
2017-02-04 10:21:44 +01:00
if values is not undefined and not value in values:
2016-09-11 20:41:36 +02:00
if len(values) == 1:
return ValueError(_('only {0} is allowed'
'').format(values[0]))
else:
return ValueError(_('only {0} are allowed'
'').format(display_list(values)))
class BoolOption(Option):
"represents a choice between ``True`` and ``False``"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('boolean')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
if not isinstance(value, bool):
2017-02-04 10:21:44 +01:00
return ValueError()
class IntOption(Option):
"represents a choice of an integer"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('integer')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
if not isinstance(value, int):
2017-02-04 10:21:44 +01:00
return ValueError()
class FloatOption(Option):
"represents a choice of a floating point number"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('float')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
if not isinstance(value, float):
2017-02-04 10:21:44 +01:00
return ValueError()
class StrOption(Option):
"represents the choice of a string"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('string')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
if not isinstance(value, str):
2017-02-04 10:21:44 +01:00
return ValueError()
2017-02-04 10:21:44 +01:00
if sys.version_info[0] >= 3: # pragma: no cover
#UnicodeOption is same as StrOption in python 3+
class UnicodeOption(StrOption):
__slots__ = tuple()
pass
else:
class UnicodeOption(Option):
"represents the choice of a unicode string"
__slots__ = tuple()
_empty = u''
2016-10-10 21:41:22 +02:00
_display_name = _('unicode string')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
if not isinstance(value, unicode):
2017-02-04 10:21:44 +01:00
return ValueError()
2016-05-17 15:28:36 +02:00
class PasswordOption(Option):
"represents the choice of a password"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('password')
2016-05-17 15:28:36 +02:00
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
2016-05-17 15:28:36 +02:00
err = self._impl_valid_unicode(value)
if err:
return err
class IPOption(Option):
"represents the choice of an ip"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('IP')
def __init__(self, name, doc, default=None, default_multi=None,
requires=None, multi=False, callback=None,
callback_params=None, validator=None, validator_params=None,
properties=None, private_only=False, allow_reserved=False,
warnings_only=False):
2014-10-25 22:11:31 +02:00
extra = {'_private_only': private_only,
'_allow_reserved': allow_reserved}
super(IPOption, self).__init__(name, doc, default=default,
default_multi=default_multi,
callback=callback,
callback_params=callback_params,
requires=requires,
multi=multi,
validator=validator,
validator_params=validator_params,
properties=properties,
2014-10-25 22:11:31 +02:00
warnings_only=warnings_only,
extra=extra)
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
# sometimes an ip term starts with a zero
# but this does not fit in some case, for example bind does not like it
err = self._impl_valid_unicode(value)
if err:
return err
if value.count('.') != 3:
2016-09-11 16:18:23 +02:00
return ValueError()
for val in value.split('.'):
if val.startswith("0") and len(val) > 1:
2017-02-04 10:21:44 +01:00
return ValueError()
# 'standard' validation
try:
IP('{0}/32'.format(value))
2017-02-04 10:21:44 +01:00
except ValueError:
2016-09-11 16:18:23 +02:00
return ValueError()
def _second_level_validation(self, value, warnings_only):
ip = IP('{0}/32'.format(value))
2017-02-04 10:21:44 +01:00
if not self._get_extra('_allow_reserved') and ip.iptype() == 'RESERVED':
if warnings_only:
2016-09-11 16:18:23 +02:00
msg = _("shouldn't in reserved class")
else:
2016-09-11 16:18:23 +02:00
msg = _("mustn't be in reserved class")
return ValueError(msg)
2017-02-04 10:21:44 +01:00
if self._get_extra('_private_only') and not ip.iptype() == 'PRIVATE':
if warnings_only:
2016-09-11 16:18:23 +02:00
msg = _("should be in private class")
else:
2016-09-11 16:18:23 +02:00
msg = _("must be in private class")
return ValueError(msg)
2016-10-10 21:41:22 +02:00
def _cons_in_network(self, current_opt, opts, vals, warnings_only):
if len(vals) != 3:
2017-02-04 10:21:44 +01:00
raise ConfigError(_('invalid len for vals'))
if None in vals:
return
ip, network, netmask = vals
2017-02-04 10:21:44 +01:00
if IP(ip) not in IP('{0}/{1}'.format(network, netmask)):
2017-06-16 18:25:01 +02:00
msg = _('{4} is not in network {0}/{1} ({2}/{3})')
2016-09-11 20:41:36 +02:00
return ValueError(msg.format(network, netmask,
2017-06-16 18:25:01 +02:00
opts[1].impl_getname(), opts[2].impl_getname(), ip))
# test if ip is not network/broadcast IP
2016-10-10 21:41:22 +02:00
return opts[2]._cons_ip_netmask(current_opt, (opts[2], opts[0]), (netmask, ip), warnings_only)
class PortOption(Option):
"""represents the choice of a port
The port numbers are divided into three ranges:
the well-known ports,
the registered ports,
and the dynamic or private ports.
You can actived this three range.
Port number 0 is reserved and can't be used.
see: http://en.wikipedia.org/wiki/Port_numbers
"""
__slots__ = tuple()
2016-01-03 21:18:52 +01:00
port_re = re.compile(r"^[0-9]*$")
2016-10-10 21:41:22 +02:00
_display_name = _('port')
def __init__(self, name, doc, default=None, default_multi=None,
requires=None, multi=False, callback=None,
callback_params=None, validator=None, validator_params=None,
properties=None, allow_range=False, allow_zero=False,
allow_wellknown=True, allow_registred=True,
allow_private=False, warnings_only=False):
extra = {'_allow_range': allow_range,
'_min_value': None,
'_max_value': None}
ports_min = [0, 1, 1024, 49152]
ports_max = [0, 1023, 49151, 65535]
is_finally = False
for index, allowed in enumerate([allow_zero,
allow_wellknown,
allow_registred,
allow_private]):
if extra['_min_value'] is None:
if allowed:
extra['_min_value'] = ports_min[index]
elif not allowed:
is_finally = True
elif allowed and is_finally:
2017-02-04 10:21:44 +01:00
raise ValueError(_('inconsistency in allowed range'))
if allowed:
extra['_max_value'] = ports_max[index]
if extra['_max_value'] is None:
2017-02-04 10:21:44 +01:00
raise ValueError(_('max value is empty'))
super(PortOption, self).__init__(name, doc, default=default,
default_multi=default_multi,
callback=callback,
callback_params=callback_params,
requires=requires,
multi=multi,
validator=validator,
validator_params=validator_params,
properties=properties,
2014-10-25 22:11:31 +02:00
warnings_only=warnings_only,
extra=extra)
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
if isinstance(value, int):
2017-02-04 10:21:44 +01:00
if sys.version_info[0] >= 3: # pragma: no cover
2016-03-19 21:27:37 +01:00
value = str(value)
else:
value = unicode(value)
err = self._impl_valid_unicode(value)
if err:
return err
2017-02-04 10:21:44 +01:00
if self._get_extra('_allow_range') and ":" in str(value):
value = str(value).split(':')
if len(value) != 2:
2016-09-11 20:41:36 +02:00
return ValueError(_('range must have two values only'))
if not value[0] < value[1]:
2016-09-11 20:41:36 +02:00
return ValueError(_('first port in range must be'
' smaller than the second one'))
else:
value = [value]
for val in value:
2016-01-03 21:18:52 +01:00
if not self.port_re.search(val):
2016-09-11 20:41:36 +02:00
return ValueError()
2016-01-03 21:18:52 +01:00
val = int(val)
2017-02-04 10:21:44 +01:00
if not self._get_extra('_min_value') <= val <= self._get_extra('_max_value'):
2016-09-11 20:41:36 +02:00
return ValueError(_('must be an integer between {0} '
'and {1}').format(self._get_extra('_min_value'),
self._get_extra('_max_value')))
class NetworkOption(Option):
"represents the choice of a network"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('network address')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
err = self._impl_valid_unicode(value)
if err:
return err
if value.count('.') != 3:
2016-09-11 20:41:36 +02:00
return ValueError()
for val in value.split('.'):
if val.startswith("0") and len(val) > 1:
2016-09-11 20:41:36 +02:00
return ValueError()
try:
IP(value)
2017-02-04 10:21:44 +01:00
except ValueError:
2016-09-11 20:41:36 +02:00
return ValueError()
def _second_level_validation(self, value, warnings_only):
ip = IP(value)
2017-02-04 10:21:44 +01:00
if ip.iptype() == 'RESERVED':
if warnings_only:
2016-09-11 20:41:36 +02:00
msg = _("shouldn't be in reserved class")
else:
2016-09-11 20:41:36 +02:00
msg = _("mustn't be in reserved class")
return ValueError(msg)
class NetmaskOption(Option):
"represents the choice of a netmask"
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('netmask address')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
err = self._impl_valid_unicode(value)
if err:
return err
if value.count('.') != 3:
2016-09-11 20:41:36 +02:00
return ValueError()
for val in value.split('.'):
if val.startswith("0") and len(val) > 1:
2016-09-11 20:41:36 +02:00
return ValueError()
try:
IP('0.0.0.0/{0}'.format(value))
2017-02-04 10:21:44 +01:00
except ValueError:
2016-09-11 20:41:36 +02:00
return ValueError()
2016-10-10 21:41:22 +02:00
def _cons_network_netmask(self, current_opt, opts, vals, warnings_only):
#opts must be (netmask, network) options
if None in vals:
return
return self.__cons_netmask(opts, vals[0], vals[1], False, warnings_only)
2016-10-10 21:41:22 +02:00
def _cons_ip_netmask(self, current_opt, opts, vals, warnings_only):
#opts must be (netmask, ip) options
if None in vals:
return
return self.__cons_netmask(opts, vals[0], vals[1], True, warnings_only)
def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net,
warnings_only):
if len(opts) != 2:
2017-02-04 10:21:44 +01:00
return ConfigError(_('invalid len for opts'))
msg = None
try:
ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
make_net=make_net)
#if cidr == 32, ip same has network
if make_net and ip.prefixlen() != 32:
val_ip = IP(val_ipnetwork)
if ip.net() == val_ip:
2016-09-11 20:41:36 +02:00
msg = _("this is a network with netmask {0} ({1})")
if ip.broadcast() == val_ip:
2016-09-11 20:41:36 +02:00
msg = _("this is a broadcast with netmask {0} ({1})")
2017-02-04 10:21:44 +01:00
except ValueError:
if not make_net:
2016-09-11 20:41:36 +02:00
msg = _('with netmask {0} ({1})')
2017-02-04 10:21:44 +01:00
if msg is not None:
2016-09-11 20:41:36 +02:00
return ValueError(msg.format(val_netmask, opts[1].impl_getname()))
class BroadcastOption(Option):
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('broadcast address')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
err = self._impl_valid_unicode(value)
if err:
return err
2017-02-04 10:21:44 +01:00
if value.count('.') != 3:
return ValueError()
for val in value.split('.'):
if val.startswith("0") and len(val) > 1:
return ValueError()
try:
IP('{0}/32'.format(value))
2017-02-04 10:21:44 +01:00
except ValueError:
2016-09-11 20:41:36 +02:00
return ValueError()
2016-10-10 21:41:22 +02:00
def _cons_broadcast(self, current_opt, opts, vals, warnings_only):
if len(vals) != 3:
2017-02-04 10:21:44 +01:00
raise ConfigError(_('invalid len for vals'))
if None in vals:
return
broadcast, network, netmask = vals
if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
2017-06-16 18:25:01 +02:00
return ValueError(_('broadcast {4} invalid with network {0}/{1} ({2}/{3})').format(
network, netmask, opts[1].impl_getname(), opts[2].impl_getname(), broadcast))
class DomainnameOption(Option):
"""represents the choice of a domain name
netbios: for MS domain
hostname: to identify the device
domainname:
fqdn: with tld, not supported yet
"""
__slots__ = tuple()
2016-10-10 21:41:22 +02:00
_display_name = _('domain name')
def __init__(self, name, doc, default=None, default_multi=None,
requires=None, multi=False, callback=None,
callback_params=None, validator=None, validator_params=None,
properties=None, allow_ip=False, type_='domainname',
warnings_only=False, allow_without_dot=False):
if type_ not in ['netbios', 'hostname', 'domainname']:
2017-02-04 10:21:44 +01:00
raise ValueError(_('unknown type_ {0} for hostname').format(type_))
2014-10-25 22:11:31 +02:00
extra = {'_dom_type': type_}
if allow_ip not in [True, False]:
2017-02-04 10:21:44 +01:00
raise ValueError(_('allow_ip must be a boolean'))
if allow_without_dot not in [True, False]:
2017-02-04 10:21:44 +01:00
raise ValueError(_('allow_without_dot must be a boolean'))
2014-10-25 22:11:31 +02:00
extra['_allow_ip'] = allow_ip
extra['_allow_without_dot'] = allow_without_dot
2017-03-13 17:32:38 +01:00
# FIXME should be
# regexp = r'^((?!-)[a-z0-9-]{1,63}(?<!-)\.{0,1})$'
if type_ == 'domainname':
if allow_without_dot:
min_time = 0
else:
min_time = 1
regexp = r'((?!-)[a-z0-9-]{{{1},{0}}}\.){{{1},}}[a-z0-9-]{{1,{0}}}'.format(self._get_len(type_), min_time)
else:
regexp = r'((?!-)[a-z0-9-]{{1,{0}}})'.format(self._get_len(type_))
if allow_ip:
regexp = r'^(?:{0}|(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){{3}}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))$'.format(regexp)
else:
regexp = r'^{0}$'.format(regexp)
extra['_domain_re'] = re.compile(regexp)
extra['_has_upper'] = re.compile('[A-Z]')
super(DomainnameOption, self).__init__(name, doc, default=default,
default_multi=default_multi,
callback=callback,
callback_params=callback_params,
requires=requires,
multi=multi,
validator=validator,
validator_params=validator_params,
properties=properties,
2014-10-25 22:11:31 +02:00
warnings_only=warnings_only,
extra=extra)
2017-03-13 17:32:38 +01:00
def _get_len(self, type_):
if type_ == 'netbios':
return 15
else:
return 63
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
err = self._impl_valid_unicode(value)
if err:
return err
def _valid_length(val):
if len(val) < 1:
2016-09-11 20:41:36 +02:00
return ValueError(_("invalid length (min 1)"))
if len(val) > part_name_length:
2016-09-11 20:41:36 +02:00
return ValueError(_("invalid length (max {0})"
"").format(part_name_length))
2017-02-04 10:21:44 +01:00
if self._get_extra('_allow_ip') is True:
try:
IP('{0}/32'.format(value))
return
except ValueError:
pass
2016-08-31 15:50:10 +02:00
else:
try:
IP('{0}/32'.format(value))
except ValueError:
pass
else:
2017-06-16 18:25:01 +02:00
return ValueError(_('must not be an IP'))
2017-03-13 17:32:38 +01:00
part_name_length = self._get_len(self._get_extra('_dom_type'))
if self._get_extra('_dom_type') == 'domainname':
if not self._get_extra('_allow_without_dot') and not "." in value:
2016-09-11 20:41:36 +02:00
return ValueError(_("must have dot"))
if len(value) > 255:
2016-09-11 20:41:36 +02:00
return ValueError(_("invalid length (max 255)"))
for dom in value.split('.'):
err = _valid_length(dom)
if err:
return err
else:
return _valid_length(value)
def _second_level_validation(self, value, warnings_only):
2017-03-13 17:32:38 +01:00
if self._get_extra('_has_upper').search(value):
return ValueError(_('some characters are uppercase'))
if not self._get_extra('_domain_re').search(value):
if warnings_only:
return ValueError(_('some characters may cause problems'))
else:
return ValueError()
class URLOption(DomainnameOption):
__slots__ = tuple()
proto_re = re.compile(r'(http|https)://')
path_re = re.compile(r"^[A-Za-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
2016-10-10 21:41:22 +02:00
_display_name = _('URL')
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
err = self._impl_valid_unicode(value)
if err:
return err
match = self.proto_re.search(value)
2017-02-04 10:21:44 +01:00
if not match:
2016-09-11 20:41:36 +02:00
return ValueError(_('must start with http:// or '
'https://'))
value = value[len(match.group(0)):]
# get domain/files
splitted = value.split('/', 1)
if len(splitted) == 1:
domain = value
files = None
else:
domain, files = splitted
# if port in domain
splitted = domain.split(':', 1)
if len(splitted) == 1:
domain = splitted[0]
port = 0
else:
domain, port = splitted
if not 0 <= int(port) <= 65535:
2016-09-11 20:41:36 +02:00
return ValueError(_('port must be an between 0 and '
2017-02-04 10:21:44 +01:00
'65536'))
# validate domainname
err = super(URLOption, self)._validate(domain)
if err:
return err
err = super(URLOption, self)._second_level_validation(domain, False)
if err:
return err
# validate file
if files is not None and files != '' and not self.path_re.search(files):
2017-02-04 10:21:44 +01:00
return ValueError(_('must ends with a valid resource name'))
def _second_level_validation(self, value, warnings_only):
pass
class _RegexpOption(Option):
__slots__ = tuple()
2016-10-14 22:20:14 +02:00
def _validate(self, value, context=undefined, current_opt=undefined):
err = self._impl_valid_unicode(value)
if err:
return err
match = self._regexp.search(value)
if not match:
return ValueError()
class EmailOption(_RegexpOption):
__slots__ = tuple()
#https://www.w3.org/TR/html-markup/input.email.html#input.email.attrs.value.single.
_regexp = re.compile(r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
_display_name = _('email address')
class UsernameOption(_RegexpOption):
__slots__ = tuple()
#regexp build with 'man 8 adduser' informations
_regexp = re.compile(r"^[a-z_][a-z0-9_-]{0,30}[$a-z0-9_-]{0,1}$")
_display_name = _('username')
class FilenameOption(_RegexpOption):
__slots__ = tuple()
_regexp = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
_display_name = _('file name')
class DateOption(Option):
__slots__ = tuple()
_display_name = _('date')
def _validate(self, value, context=undefined, current_opt=undefined):
err = self._impl_valid_unicode(value)
if err:
return err
try:
datetime.datetime.strptime(value,"%Y-%m-%d")
2017-07-09 09:49:03 +02:00
except ValueError:
return ValueError()