refactor validation
[tiramisu.git] / tiramisu / option / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description"
3 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 # The original `Config` design model is unproudly borrowed from
19 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
20 # the whole pypy projet is under MIT licence
21 # ____________________________________________________________
22 import re
23 import sys
24 from IPy import IP
25 from types import FunctionType
26 from tiramisu.setting import log, undefined
27
28 from tiramisu.error import ConfigError, ContextError
29 from tiramisu.i18n import _
30 from .baseoption import Option, validate_callback
31 from tiramisu.autolib import carry_out_calculation
32
33
34 class ChoiceOption(Option):
35     """represents a choice out of several objects.
36
37     The option can also have the value ``None``
38     """
39     __slots__ = tuple()
40
41     def __init__(self, name, doc, values, default=None,
42                  values_params=None, default_multi=None, requires=None,
43                  multi=False, callback=None, callback_params=None,
44                  validator=None, validator_params=None,
45                  properties=None, warnings_only=False):
46         """
47         :param values: is a list of values the option can possibly take
48         """
49         if isinstance(values, FunctionType):
50             validate_callback(values, values_params, 'values')
51         else:
52             if values_params is not None:
53                 raise ValueError(_('values is not a function, so values_params must be None'))
54             if not isinstance(values, tuple):  # pragma: optional cover
55                 raise TypeError(_('values must be a tuple or a function for {0}'
56                                   ).format(name))
57         self._choice_values = values
58         self._choice_values_params = values_params
59         super(ChoiceOption, self).__init__(name, doc, default=default,
60                                            default_multi=default_multi,
61                                            callback=callback,
62                                            callback_params=callback_params,
63                                            requires=requires,
64                                            multi=multi,
65                                            validator=validator,
66                                            validator_params=validator_params,
67                                            properties=properties,
68                                            warnings_only=warnings_only)
69
70     def impl_get_values(self, context):
71         #FIXME cache? but in context...
72         values = self._choice_values
73         if isinstance(values, FunctionType):
74             values_params = self._choice_values_params
75             if values_params is None:
76                 values_params = {}
77             values = carry_out_calculation(self, context=context,
78                                            callback=values,
79                                            callback_params=values_params)
80             if not isinstance(values, list):  # pragma: optional cover
81                 raise ConfigError(_('calculated values for {0} is not a list'
82                                     '').format(self.impl_getname()))
83         return values
84
85     def _validate(self, value, context=undefined):
86         try:
87             values = self.impl_get_values(context)
88             if not value in values:  # pragma: optional cover
89                 raise ValueError(_('value {0} is not permitted, '
90                                  'only {1} is allowed'
91                                  '').format(value,
92                                             values))
93         except ContextError:  # pragma: optional cover
94             log.debug('ChoiceOption validation, disabled because no context')
95
96
97 class BoolOption(Option):
98     "represents a choice between ``True`` and ``False``"
99     __slots__ = tuple()
100
101     def _validate(self, value, context=undefined):
102         if not isinstance(value, bool):
103             raise ValueError(_('invalid boolean'))  # pragma: optional cover
104
105
106 class IntOption(Option):
107     "represents a choice of an integer"
108     __slots__ = tuple()
109
110     def _validate(self, value, context=undefined):
111         if not isinstance(value, int):
112             raise ValueError(_('invalid integer'))  # pragma: optional cover
113
114
115 class FloatOption(Option):
116     "represents a choice of a floating point number"
117     __slots__ = tuple()
118
119     def _validate(self, value, context=undefined):
120         if not isinstance(value, float):
121             raise ValueError(_('invalid float'))  # pragma: optional cover
122
123
124 class StrOption(Option):
125     "represents the choice of a string"
126     __slots__ = tuple()
127
128     def _validate(self, value, context=undefined):
129         if not isinstance(value, str):
130             raise ValueError(_('invalid string'))  # pragma: optional cover
131
132
133 if sys.version_info[0] >= 3:  # pragma: optional cover
134     #UnicodeOption is same as StrOption in python 3+
135     class UnicodeOption(StrOption):
136         __slots__ = tuple()
137         pass
138 else:
139     class UnicodeOption(Option):
140         "represents the choice of a unicode string"
141         __slots__ = tuple()
142         _empty = u''
143
144         def _validate(self, value, context=undefined):
145             if not isinstance(value, unicode):
146                 raise ValueError(_('invalid unicode'))  # pragma: optional cover
147
148
149 class IPOption(Option):
150     "represents the choice of an ip"
151     __slots__ = tuple()
152
153     def __init__(self, name, doc, default=None, default_multi=None,
154                  requires=None, multi=False, callback=None,
155                  callback_params=None, validator=None, validator_params=None,
156                  properties=None, private_only=False, allow_reserved=False,
157                  warnings_only=False):
158         extra = {'_private_only': private_only,
159                  '_allow_reserved': allow_reserved}
160         super(IPOption, self).__init__(name, doc, default=default,
161                                        default_multi=default_multi,
162                                        callback=callback,
163                                        callback_params=callback_params,
164                                        requires=requires,
165                                        multi=multi,
166                                        validator=validator,
167                                        validator_params=validator_params,
168                                        properties=properties,
169                                        warnings_only=warnings_only,
170                                        extra=extra)
171
172     def _validate(self, value, context=undefined):
173         # sometimes an ip term starts with a zero
174         # but this does not fit in some case, for example bind does not like it
175         try:
176             for val in value.split('.'):
177                 if val.startswith("0") and len(val) > 1:
178                     raise ValueError(_('invalid IP'))  # pragma: optional cover
179         except AttributeError:  # pragma: optional cover
180             #if integer for example
181             raise ValueError(_('invalid IP'))
182         # 'standard' validation
183         try:
184             IP('{0}/32'.format(value))
185         except ValueError:  # pragma: optional cover
186             raise ValueError(_('invalid IP'))
187
188     def _second_level_validation(self, value, warnings_only):
189         ip = IP('{0}/32'.format(value))
190         if not self._get_extra('_allow_reserved') and ip.iptype() == 'RESERVED':  # pragma: optional cover
191             if warnings_only:
192                 msg = _("IP is in reserved class")
193             else:
194                 msg = _("invalid IP, mustn't be in reserved class")
195             raise ValueError(msg)
196         if self._get_extra('_private_only') and not ip.iptype() == 'PRIVATE':  # pragma: optional cover
197             if warnings_only:
198                 msg = _("IP is not in private class")
199             else:
200                 msg = _("invalid IP, must be in private class")
201             raise ValueError(msg)
202
203     def _cons_in_network(self, opts, vals, warnings_only):
204         if len(vals) != 3:
205             raise ConfigError(_('invalid len for vals'))  # pragma: optional cover
206         if None in vals:
207             return
208         ip, network, netmask = vals
209         if IP(ip) not in IP('{0}/{1}'.format(network, netmask)):  # pragma: optional cover
210             if warnings_only:
211                 msg = _('IP {0} ({1}) not in network {2} ({3}) with netmask {4}'
212                         ' ({5})')
213             else:
214                 msg = _('invalid IP {0} ({1}) not in network {2} ({3}) with '
215                         'netmask {4} ({5})')
216             raise ValueError(msg.format(ip, opts[0].impl_getname(), network,
217                              opts[1].impl_getname(), netmask, opts[2].impl_getname()))
218         # test if ip is not network/broadcast IP
219         opts[2]._cons_ip_netmask((opts[2], opts[0]), (netmask, ip), warnings_only)
220
221
222 class PortOption(Option):
223     """represents the choice of a port
224     The port numbers are divided into three ranges:
225     the well-known ports,
226     the registered ports,
227     and the dynamic or private ports.
228     You can actived this three range.
229     Port number 0 is reserved and can't be used.
230     see: http://en.wikipedia.org/wiki/Port_numbers
231     """
232     __slots__ = tuple()
233
234     def __init__(self, name, doc, default=None, default_multi=None,
235                  requires=None, multi=False, callback=None,
236                  callback_params=None, validator=None, validator_params=None,
237                  properties=None, allow_range=False, allow_zero=False,
238                  allow_wellknown=True, allow_registred=True,
239                  allow_private=False, warnings_only=False):
240         extra = {'_allow_range': allow_range,
241                  '_min_value': None,
242                  '_max_value': None}
243         ports_min = [0, 1, 1024, 49152]
244         ports_max = [0, 1023, 49151, 65535]
245         is_finally = False
246         for index, allowed in enumerate([allow_zero,
247                                          allow_wellknown,
248                                          allow_registred,
249                                          allow_private]):
250             if extra['_min_value'] is None:
251                 if allowed:
252                     extra['_min_value'] = ports_min[index]
253             elif not allowed:
254                 is_finally = True
255             elif allowed and is_finally:
256                 raise ValueError(_('inconsistency in allowed range'))  # pragma: optional cover
257             if allowed:
258                 extra['_max_value'] = ports_max[index]
259
260         if extra['_max_value'] is None:
261             raise ValueError(_('max value is empty'))  # pragma: optional cover
262
263         super(PortOption, self).__init__(name, doc, default=default,
264                                          default_multi=default_multi,
265                                          callback=callback,
266                                          callback_params=callback_params,
267                                          requires=requires,
268                                          multi=multi,
269                                          validator=validator,
270                                          validator_params=validator_params,
271                                          properties=properties,
272                                          warnings_only=warnings_only,
273                                          extra=extra)
274
275     def _validate(self, value, context=undefined):
276         if self._get_extra('_allow_range') and ":" in str(value):  # pragma: optional cover
277             value = str(value).split(':')
278             if len(value) != 2:
279                 raise ValueError(_('invalid port, range must have two values '
280                                  'only'))
281             if not value[0] < value[1]:
282                 raise ValueError(_('invalid port, first port in range must be'
283                                  ' smaller than the second one'))
284         else:
285             value = [value]
286
287         for val in value:
288             try:
289                 val = int(val)
290             except ValueError:  # pragma: optional cover
291                 raise ValueError(_('invalid port'))
292             if not self._get_extra('_min_value') <= val <= self._get_extra('_max_value'):  # pragma: optional cover
293                 raise ValueError(_('invalid port, must be an integer between {0} '
294                                    'and {1}').format(self._get_extra('_min_value'),
295                                                      self._get_extra('_max_value')))
296
297
298 class NetworkOption(Option):
299     "represents the choice of a network"
300     __slots__ = tuple()
301
302     def _validate(self, value, context=undefined):
303         try:
304             IP(value)
305         except ValueError:  # pragma: optional cover
306             raise ValueError(_('invalid network address'))
307
308     def _second_level_validation(self, value, warnings_only):
309         ip = IP(value)
310         if ip.iptype() == 'RESERVED':  # pragma: optional cover
311             if warnings_only:
312                 msg = _("network address is in reserved class")
313             else:
314                 msg = _("invalid network address, mustn't be in reserved class")
315             raise ValueError(msg)
316
317
318 class NetmaskOption(Option):
319     "represents the choice of a netmask"
320     __slots__ = tuple()
321
322     def _validate(self, value, context=undefined):
323         try:
324             IP('0.0.0.0/{0}'.format(value))
325         except ValueError:  # pragma: optional cover
326             raise ValueError(_('invalid netmask address'))
327
328     def _cons_network_netmask(self, opts, vals, warnings_only):
329         #opts must be (netmask, network) options
330         if None in vals:
331             return
332         self.__cons_netmask(opts, vals[0], vals[1], False, warnings_only)
333
334     def _cons_ip_netmask(self, opts, vals, warnings_only):
335         #opts must be (netmask, ip) options
336         if None in vals:
337             return
338         self.__cons_netmask(opts, vals[0], vals[1], True, warnings_only)
339
340     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net,
341                        warnings_only):
342         if len(opts) != 2:
343             raise ConfigError(_('invalid len for opts'))  # pragma: optional cover
344         msg = None
345         try:
346             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
347                     make_net=make_net)
348             #if cidr == 32, ip same has network
349             if make_net and ip.prefixlen() != 32:
350                 val_ip = IP(val_ipnetwork)
351                 if ip.net() == val_ip:
352                     msg = _("invalid IP {0} ({1}) with netmask {2},"
353                             " this IP is a network")
354                 if ip.broadcast() == val_ip:
355                     msg = _("invalid IP {0} ({1}) with netmask {2},"
356                             " this IP is a broadcast")
357
358         except ValueError:  # pragma: optional cover
359             if not make_net:
360                 msg = _('invalid network {0} ({1}) with netmask {2}')
361         if msg is not None:  # pragma: optional cover
362             raise ValueError(msg.format(val_ipnetwork, opts[1].impl_getname(),
363                                         val_netmask))
364
365
366 class BroadcastOption(Option):
367     __slots__ = tuple()
368
369     def _validate(self, value, context=undefined):
370         try:
371             IP('{0}/32'.format(value))
372         except ValueError:  # pragma: optional cover
373             raise ValueError(_('invalid broadcast address'))
374
375     def _cons_broadcast(self, opts, vals, warnings_only):
376         if len(vals) != 3:
377             raise ConfigError(_('invalid len for vals'))  # pragma: optional cover
378         if None in vals:
379             return
380         broadcast, network, netmask = vals
381         if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
382             raise ValueError(_('invalid broadcast {0} ({1}) with network {2} '
383                                '({3}) and netmask {4} ({5})').format(
384                                    broadcast, opts[0].impl_getname(), network,
385                                    opts[1].impl_getname(), netmask, opts[2].impl_getname()))  # pragma: optional cover
386
387
388 class DomainnameOption(Option):
389     """represents the choice of a domain name
390     netbios: for MS domain
391     hostname: to identify the device
392     domainname:
393     fqdn: with tld, not supported yet
394     """
395     __slots__ = tuple()
396
397     def __init__(self, name, doc, default=None, default_multi=None,
398                  requires=None, multi=False, callback=None,
399                  callback_params=None, validator=None, validator_params=None,
400                  properties=None, allow_ip=False, type_='domainname',
401                  warnings_only=False, allow_without_dot=False):
402         if type_ not in ['netbios', 'hostname', 'domainname']:
403             raise ValueError(_('unknown type_ {0} for hostname').format(type_))  # pragma: optional cover
404         extra = {'_dom_type': type_}
405         if allow_ip not in [True, False]:
406             raise ValueError(_('allow_ip must be a boolean'))  # pragma: optional cover
407         if allow_without_dot not in [True, False]:
408             raise ValueError(_('allow_without_dot must be a boolean'))  # pragma: optional cover
409         extra['_allow_ip'] = allow_ip
410         extra['_allow_without_dot'] = allow_without_dot
411         extra['_domain_re'] = re.compile(r'^[a-z\d][a-z\d\-]*$')
412         extra['_has_upper'] = re.compile('[A-Z]')
413
414         super(DomainnameOption, self).__init__(name, doc, default=default,
415                                                default_multi=default_multi,
416                                                callback=callback,
417                                                callback_params=callback_params,
418                                                requires=requires,
419                                                multi=multi,
420                                                validator=validator,
421                                                validator_params=validator_params,
422                                                properties=properties,
423                                                warnings_only=warnings_only,
424                                                extra=extra)
425
426     def _validate(self, value, context=undefined):
427         def _valid_length(val):
428             if len(val) < 2:
429                 raise ValueError(_("invalid domainname's length (min 2)"))
430             if len(val) > part_name_length:
431                 raise ValueError(_("invalid domainname's length (max {0})"
432                                    "").format(part_name_length))
433
434         if self._get_extra('_allow_ip') is True:  # pragma: optional cover
435             try:
436                 IP('{0}/32'.format(value))
437                 return
438             except ValueError:
439                 pass
440         if self._get_extra('_dom_type') == 'netbios':
441             part_name_length = 15
442         else:
443             part_name_length = 63
444         if self._get_extra('_dom_type') == 'domainname':
445             if not self._get_extra('_allow_without_dot') and not "." in value:
446                 raise ValueError(_("invalid domainname, must have dot"))
447             if len(value) > 255:
448                 raise ValueError(_("invalid domainname's length (max 255)"))
449             for dom in value.split('.'):
450                 _valid_length(dom)
451         else:
452             _valid_length(value)
453
454     def _second_level_validation(self, value, warnings_only):
455         def _valid_char(val):
456             if self._get_extra('_has_upper').search(val):
457                 raise ValueError(_('some characters are uppercase'))
458             if not self._get_extra('_domain_re').search(val):
459                 if warnings_only:
460                     raise ValueError(_('some characters may cause problems'))
461                 else:
462                     raise ValueError(_('invalid domainname'))
463         #not for IP
464         if self._get_extra('_allow_ip') is True:
465             try:
466                 IP('{0}/32'.format(value))
467                 return
468             except ValueError:
469                 pass
470         if self._get_extra('_dom_type') == 'domainname':
471             for dom in value.split('.'):
472                 _valid_char(dom)
473         else:
474             _valid_char(value)
475
476
477 class EmailOption(DomainnameOption):
478     __slots__ = tuple()
479     username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
480
481     def _validate(self, value, context=undefined):
482         splitted = value.split('@', 1)
483         try:
484             username, domain = splitted
485         except ValueError:  # pragma: optional cover
486             raise ValueError(_('invalid email address, must contains one @'
487                                ))
488         if not self.username_re.search(username):
489             raise ValueError(_('invalid username in email address'))  # pragma: optional cover
490         super(EmailOption, self)._validate(domain)
491         super(EmailOption, self)._second_level_validation(domain, False)
492
493     def _second_level_validation(self, value, warnings_only):
494         pass
495
496
497 class URLOption(DomainnameOption):
498     __slots__ = tuple()
499     proto_re = re.compile(r'(http|https)://')
500     path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
501
502     def _validate(self, value, context=undefined):
503         match = self.proto_re.search(value)
504         if not match:  # pragma: optional cover
505             raise ValueError(_('invalid url, must start with http:// or '
506                                'https://'))
507         value = value[len(match.group(0)):]
508         # get domain/files
509         splitted = value.split('/', 1)
510         try:
511             domain, files = splitted
512         except ValueError:
513             domain = value
514             files = None
515         # if port in domain
516         splitted = domain.split(':', 1)
517         try:
518             domain, port = splitted
519
520         except ValueError:  # pragma: optional cover
521             domain = splitted[0]
522             port = 0
523         if not 0 <= int(port) <= 65535:
524             raise ValueError(_('invalid url, port must be an between 0 and '
525                                '65536'))  # pragma: optional cover
526         # validate domainname
527         super(URLOption, self)._validate(domain)
528         super(URLOption, self)._second_level_validation(domain, False)
529         # validate file
530         if files is not None and files != '' and not self.path_re.search(files):
531             raise ValueError(_('invalid url, must ends with filename'))  # pragma: optional cover
532
533     def _second_level_validation(self, value, warnings_only):
534         pass
535
536
537 class UsernameOption(Option):
538     __slots__ = tuple()
539     #regexp build with 'man 8 adduser' informations
540     username_re = re.compile(r"^[a-z_][a-z0-9_-]{0,30}[$a-z0-9_-]{0,1}$")
541
542     def _validate(self, value, context=undefined):
543         match = self.username_re.search(value)
544         if not match:
545             raise ValueError(_('invalid username'))  # pragma: optional cover
546
547
548 class FilenameOption(Option):
549     __slots__ = tuple()
550     path_re = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
551
552     def _validate(self, value, context=undefined):
553         match = self.path_re.search(value)
554         if not match:
555             raise ValueError(_('invalid filename'))  # pragma: optional cover