python 3.5 support
[tiramisu.git] / tiramisu / option / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description"
3 # Copyright (C) 2012-2017 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 import datetime
25 from IPy import IP
26 from types import FunctionType
27 from ..setting import undefined
28
29 from ..error import ConfigError
30 from ..i18n import _
31 from .baseoption import Option, validate_callback, display_list
32 from ..autolib import carry_out_calculation
33
34
35 class ChoiceOption(Option):
36     """represents a choice out of several objects.
37
38     The option can also have the value ``None``
39     """
40     __slots__ = tuple()
41     _display_name = _('choice')
42
43     def __init__(self, name, doc, values, default=None,
44                  values_params=None, default_multi=None, requires=None,
45                  multi=False, callback=None, callback_params=None,
46                  validator=None, validator_params=None,
47                  properties=None, warnings_only=False):
48         """
49         :param values: is a list of values the option can possibly take
50         """
51         if isinstance(values, FunctionType):
52             validate_callback(values, values_params, 'values', self)
53         else:
54             if values_params is not None:
55                 raise ValueError(_('values is not a function, so values_params must be None'))
56             if not isinstance(values, tuple):
57                 raise TypeError(_('values must be a tuple or a function for {0}'
58                                   ).format(name))
59         session = self.getsession()
60         self.impl_set_choice_values_params(values, values_params, session)
61         super(ChoiceOption, self).__init__(name, doc, default=default,
62                                            default_multi=default_multi,
63                                            callback=callback,
64                                            callback_params=callback_params,
65                                            requires=requires,
66                                            multi=multi,
67                                            validator=validator,
68                                            validator_params=validator_params,
69                                            properties=properties,
70                                            warnings_only=warnings_only,
71                                            session=session)
72         self.commit(session)
73
74     def impl_get_values(self, context, current_opt=undefined):
75         if current_opt is undefined:
76             current_opt = self
77         #FIXME cache? but in context...
78         values = self._choice_values
79         if isinstance(values, FunctionType):
80             if context is None:
81                 values = []
82             else:
83                 values = carry_out_calculation(current_opt, context=context,
84                                                callback=values,
85                                                callback_params=self.impl_get_choice_values_params())
86                 if isinstance(values, Exception):
87                     return values
88                 if values is not undefined and not isinstance(values, list):
89                     raise ConfigError(_('calculated values for {0} is not a list'
90                                         '').format(self.impl_getname()))
91         return values
92
93
94     def _validate(self, value, context=undefined, current_opt=undefined):
95         values = self.impl_get_values(context, current_opt=current_opt)
96         if isinstance(values, Exception):
97             return values
98         if values is not undefined and not value in values:
99             if len(values) == 1:
100                 return ValueError(_('only {0} is allowed'
101                                     '').format(values[0]))
102             else:
103                 return ValueError(_('only {0} are allowed'
104                                     '').format(display_list(values)))
105
106
107 class BoolOption(Option):
108     "represents a choice between ``True`` and ``False``"
109     __slots__ = tuple()
110     _display_name = _('boolean')
111
112     def _validate(self, value, context=undefined, current_opt=undefined):
113         if not isinstance(value, bool):
114             return ValueError()
115
116
117 class IntOption(Option):
118     "represents a choice of an integer"
119     __slots__ = tuple()
120     _display_name = _('integer')
121
122     def _validate(self, value, context=undefined, current_opt=undefined):
123         if not isinstance(value, int):
124             return ValueError()
125
126
127 class FloatOption(Option):
128     "represents a choice of a floating point number"
129     __slots__ = tuple()
130     _display_name = _('float')
131
132     def _validate(self, value, context=undefined, current_opt=undefined):
133         if not isinstance(value, float):
134             return ValueError()
135
136
137 class StrOption(Option):
138     "represents the choice of a string"
139     __slots__ = tuple()
140     _display_name = _('string')
141
142     def _validate(self, value, context=undefined, current_opt=undefined):
143         if not isinstance(value, str):
144             return ValueError()
145
146
147 if sys.version_info[0] >= 3:  # pragma: no cover
148     #UnicodeOption is same as StrOption in python 3+
149     class UnicodeOption(StrOption):
150         __slots__ = tuple()
151         pass
152 else:
153     class UnicodeOption(Option):
154         "represents the choice of a unicode string"
155         __slots__ = tuple()
156         _empty = u''
157         _display_name = _('unicode string')
158
159         def _validate(self, value, context=undefined, current_opt=undefined):
160             if not isinstance(value, unicode):
161                 return ValueError()
162
163
164 class PasswordOption(Option):
165     "represents the choice of a password"
166     __slots__ = tuple()
167     _display_name = _('password')
168
169     def _validate(self, value, context=undefined, current_opt=undefined):
170         err = self._impl_valid_unicode(value)
171         if err:
172             return err
173
174
175 class IPOption(Option):
176     "represents the choice of an ip"
177     __slots__ = tuple()
178     _display_name = _('IP')
179
180     def __init__(self, name, doc, default=None, default_multi=None,
181                  requires=None, multi=False, callback=None,
182                  callback_params=None, validator=None, validator_params=None,
183                  properties=None, private_only=False, allow_reserved=False,
184                  warnings_only=False):
185         extra = {'_private_only': private_only,
186                  '_allow_reserved': allow_reserved}
187         super(IPOption, self).__init__(name, doc, default=default,
188                                        default_multi=default_multi,
189                                        callback=callback,
190                                        callback_params=callback_params,
191                                        requires=requires,
192                                        multi=multi,
193                                        validator=validator,
194                                        validator_params=validator_params,
195                                        properties=properties,
196                                        warnings_only=warnings_only,
197                                        extra=extra)
198
199     def _validate(self, value, context=undefined, current_opt=undefined):
200         # sometimes an ip term starts with a zero
201         # but this does not fit in some case, for example bind does not like it
202         err = self._impl_valid_unicode(value)
203         if err:
204             return err
205         if value.count('.') != 3:
206             return ValueError()
207         for val in value.split('.'):
208             if val.startswith("0") and len(val) > 1:
209                 return ValueError()
210         # 'standard' validation
211         try:
212             IP('{0}/32'.format(value))
213         except ValueError:
214             return ValueError()
215
216     def _second_level_validation(self, value, warnings_only):
217         ip = IP('{0}/32'.format(value))
218         if not self._get_extra('_allow_reserved') and ip.iptype() == 'RESERVED':
219             if warnings_only:
220                 msg = _("shouldn't in reserved class")
221             else:
222                 msg = _("mustn't be in reserved class")
223             return ValueError(msg)
224         if self._get_extra('_private_only') and not ip.iptype() == 'PRIVATE':
225             if warnings_only:
226                 msg = _("should be in private class")
227             else:
228                 msg = _("must be in private class")
229             return ValueError(msg)
230
231     def _cons_in_network(self, current_opt, opts, vals, warnings_only):
232         if len(vals) != 3:
233             raise ConfigError(_('invalid len for vals'))
234         if None in vals:
235             return
236         ip, network, netmask = vals
237         if IP(ip) not in IP('{0}/{1}'.format(network, netmask)):
238             msg = _('{4} is not in network {0}/{1} ({2}/{3})')
239             return ValueError(msg.format(network, netmask,
240                               opts[1].impl_getname(), opts[2].impl_getname(), ip))
241         # test if ip is not network/broadcast IP
242         return opts[2]._cons_ip_netmask(current_opt, (opts[2], opts[0]), (netmask, ip), warnings_only)
243
244
245 class PortOption(Option):
246     """represents the choice of a port
247     The port numbers are divided into three ranges:
248     the well-known ports,
249     the registered ports,
250     and the dynamic or private ports.
251     You can actived this three range.
252     Port number 0 is reserved and can't be used.
253     see: http://en.wikipedia.org/wiki/Port_numbers
254     """
255     __slots__ = tuple()
256     port_re = re.compile(r"^[0-9]*$")
257     _display_name = _('port')
258
259     def __init__(self, name, doc, default=None, default_multi=None,
260                  requires=None, multi=False, callback=None,
261                  callback_params=None, validator=None, validator_params=None,
262                  properties=None, allow_range=False, allow_zero=False,
263                  allow_wellknown=True, allow_registred=True,
264                  allow_private=False, warnings_only=False):
265         extra = {'_allow_range': allow_range,
266                  '_min_value': None,
267                  '_max_value': None}
268         ports_min = [0, 1, 1024, 49152]
269         ports_max = [0, 1023, 49151, 65535]
270         is_finally = False
271         for index, allowed in enumerate([allow_zero,
272                                          allow_wellknown,
273                                          allow_registred,
274                                          allow_private]):
275             if extra['_min_value'] is None:
276                 if allowed:
277                     extra['_min_value'] = ports_min[index]
278             elif not allowed:
279                 is_finally = True
280             elif allowed and is_finally:
281                 raise ValueError(_('inconsistency in allowed range'))
282             if allowed:
283                 extra['_max_value'] = ports_max[index]
284
285         if extra['_max_value'] is None:
286             raise ValueError(_('max value is empty'))
287
288         super(PortOption, self).__init__(name, doc, default=default,
289                                          default_multi=default_multi,
290                                          callback=callback,
291                                          callback_params=callback_params,
292                                          requires=requires,
293                                          multi=multi,
294                                          validator=validator,
295                                          validator_params=validator_params,
296                                          properties=properties,
297                                          warnings_only=warnings_only,
298                                          extra=extra)
299
300     def _validate(self, value, context=undefined, current_opt=undefined):
301         if isinstance(value, int):
302             if sys.version_info[0] >= 3:  # pragma: no cover
303                 value = str(value)
304             else:
305                 value = unicode(value)
306         err = self._impl_valid_unicode(value)
307         if err:
308             return err
309         if self._get_extra('_allow_range') and ":" in str(value):
310             value = str(value).split(':')
311             if len(value) != 2:
312                 return ValueError(_('range must have two values only'))
313             if not value[0] < value[1]:
314                 return ValueError(_('first port in range must be'
315                                   ' smaller than the second one'))
316         else:
317             value = [value]
318
319         for val in value:
320             if not self.port_re.search(val):
321                 return ValueError()
322             val = int(val)
323             if not self._get_extra('_min_value') <= val <= self._get_extra('_max_value'):
324                 return ValueError(_('must be an integer between {0} '
325                                     'and {1}').format(self._get_extra('_min_value'),
326                                                       self._get_extra('_max_value')))
327
328
329 class NetworkOption(Option):
330     "represents the choice of a network"
331     __slots__ = tuple()
332     _display_name = _('network address')
333
334     def _validate(self, value, context=undefined, current_opt=undefined):
335         err = self._impl_valid_unicode(value)
336         if err:
337             return err
338         if value.count('.') != 3:
339             return ValueError()
340         for val in value.split('.'):
341             if val.startswith("0") and len(val) > 1:
342                 return ValueError()
343         try:
344             IP(value)
345         except ValueError:
346             return ValueError()
347
348     def _second_level_validation(self, value, warnings_only):
349         ip = IP(value)
350         if ip.iptype() == 'RESERVED':
351             if warnings_only:
352                 msg = _("shouldn't be in reserved class")
353             else:
354                 msg = _("mustn't be in reserved class")
355             return ValueError(msg)
356
357
358 class NetmaskOption(Option):
359     "represents the choice of a netmask"
360     __slots__ = tuple()
361     _display_name = _('netmask address')
362
363     def _validate(self, value, context=undefined, current_opt=undefined):
364         err = self._impl_valid_unicode(value)
365         if err:
366             return err
367         if value.count('.') != 3:
368             return ValueError()
369         for val in value.split('.'):
370             if val.startswith("0") and len(val) > 1:
371                 return ValueError()
372         try:
373             IP('0.0.0.0/{0}'.format(value))
374         except ValueError:
375             return ValueError()
376
377     def _cons_network_netmask(self, current_opt, opts, vals, warnings_only):
378         #opts must be (netmask, network) options
379         if None in vals:
380             return
381         return self.__cons_netmask(opts, vals[0], vals[1], False, warnings_only)
382
383     def _cons_ip_netmask(self, current_opt, opts, vals, warnings_only):
384         #opts must be (netmask, ip) options
385         if None in vals:
386             return
387         return self.__cons_netmask(opts, vals[0], vals[1], True, warnings_only)
388
389     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net,
390                        warnings_only):
391         if len(opts) != 2:
392             return ConfigError(_('invalid len for opts'))
393         msg = None
394         try:
395             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
396                     make_net=make_net)
397             #if cidr == 32, ip same has network
398             if make_net and ip.prefixlen() != 32:
399                 val_ip = IP(val_ipnetwork)
400                 if ip.net() == val_ip:
401                     msg = _("this is a network with netmask {0} ({1})")
402                 if ip.broadcast() == val_ip:
403                     msg = _("this is a broadcast with netmask {0} ({1})")
404
405         except ValueError:
406             if not make_net:
407                 msg = _('with netmask {0} ({1})')
408         if msg is not None:
409             return ValueError(msg.format(val_netmask, opts[1].impl_getname()))
410
411
412 class BroadcastOption(Option):
413     __slots__ = tuple()
414     _display_name = _('broadcast address')
415
416     def _validate(self, value, context=undefined, current_opt=undefined):
417         err = self._impl_valid_unicode(value)
418         if err:
419             return err
420         if value.count('.') != 3:
421             return ValueError()
422         for val in value.split('.'):
423             if val.startswith("0") and len(val) > 1:
424                 return ValueError()
425         try:
426             IP('{0}/32'.format(value))
427         except ValueError:
428             return ValueError()
429
430     def _cons_broadcast(self, current_opt, opts, vals, warnings_only):
431         if len(vals) != 3:
432             raise ConfigError(_('invalid len for vals'))
433         if None in vals:
434             return
435         broadcast, network, netmask = vals
436         if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
437             return ValueError(_('broadcast {4} invalid with network {0}/{1} ({2}/{3})').format(
438                 network, netmask, opts[1].impl_getname(), opts[2].impl_getname(), broadcast))
439
440
441 class DomainnameOption(Option):
442     """represents the choice of a domain name
443     netbios: for MS domain
444     hostname: to identify the device
445     domainname:
446     fqdn: with tld, not supported yet
447     """
448     __slots__ = tuple()
449     _display_name = _('domain name')
450
451     def __init__(self, name, doc, default=None, default_multi=None,
452                  requires=None, multi=False, callback=None,
453                  callback_params=None, validator=None, validator_params=None,
454                  properties=None, allow_ip=False, type_='domainname',
455                  warnings_only=False, allow_without_dot=False):
456         if type_ not in ['netbios', 'hostname', 'domainname']:
457             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
458         extra = {'_dom_type': type_}
459         if allow_ip not in [True, False]:
460             raise ValueError(_('allow_ip must be a boolean'))
461         if allow_without_dot not in [True, False]:
462             raise ValueError(_('allow_without_dot must be a boolean'))
463         extra['_allow_ip'] = allow_ip
464         extra['_allow_without_dot'] = allow_without_dot
465         # FIXME should be
466         # regexp = r'^((?!-)[a-z0-9-]{1,63}(?<!-)\.{0,1})$'
467         if type_ == 'domainname':
468             if allow_without_dot:
469                 min_time = 0
470             else:
471                 min_time = 1
472             regexp = r'((?!-)[a-z0-9-]{{{1},{0}}}\.){{{1},}}[a-z0-9-]{{1,{0}}}'.format(self._get_len(type_), min_time)
473         else:
474             regexp = r'((?!-)[a-z0-9-]{{1,{0}}})'.format(self._get_len(type_))
475         if allow_ip:
476             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)
477         else:
478             regexp = r'^{0}$'.format(regexp)
479         extra['_domain_re'] = re.compile(regexp)
480         extra['_has_upper'] = re.compile('[A-Z]')
481
482         super(DomainnameOption, self).__init__(name, doc, default=default,
483                                                default_multi=default_multi,
484                                                callback=callback,
485                                                callback_params=callback_params,
486                                                requires=requires,
487                                                multi=multi,
488                                                validator=validator,
489                                                validator_params=validator_params,
490                                                properties=properties,
491                                                warnings_only=warnings_only,
492                                                extra=extra)
493
494     def _get_len(self, type_):
495         if type_ == 'netbios':
496             return 15
497         else:
498             return 63
499
500     def _validate(self, value, context=undefined, current_opt=undefined):
501         err = self._impl_valid_unicode(value)
502         if err:
503             return err
504
505         def _valid_length(val):
506             if len(val) < 1:
507                 return ValueError(_("invalid length (min 1)"))
508             if len(val) > part_name_length:
509                 return ValueError(_("invalid length (max {0})"
510                                     "").format(part_name_length))
511
512         if self._get_extra('_allow_ip') is True:
513             try:
514                 IP('{0}/32'.format(value))
515                 return
516             except ValueError:
517                 pass
518         else:
519             try:
520                 IP('{0}/32'.format(value))
521             except ValueError:
522                 pass
523             else:
524                 return ValueError(_('must not be an IP'))
525         part_name_length = self._get_len(self._get_extra('_dom_type'))
526         if self._get_extra('_dom_type') == 'domainname':
527             if not self._get_extra('_allow_without_dot') and not "." in value:
528                 return ValueError(_("must have dot"))
529             if len(value) > 255:
530                 return ValueError(_("invalid length (max 255)"))
531             for dom in value.split('.'):
532                 err = _valid_length(dom)
533                 if err:
534                     return err
535         else:
536             return _valid_length(value)
537
538     def _second_level_validation(self, value, warnings_only):
539         if self._get_extra('_has_upper').search(value):
540             return ValueError(_('some characters are uppercase'))
541         if not self._get_extra('_domain_re').search(value):
542             if warnings_only:
543                 return ValueError(_('some characters may cause problems'))
544             else:
545                 return ValueError()
546
547
548 class URLOption(DomainnameOption):
549     __slots__ = tuple()
550     proto_re = re.compile(r'(http|https)://')
551     path_re = re.compile(r"^[A-Za-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
552     _display_name = _('URL')
553
554     def _validate(self, value, context=undefined, current_opt=undefined):
555         err = self._impl_valid_unicode(value)
556         if err:
557             return err
558         match = self.proto_re.search(value)
559         if not match:
560             return ValueError(_('must start with http:// or '
561                                 'https://'))
562         value = value[len(match.group(0)):]
563         # get domain/files
564         splitted = value.split('/', 1)
565         if len(splitted) == 1:
566             domain = value
567             files = None
568         else:
569             domain, files = splitted
570         # if port in domain
571         splitted = domain.split(':', 1)
572         if len(splitted) == 1:
573             domain = splitted[0]
574             port = 0
575         else:
576             domain, port = splitted
577         if not 0 <= int(port) <= 65535:
578             return ValueError(_('port must be an between 0 and '
579                                 '65536'))
580         # validate domainname
581         err = super(URLOption, self)._validate(domain)
582         if err:
583             return err
584         err = super(URLOption, self)._second_level_validation(domain, False)
585         if err:
586             return err
587         # validate file
588         if files is not None and files != '' and not self.path_re.search(files):
589             return ValueError(_('must ends with a valid resource name'))
590
591     def _second_level_validation(self, value, warnings_only):
592         pass
593
594
595 class _RegexpOption(Option):
596     __slots__ = tuple()
597     def _validate(self, value, context=undefined, current_opt=undefined):
598         err = self._impl_valid_unicode(value)
599         if err:
600             return err
601         match = self._regexp.search(value)
602         if not match:
603             return ValueError()
604
605
606 class EmailOption(_RegexpOption):
607     __slots__ = tuple()
608     #https://www.w3.org/TR/html-markup/input.email.html#input.email.attrs.value.single.
609     _regexp = re.compile(r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
610     _display_name = _('email address')
611
612
613 class UsernameOption(_RegexpOption):
614     __slots__ = tuple()
615     #regexp build with 'man 8 adduser' informations
616     _regexp = re.compile(r"^[a-z_][a-z0-9_-]{0,30}[$a-z0-9_-]{0,1}$")
617     _display_name = _('username')
618
619
620 class FilenameOption(_RegexpOption):
621     __slots__ = tuple()
622     _regexp = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
623     _display_name = _('file name')
624
625
626 class DateOption(Option):
627     __slots__ = tuple()
628     _display_name = _('date')
629
630     def _validate(self, value, context=undefined, current_opt=undefined):
631         err = self._impl_valid_unicode(value)
632         if err:
633             return err
634         try:
635             datetime.datetime.strptime(value,"%Y-%m-%d")
636         except ValueError:
637             return ValueError()