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