From a929ac333833a5cbf503d1fcbdee150658d933a4 Mon Sep 17 00:00:00 2001 From: Jan Cholasta Date: Nov 11 2016 11:17:25 +0000 Subject: install: use standard Python classes to declare knob types Use type(None) rather than bool to define knobs which are represented as command line flags. This allows declaring both "--option" and "--option={0,1}"-style command line options. Use enum.Enum subclasses instead of set literals to declare enumerations. Use typing.List[T] instead of (list, T) to declare lists. (Note that a minimal reimplementation of typing.List is used instead of the Python 2 backport of the typing module due to non-technical reasons.) Use CheckedIPAddress instead of 'ip' and 'ip-local' to declare IP addresses. https://fedorahosted.org/freeipa/ticket/6392 Reviewed-By: Martin Basti --- diff --git a/freeipa.spec.in b/freeipa.spec.in index bcde59e..b49f937 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -136,6 +136,7 @@ BuildRequires: python-jwcrypto BuildRequires: python-custodia BuildRequires: dbus-python BuildRequires: python-dateutil +BuildRequires: python-enum34 BuildRequires: python-netifaces BuildRequires: python-sss BuildRequires: python-sss-murmur @@ -525,6 +526,7 @@ Requires: python-ldap >= 2.4.15 Requires: python-requests Requires: python-custodia Requires: python-dns >= 1.11.1 +Requires: python-enum34 Requires: python-netifaces >= 0.10.4 Requires: pyusb diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py index a91afe5..65fc952 100644 --- a/ipapython/install/cli.py +++ b/ipapython/install/cli.py @@ -7,6 +7,7 @@ Command line support. """ import collections +import enum import functools import optparse import signal @@ -14,7 +15,7 @@ import signal import six from ipapython import admintool, ipa_log_manager -from ipapython.ipautil import private_ccache +from ipapython.ipautil import CheckedIPAddress, private_ccache from . import core, common @@ -23,6 +24,8 @@ __all__ = ['install_tool', 'uninstall_tool'] if six.PY3: long = int +NoneType = type(None) + def _get_usage(configurable_class): usage = '%prog [options]' @@ -143,13 +146,17 @@ class ConfigureTool(admintool.AdminTool): parser, "{0} options".format(group_cls.description)) knob_type = knob_cls.type - if isinstance(knob_type, tuple): - knob_scalar_type = knob_type[1] + if issubclass(knob_type, list): + try: + # typing.List[X].__parameters__ == (X,) + knob_scalar_type = knob_type.__parameters__[0] + except AttributeError: + knob_scalar_type = str else: knob_scalar_type = knob_type kwargs = dict() - if knob_scalar_type is bool: + if knob_scalar_type is NoneType: kwargs['type'] = None kwargs['const'] = True kwargs['default'] = False @@ -159,16 +166,16 @@ class ConfigureTool(admintool.AdminTool): kwargs['type'] = 'int' elif knob_scalar_type is long: kwargs['type'] = 'long' - elif knob_scalar_type in ('ip', 'ip-local'): - kwargs['type'] = knob_scalar_type - elif isinstance(knob_scalar_type, set): + elif knob_scalar_type is CheckedIPAddress: + kwargs['type'] = 'ip' + elif issubclass(knob_scalar_type, enum.Enum): kwargs['type'] = 'choice' - kwargs['choices'] = list(knob_scalar_type) + kwargs['choices'] = [i.value for i in knob_scalar_type] else: kwargs['nargs'] = 1 kwargs['callback_args'] = (knob_scalar_type,) kwargs['dest'] = name - if isinstance(knob_type, tuple): + if issubclass(knob_type, list): if 'type' not in kwargs: kwargs['action'] = 'callback' kwargs['callback'] = ( diff --git a/ipapython/install/core.py b/ipapython/install/core.py index c3dc908..111b710 100644 --- a/ipapython/install/core.py +++ b/ipapython/install/core.py @@ -8,20 +8,25 @@ The framework core. import abc import collections +import enum import functools import itertools +import re import sys import six from ipapython.ipa_log_manager import root_logger +from ipapython.ipautil import CheckedIPAddress -from . import util +from . import util, typing from .util import from_ __all__ = ['InvalidStateError', 'KnobValueError', 'Property', 'Knob', 'Configurable', 'Group', 'Component', 'Composite'] +NoneType = type(None) + # Configurable states _VALIDATE_PENDING = 'VALIDATE_PENDING' _VALIDATE_RUNNING = 'VALIDATE_RUNNING' @@ -145,11 +150,15 @@ def knob(type_or_base, default=_missing, sensitive=_missing, deprecated=_missing, description=_missing, cli_positional=_missing, cli_name=_missing, cli_short_name=_missing, cli_aliases=_missing, cli_metavar=_missing): + if type_or_base is None: + type_or_base = NoneType + + assert isinstance(type_or_base, type) + class_dict = {} class_dict['_order'] = next(_counter) - if (not isinstance(type_or_base, type) or - not issubclass(type_or_base, KnobBase)): + if not issubclass(type_or_base, KnobBase): class_dict['type'] = type_or_base type_or_base = KnobBase @@ -179,6 +188,27 @@ def Knob(type_or_base, default=_missing, sensitive=_missing, deprecated=_missing, description=_missing, cli_positional=_missing, cli_name=_missing, cli_short_name=_missing, cli_aliases=_missing, cli_metavar=_missing): + if isinstance(type_or_base, tuple): + assert type_or_base[0] is list + scalar_type = type_or_base[1] + else: + scalar_type = type_or_base + + if scalar_type is bool: + scalar_type = NoneType + elif scalar_type == 'ip': + scalar_type = CheckedIPAddress + elif isinstance(scalar_type, set): + scalar_type = type( + 'Enum', + (enum.Enum,), + {re.sub(r'[^0-9A-Za-z_]', '', n): n for n in scalar_type}) + + if isinstance(type_or_base, tuple): + type_or_base = typing.List[scalar_type] + else: + type_or_base = scalar_type + return knob(type_or_base, default=default, sensitive=sensitive, diff --git a/ipapython/install/typing.py b/ipapython/install/typing.py new file mode 100644 index 0000000..d86fc8f --- /dev/null +++ b/ipapython/install/typing.py @@ -0,0 +1,34 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +import weakref + +import six + +_cache = weakref.WeakValueDictionary() + + +class ListMeta(type): + def __getitem__(cls, key): + if not isinstance(key, type): + raise TypeError("Parameters to generic types must be types. " + "Got {!r}.".format(key)) + + t = ListMeta( + cls.__name__, + cls.__bases__, + { + '__parameters__': (key,), + '__init__': cls.__init__, + } + ) + + return _cache.get(key, t) + + +class List(six.with_metaclass(ListMeta, list)): + __parameters__ = () + + def __init__(self, *_args, **_kwargs): + raise TypeError("Type List cannot be instantiated; use list() instead") diff --git a/ipaserver/install/server/common.py b/ipaserver/install/server/common.py index e6093d1..ad3282a 100644 --- a/ipaserver/install/server/common.py +++ b/ipaserver/install/server/common.py @@ -10,6 +10,7 @@ import sys import six from ipapython.dn import DN +from ipapython.ipautil import CheckedIPAddress from ipapython.install import common, core from ipapython.install.core import Knob from ipalib.util import validate_domain_name @@ -308,13 +309,22 @@ class BaseServer(common.Installable, common.Interactive, core.Composite): ) ip_addresses = Knob( - (list, 'ip-local'), None, + (list, 'ip'), None, description=("Master Server IP Address. This option can be used " "multiple times"), cli_name='ip-address', cli_metavar='IP_ADDRESS', ) + @ip_addresses.validator + def ip_addresses(self, values): + for value in values: + try: + CheckedIPAddress(value, match_local=True) + except Exception as e: + raise ValueError("invalid IP address {0}: {1}".format( + value, e)) + no_host_dns = Knob( bool, False, description="Do not use DNS for hostname lookup during installation",