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