From 1df696f5432a673a24ff5cb273fe068a7d88d6ea Mon Sep 17 00:00:00 2001 From: Tomas Babej Date: Thu, 9 Jan 2014 11:14:56 +0100 Subject: ipalib: Add DateTime parameter Adds a parameter that represents a DateTime format using datetime.datetime object from python's native datetime library. In the CLI, accepts one of the following formats: Accepts LDAP Generalized time without in the following format: '%Y%m%d%H%M%SZ' Accepts subset of values defined by ISO 8601: '%Y-%m-%dT%H:%M:%SZ' '%Y-%m-%dT%H:%MZ' '%Y-%m-%dZ' Also accepts above formats using ' ' (space) as a separator instead of 'T'. As a simplification, it does not deal with timezone info and ISO 8601 values with timezone info (+-hhmm) are rejected. Values are expected to be in the UTC timezone. Values are saved to LDAP as LDAP Generalized time values in the format '%Y%m%d%H%SZ' (no time fractions and UTC timezone is assumed). To avoid confusion, in addition to subset of ISO 8601 values, the LDAP generalized time in the format '%Y%m%d%H%M%SZ' is also accepted as an input (as this is the format user will see on the output). Part of: https://fedorahosted.org/freeipa/ticket/3306 Reviewed-By: Jan Cholasta --- API.txt | 1 + VERSION | 4 ++-- ipalib/__init__.py | 2 +- ipalib/capabilities.py | 3 +++ ipalib/cli.py | 6 +++++- ipalib/constants.py | 2 ++ ipalib/parameters.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++- ipalib/rpc.py | 27 +++++++++++++++++++++++--- ipapython/ipaldap.py | 7 +++++++ 9 files changed, 96 insertions(+), 8 deletions(-) diff --git a/API.txt b/API.txt index c2654b144..eb8a61f64 100644 --- a/API.txt +++ b/API.txt @@ -4007,3 +4007,4 @@ capability: messages 2.52 capability: optional_uid_params 2.54 capability: permissions2 2.69 capability: primary_key_types 2.83 +capability: datetime_values 2.84 diff --git a/VERSION b/VERSION index 3b48d6407..32bddcf9d 100644 --- a/VERSION +++ b/VERSION @@ -89,5 +89,5 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=83 -# Last change: jcholast - add 'primary_key_types' capability +IPA_API_VERSION_MINOR=84 +# Last change: tbabej - added datetime value support diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 553c07197..2a87103b8 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -886,7 +886,7 @@ from frontend import Command, LocalOrRemote, Updater, Advice from frontend import Object, Method from crud import Create, Retrieve, Update, Delete, Search from parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam, DeprecatedParam -from parameters import BytesEnum, StrEnum, IntEnum, AccessTime, File +from parameters import BytesEnum, StrEnum, IntEnum, AccessTime, File, DateTime from errors import SkipPluginModule from text import _, ngettext, GettextFactory, NGettextFactory diff --git a/ipalib/capabilities.py b/ipalib/capabilities.py index 3dd93294f..f2e45a0f6 100644 --- a/ipalib/capabilities.py +++ b/ipalib/capabilities.py @@ -48,6 +48,9 @@ capabilities = dict( # primary_key_types: Non-unicode primary keys in command output primary_key_types=u'2.83', + + # support for datetime values on the client + datetime_values=u'2.84' ) diff --git a/ipalib/cli.py b/ipalib/cli.py index 4250aaf54..f03db9c61 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -46,11 +46,13 @@ import plugable from errors import (PublicError, CommandError, HelpError, InternalError, NoSuchNamespaceError, ValidationError, NotFound, NotConfiguredError, PromptFailed) -from constants import CLI_TAB +from constants import CLI_TAB, LDAP_GENERALIZED_TIME_FORMAT from parameters import File, Str, Enum, Any from text import _ from ipapython.version import API_VERSION +import datetime + def to_cli(name): """ @@ -155,6 +157,8 @@ class textui(backend.Backend): """ if type(value) is str: return base64.b64encode(value) + elif type(value) is datetime.datetime: + return value.strftime(LDAP_GENERALIZED_TIME_FORMAT) else: return value diff --git a/ipalib/constants.py b/ipalib/constants.py index 6cc50eacf..e98eee6f8 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -206,3 +206,5 @@ DEFAULT_CONFIG = ( ('jsonrpc_uri', object), # derived from xmlrpc_uri in Env._finalize_core() ) + +LDAP_GENERALIZED_TIME_FORMAT = "%Y%m%d%H%M%SZ" diff --git a/ipalib/parameters.py b/ipalib/parameters.py index fc5e64981..33ae182b5 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -102,6 +102,7 @@ a more detailed description for clarity. import re import decimal import base64 +import datetime from xmlrpclib import MAXINT, MININT from types import NoneType @@ -109,7 +110,7 @@ from text import _ as ugettext from plugable import ReadOnly, lock, check_name from errors import ConversionError, RequirementError, ValidationError from errors import PasswordMismatch, Base64DecodeError -from constants import TYPE_ERROR, CALLABLE_ERROR +from constants import TYPE_ERROR, CALLABLE_ERROR, LDAP_GENERALIZED_TIME_FORMAT from text import Gettext, FixMe from util import json_serialize from ipapython.dn import DN @@ -1609,6 +1610,55 @@ class File(Str): ('noextrawhitespace', bool, False), ) +class DateTime(Param): + """ + DateTime parameter type. + + Accepts LDAP Generalized time without in the following format: + '%Y%m%d%H%M%SZ' + + Accepts subset of values defined by ISO 8601: + '%Y-%m-%dT%H:%M:%SZ' + '%Y-%m-%dT%H:%MZ' + '%Y-%m-%dZ' + + Also accepts above formats using ' ' (space) as a separator instead of 'T'. + + Refer to the `man strftime` for the explanations for the %Y,%m,%d,%H.%M,%S. + """ + + accepted_formats = [LDAP_GENERALIZED_TIME_FORMAT, # generalized time + '%Y-%m-%dT%H:%M:%SZ', # ISO 8601, second precision + '%Y-%m-%dT%H:%MZ', # ISO 8601, minute precision + '%Y-%m-%dZ', # ISO 8601, date only + '%Y-%m-%d %H:%M:%SZ', # non-ISO 8601, second precision + '%Y-%m-%d %H:%MZ'] # non-ISO 8601, minute precision + + + type = datetime.datetime + type_error = _('must be datetime value') + + def _convert_scalar(self, value, index=None): + if isinstance(value, basestring): + for date_format in self.accepted_formats: + try: + time = datetime.datetime.strptime(value, date_format) + return time + except ValueError: + pass + + # If we get here, the strptime call did not succeed for any + # the accepted formats, therefore raise error + + error = (_("does not match any of accepted formats: ") + + (', '.join(self.accepted_formats))) + + raise ConversionError(name=self.get_param_name(), + index=index, + error=error) + + return super(DateTime, self)._convert_scalar(value, index) + class AccessTime(Str): """ diff --git a/ipalib/rpc.py b/ipalib/rpc.py index 73ae115b3..c44ffb6e1 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -33,6 +33,7 @@ Also see the `ipaserver.rpcserver` module. from types import NoneType from decimal import Decimal import sys +import datetime import os import locale import base64 @@ -41,17 +42,18 @@ import json import socket from urllib2 import urlparse -from xmlrpclib import (Binary, Fault, dumps, loads, ServerProxy, Transport, - ProtocolError, MININT, MAXINT) +from xmlrpclib import (Binary, Fault, DateTime, dumps, loads, ServerProxy, + Transport, ProtocolError, MININT, MAXINT) import kerberos from dns import resolver, rdatatype from dns.exception import DNSException from nss.error import NSPRError from ipalib.backend import Connectible +from ipalib.constants import LDAP_GENERALIZED_TIME_FORMAT from ipalib.errors import (public_errors, UnknownError, NetworkError, KerberosError, XMLRPCMarshallError, JSONError, ConversionError) -from ipalib import errors +from ipalib import errors, capabilities from ipalib.request import context, Connection from ipalib.util import get_current_principal from ipapython.ipa_log_manager import root_logger @@ -163,6 +165,14 @@ def xml_wrap(value, version): return unicode(value) if isinstance(value, DN): return str(value) + + # Encode datetime.datetime objects as xmlrpclib.DateTime objects + if isinstance(value, datetime.datetime): + if capabilities.client_has_capability(version, 'datetime_values'): + return DateTime(value) + else: + return value.strftime(LDAP_GENERALIZED_TIME_FORMAT) + assert type(value) in (unicode, int, long, float, bool, NoneType) return value @@ -196,6 +206,9 @@ def xml_unwrap(value, encoding='UTF-8'): if isinstance(value, Binary): assert type(value.data) is str return value.data + if isinstance(value, DateTime): + # xmlprc DateTime is converted to string of %Y%m%dT%H:%M:%S format + return datetime.datetime.strptime(str(value), "%Y%m%dT%H:%M:%S") assert type(value) in (unicode, int, float, bool, NoneType) return value @@ -266,6 +279,11 @@ def json_encode_binary(val, version): return {'__base64__': base64.b64encode(str(val))} elif isinstance(val, DN): return str(val) + elif isinstance(val, datetime.datetime): + if capabilities.client_has_capability(version, 'datetime_values'): + return {'__datetime__': val.strftime(LDAP_GENERALIZED_TIME_FORMAT)} + else: + return val.strftime(LDAP_GENERALIZED_TIME_FORMAT) else: return val @@ -293,6 +311,9 @@ def json_decode_binary(val): if isinstance(val, dict): if '__base64__' in val: return base64.b64decode(val['__base64__']) + elif '__datetime__' in val: + return datetime.datetime.strptime(val['__datetime__'], + LDAP_GENERALIZED_TIME_FORMAT) else: return dict((k, json_decode_binary(v)) for k, v in val.items()) elif isinstance(val, list): diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py index 56e7d1384..12450e8f2 100644 --- a/ipapython/ipaldap.py +++ b/ipapython/ipaldap.py @@ -21,6 +21,7 @@ import string import time +import datetime import shutil from decimal import Decimal from copy import deepcopy @@ -35,6 +36,7 @@ from ldap.controls import SimplePagedResultsControl import ldapurl from ipalib import errors, _ +from ipalib.constants import LDAP_GENERALIZED_TIME_FORMAT from ipapython import ipautil from ipapython.ipautil import ( format_netloc, wait_for_open_socket, wait_for_open_ports, CIDict) @@ -239,6 +241,7 @@ class IPASimpleLDAPObject(object): '2.16.840.1.113719.1.301.4.41.1' : DN, # krbSubTrees '2.16.840.1.113719.1.301.4.52.1' : DN, # krbObjectReferences '2.16.840.1.113719.1.301.4.53.1' : DN, # krbPrincContainerRef + '1.3.6.1.4.1.1466.115.121.1.24' : datetime.datetime, } # In most cases we lookup the syntax from the schema returned by @@ -408,6 +411,8 @@ class IPASimpleLDAPObject(object): elif isinstance(val, dict): dct = dict((self.encode(k), self.encode(v)) for k, v in val.iteritems()) return dct + elif isinstance(val, datetime.datetime): + return val.strftime(LDAP_GENERALIZED_TIME_FORMAT) elif val is None: return None else: @@ -426,6 +431,8 @@ class IPASimpleLDAPObject(object): return val elif target_type is unicode: return val.decode('utf-8') + elif target_type is datetime.datetime: + return datetime.datetime.strptime(val, LDAP_GENERALIZED_TIME_FORMAT) else: return target_type(val) except Exception, e: -- cgit