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