Compare commits

...

8 commits

16 changed files with 269 additions and 42 deletions

View file

@ -42,7 +42,6 @@ def calc_disk_usage(path, size='bytes'):
# do not calc if path is None
if path is None:
return None
if size == 'bytes':
div = 1
else:
@ -60,12 +59,13 @@ usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
Finally add those options in option description and a Config:
```
```python
disk = OptionDescription('disk', 'Verify disk usage', [filename, usage])
root = OptionDescription('root', 'root', [disk])
async def main():
config = await Config(root)
await config.property.read_write()
return config
config = run(main())
```
@ -109,26 +109,29 @@ returns:
When you enter a value it is validated:
>>> try:
>>> config.option('disk.path').value.set('/unknown')
>>> except ValueError as err:
>>> print(err)
```python
async def main():
try:
await config.option('disk.path').value.set('/unknown')
except ValueError as err:
print(err)
run(main())
```
returns:
```
"/unknown" is an invalid file name for "Path", this directory does not exist
We can also set a :doc:`calculation` as value. For example, we want to launch previous function but with in_gb to True as second argument:
>>> calc = Calculation(calc_disk_usage, Params((ParamOption(filename),
... ParamValue('gigabytes'))))
>>> config.option('disk.usage').value.set(calc)
>>> config.option('disk.usage').value.get()
622.6080360412598
```
#### Is value is valid?
To check is a value is valid:
>>> config.option('disk.path').value.valid()
True
```python
await config.option('disk.path').value.valid()
```
#### Display the default value

View file

@ -471,6 +471,22 @@ from tiramisu import FilenameOption
FilenameOption('file', 'file', '/etc/tiramisu/tiramisu.conf')
```
## Unix file permissions: PermissionsOption
Valid the representing Unix permissions is an octal (base-8) notation.
```python
from tiramisu import PermissionsOption
PermissionsOption('perms', 'perms', 755)
PermissionsOption('perms', 'perms', 1755)
```
This option doesn't allow (or display a warning with warnings_only):
- 777 (two weak value)
- others have more right than group
- group has more right than user
# Date option
## Date option: DateOption

View file

@ -436,6 +436,7 @@ async def test_config_od_type(config_type):
o2 = OptionDescription('val', '', [o])
async with await Config(o2) as cfg:
cfg = await get_config(cfg, config_type)
assert await cfg.option('val').option.type() == 'optiondescription'
assert await cfg.option('val.i').option.type() == 'integer'
assert not await list_sessions()

View file

@ -881,6 +881,51 @@ async def test_requires_dyndescription_in_dyn():
assert not await list_sessions()
def calc_value_not_same(param, condition, expected, default, suffix):
if suffix == 'val1':
index = 0
else:
index = 1
return calc_value(param, condition=condition[index], expected=expected, default=default)
@pytest.mark.asyncio
async def test_requires_dyndescription_in_dyn_not_same():
boolean = BoolOption('boolean', '', True)
disabled_property = Calculation(calc_value_not_same,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(boolean, raisepropertyerror=True),
'expected': ParamValue(False),
'default': ParamValue(None),
'suffix': ParamSuffix()}))
st = StrOption('st', '', properties=(disabled_property,))
dod1 = DynOptionDescription('dod1', '', [boolean], suffixes=Calculation(return_list))
dod2 = DynOptionDescription('dod2', '', [st], suffixes=Calculation(return_list))
od = OptionDescription('od', '', [dod1, dod2])
od2 = OptionDescription('od', '', [od])
async with await Config(od2) as cfg:
await cfg.property.read_write()
assert await cfg.option('od.dod2val1.stval1').value.get() is None
assert await cfg.option('od.dod2val2.stval2').value.get() is None
#
await cfg.option('od.dod1val1.booleanval1').value.set(False)
props = []
try:
await cfg.option('od.dod2val1.stval1').value.get()
except PropertiesOptionError as err:
props = err.proptype
assert props == frozenset(['disabled'])
props = []
await cfg.option('od.dod2val2.stval2').value.get()
#
await cfg.option('od.dod1val1.booleanval1').value.set(True)
assert await cfg.option('od.dod2val1.stval1').value.get() is None
assert await cfg.option('od.dod2val2.stval2').value.get() is None
assert not await list_sessions()
@pytest.mark.asyncio
async def test_requires_dyndescription2():
boolean = BoolOption('boolean', '', True)

View file

@ -1074,3 +1074,16 @@ async def test_follower_properties():
await cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).property.get() == ('aproperty', 'newproperty', 'newproperty1')
await cfg.option('ip_admin_eth0.netmask_admin_eth0', 1).property.get() == ('aproperty', 'newproperty1')
assert not await list_sessions()
@pytest.mark.asyncio
async def test_api_get_leader(config_type):
ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True)
interface1 = Leadership('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
maconfig = OptionDescription('conf', '', [interface1])
async with await Config(maconfig) as cfg:
option = await cfg.option('ip_admin_eth0.netmask_admin_eth0').option.leader()
assert await option.option.get() == ip_admin_eth0
assert not await list_sessions()

View file

@ -1514,6 +1514,18 @@ async def test_calc_value_remove_duplicate(config_type):
assert not await list_sessions()
@pytest.mark.asyncio
async def test_calc_value_remove_duplicate2(config_type):
val1 = StrOption('val1', "", ['val1', 'val1'], multi=True, properties=('notunique',))
val2 = StrOption('val2', "", ['val1', 'val1'], multi=True, properties=('notunique',))
val3 = StrOption('val3', "", Calculation(calc_value, Params((ParamOption(val1), ParamOption(val2)), multi=ParamValue(True), remove_duplicate_value=ParamValue(True), join=ParamValue('-'))), multi=True)
od = OptionDescription('root', '', [val1, val2, val3])
async with await Config(od) as cfg:
cfg = await get_config(cfg, config_type)
assert await cfg.value.dict() == {'val1': ['val1', 'val1'], 'val2': ['val1', 'val1'], 'val3': ['val1-val1']}
assert not await list_sessions()
@pytest.mark.asyncio
async def test_calc_value_join(config_type):
val1 = StrOption('val1', "", 'val1')

View file

@ -0,0 +1,36 @@
"configuration objects global API"
from .autopath import do_autopath
do_autopath()
import pytest
from tiramisu import PermissionsOption
def test_permissions():
PermissionsOption('a', '', 640)
PermissionsOption('a', '', 642)
PermissionsOption('a', '', 751)
PermissionsOption('a', '', 753)
PermissionsOption('a', '', 7555)
PermissionsOption('a', '', 1755)
with pytest.raises(ValueError):
PermissionsOption('a', '', 800)
with pytest.raises(ValueError):
PermissionsOption('a', '', 75)
with pytest.raises(ValueError):
PermissionsOption('a', '', 77775)
with pytest.raises(ValueError):
PermissionsOption('a', '', '755')
with pytest.raises(ValueError):
PermissionsOption('a', '', 'string')
with pytest.raises(ValueError):
PermissionsOption('a', '', 800)
with pytest.raises(ValueError):
PermissionsOption('a', '', 1575)
with pytest.raises(ValueError):
PermissionsOption('a', '', 1557)
with pytest.raises(ValueError):
PermissionsOption('a', '', 777)
with pytest.raises(ValueError):
PermissionsOption('a', '', 1777)

View file

@ -407,6 +407,8 @@ class TiramisuOptionOption(_TiramisuOptionOptionDescription):
@option_and_connection
async def type(self):
if self._option_bag.option.impl_is_optiondescription():
return 'optiondescription'
return self._option_bag.option.get_type()
@option_and_connection
@ -437,6 +439,12 @@ class TiramisuOptionOption(_TiramisuOptionOptionDescription):
self._option_bag.config_bag,
)
@option_and_connection
async def leader(self):
return TiramisuOption(self._option_bag.option.impl_get_leadership().get_leader().impl_getpath(),
None,
self._option_bag.config_bag)
class TiramisuOptionOwner(CommonTiramisuOption):
#FIXME optiondescription must not have Owner!
@ -699,7 +707,8 @@ class TiramisuOptionValue(CommonTiramisuOption):
flatten=False,
withwarning: bool=False,
fullpath=False,
leader_to_list=False):
leader_to_list=False,
):
"""Dict with path as key and value"""
name = self._option_bag.option.impl_getname()
subconfig = await self._subconfig.get_subconfig(self._option_bag)
@ -768,6 +777,7 @@ class TiramisuOptionValue(CommonTiramisuOption):
option = self._option_bag.option
values = self._option_bag.config_bag.context.cfgimpl_get_values()
if option.impl_is_follower() and self._option_bag.index is None:
# IF OU PAS IF ?? if self._option_bag.option.impl_is_symlinkoption():
value = []
length = await self._subconfig.cfgimpl_get_length_leadership(self._option_bag)
settings = self._option_bag.config_bag.context.cfgimpl_get_settings()
@ -779,6 +789,7 @@ class TiramisuOptionValue(CommonTiramisuOption):
soption_bag.properties = await settings.getproperties(soption_bag)
value.append(await values.getdefaultvalue(soption_bag))
return value
# raise APIError('index must be set with a follower option')
else:
return await values.getdefaultvalue(self._option_bag)

View file

@ -90,6 +90,7 @@ class ParamDynOption(ParamOption):
dynoptiondescription: 'DynOptionDescription',
notraisepropertyerror: bool=False,
raisepropertyerror: bool=False,
optional: bool=False,
todict: bool=False,
) -> None:
super().__init__(option,
@ -99,6 +100,7 @@ class ParamDynOption(ParamOption):
)
self.suffix = suffix
self.dynoptiondescription = dynoptiondescription
self.optional = optional
class ParamSelfOption(Param):
@ -273,13 +275,19 @@ async def manager_callback(callbk: Param,
except PropertiesOptionError as err:
# raise PropertiesOptionError (which is catched) because must not add value None in carry_out_calculation
if callbk.notraisepropertyerror or callbk.raisepropertyerror:
raise err
raise err from err
raise ConfigError(_('unable to carry out a calculation for "{}"'
', {}').format(option.impl_get_display_name(), err), err)
', {}').format(option.impl_get_display_name(), err), err) from err
except ValueError as err:
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option_bag.option.impl_get_display_name(), err))
raise ValueError(_('the option "{0}" is used in a calculation but is invalid ({1})').format(option_bag.option.impl_get_display_name(), err)) from err
except AttributeError as err:
raise ConfigError(_(f'unable to get value for calculating "{option_bag.option.impl_get_display_name()}", {err}'))
if isinstance(callbk, ParamDynOption) and callbk.optional:
# cannot acces, simulate a propertyerror
raise PropertiesOptionError(option_bag,
['configerror'],
config_bag.context.cfgimpl_get_settings(),
)
raise ConfigError(_(f'unable to get value for calculating "{option_bag.option.impl_get_display_name()}", {err}')) from err
return value
async def get_option_bag(config_bag,
@ -351,6 +359,7 @@ async def manager_callback(callbk: Param,
callbk_option = callbk.option
callbk_options = None
if callbk_option.issubdyn():
found = False
if isinstance(callbk, ParamDynOption):
subdyn = callbk.dynoptiondescription
rootpath = subdyn.impl_getpath() + callbk.suffix
@ -358,7 +367,22 @@ async def manager_callback(callbk: Param,
callbk_option = callbk_option.to_dynoption(rootpath,
suffix,
subdyn)
elif not option.impl_is_dynsymlinkoption():
found = True
elif option.impl_is_dynsymlinkoption():
rootpath = option.rootpath
call_path = callbk_option.impl_getpath()
if call_path.startswith(option.opt.impl_getpath().rsplit('.', 1)[0]):
# in same dynoption
if len(callbk_option.impl_getpath().split('.')) == len(rootpath.split('.')):
rootpath = rootpath.rsplit('.', 1)[0]
suffix = option.impl_getsuffix()
subdyn = callbk_option.getsubdyn()
callbk_option = callbk_option.to_dynoption(rootpath,
suffix,
subdyn,
)
found = True
if not found:
callbk_options = []
dynopt = callbk_option.getsubdyn()
rootpath = dynopt.impl_getpath()
@ -370,16 +394,6 @@ async def manager_callback(callbk: Param,
suffix,
dynopt)
callbk_options.append(doption)
else:
#FIXME in same dynamic option?
rootpath = option.rootpath
if len(callbk_option.impl_getpath().split('.')) == len(rootpath.split('.')):
rootpath = rootpath.rsplit('.', 1)[0]
suffix = option.impl_getsuffix()
subdyn = callbk_option.getsubdyn()
callbk_option = callbk_option.to_dynoption(rootpath,
suffix,
subdyn)
if leadership_must_have_index and callbk_option.impl_is_follower() and index is None:
raise Break()
if config_bag is undefined:

View file

@ -361,7 +361,7 @@ class CalcValue:
value = []
elif None in value and not allow_none:
value = []
elif remove_duplicate_value:
if remove_duplicate_value:
new_value = []
for val in value:
if val not in new_value:

View file

@ -24,6 +24,7 @@ from .dateoption import DateOption
from .filenameoption import FilenameOption
from .passwordoption import PasswordOption
from .macoption import MACOption
from .permissionsoption import PermissionsOption
__all__ = ('Leadership', 'OptionDescription', 'DynOptionDescription',
@ -33,4 +34,4 @@ __all__ = ('Leadership', 'OptionDescription', 'DynOptionDescription',
'IPOption', 'PortOption', 'NetworkOption', 'NetmaskOption',
'BroadcastOption', 'DomainnameOption', 'EmailOption', 'URLOption',
'UsernameOption', 'GroupnameOption', 'FilenameOption', 'PasswordOption', 'submulti',
'RegexpOption', 'MACOption')
'RegexpOption', 'MACOption', 'PermissionsOption')

View file

@ -74,12 +74,10 @@ class DomainnameOption(StrOption):
else:
min_time = 1
regexp = r'((?!-)[a-z0-9-]{{{1},{0}}}\.){{{1},}}[a-z0-9-]{{1,{0}}}'.format(self._get_len(type), min_time)
msg = _('only lowercase, number, "-" and "." characters are allowed')
msg_warning = _('only lowercase, number, "-" and "." characters are recommanded')
else:
regexp = r'((?!-)[a-z0-9-]{{1,{0}}})'.format(self._get_len(type))
msg = _('only lowercase, number and "-" characters are allowed')
msg_warning = _('only lowercase, number and "-" characters are recommanded')
msg = _('must start with lowercase characters followed by lowercase characters, number, "-" and "." characters are allowed')
msg_warning = _('must start with lowercase characters followed by lowercase characters, number, "-" and "." characters are recommanded')
if allow_ip:
msg = _('could be a IP, otherwise {}').format(msg)
msg_warning = _('could be a IP, otherwise {}').format(msg_warning)

View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Team tiramisu (see AUTHORS for all contributors)
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# The original `Config` design model is unproudly borrowed from
# the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
# the whole pypy projet is under MIT licence
# ____________________________________________________________
import re
import sys
from ..setting import undefined, Undefined, OptionBag
from ..i18n import _
from .option import Option
from .intoption import IntOption
class PermissionsOption(IntOption):
"""Unix file permissions
Valid the representing Unix permissions is an octal (base-8) notation.
This notation consists of at least three digits (owner, group, and others).
If a fourth digit is present to the setuid bit, the setgid bit and the sticky bit attributes.
This option is an integer value.
"""
__slots__ = tuple()
perm_re = re.compile(r"^[0-7]{3,4}$")
_type = 'permissions'
_display_name = _('unix file permissions')
def __init__(self,
*args,
**kwargs,
) -> None:
#do not display intoption attributs
super().__init__(*args,
**kwargs)
def validate(self,
value: str) -> None:
super().validate(value)
if not self.perm_re.search(str(value)):
raise ValueError(_('only 3 or 4 octal digits are allowed'))
def second_level_validation(self,
value: str,
warnings_only: bool) -> None:
old_digit = 7
str_value = str(value)
if len(str_value) == 4:
str_value = str_value[1:]
for idx, digit in enumerate(str_value):
new_digit = int(digit)
if old_digit < new_digit:
if idx == 1:
old = _('user')
new = _('group')
else:
old = _('group')
new = _('other')
raise ValueError(_(f'{new} has more right than {old}'))
old_digit = new_digit
if str_value == '777':
raise ValueError(_(f'too weak'))

View file

@ -19,11 +19,8 @@
# the whole pypy projet is under MIT licence
# ____________________________________________________________
import re
import sys
from ..setting import undefined, Undefined, OptionBag
from ..i18n import _
from .option import Option
from .stroption import StrOption

View file

@ -117,6 +117,7 @@ FORBIDDEN_SET_PERMISSIVES = frozenset(['force_default_on_freeze',
'force_metaconfig_on_freeze',
'force_store_value'])
ALLOWED_LEADER_PROPERTIES = frozenset(['empty',
'notempty',
'notunique',
'unique',
'force_store_value',

View file

@ -175,7 +175,11 @@ class Values:
value,
reset_cache=True):
if isinstance(value, Calculation):
value = await value.execute(option_bag)
try:
value = await value.execute(option_bag)
except ConfigError as err:
msg = _(f'error when calculating "{option_bag.option.impl_get_display_name()}": {err} : {option_bag.path}')
raise ConfigError(msg) from err
elif isinstance(value, (list, tuple)):
value = await self._do_value_list(value, option_bag)
if reset_cache: