in_network's consistency now verify that IP is not network or broadcast's IP + ip_net...
[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         # test if ip is not network/broadcast IP
219         opts[2]._cons_ip_netmask((opts[2], opts[0]), (netmask, ip), warnings_only)
220
221
222 class PortOption(Option):
223     """represents the choice of a port
224     The port numbers are divided into three ranges:
225     the well-known ports,
226     the registered ports,
227     and the dynamic or private ports.
228     You can actived this three range.
229     Port number 0 is reserved and can't be used.
230     see: http://en.wikipedia.org/wiki/Port_numbers
231     """
232     __slots__ = tuple()
233
234     def __init__(self, name, doc, default=None, default_multi=None,
235                  requires=None, multi=False, callback=None,
236                  callback_params=None, validator=None, validator_params=None,
237                  properties=None, allow_range=False, allow_zero=False,
238                  allow_wellknown=True, allow_registred=True,
239                  allow_private=False, warnings_only=False):
240         extra = {'_allow_range': allow_range,
241                  '_min_value': None,
242                  '_max_value': None}
243         ports_min = [0, 1, 1024, 49152]
244         ports_max = [0, 1023, 49151, 65535]
245         is_finally = False
246         for index, allowed in enumerate([allow_zero,
247                                          allow_wellknown,
248                                          allow_registred,
249                                          allow_private]):
250             if extra['_min_value'] is None:
251                 if allowed:
252                     extra['_min_value'] = ports_min[index]
253             elif not allowed:
254                 is_finally = True
255             elif allowed and is_finally:
256                 raise ValueError(_('inconsistency in allowed range'))  # pragma: optional cover
257             if allowed:
258                 extra['_max_value'] = ports_max[index]
259
260         if extra['_max_value'] is None:
261             raise ValueError(_('max value is empty'))  # pragma: optional cover
262
263         super(PortOption, self).__init__(name, doc, default=default,
264                                          default_multi=default_multi,
265                                          callback=callback,
266                                          callback_params=callback_params,
267                                          requires=requires,
268                                          multi=multi,
269                                          validator=validator,
270                                          validator_params=validator_params,
271                                          properties=properties,
272                                          warnings_only=warnings_only,
273                                          extra=extra)
274
275     def _validate(self, value, context=undefined):
276         if self._get_extra('_allow_range') and ":" in str(value):  # pragma: optional cover
277             value = str(value).split(':')
278             if len(value) != 2:
279                 raise ValueError(_('invalid port, range must have two values '
280                                  'only'))
281             if not value[0] < value[1]:
282                 raise ValueError(_('invalid port, first port in range must be'
283                                  ' smaller than the second one'))
284         else:
285             value = [value]
286
287         for val in value:
288             try:
289                 val = int(val)
290             except ValueError:  # pragma: optional cover
291                 raise ValueError(_('invalid port'))
292             if not self._get_extra('_min_value') <= val <= self._get_extra('_max_value'):  # pragma: optional cover
293                 raise ValueError(_('invalid port, must be an between {0} '
294                                    'and {1}').format(self._get_extra('_min_value'),
295                                                      self._get_extra('_max_value')))
296
297
298 class NetworkOption(Option):
299     "represents the choice of a network"
300     __slots__ = tuple()
301
302     def _validate(self, value, context=undefined):
303         try:
304             IP(value)
305         except ValueError:  # pragma: optional cover
306             raise ValueError(_('invalid network address'))
307
308     def _second_level_validation(self, value, warnings_only):
309         ip = IP(value)
310         if ip.iptype() == 'RESERVED':  # pragma: optional cover
311             if warnings_only:
312                 msg = _("network address is in reserved class")
313             else:
314                 msg = _("invalid network address, mustn't be in reserved class")
315             raise ValueError(msg)
316
317
318 class NetmaskOption(Option):
319     "represents the choice of a netmask"
320     __slots__ = tuple()
321
322     def _validate(self, value, context=undefined):
323         try:
324             IP('0.0.0.0/{0}'.format(value))
325         except ValueError:  # pragma: optional cover
326             raise ValueError(_('invalid netmask address'))
327
328     def _cons_network_netmask(self, opts, vals, warnings_only):
329         #opts must be (netmask, network) options
330         if None in vals:
331             return
332         self.__cons_netmask(opts, vals[0], vals[1], False, warnings_only)
333
334     def _cons_ip_netmask(self, opts, vals, warnings_only):
335         #opts must be (netmask, ip) options
336         if None in vals:
337             return
338         self.__cons_netmask(opts, vals[0], vals[1], True, warnings_only)
339
340     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net,
341                        warnings_only):
342         if len(opts) != 2:
343             raise ConfigError(_('invalid len for opts'))  # pragma: optional cover
344         msg = None
345         try:
346             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
347                     make_net=make_net)
348             #if cidr == 32, ip same has network
349             if make_net and ip.prefixlen() != 32:
350                 val_ip = IP(val_ipnetwork)
351                 if ip.net() == val_ip:
352                     msg = _("invalid IP {0} ({1}) with netmask {2},"
353                             " this IP is a network")
354                 if ip.broadcast() == val_ip:
355                     msg = _("invalid IP {0} ({1}) with netmask {2},"
356                             " this IP is a broadcast")
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         extra['_domain_re'] = re.compile(r'^[a-z\d][a-z\d\-]*$')
412
413         super(DomainnameOption, self).__init__(name, doc, default=default,
414                                                default_multi=default_multi,
415                                                callback=callback,
416                                                callback_params=callback_params,
417                                                requires=requires,
418                                                multi=multi,
419                                                validator=validator,
420                                                validator_params=validator_params,
421                                                properties=properties,
422                                                warnings_only=warnings_only,
423                                                extra=extra)
424
425     def _validate(self, value, context=undefined):
426         def _valid_length(val):
427             if len(val) < 2:
428                 raise ValueError(_("invalid domainname's length (min 2)"))
429             if len(val) > part_name_length:
430                 raise ValueError(_("invalid domainname's length (max {0})"
431                                    "").format(part_name_length))
432
433         if self._get_extra('_allow_ip') is True:  # pragma: optional cover
434             try:
435                 IP('{0}/32'.format(value))
436                 return
437             except ValueError:
438                 pass
439         if self._get_extra('_dom_type') == 'netbios':
440             part_name_length = 15
441         else:
442             part_name_length = 63
443         if self._get_extra('_dom_type') == 'domainname':
444             if not self._get_extra('_allow_without_dot') and not "." in value:
445                 raise ValueError(_("invalid domainname, must have dot"))
446             if len(value) > 255:
447                 raise ValueError(_("invalid domainname's length (max 255)"))
448             for dom in value.split('.'):
449                 _valid_length(dom)
450         else:
451             _valid_length(value)
452
453     def _second_level_validation(self, value, warnings_only):
454         def _valid_char(val):
455             if not self._get_extra('_domain_re').search(val):
456                 if warnings_only:
457                     raise ValueError(_('same characters may cause problems'))
458                 else:
459                     raise ValueError(_('invalid domainname'))
460         #not for IP
461         if self._get_extra('_allow_ip') is True:
462             try:
463                 IP('{0}/32'.format(value))
464                 return
465             except ValueError:
466                 pass
467         if self._get_extra('_dom_type') == 'domainname':
468             for dom in value.split('.'):
469                 _valid_char(dom)
470         else:
471             _valid_char(value)
472
473
474 class EmailOption(DomainnameOption):
475     __slots__ = tuple()
476     username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
477
478     def _validate(self, value, context=undefined):
479         splitted = value.split('@', 1)
480         try:
481             username, domain = splitted
482         except ValueError:  # pragma: optional cover
483             raise ValueError(_('invalid email address, must contains one @'
484                                ))
485         if not self.username_re.search(username):
486             raise ValueError(_('invalid username in email address'))  # pragma: optional cover
487         super(EmailOption, self)._validate(domain)
488         super(EmailOption, self)._second_level_validation(domain, False)
489
490     def _second_level_validation(self, value, warnings_only):
491         pass
492
493
494 class URLOption(DomainnameOption):
495     __slots__ = tuple()
496     proto_re = re.compile(r'(http|https)://')
497     path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
498
499     def _validate(self, value, context=undefined):
500         match = self.proto_re.search(value)
501         if not match:  # pragma: optional cover
502             raise ValueError(_('invalid url, must start with http:// or '
503                                'https://'))
504         value = value[len(match.group(0)):]
505         # get domain/files
506         splitted = value.split('/', 1)
507         try:
508             domain, files = splitted
509         except ValueError:
510             domain = value
511             files = None
512         # if port in domain
513         splitted = domain.split(':', 1)
514         try:
515             domain, port = splitted
516
517         except ValueError:  # pragma: optional cover
518             domain = splitted[0]
519             port = 0
520         if not 0 <= int(port) <= 65535:
521             raise ValueError(_('invalid url, port must be an between 0 and '
522                                '65536'))  # pragma: optional cover
523         # validate domainname
524         super(URLOption, self)._validate(domain)
525         super(URLOption, self)._second_level_validation(domain, False)
526         # validate file
527         if files is not None and files != '' and not self.path_re.search(files):
528             raise ValueError(_('invalid url, must ends with filename'))  # pragma: optional cover
529
530     def _second_level_validation(self, value, warnings_only):
531         pass
532
533
534 class UsernameOption(Option):
535     __slots__ = tuple()
536     #regexp build with 'man 8 adduser' informations
537     username_re = re.compile(r"^[a-z_][a-z0-9_-]{0,30}[$a-z0-9_-]{0,1}$")
538
539     def _validate(self, value, context=undefined):
540         match = self.username_re.search(value)
541         if not match:
542             raise ValueError(_('invalid username'))  # pragma: optional cover
543
544
545 class FilenameOption(Option):
546     __slots__ = tuple()
547     path_re = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
548
549     def _validate(self, value, context=undefined):
550         match = self.path_re.search(value)
551         if not match:
552             raise ValueError(_('invalid filename'))  # pragma: optional cover