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