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