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