687a5a8a2c10bb2ac56b827418d187541fdd7e48
[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 tiramisu.setting import log, undefined
27
28 from tiramisu.error import ConfigError, ContextError
29 from tiramisu.i18n import _
30 from .baseoption import Option, validate_callback
31 from tiramisu.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
41     def __init__(self, name, doc, values, default=None,
42                  values_params=None, default_multi=None, requires=None,
43                  multi=False, callback=None, callback_params=None,
44                  validator=None, validator_params=None,
45                  properties=None, warnings_only=False):
46         """
47         :param values: is a list of values the option can possibly take
48         """
49         if isinstance(values, FunctionType):
50             validate_callback(values, values_params, 'values')
51         else:
52             if values_params is not None:
53                 raise ValueError(_('values is not a function, so values_params must be None'))
54             if not isinstance(values, tuple):  # pragma: optional cover
55                 raise TypeError(_('values must be a tuple or a function for {0}'
56                                   ).format(name))
57         self._choice_values = values
58         self._choice_values_params = values_params
59         super(ChoiceOption, self).__init__(name, doc, default=default,
60                                            default_multi=default_multi,
61                                            callback=callback,
62                                            callback_params=callback_params,
63                                            requires=requires,
64                                            multi=multi,
65                                            validator=validator,
66                                            validator_params=validator_params,
67                                            properties=properties,
68                                            warnings_only=warnings_only)
69
70     def impl_get_values(self, context):
71         #FIXME cache? but in context...
72         values = self._choice_values
73         if isinstance(values, FunctionType):
74             values_params = self._choice_values_params
75             if values_params is None:
76                 values_params = {}
77             values = carry_out_calculation(self, config=context,
78                                            callback=values,
79                                            callback_params=values_params)
80             if not isinstance(values, list):  # pragma: optional cover
81                 raise ConfigError(_('calculated values for {0} is not a list'
82                                     '').format(self.impl_getname()))
83         return values
84
85     def _validate(self, value, context=undefined):
86         try:
87             values = self.impl_get_values(context)
88             if not value in values:  # pragma: optional cover
89                 raise ValueError(_('value {0} is not permitted, '
90                                  'only {1} is allowed'
91                                  '').format(value,
92                                             values))
93         except ContextError:  # pragma: optional cover
94             log.debug('ChoiceOption validation, disabled because no context')
95
96
97 class BoolOption(Option):
98     "represents a choice between ``True`` and ``False``"
99     __slots__ = tuple()
100
101     def _validate(self, value, context=undefined):
102         if not isinstance(value, bool):
103             raise ValueError(_('invalid boolean'))  # pragma: optional cover
104
105
106 class IntOption(Option):
107     "represents a choice of an integer"
108     __slots__ = tuple()
109
110     def _validate(self, value, context=undefined):
111         if not isinstance(value, int):
112             raise ValueError(_('invalid integer'))  # pragma: optional cover
113
114
115 class FloatOption(Option):
116     "represents a choice of a floating point number"
117     __slots__ = tuple()
118
119     def _validate(self, value, context=undefined):
120         if not isinstance(value, float):
121             raise ValueError(_('invalid float'))  # pragma: optional cover
122
123
124 class StrOption(Option):
125     "represents the choice of a string"
126     __slots__ = tuple()
127
128     def _validate(self, value, context=undefined):
129         if not isinstance(value, str):
130             raise ValueError(_('invalid string'))  # pragma: optional cover
131
132
133 if sys.version_info[0] >= 3:  # pragma: optional cover
134     #UnicodeOption is same as StrOption in python 3+
135     class UnicodeOption(StrOption):
136         __slots__ = tuple()
137         pass
138 else:
139     class UnicodeOption(Option):
140         "represents the choice of a unicode string"
141         __slots__ = tuple()
142         _empty = u''
143
144         def _validate(self, value, context=undefined):
145             if not isinstance(value, unicode):
146                 raise ValueError(_('invalid unicode'))  # pragma: optional cover
147
148
149 class IPOption(Option):
150     "represents the choice of an ip"
151     __slots__ = tuple()
152
153     def __init__(self, name, doc, default=None, default_multi=None,
154                  requires=None, multi=False, callback=None,
155                  callback_params=None, validator=None, validator_params=None,
156                  properties=None, private_only=False, allow_reserved=False,
157                  warnings_only=False):
158         extra = {'_private_only': private_only,
159                  '_allow_reserved': allow_reserved}
160         super(IPOption, self).__init__(name, doc, default=default,
161                                        default_multi=default_multi,
162                                        callback=callback,
163                                        callback_params=callback_params,
164                                        requires=requires,
165                                        multi=multi,
166                                        validator=validator,
167                                        validator_params=validator_params,
168                                        properties=properties,
169                                        warnings_only=warnings_only,
170                                        extra=extra)
171
172     def _validate(self, value, context=undefined):
173         # sometimes an ip term starts with a zero
174         # but this does not fit in some case, for example bind does not like it
175         try:
176             for val in value.split('.'):
177                 if val.startswith("0") and len(val) > 1:
178                     raise ValueError(_('invalid IP'))  # pragma: optional cover
179         except AttributeError:  # pragma: optional cover
180             #if integer for example
181             raise ValueError(_('invalid IP'))
182         # 'standard' validation
183         try:
184             IP('{0}/32'.format(value))
185         except ValueError:  # pragma: optional cover
186             raise ValueError(_('invalid IP'))
187
188     def _second_level_validation(self, value, warnings_only):
189         ip = IP('{0}/32'.format(value))
190         if not self._get_extra('_allow_reserved') and ip.iptype() == 'RESERVED':  # pragma: optional cover
191             if warnings_only:
192                 msg = _("IP is in reserved class")
193             else:
194                 msg = _("invalid IP, mustn't be in reserved class")
195             raise ValueError(msg)
196         if self._get_extra('_private_only') and not ip.iptype() == 'PRIVATE':  # pragma: optional cover
197             if warnings_only:
198                 msg = _("IP is not in private class")
199             else:
200                 msg = _("invalid IP, must be in private class")
201             raise ValueError(msg)
202
203     def _cons_in_network(self, opts, vals, warnings_only):
204         if len(vals) != 3:
205             raise ConfigError(_('invalid len for vals'))  # pragma: optional cover
206         if None in vals:
207             return
208         ip, network, netmask = vals
209         if IP(ip) not in IP('{0}/{1}'.format(network, netmask)):  # pragma: optional cover
210             if warnings_only:
211                 msg = _('IP {0} ({1}) not in network {2} ({3}) with netmask {4}'
212                         ' ({5})')
213             else:
214                 msg = _('invalid IP {0} ({1}) not in network {2} ({3}) with '
215                         'netmask {4} ({5})')
216             raise ValueError(msg.format(ip, opts[0].impl_getname(), network,
217                              opts[1].impl_getname(), netmask, opts[2].impl_getname()))
218
219
220 class PortOption(Option):
221     """represents the choice of a port
222     The port numbers are divided into three ranges:
223     the well-known ports,
224     the registered ports,
225     and the dynamic or private ports.
226     You can actived this three range.
227     Port number 0 is reserved and can't be used.
228     see: http://en.wikipedia.org/wiki/Port_numbers
229     """
230     __slots__ = tuple()
231
232     def __init__(self, name, doc, default=None, default_multi=None,
233                  requires=None, multi=False, callback=None,
234                  callback_params=None, validator=None, validator_params=None,
235                  properties=None, allow_range=False, allow_zero=False,
236                  allow_wellknown=True, allow_registred=True,
237                  allow_private=False, warnings_only=False):
238         extra = {'_allow_range': allow_range,
239                  '_min_value': None,
240                  '_max_value': None}
241         ports_min = [0, 1, 1024, 49152]
242         ports_max = [0, 1023, 49151, 65535]
243         is_finally = False
244         for index, allowed in enumerate([allow_zero,
245                                          allow_wellknown,
246                                          allow_registred,
247                                          allow_private]):
248             if extra['_min_value'] is None:
249                 if allowed:
250                     extra['_min_value'] = ports_min[index]
251             elif not allowed:
252                 is_finally = True
253             elif allowed and is_finally:
254                 raise ValueError(_('inconsistency in allowed range'))  # pragma: optional cover
255             if allowed:
256                 extra['_max_value'] = ports_max[index]
257
258         if extra['_max_value'] is None:
259             raise ValueError(_('max value is empty'))  # pragma: optional cover
260
261         super(PortOption, self).__init__(name, doc, default=default,
262                                          default_multi=default_multi,
263                                          callback=callback,
264                                          callback_params=callback_params,
265                                          requires=requires,
266                                          multi=multi,
267                                          validator=validator,
268                                          validator_params=validator_params,
269                                          properties=properties,
270                                          warnings_only=warnings_only,
271                                          extra=extra)
272
273     def _validate(self, value, context=undefined):
274         if self._get_extra('_allow_range') and ":" in str(value):  # pragma: optional cover
275             value = str(value).split(':')
276             if len(value) != 2:
277                 raise ValueError(_('invalid port, range must have two values '
278                                  'only'))
279             if not value[0] < value[1]:
280                 raise ValueError(_('invalid port, first port in range must be'
281                                  ' smaller than the second one'))
282         else:
283             value = [value]
284
285         for val in value:
286             try:
287                 val = int(val)
288             except ValueError:  # pragma: optional cover
289                 raise ValueError(_('invalid port'))
290             if not self._get_extra('_min_value') <= val <= self._get_extra('_max_value'):  # pragma: optional cover
291                 raise ValueError(_('invalid port, must be an between {0} '
292                                    'and {1}').format(self._get_extra('_min_value'),
293                                                      self._get_extra('_max_value')))
294
295
296 class NetworkOption(Option):
297     "represents the choice of a network"
298     __slots__ = tuple()
299
300     def _validate(self, value, context=undefined):
301         try:
302             IP(value)
303         except ValueError:  # pragma: optional cover
304             raise ValueError(_('invalid network address'))
305
306     def _second_level_validation(self, value, warnings_only):
307         ip = IP(value)
308         if ip.iptype() == 'RESERVED':  # pragma: optional cover
309             if warnings_only:
310                 msg = _("network address is in reserved class")
311             else:
312                 msg = _("invalid network address, mustn't be in reserved class")
313             raise ValueError(msg)
314
315
316 class NetmaskOption(Option):
317     "represents the choice of a netmask"
318     __slots__ = tuple()
319
320     def _validate(self, value, context=undefined):
321         try:
322             IP('0.0.0.0/{0}'.format(value))
323         except ValueError:  # pragma: optional cover
324             raise ValueError(_('invalid netmask address'))
325
326     def _cons_network_netmask(self, opts, vals, warnings_only):
327         #opts must be (netmask, network) options
328         if None in vals:
329             return
330         self.__cons_netmask(opts, vals[0], vals[1], False, warnings_only)
331
332     def _cons_ip_netmask(self, opts, vals, warnings_only):
333         #opts must be (netmask, ip) options
334         if None in vals:
335             return
336         self.__cons_netmask(opts, vals[0], vals[1], True, warnings_only)
337
338     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net,
339                        warnings_only):
340         if len(opts) != 2:
341             raise ConfigError(_('invalid len for opts'))  # pragma: optional cover
342         msg = None
343         try:
344             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
345                     make_net=make_net)
346             #if cidr == 32, ip same has network
347             if ip.prefixlen() != 32:
348                 try:
349                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
350                         make_net=not make_net)
351                 except ValueError:
352                     pass
353                 else:
354                     if make_net:  # pragma: optional cover
355                         msg = _("invalid IP {0} ({1}) with netmask {2},"
356                                 " this IP is a network")
357
358         except ValueError:  # pragma: optional cover
359             if not make_net:
360                 msg = _('invalid network {0} ({1}) with netmask {2}')
361         if msg is not None:  # pragma: optional cover
362             raise ValueError(msg.format(val_ipnetwork, opts[1].impl_getname(),
363                                         val_netmask))
364
365
366 class BroadcastOption(Option):
367     __slots__ = tuple()
368
369     def _validate(self, value, context=undefined):
370         try:
371             IP('{0}/32'.format(value))
372         except ValueError:  # pragma: optional cover
373             raise ValueError(_('invalid broadcast address'))
374
375     def _cons_broadcast(self, opts, vals, warnings_only):
376         if len(vals) != 3:
377             raise ConfigError(_('invalid len for vals'))  # pragma: optional cover
378         if None in vals:
379             return
380         broadcast, network, netmask = vals
381         if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
382             raise ValueError(_('invalid broadcast {0} ({1}) with network {2} '
383                                '({3}) and netmask {4} ({5})').format(
384                                    broadcast, opts[0].impl_getname(), network,
385                                    opts[1].impl_getname(), netmask, opts[2].impl_getname()))  # pragma: optional cover
386
387
388 class DomainnameOption(Option):
389     """represents the choice of a domain name
390     netbios: for MS domain
391     hostname: to identify the device
392     domainname:
393     fqdn: with tld, not supported yet
394     """
395     __slots__ = tuple()
396
397     def __init__(self, name, doc, default=None, default_multi=None,
398                  requires=None, multi=False, callback=None,
399                  callback_params=None, validator=None, validator_params=None,
400                  properties=None, allow_ip=False, type_='domainname',
401                  warnings_only=False, allow_without_dot=False):
402         if type_ not in ['netbios', 'hostname', 'domainname']:
403             raise ValueError(_('unknown type_ {0} for hostname').format(type_))  # pragma: optional cover
404         extra = {'_dom_type': type_}
405         if allow_ip not in [True, False]:
406             raise ValueError(_('allow_ip must be a boolean'))  # pragma: optional cover
407         if allow_without_dot not in [True, False]:
408             raise ValueError(_('allow_without_dot must be a boolean'))  # pragma: optional cover
409         extra['_allow_ip'] = allow_ip
410         extra['_allow_without_dot'] = allow_without_dot
411         end = ''
412         extrachar = ''
413         extrachar_mandatory = ''
414         if extra['_dom_type'] != 'netbios':
415             allow_number = '\d'
416         else:
417             allow_number = ''  # pragma: optional cover
418         if extra['_dom_type'] == 'netbios':
419             length = 14  # pragma: optional cover
420         elif extra['_dom_type'] == 'hostname':
421             length = 62  # pragma: optional cover
422         elif extra['_dom_type'] == 'domainname':
423             length = 62
424             if allow_without_dot is False:
425                 extrachar_mandatory = '\.'
426             else:
427                 extrachar = '\.'  # pragma: optional cover
428             end = '+[a-z]*'
429         extra['_domain_re'] = re.compile(r'^(?:[a-z{0}][a-z\d\-{1}]{{,{2}}}{3}){4}$'
430                                          ''.format(allow_number, extrachar, length,
431                                                    extrachar_mandatory, end))
432         super(DomainnameOption, self).__init__(name, doc, default=default,
433                                                default_multi=default_multi,
434                                                callback=callback,
435                                                callback_params=callback_params,
436                                                requires=requires,
437                                                multi=multi,
438                                                validator=validator,
439                                                validator_params=validator_params,
440                                                properties=properties,
441                                                warnings_only=warnings_only,
442                                                extra=extra)
443
444     def _validate(self, value, context=undefined):
445         if self._get_extra('_allow_ip') is True:  # pragma: optional cover
446             try:
447                 IP('{0}/32'.format(value))
448                 return
449             except ValueError:
450                 pass
451         if self._get_extra('_dom_type') == 'domainname' and \
452                 not self._get_extra('_allow_without_dot') and \
453                 '.' not in value:  # pragma: optional cover
454             raise ValueError(_("invalid domainname, must have dot"))
455         if len(value) > 255:
456             raise ValueError(_("invalid domainname's length (max 255)"))  # pragma: optional cover
457         if len(value) < 2:
458             raise ValueError(_("invalid domainname's length (min 2)"))  # pragma: optional cover
459         if not self._get_extra('_domain_re').search(value):
460             raise ValueError(_('invalid domainname'))  # pragma: optional cover
461
462
463 class EmailOption(DomainnameOption):
464     __slots__ = tuple()
465     username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
466
467     def _validate(self, value, context=undefined):
468         splitted = value.split('@', 1)
469         try:
470             username, domain = splitted
471         except ValueError:  # pragma: optional cover
472             raise ValueError(_('invalid email address, must contains one @'
473                                ))
474         if not self.username_re.search(username):
475             raise ValueError(_('invalid username in email address'))  # pragma: optional cover
476         super(EmailOption, self)._validate(domain)
477
478
479 class URLOption(DomainnameOption):
480     __slots__ = tuple()
481     proto_re = re.compile(r'(http|https)://')
482     path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
483
484     def _validate(self, value, context=undefined):
485         match = self.proto_re.search(value)
486         if not match:  # pragma: optional cover
487             raise ValueError(_('invalid url, must start with http:// or '
488                                'https://'))
489         value = value[len(match.group(0)):]
490         # get domain/files
491         splitted = value.split('/', 1)
492         try:
493             domain, files = splitted
494         except ValueError:
495             domain = value
496             files = None
497         # if port in domain
498         splitted = domain.split(':', 1)
499         try:
500             domain, port = splitted
501
502         except ValueError:  # pragma: optional cover
503             domain = splitted[0]
504             port = 0
505         if not 0 <= int(port) <= 65535:
506             raise ValueError(_('invalid url, port must be an between 0 and '
507                                '65536'))  # pragma: optional cover
508         # validate domainname
509         super(URLOption, self)._validate(domain)
510         # validate file
511         if files is not None and files != '' and not self.path_re.search(files):
512             raise ValueError(_('invalid url, must ends with filename'))  # pragma: optional cover
513
514
515 class UsernameOption(Option):
516     __slots__ = tuple()
517     #regexp build with 'man 8 adduser' informations
518     username_re = re.compile(r"^[a-z_][a-z0-9_-]{0,30}[$a-z0-9_-]{0,1}$")
519
520     def _validate(self, value, context=undefined):
521         match = self.username_re.search(value)
522         if not match:
523             raise ValueError(_('invalid username'))  # pragma: optional cover
524
525
526 class FilenameOption(Option):
527     __slots__ = tuple()
528     path_re = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
529
530     def _validate(self, value, context=undefined):
531         match = self.path_re.search(value)
532         if not match:
533             raise ValueError(_('invalid filename'))  # pragma: optional cover