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