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