RPKI Engine
1.0
|
00001 """ 00002 Unified RPKI date/time handling, based on the standard Python datetime module. 00003 00004 Module name chosen to sidestep a nightmare of import-related errors 00005 that occur with the more obvious module names. 00006 00007 List of arithmetic methods that require result casting was derived by 00008 inspection of the datetime module, to wit: 00009 00010 >>> import datetime 00011 >>> for t in (datetime.datetime, datetime.timedelta): 00012 ... for k in t.__dict__.keys(): 00013 ... if k.startswith("__"): 00014 ... print "%s.%s()" % (t.__name__, k) 00015 00016 $Id: sundial.py 4015 2011-10-05 17:45:34Z sra $ 00017 00018 Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") 00019 00020 Permission to use, copy, modify, and distribute this software for any 00021 purpose with or without fee is hereby granted, provided that the above 00022 copyright notice and this permission notice appear in all copies. 00023 00024 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH 00025 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 00026 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, 00027 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 00028 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 00029 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 00030 PERFORMANCE OF THIS SOFTWARE. 00031 00032 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") 00033 00034 Permission to use, copy, modify, and distribute this software for any 00035 purpose with or without fee is hereby granted, provided that the above 00036 copyright notice and this permission notice appear in all copies. 00037 00038 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH 00039 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 00040 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, 00041 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 00042 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 00043 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 00044 PERFORMANCE OF THIS SOFTWARE. 00045 """ 00046 00047 import datetime as pydatetime 00048 import re 00049 00050 def now(): 00051 """ 00052 Get current timestamp. 00053 """ 00054 return datetime.utcnow() 00055 00056 class ParseFailure(Exception): 00057 """ 00058 Parse failure constructing timedelta. 00059 """ 00060 00061 class datetime(pydatetime.datetime): 00062 """ 00063 RPKI extensions to standard datetime.datetime class. All work here 00064 is in UTC, so we use naive datetime objects. 00065 """ 00066 00067 def totimestamp(self): 00068 """ 00069 Convert to seconds from epoch (like time.time()). Conversion 00070 method is a bit silly, but avoids time module timezone whackiness. 00071 """ 00072 return int(self.strftime("%s")) 00073 00074 @classmethod 00075 def fromUTCTime(cls, x): 00076 """ 00077 Convert from ASN.1 UTCTime. 00078 """ 00079 x = str(x) 00080 return cls.fromGeneralizedTime(("19" if x[0] >= "5" else "20") + x) 00081 00082 def toUTCTime(self): 00083 """ 00084 Convert to ASN.1 UTCTime. 00085 """ 00086 return self.strftime("%y%m%d%H%M%SZ") 00087 00088 @classmethod 00089 def fromGeneralizedTime(cls, x): 00090 """ 00091 Convert from ASN.1 GeneralizedTime. 00092 """ 00093 return cls.strptime(x, "%Y%m%d%H%M%SZ") 00094 00095 def toGeneralizedTime(self): 00096 """ 00097 Convert to ASN.1 GeneralizedTime. 00098 """ 00099 return self.strftime("%Y%m%d%H%M%SZ") 00100 00101 @classmethod 00102 def fromASN1tuple(cls, x): 00103 """ 00104 Convert from ASN.1 tuple representation. 00105 """ 00106 assert isinstance(x, tuple) and len(x) == 2 and x[0] in ("utcTime", "generalTime") 00107 if x[0] == "utcTime": 00108 return cls.fromUTCTime(x[1]) 00109 else: 00110 return cls.fromGeneralizedTime(x[1]) 00111 00112 ## @var PKIX_threshhold 00113 # Threshold specified in RFC 3280 for switchover from UTCTime to GeneralizedTime. 00114 00115 PKIX_threshhold = pydatetime.datetime(2050, 1, 1) 00116 00117 def toASN1tuple(self): 00118 """ 00119 Convert to ASN.1 tuple representation. 00120 """ 00121 if self < self.PKIX_threshhold: 00122 return "utcTime", self.toUTCTime() 00123 else: 00124 return "generalTime", self.toGeneralizedTime() 00125 00126 @classmethod 00127 def fromXMLtime(cls, x): 00128 """ 00129 Convert from XML time representation. 00130 """ 00131 if x is None: 00132 return None 00133 else: 00134 return cls.strptime(x, "%Y-%m-%dT%H:%M:%SZ") 00135 00136 def toXMLtime(self): 00137 """ 00138 Convert to XML time representation. 00139 """ 00140 return self.strftime("%Y-%m-%dT%H:%M:%SZ") 00141 00142 def __str__(self): 00143 return self.toXMLtime() 00144 00145 @classmethod 00146 def fromdatetime(cls, x): 00147 """ 00148 Convert a datetime.datetime object into this subclass. This is 00149 whacky due to the weird constructors for datetime. 00150 """ 00151 return cls.combine(x.date(), x.time()) 00152 00153 @classmethod 00154 def fromOpenSSL(cls, x): 00155 """ 00156 Convert from the format OpenSSL's command line tool uses into this 00157 subclass. May require rewriting if we run into locale problems. 00158 """ 00159 if x.startswith("notBefore=") or x.startswith("notAfter="): 00160 x = x.partition("=")[2] 00161 return cls.strptime(x, "%b %d %H:%M:%S %Y GMT") 00162 00163 @classmethod 00164 def from_sql(cls, x): 00165 """ 00166 Convert from SQL storage format. 00167 """ 00168 return cls.fromdatetime(x) 00169 00170 def to_sql(self): 00171 """ 00172 Convert to SQL storage format. 00173 00174 There's something whacky going on in the MySQLdb module, it throws 00175 range errors when storing a derived type into a DATETIME column. 00176 Investigate some day, but for now brute force this by copying the 00177 relevant fields into a datetime.datetime for MySQLdb's 00178 consumption. 00179 00180 """ 00181 return pydatetime.datetime(year = self.year, month = self.month, day = self.day, 00182 hour = self.hour, minute = self.minute, second = self.second, 00183 microsecond = 0, tzinfo = None) 00184 00185 def later(self, other): 00186 """ 00187 Return the later of two timestamps. 00188 """ 00189 return other if other > self else self 00190 00191 def earlier(self, other): 00192 """ 00193 Return the earlier of two timestamps. 00194 """ 00195 return other if other < self else self 00196 00197 def __add__(self, y): return _cast(pydatetime.datetime.__add__(self, y)) 00198 def __radd__(self, y): return _cast(pydatetime.datetime.__radd__(self, y)) 00199 def __rsub__(self, y): return _cast(pydatetime.datetime.__rsub__(self, y)) 00200 def __sub__(self, y): return _cast(pydatetime.datetime.__sub__(self, y)) 00201 00202 class timedelta(pydatetime.timedelta): 00203 """ 00204 Timedelta with text parsing. This accepts two input formats: 00205 00206 - A simple integer, indicating a number of seconds. 00207 00208 - A string of the form "uY vW wD xH yM zS" where u, v, w, x, y, and z 00209 are integers and Y, W, D, H, M, and S indicate years, weeks, days, 00210 hours, minutes, and seconds. All of the fields are optional, but 00211 at least one must be specified. Eg,"3D4H" means "three days plus 00212 four hours". 00213 00214 There is no "months" format, because the definition of a month is too 00215 fuzzy to be useful (what day is six months from August 30th?) 00216 00217 Similarly, the "years" conversion may produce surprising results, as 00218 "one year" in conventional English does not refer to a fixed interval 00219 but rather a fixed (and in some cases undefined) offset within the 00220 Gregorian calendar (what day is one year from February 29th?) 1Y as 00221 implemented by this code refers to a specific number of seconds. 00222 If you mean 365 days or 52 weeks, say that instead. 00223 """ 00224 00225 ## @var regexp 00226 # Hideously ugly regular expression to parse the complex text form. 00227 # Tags are intended for use with re.MatchObject.groupdict() and map 00228 # directly to the keywords expected by the timedelta constructor. 00229 00230 regexp = re.compile("\\s*".join(("^", 00231 "(?:(?P<years>\\d+)Y)?", 00232 "(?:(?P<weeks>\\d+)W)?", 00233 "(?:(?P<days>\\d+)D)?", 00234 "(?:(?P<hours>\\d+)H)?", 00235 "(?:(?P<minutes>\\d+)M)?", 00236 "(?:(?P<seconds>\\d+)S)?", 00237 "$")), 00238 re.I) 00239 00240 ## @var years_to_seconds 00241 # Conversion factor from years to seconds (value furnished by the 00242 # "units" program). 00243 00244 years_to_seconds = 31556926 00245 00246 @classmethod 00247 def parse(cls, arg): 00248 """ 00249 Parse text into a timedelta object. 00250 """ 00251 if not isinstance(arg, str): 00252 return cls(seconds = arg) 00253 elif arg.isdigit(): 00254 return cls(seconds = int(arg)) 00255 else: 00256 match = cls.regexp.match(arg) 00257 if match: 00258 #return cls(**dict((k, int(v)) for (k, v) in match.groupdict().items() if v is not None)) 00259 d = match.groupdict("0") 00260 for k, v in d.iteritems(): 00261 d[k] = int(v) 00262 d["days"] += d.pop("weeks") * 7 00263 d["seconds"] += d.pop("years") * cls.years_to_seconds 00264 return cls(**d) 00265 else: 00266 raise ParseFailure, "Couldn't parse timedelta %r" % (arg,) 00267 00268 def convert_to_seconds(self): 00269 """ 00270 Convert a timedelta interval to seconds. 00271 """ 00272 return self.days * 24 * 60 * 60 + self.seconds 00273 00274 @classmethod 00275 def fromtimedelta(cls, x): 00276 """ 00277 Convert a datetime.timedelta object into this subclass. 00278 """ 00279 return cls(days = x.days, seconds = x.seconds, microseconds = x.microseconds) 00280 00281 def __abs__(self): return _cast(pydatetime.timedelta.__abs__(self)) 00282 def __add__(self, x): return _cast(pydatetime.timedelta.__add__(self, x)) 00283 def __div__(self, x): return _cast(pydatetime.timedelta.__div__(self, x)) 00284 def __floordiv__(self, x): return _cast(pydatetime.timedelta.__floordiv__(self, x)) 00285 def __mul__(self, x): return _cast(pydatetime.timedelta.__mul__(self, x)) 00286 def __neg__(self): return _cast(pydatetime.timedelta.__neg__(self)) 00287 def __pos__(self): return _cast(pydatetime.timedelta.__pos__(self)) 00288 def __radd__(self, x): return _cast(pydatetime.timedelta.__radd__(self, x)) 00289 def __rdiv__(self, x): return _cast(pydatetime.timedelta.__rdiv__(self, x)) 00290 def __rfloordiv__(self, x): return _cast(pydatetime.timedelta.__rfloordiv__(self, x)) 00291 def __rmul__(self, x): return _cast(pydatetime.timedelta.__rmul__(self, x)) 00292 def __rsub__(self, x): return _cast(pydatetime.timedelta.__rsub__(self, x)) 00293 def __sub__(self, x): return _cast(pydatetime.timedelta.__sub__(self, x)) 00294 00295 def _cast(x): 00296 """ 00297 Cast result of arithmetic operations back into correct subtype. 00298 """ 00299 if isinstance(x, pydatetime.datetime): 00300 return datetime.fromdatetime(x) 00301 if isinstance(x, pydatetime.timedelta): 00302 return timedelta.fromtimedelta(x) 00303 return x 00304 00305 if __name__ == "__main__": 00306 00307 def test(t): 00308 print 00309 print "str: ", t 00310 print "repr: ", repr(t) 00311 print "seconds since epoch:", t.strftime("%s") 00312 print "UTCTime: ", t.toUTCTime() 00313 print "GeneralizedTime: ", t.toGeneralizedTime() 00314 print "ASN1tuple: ", t.toASN1tuple() 00315 print "XMLtime: ", t.toXMLtime() 00316 print 00317 00318 print 00319 print "Testing time conversion routines" 00320 test(now()) 00321 test(now() + timedelta(days = 30)) 00322 test(now() + timedelta.parse("3d5s")) 00323 test(now() + timedelta.parse(" 3d 5s ")) 00324 test(now() + timedelta.parse("1y3d5h"))