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