rda-python-common 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1352 @@
1
+ #
2
+ ###############################################################################
3
+ #
4
+ # Title : pg_log.py -- Module for logging messages.
5
+ # Author : Zaihua Ji, zji@ucar.edu
6
+ # Date : 03/02/2016
7
+ # 2025-01-10 transferred to package rda_python_common from
8
+ # https://github.com/NCAR/rda-shared-libraries.git
9
+ # 2025-11-20 convert to class PgLOG
10
+ # Purpose : Python library module to log message and also do other things
11
+ # according to the value of logact, like display the error
12
+ # message on screen and exit script
13
+ #
14
+ # Github : https://github.com/NCAR/rda-python-common.git
15
+ #
16
+ ###############################################################################
17
+
18
+ import sys
19
+ import os
20
+ import re
21
+ import pwd
22
+ import grp
23
+ import shlex
24
+ import smtplib
25
+ from email.message import EmailMessage
26
+ from subprocess import Popen, PIPE
27
+ from os import path as op
28
+ import time
29
+ import socket
30
+ import shutil
31
+ import traceback
32
+ from unidecode import unidecode
33
+
34
+ class PgLOG:
35
+
36
+ # define some constants for logging actions
37
+ MSGLOG = (0x00001) # logging message
38
+ WARNLG = (0x00002) # show logging message as warning
39
+ EXITLG = (0x00004) # exit after logging
40
+ LOGWRN = (0x00003) # MSGLOG|WARNLG
41
+ LOGEXT = (0x00005) # MSGLOG|EXITLG
42
+ WRNEXT = (0x00006) # WARNLG|EXITLG
43
+ LGWNEX = (0x00007) # MSGLOG|WARNLG|EXITLG
44
+ EMLLOG = (0x00008) # append message to email buffer
45
+ LGWNEM = (0x0000B) # MSGLOG|WARNLG|EMLLOG
46
+ LWEMEX = (0x0000F) # MSGLOG|WARNLG|EMLLOG|EXITLG
47
+ ERRLOG = (0x00010) # error log only, output to STDERR
48
+ LOGERR = (0x00011) # MSGLOG|ERRLOG
49
+ LGEREX = (0x00015) # MSGLOG|ERRLOG|EXITLG
50
+ LGEREM = (0x00019) # MSGLOG|ERRLOG|EMLLOG
51
+ DOLOCK = (0x00020) # action to lock table record(s)
52
+ ENDLCK = (0x00040) # action to end locking table record(s)
53
+ AUTOID = (0x00080) # action to retrieve the last auto added id
54
+ DODFLT = (0x00100) # action to set empty values to default ones
55
+ SNDEML = (0x00200) # action to send email now
56
+ RETMSG = (0x00400) # action to return the message back
57
+ FRCLOG = (0x00800) # force logging message
58
+ SEPLIN = (0x01000) # add a separating line for email/STDOUT/STDERR
59
+ BRKLIN = (0x02000) # add a line break for email/STDOUT/STDERR
60
+ EMLTOP = (0x04000) # prepend message to email buffer
61
+ RCDMSG = (0x00814) # make sure to record logging message
62
+ MISLOG = (0x00811) # cannot access logfile
63
+ EMLSUM = (0x08000) # record as email summary
64
+ EMEROL = (0x10000) # record error as email only
65
+ EMLALL = (0x1D208) # all email acts
66
+ DOSUDO = (0x20000) # add 'sudo -u self.PGLOG['GDEXUSER']'
67
+ NOTLOG = (0x40000) # do not log any thing
68
+ OVRIDE = (0x80000) # do override existing file or record
69
+ NOWAIT = (0x100000) # do not wait on globus task to finish
70
+ ADDTBL = (0x200000) # action to add a new table if it does not exist
71
+ SKPTRC = (0x400000) # action to skip tracing when log errors
72
+ UCNAME = (0x800000) # action to change query field names to upper case
73
+ UCLWEX = (0x800015) # UCNAME|MSGLOG|WARNLG|EXITLG
74
+ PFSIZE = (0x1000000) # total file size under a path
75
+ SUCCESS = 1 # Successful function call
76
+ FINISH = 2 # go through a function, including time out
77
+ FAILURE = 0 # Unsuccessful function call
78
+
79
+ def __init__(self):
80
+ self.PGLOG = {
81
+ # more defined in untaint_suid() with environment variables
82
+ 'EMLADDR' : '',
83
+ 'CCDADDR' : '',
84
+ 'SEPLINE' : "===========================================================\n",
85
+ 'TWOGBS' : 2147483648,
86
+ 'ONEGBS' : 1073741824,
87
+ 'MINSIZE' : 100, # minimal file size in bytes to be valid
88
+ 'LOGMASK' : (0xFFFFFF), # log mask to turn off certain log action bits
89
+ 'BCKGRND' : 0, # background process flag -b
90
+ 'ERRCNT' : 0, # record number of errors for email
91
+ 'ERRMSG' : '', # record error message for email
92
+ 'SUMMSG' : '', # record summary message for email
93
+ 'EMLMSG' : '', # record detail message for email
94
+ 'PRGMSG' : '', # record progressing message for email, replaced each time
95
+ 'GMTZ' : 0, # 0 - use local time, 1 - use greenwich mean time
96
+ 'NOLEAP' : 0, # 1 - skip 29 of Feburary while add days to date
97
+ 'GMTDIFF' : 6, # gmt is 6 hours ahead of us
98
+ 'CURUID' : None, # the login name who executes the program
99
+ 'SETUID' : '', # the login name for suid if it is different to the CURUID
100
+ 'FILEMODE': 0o664, # default 8-base file mode
101
+ 'EXECMODE': 0o775, # default 8-base executable file mode or directory mode
102
+ 'GDEXUSER' : "gdexdata", # common gdex user name
103
+ 'GDEXEMAIL' : "zji", # specialist to receipt email intead of common gdex user name
104
+ 'SUDOGDEX' : 0, # 1 to allow sudo to self.PGLOG['GDEXUSER']
105
+ 'HOSTNAME' : '', # current host name the process in running on
106
+ 'OBJCTSTR' : "object",
107
+ 'BACKUPNM' : "quasar",
108
+ 'DRDATANM' : "drdata",
109
+ 'GPFSNAME' : "glade",
110
+ 'PBSNAME' : "PBS",
111
+ 'DSIDCHRS' : "d",
112
+ 'DOSHELL' : False,
113
+ 'NEWDSID' : True,
114
+ 'PUSGDIR' : None,
115
+ 'BCHHOSTS' : "PBS",
116
+ 'HOSTTYPE' : 'dav', # default HOSTTYPE
117
+ 'EMLMAX' : 256, # up limit of email line count
118
+ 'PGBATCH' : '', # current batch service name, SLURM or PBS
119
+ 'PGBINDIR' : '',
120
+ 'SLMTIME' : 604800, # max runtime for SLURM bath job, (7x24x60x60 seconds)
121
+ 'PBSTIME' : 86400, # max runtime for PBS bath job, (24x60x60 seconds)
122
+ 'MSSGRP' : None, # set if set to different HPSS group
123
+ 'GDEXGRP' : "decs",
124
+ 'EMLSEND' : None, # path to sendmail, None if not exists
125
+ 'DSCHECK' : None, # carry some cached dscheck information
126
+ 'PGDBBUF' : None, # reference to a connected database object
127
+ 'HPSSLMT' : 10, # up limit of HPSS streams
128
+ 'NOQUIT' : 0, # do not quit if this flag is set for daemons
129
+ 'DBRETRY' : 2, # db retry count after error
130
+ 'TIMEOUT' : 15, # default timeout (in seconds) for tosystem()
131
+ 'CMDTIME' : 120, # default command time (in seconds) for pgsystem() to record end time
132
+ 'SYSERR' : None, # cache the error message generated inside pgsystem()
133
+ 'ERR2STD' : [], # if non-empty reference to array of strings, change stderr to stdout if match
134
+ 'STD2ERR' : [], # if non-empty reference to array of strings, change stdout to stderr if match
135
+ 'MISSFILE': "No such file or directory",
136
+ 'GITHUB' : "https://github.com" , # github server
137
+ 'EMLSRVR' : "ndir.ucar.edu", # UCAR email server and port
138
+ 'EMLPORT' : 25
139
+ }
140
+ self.PGLOG['RDAUSER'] = self.PGLOG['GDEXUSER']
141
+ self.PGLOG['RDAGRP'] = self.PGLOG['GDEXGRP']
142
+ self.PGLOG['RDAEMAIL'] = self.PGLOG['GDEXEMAIL']
143
+ self.PGLOG['SUDORDA'] = self.PGLOG['SUDOGDEX']
144
+ self.HOSTTYPES = {
145
+ 'rda' : 'dsg_mach',
146
+ 'casper' : 'dav',
147
+ 'crhtc' : 'dav',
148
+ 'cron' : 'dav',
149
+ }
150
+ self.CPID = {
151
+ 'PID' : "",
152
+ 'CTM' : int(time.time()),
153
+ 'CMD' : "",
154
+ 'CPID' : "",
155
+ }
156
+ self.BCHCMDS = {'PBS' : 'qsub'}
157
+ # global dists to cashe information
158
+ self.COMMANDS = {}
159
+ self.SLMHOSTS = []
160
+ self.SLMSTATS = {}
161
+ self.PBSHOSTS = []
162
+ self.PBSSTATS = {}
163
+ # set additional common PGLOG values
164
+ self.set_common_pglog()
165
+
166
+ # get time string in format YYMMDDHHNNSS for given ctime; or current time if ctime is 0
167
+ def current_datetime(self, ctime = 0):
168
+ if self.PGLOG['GMTZ']:
169
+ dt = time.gmtime(ctime) if ctime else time.gmtime()
170
+ else:
171
+ dt = time.localtime(ctime) if ctime else time.localtime()
172
+ return "{:02}{:02}{:02}{:02}{:02}{:02}".format(dt[0], dt[1], dt[2], dt[3], dt[4], dt[5])
173
+
174
+ # get an environment variable and untaint it
175
+ def get_environment(self, name, default = None, logact = 0):
176
+ env = os.getenv(name, default)
177
+ if env is None and logact:
178
+ self.pglog(name + ": Environment variable is not defined", logact)
179
+ return env
180
+
181
+ # cache the msg string to global email entries for later call of send_email()
182
+ def set_email(self, msg, logact = 0):
183
+ if logact and msg:
184
+ if logact&self.EMLTOP:
185
+ if self.PGLOG['PRGMSG']:
186
+ msg = self.PGLOG['PRGMSG'] + "\n" + msg
187
+ self.PGLOG['PRGMSG'] = ""
188
+ if self.PGLOG['ERRCNT'] == 0:
189
+ if not re.search(r'\n$', msg): msg += "!\n"
190
+ else:
191
+ if self.PGLOG['ERRCNT'] == 1:
192
+ msg += " with 1 Error:\n"
193
+ else:
194
+ msg += " with {} Errors:\n".format(self.PGLOG['ERRCNT'])
195
+ msg += self.break_long_string(self.PGLOG['ERRMSG'], 512, None, self.PGLOG['EMLMAX']/2, None, 50, 25)
196
+ self.PGLOG['ERRCNT'] = 0
197
+ self.PGLOG['ERRMSG'] = ''
198
+ if self.PGLOG['SUMMSG']:
199
+ msg += self.PGLOG['SEPLINE']
200
+ if self.PGLOG['SUMMSG']: msg += "Summary:\n"
201
+ msg += self.break_long_string(self.PGLOG['SUMMSG'], 512, None, self.PGLOG['EMLMAX']/2, None, 50, 25)
202
+ if self.PGLOG['EMLMSG']:
203
+ msg += self.PGLOG['SEPLINE']
204
+ if self.PGLOG['SUMMSG']: msg += "Detail Information:\n"
205
+ self.PGLOG['EMLMSG'] = msg + self.break_long_string(self.PGLOG['EMLMSG'], 512, None, self.PGLOG['EMLMAX'], None, 50, 40)
206
+ self.PGLOG['SUMMSG'] = "" # in case not
207
+ else:
208
+ if logact&self.ERRLOG: # record error for email summary
209
+ self.PGLOG['ERRCNT'] += 1
210
+ if logact&self.BRKLIN: self.PGLOG['ERRMSG'] += "\n"
211
+ self.PGLOG['ERRMSG'] += "{}. {}".format(self.PGLOG['ERRCNT'], msg)
212
+ elif logact&self.EMLSUM:
213
+ if self.PGLOG['SUMMSG']:
214
+ if logact&self.BRKLIN: self.PGLOG['SUMMSG'] += "\n"
215
+ if logact&self.SEPLIN: self.PGLOG['SUMMSG'] += self.PGLOG['SEPLINE']
216
+ self.PGLOG['SUMMSG'] += msg # append
217
+ if logact&self.EMLLOG:
218
+ if self.PGLOG['EMLMSG']:
219
+ if logact&self.BRKLIN: self.PGLOG['EMLMSG'] += "\n"
220
+ if logact&self.SEPLIN: self.PGLOG['EMLMSG'] += self.PGLOG['SEPLINE']
221
+ self.PGLOG['EMLMSG'] += msg # append
222
+ elif msg is None:
223
+ self.PGLOG['EMLMSG'] = ""
224
+
225
+ # retrieve the cached email message
226
+ def get_email(self):
227
+ return self.PGLOG['EMLMSG']
228
+
229
+ # send a customized email with all entries included
230
+ def send_customized_email(self, logmsg, emlmsg, logact = None):
231
+ if logact is None: logact = self.LOGWRN
232
+ entries = {
233
+ 'fr' : ["From", 1, None],
234
+ 'to' : ["To", 1, None],
235
+ 'cc' : ["Cc", 0, ''],
236
+ 'sb' : ["Subject", 1, None]
237
+ }
238
+ if logmsg:
239
+ logmsg += ': '
240
+ else:
241
+ logmsg = ''
242
+ msg = emlmsg
243
+ for ekey in entries:
244
+ entry = entries[ekey][0]
245
+ ms = re.search(r'(^|\n)({}: *(.*)\n)'.format(entry), emlmsg, re.I)
246
+ if ms:
247
+ vals = ms.groups()
248
+ msg = msg.replace(vals[1], '')
249
+ if vals[2]: entries[ekey][2] = vals[2]
250
+ elif entries[ekey][1]:
251
+ return self.pglog("{}Missing Entry '{}' for sending email".format(logmsg, entry), logact|self.ERRLOG)
252
+ ret = self.send_python_email(entries['sb'][2], entries['to'][2], msg, entries['fr'][2], entries['cc'][2], logact)
253
+ if ret == self.SUCCESS or not self.PGLOG['EMLSEND']: return ret
254
+ # try commandline sendmail
255
+ ret = self.pgsystem(self.PGLOG['EMLSEND'], logact, 4, emlmsg)
256
+ logmsg += "Email " + entries['to'][2]
257
+ if entries['cc'][2]: logmsg += " Cc'd " + entries['cc'][2]
258
+ logmsg += " Subject: " + entries['sb'][2]
259
+ if ret:
260
+ self.log_email(emlmsg)
261
+ self.pglog(logmsg, logact&(~self.EXITLG))
262
+ else:
263
+ errmsg = "Error sending email: " + logmsg
264
+ self.pglog(errmsg, (logact|self.ERRLOG)&~self.EXITLG)
265
+ return ret
266
+
267
+ # send an email; if empty msg send email message saved in self.PGLOG['EMLMSG'] instead
268
+ def send_email(self, subject = None, receiver = None, msg = None, sender = None, logact = None):
269
+ if logact is None: logact = self.LOGWRN
270
+ return self.send_python_email(subject, receiver, msg, sender, None, logact)
271
+
272
+ # send an email via python module smtplib; if empty msg send email message saved
273
+ # in self.PGLOG['EMLMSG'] instead. pass cc = '' for skipping 'Cc: '
274
+ def send_python_email(self, subject = None, receiver = None, msg = None, sender = None, cc = None, logact = None):
275
+ if logact is None: logact = self.LOGWRN
276
+ if not msg:
277
+ if self.PGLOG['EMLMSG']:
278
+ msg = self.PGLOG['EMLMSG']
279
+ self.PGLOG['EMLMSG'] = ''
280
+ else:
281
+ return ''
282
+ docc = False if cc else True
283
+ if not sender:
284
+ sender = self.PGLOG['CURUID']
285
+ if sender != self.PGLOG['GDEXUSER']: docc = False
286
+ if sender == self.PGLOG['GDEXUSER']: sender = self.PGLOG['GDEXEMAIL']
287
+ if sender.find('@') == -1: sender += "@ucar.edu"
288
+ if not receiver:
289
+ receiver = self.PGLOG['EMLADDR'] if self.PGLOG['EMLADDR'] else self.PGLOG['CURUID']
290
+ if receiver == self.PGLOG['GDEXUSER']: receiver = self.PGLOG['GDEXEMAIL']
291
+ if receiver.find('@') == -1: receiver += "@ucar.edu"
292
+ if docc and not re.match(self.PGLOG['GDEXUSER'], sender): self.add_carbon_copy(sender, 1)
293
+ emlmsg = EmailMessage()
294
+ emlmsg.set_content(msg)
295
+ emlmsg['From'] = sender
296
+ emlmsg['To'] = receiver
297
+ logmsg = "Email " + receiver
298
+ if cc == None: cc = self.PGLOG['CCDADDR']
299
+ if cc:
300
+ emlmsg['Cc'] = cc
301
+ logmsg += " Cc'd " + cc
302
+ if not subject: subject = "Message from {}-{}".format(self.PGLOG['HOSTNAME'], self.self.get_command())
303
+ # if not re.search(r'!$', subject): subject += '!'
304
+ emlmsg['Subject'] = subject
305
+ if self.CPID['CPID']: logmsg += " in " + self.CPID['CPID']
306
+ logmsg += ", Subject: {}\n".format(subject)
307
+ try:
308
+ eml = smtplib.SMTP(self.PGLOG['EMLSRVR'], self.PGLOG['EMLPORT'])
309
+ eml.send_message(emlmsg)
310
+ except smtplib.SMTPException as err:
311
+ errmsg = f"Error sending email:\n{err}\n{logmsg}"
312
+ return self.pglog(errmsg, (logact|self.ERRLOG)&~self.EXITLG)
313
+ finally:
314
+ eml.quit()
315
+ self.log_email(str(emlmsg))
316
+ self.pglog(logmsg, logact&~self.EXITLG)
317
+ return self.SUCCESS
318
+
319
+ # log email sent
320
+ def log_email(self, emlmsg):
321
+ if not self.CPID['PID']: self.CPID['PID'] = "{}-{}-{}".format(self.PGLOG['HOSTNAME'], self.get_command(), self.PGLOG['CURUID'])
322
+ cmdstr = "{} {} at {}\n".format(self.CPID['PID'], self.break_long_string(self.CPID['CMD'], 40, "...", 1), self.current_datetime())
323
+ fn = "{}/{}".format(self.PGLOG['LOGPATH'], self.PGLOG['EMLFILE'])
324
+ try:
325
+ f = open(fn, 'a')
326
+ f.write(cmdstr + emlmsg)
327
+ f.close()
328
+ except FileNotFoundError as e:
329
+ print(e)
330
+
331
+ # Function: cmdlog(cmdline)
332
+ # cmdline - program name and all arguments
333
+ # ctime - time (in seconds) when the command starts
334
+ def cmdlog(self, cmdline = None, ctime = 0, logact = None):
335
+ if logact is None: logact = self.MSGLOG|self.FRCLOG
336
+ if not ctime: ctime = int(time.time())
337
+ if not cmdline or re.match('(end|quit|exit|abort)', cmdline, re.I):
338
+ cmdline = cmdline.capitalize() if cmdline else "Ends"
339
+ cinfo = self.cmd_execute_time("{} {}".format(self.CPID['PID'], cmdline), (ctime - self.CPID['CTM'])) + ": "
340
+ if self.CPID['CPID']: cinfo += self.CPID['CPID'] + " <= "
341
+ cinfo += self.break_long_string(self.CPID['CMD'], 40, "...", 1)
342
+ if logact: self.pglog(cinfo, logact)
343
+ else:
344
+ cinfo = self.current_datetime(ctime)
345
+ if re.match(r'CPID \d+', cmdline):
346
+ self.CPID['PID'] = "{}({})-{}{}".format(self.PGLOG['HOSTNAME'], os.getpid(), self.PGLOG['CURUID'], cinfo)
347
+ if logact: self.pglog("{}: {}".format(self.CPID['PID'], cmdline), logact)
348
+ self.CPID['CPID'] = cmdline
349
+ elif self.CPID['PID'] and re.match(r'(starts|catches) ', cmdline):
350
+ if logact: self.pglog("{}: {} at {}".format(self.CPID['PID'], cmdline, cinfo), logact)
351
+ else:
352
+ self.CPID['PID'] = "{}({})-{}{}".format(self.PGLOG['HOSTNAME'], os.getpid(), self.PGLOG['CURUID'], cinfo)
353
+ if logact: self.pglog("{}: {}".format(self.CPID['PID'], cmdline), logact)
354
+ self.CPID['CMD'] = cmdline
355
+ self.CPID['CTM'] = ctime
356
+
357
+ # Function: self.pglog(msg, logact) return self.FAILURE or log message if not exit
358
+ # msg -- message to log
359
+ # locact -- logging actions: MSGLOG, WARNLG, ERRLOG, EXITLG, EMLLOG, & SNDEML
360
+ # log and display message/error and exit program according logact value
361
+ def pglog(self, msg, logact = None):
362
+ if logact is None: logact = self.MSGLOG
363
+ retmsg = None
364
+ logact &= self.PGLOG['LOGMASK'] # filtering the log actions
365
+ if logact&self.RCDMSG: logact |= self.MSGLOG
366
+ if self.PGLOG['NOQUIT']: logact &= ~self.EXITLG
367
+ if logact&self.EMEROL:
368
+ if logact&self.EMLLOG: logact &= ~self.EMLLOG
369
+ if not logact&self.ERRLOG: logact &= ~self.EMEROL
370
+ msg = msg.lstrip() if msg else '' # remove leading whitespaces for logging message
371
+ if logact&self.EXITLG:
372
+ ext = "Exit 1 in {}\n".format(os.getcwd())
373
+ if msg: msg = msg.rstrip() + "; "
374
+ msg += ext
375
+ else:
376
+ if msg and not re.search(r'(\n|\r)$', msg): msg += "\n"
377
+ if logact&self.RETMSG: retmsg = msg
378
+ if logact&self.EMLALL:
379
+ if logact&self.SNDEML or not msg:
380
+ title = (msg if msg else "Message from {}-{}".format(self.PGLOG['HOSTNAME'], self.get_command()))
381
+ msg = title
382
+ self.send_email(title.rstrip())
383
+ elif msg:
384
+ self.set_email(msg, logact)
385
+ if not msg: return (retmsg if retmsg else self.FAILURE)
386
+ if logact&self.EXITLG and (self.PGLOG['EMLMSG'] or self.PGLOG['SUMMSG'] or self.PGLOG['ERRMSG'] or self.PGLOG['PRGMSG']):
387
+ if not logact&self.EMLALL: self.set_email(msg, logact)
388
+ title = "ABORTS {}-{}".format(self.PGLOG['HOSTNAME'], self.get_command())
389
+ self.set_email((("ABORTS " + self.CPID['PID']) if self.CPID['PID'] else title), self.EMLTOP)
390
+ msg = title + '\n' + msg
391
+ self.send_email(title)
392
+ if logact&self.LOGERR: # make sure error is always logged
393
+ msg = self.break_long_string(msg)
394
+ if logact&(self.ERRLOG|self.EXITLG):
395
+ cmdstr = self.get_error_command(int(time.time()), logact)
396
+ msg = cmdstr + msg
397
+ if not logact&self.NOTLOG:
398
+ if logact&self.ERRLOG:
399
+ if not self.PGLOG['ERRFILE']: self.PGLOG['ERRFILE'] = re.sub(r'.log$', '.err', self.PGLOG['LOGFILE'])
400
+ self.write_message(msg, f"{self.PGLOG['LOGPATH']}/{self.PGLOG['ERRFILE']}", logact)
401
+ if logact&self.EXITLG:
402
+ self.write_message(cmdstr, f"{self.PGLOG['LOGPATH']}/{self.PGLOG['LOGFILE']}", logact)
403
+ else:
404
+ self.write_message(msg, f"{self.PGLOG['LOGPATH']}/{self.PGLOG['LOGFILE']}", logact)
405
+ if not self.PGLOG['BCKGRND'] and logact&(self.ERRLOG|self.WARNLG):
406
+ self.write_message(msg, None, logact)
407
+
408
+ if logact&self.EXITLG:
409
+ self.pgexit(1)
410
+ else:
411
+ return (retmsg if retmsg else self.FAILURE)
412
+
413
+ # write a log message
414
+ def write_message(self, msg, file, logact):
415
+ doclose = False
416
+ errlog = logact&self.ERRLOG
417
+ if file:
418
+ try:
419
+ OUT = open(file, 'a')
420
+ doclose = True
421
+ except FileNotFoundError:
422
+ OUT = sys.stderr if logact&(self.ERRLOG|self.EXITLG) else sys.stdout
423
+ OUT.write(f"Log File not found: {file}")
424
+ else:
425
+ OUT = sys.stderr if logact&(self.ERRLOG|self.EXITLG) else sys.stdout
426
+ if logact&self.BRKLIN: OUT.write("\n")
427
+ if logact&self.SEPLIN: OUT.write(self.PGLOG['SEPLINE'])
428
+ OUT.write(msg)
429
+ if errlog and file and not logact&(self.EMLALL|self.SKPTRC): OUT.write(self.get_call_trace())
430
+ if doclose: OUT.close()
431
+
432
+ # check and disconnet database before exit
433
+ def pgexit(self, stat = 0):
434
+ if self.PGLOG['PGDBBUF']: self.PGLOG['PGDBBUF'].close()
435
+ sys.exit(stat)
436
+
437
+ # get a command string for error log dump
438
+ def get_error_command(self, ctime, logact):
439
+ if not self.CPID['PID']: self.CPID['PID'] = "{}-{}-{}".format(self.PGLOG['HOSTNAME'], self.get_command(), self.PGLOG['CURUID'])
440
+ cmdstr = "{} {}".format((("ABORTS" if logact&self.ERRLOG else "QUITS") if logact&self.EXITLG else "ERROR"), self.CPID['PID'])
441
+ cmdstr = self.cmd_execute_time(cmdstr, (ctime - self.CPID['CTM']))
442
+ if self.CPID['CPID']: cmdstr += " {} <=".format(self.CPID['CPID'])
443
+ cmdstr += " {} at {}\n".format(self.break_long_string(self.CPID['CMD'], 40, "...", 1), self.current_datetime(ctime))
444
+ return cmdstr
445
+
446
+ # get call trace track
447
+ @staticmethod
448
+ def get_call_trace(cut = 1):
449
+ t = traceback.extract_stack()
450
+ n = len(t) - cut
451
+ str = ''
452
+ sep = 'Trace: '
453
+ for i in range(n):
454
+ tc = t[i]
455
+ str += "{}{}({}){}".format(sep, tc[0], tc[1], ("" if tc[2] == '<module>' else "{%s()}" % tc[2]))
456
+ if i == 0: sep = '=>'
457
+ return str + "\n" if str else ""
458
+
459
+ # get caller file name
460
+ @staticmethod
461
+ def get_caller_file(cidx = 0):
462
+ return traceback.extract_stack()[cidx][0]
463
+
464
+ # log message, msg, for degugging processes according to the debug level
465
+ def pgdbg(self, level, msg = None, do_trace = True):
466
+ if not self.PGLOG['DBGLEVEL']: return # no further action
467
+ if not isinstance(level, int):
468
+ ms = re.match(r'^(\d+)', level)
469
+ level = int(ms.group(1)) if ms else 0
470
+ levels = [0, 0]
471
+ if isinstance(self.PGLOG['DBGLEVEL'], int):
472
+ levels[1] = self.PGLOG['DBGLEVEL']
473
+ else:
474
+ ms = re.match(r'^(\d+)$', self.PGLOG['DBGLEVEL'])
475
+ if ms:
476
+ levels[1] = int(ms.group(1))
477
+ else:
478
+ ms = re.match(r'(\d*)-(\d*)', self.PGLOG['DBGLEVEL'])
479
+ if ms:
480
+ levels[0] = int(ms.group(1)) if ms.group(1) else 0
481
+ levels[1] = int(ms.group(2)) if ms.group(2) else 9999
482
+ if level > levels[1] or level < levels[0]: return # debug level is out of range
483
+ if 'DBGPATH' in self.PGLOG:
484
+ dfile = self.PGLOG['DBGPATH'] + '/' + self.PGLOG['DBGFILE']
485
+ else:
486
+ dfile = self.PGLOG['DBGFILE']
487
+ if not msg:
488
+ self.pglog("Append debug Info (levels {}-{}) to {}".format(levels[0], levels[1], dfile), self.WARNLG)
489
+ msg = "DEBUG for " + self.CPID['PID'] + " "
490
+ if self.CPID['CPID']: msg += self.CPID['CPID'] + " <= "
491
+ msg += self.break_long_string(self.CPID['CMD'], 40, "...", 1)
492
+ # logging debug info
493
+ DBG = open(dfile, 'a')
494
+ DBG.write("{}:{}\n".format(level, msg))
495
+ if do_trace: DBG.write(self.get_call_trace())
496
+ DBG.close()
497
+
498
+ # return trimed string (strip leading and trailling spaces); remove comments led by '#' if rmcmt > 0
499
+ @staticmethod
500
+ def pgtrim(line, rmcmt = 1):
501
+ if line:
502
+ if rmcmt:
503
+ if re.match(r'^\s*#', line): # comment line
504
+ line = ''
505
+ elif rmcmt > 1:
506
+ ms = re.search(r'^(.+)\s\s+\#', line)
507
+ if ms: line = ms.group(1) # remove comment and its leading whitespaces
508
+ else:
509
+ ms = re.search(r'^(.+)\s+\#', line)
510
+ if ms: line = ms.group(1) # remove comment and its leading whitespace
511
+ line = line.strip() # remove leading and trailing whitespaces
512
+ return line
513
+
514
+ # set self.PGLOG['PUSGDIR'] from the program file with full path
515
+ def set_help_path(self, progfile):
516
+ self.PGLOG['PUSGDIR'] = op.dirname(op.abspath(progfile))
517
+
518
+ # Function: show_usage(progname: Perl program name to get file "progname.usg")
519
+ # show program usage in file "self.PGLOG['PUSGDIR']/progname.usg" on screen with unix
520
+ # system function 'pg', exit program when done.
521
+ def show_usage(self, progname, opts = None):
522
+ if self.PGLOG['PUSGDIR'] is None: self.set_help_path(self.get_caller_file(1))
523
+ usgname = self.join_paths(self.PGLOG['PUSGDIR'], progname + '.usg')
524
+ if opts: # show usage for individual option of dsarch
525
+ for opt in opts:
526
+ if opts[opt][0] == 0:
527
+ msg = "Mode"
528
+ elif opts[opt][0] == 1:
529
+ msg = "Single-Value Information"
530
+ elif opts[opt][0] == 2:
531
+ msg = "Multi-Value Information"
532
+ else:
533
+ msg = "Action"
534
+ sys.stdout.write("\nDescription of {} Option -{}:\n".format(msg, opt))
535
+ IN = open(usgname, 'r')
536
+ nilcnt = begin = 0
537
+ for line in IN:
538
+ if begin == 0:
539
+ rx = " -{} or -".format(opt)
540
+ if re.match(rx, line): begin = 1
541
+ elif re.match(r'^\s*$', line):
542
+ if nilcnt: break
543
+ nilcnt = 1
544
+ else:
545
+ if re.match(r'\d[\.\s\d]', line): break # section title
546
+ if nilcnt and re.match(r' -\w\w or -', line): break
547
+ nilcnt = 0
548
+ if begin: sys.stdout.write(line)
549
+ IN.close()
550
+ else:
551
+ os.system("more " + usgname)
552
+ self.pgexit(0)
553
+
554
+ # compare error message to patterns saved in self.PGLOG['ERR2STD']
555
+ # return 1 if matched; 0 otherwise
556
+ def err2std(self, line):
557
+ for err in self.PGLOG['ERR2STD']:
558
+ if line.find(err) > -1: return 1
559
+ return 0
560
+
561
+ # compare message to patterns saved in self.PGLOG['STD2ERR']
562
+ # return 1 if matched; 0 otherwise
563
+ def std2err(self, line):
564
+ for out in self.PGLOG['STD2ERR']:
565
+ if line.find(out) > -1: return 1
566
+ return 0
567
+
568
+ # Function: pgsystem(pgcmd, logact, cmdopt, instr)
569
+ # pgcmd - Linux system command, can be a string, "ls -l", or a list, ['ls', '-l']
570
+ # logact - logging action option, defaults to self.LOGWRN
571
+ # cmdopt - command control option, default to 5 (1+4)
572
+ # 0 - no command control,
573
+ # 1 - log pgcmd (include the sub command calls),
574
+ # 2 - log standard output,
575
+ # 4 - log error output
576
+ # 7 - log all (pgcmd, and standard/error outputs),
577
+ # 8 - log command with time,
578
+ # 16 - return standard output message upon success
579
+ # 32 - log error as standard output
580
+ # 64 - force returning self.FAILURE if called process aborts
581
+ # 128 - tries 2 times for failed command before quits
582
+ # 256 - cache standard error message
583
+ # 512 - log instr & seconds with pgcmd if cmdopt&1
584
+ # 1024 - turn on shell
585
+ # instr - input string passing to the command via stdin if not None
586
+ # seconds - number of seconds to wait for a timeout process if > 0
587
+ def pgsystem(self, pgcmd, logact = None, cmdopt = 5, instr = None, seconds = 0):
588
+ if logact is None: logact = self.LOGWRN
589
+ ret = self.SUCCESS
590
+ if not pgcmd: return ret # empty command
591
+ act = logact&~self.EXITLG
592
+ if act&self.ERRLOG:
593
+ act &= ~self.ERRLOG
594
+ act |= self.WARNLG
595
+ if act&self.MSGLOG: act |= self.FRCLOG # make sure system calls always logged
596
+ cmdact = act if cmdopt&1 else 0
597
+ doshell = True if cmdopt&1024 else self.PGLOG['DOSHELL']
598
+ if isinstance(pgcmd, str):
599
+ cmdstr = pgcmd
600
+ if not doshell and re.search(r'[*?<>|;]', pgcmd): doshell = True
601
+ execmd = pgcmd if doshell else shlex.split(pgcmd)
602
+ else:
603
+ cmdstr = shlex.join(pgcmd)
604
+ execmd = cmdstr if doshell else pgcmd
605
+ if cmdact:
606
+ if cmdopt&8:
607
+ self.cmdlog("starts '{}'".format(cmdstr), None, cmdact)
608
+ else:
609
+ self.pglog("> " + cmdstr, cmdact)
610
+ if cmdopt&512 and (instr or seconds):
611
+ msg = ''
612
+ if seconds: msg = 'Timeout = {} Seconds'.format(seconds)
613
+ if instr: msg += ' With STDIN:\n' + instr
614
+ if msg: self.pglog(msg, cmdact)
615
+ stdlog = act if cmdopt&2 else 0
616
+ cmdflg = cmdact|stdlog
617
+ abort = -1 if cmdopt&64 else 0
618
+ loops = 2 if cmdopt&128 else 1
619
+ self.PGLOG['SYSERR'] = error = retbuf = outbuf = errbuf = ''
620
+ for loop in range(1, loops+1):
621
+ last = time.time()
622
+ try:
623
+ if instr:
624
+ FD = Popen(execmd, shell=doshell, stdout=PIPE, stderr=PIPE, stdin=PIPE)
625
+ if seconds:
626
+ outbuf, errbuf = FD.communicate(input=instr.encode(), timeout=seconds)
627
+ else:
628
+ outbuf, errbuf = FD.communicate(input=instr.encode())
629
+ else:
630
+ FD = Popen(execmd, shell=doshell, stdout=PIPE, stderr=PIPE)
631
+ if seconds:
632
+ outbuf, errbuf = FD.communicate(timeout=seconds)
633
+ else:
634
+ outbuf, errbuf = FD.communicate()
635
+ except TimeoutError as e:
636
+ errbuf = str(e)
637
+ FD.kill()
638
+ ret = self.FAILURE
639
+ except Exception as e:
640
+ errbuf = str(e)
641
+ ret = self.FAILURE
642
+ else:
643
+ ret = self.FAILURE if FD.returncode else self.SUCCESS
644
+ if isinstance(outbuf, bytes): outbuf = str(outbuf, errors='replace')
645
+ if isinstance(errbuf, bytes): errbuf = str(errbuf, errors='replace')
646
+ if errbuf and cmdopt&32:
647
+ outbuf += errbuf
648
+ if cmdopt&256: self.PGLOG['SYSERR'] = errbuf
649
+ errbuf = ''
650
+ if outbuf:
651
+ lines = outbuf.split('\n')
652
+ for line in lines:
653
+ line = self.strip_output_line(line.strip())
654
+ if not line: continue
655
+ if self.PGLOG['STD2ERR'] and self.std2err(line):
656
+ if cmdopt&260: error += line + "\n"
657
+ if abort == -1 and re.match('ABORTS ', line): abort = 1
658
+ else:
659
+ if re.match(r'^>+ ', line):
660
+ line = '>' + line
661
+ if cmdflg: self.pglog(line, cmdflg)
662
+ elif stdlog:
663
+ self.pglog(line, stdlog)
664
+ if cmdopt&16: retbuf += line + "\n"
665
+ if errbuf:
666
+ lines = errbuf.split('\n')
667
+ for line in lines:
668
+ line = self.strip_output_line(line.strip())
669
+ if not line: continue
670
+ if self.PGLOG['ERR2STD'] and self.err2std(line):
671
+ if stdlog: self.pglog(line, stdlog)
672
+ if cmdopt&16: retbuf += line + "\n"
673
+ else:
674
+ if cmdopt&260: error += line + "\n"
675
+ if abort == -1 and re.match('ABORTS ', line): abort = 1
676
+ if ret == self.SUCCESS and abort == 1: ret = self.FAILURE
677
+ end = time.time()
678
+ last = end - last
679
+ if error:
680
+ if ret == self.FAILURE:
681
+ error = "Error Execute: {}\n{}".format(cmdstr, error)
682
+ else:
683
+ error = "Error From: {}\n{}".format(cmdstr, error)
684
+ if loop > 1: error = "Retry "
685
+ if cmdopt&256: self.PGLOG['SYSERR'] += error
686
+ if cmdopt&4:
687
+ errlog = (act|self.ERRLOG)
688
+ if ret == self.FAILURE and loop >= loops: errlog |= logact
689
+ self.pglog(error, errlog)
690
+ if last > self.PGLOG['CMDTIME'] and not re.search(r'(^|/|\s)(dsarch|dsupdt|dsrqst)\s', cmdstr):
691
+ cmdstr = "> {} Ends By {}".format(self.break_long_string(cmdstr, 100, "...", 1), self.current_datetime())
692
+ self.cmd_execute_time(cmdstr, last, cmdact)
693
+ if ret == self.SUCCESS or loop >= loops: break
694
+ time.sleep(6)
695
+ if ret == self.FAILURE and retbuf and cmdopt&272 == 272:
696
+ if self.PGLOG['SYSERR']: self.PGLOG['SYSERR'] += '\n'
697
+ self.PGLOG['SYSERR'] += retbuf
698
+ retbuf = ''
699
+ return (retbuf if cmdopt&16 else ret)
700
+
701
+ # strip carrage return '\r', but keep ending newline '\n'
702
+ @staticmethod
703
+ def strip_output_line(line):
704
+ ms = re.search(r'\r([^\r]+)\r*$', line)
705
+ if ms: return ms.group(1)
706
+ ms = re.search(r'\s\.+\s+(\d+)%\s+', line)
707
+ if ms and int(ms.group(1)) != 100: return None
708
+ return line
709
+
710
+ # show command running time string formated by seconds_to_string_time()
711
+ def cmd_execute_time(self, cmdstr, last, logact = None):
712
+ msg = cmdstr
713
+ if last >= self.PGLOG['CMDTIME']: # show running for at least one minute
714
+ msg += " ({})".format(self.seconds_to_string_time(last))
715
+ if logact:
716
+ return self.pglog(msg, logact)
717
+ else:
718
+ return msg
719
+
720
+ # convert given seconds to string time with units of S-Second,M-Minute,H-Hour,D-Day
721
+ @staticmethod
722
+ def seconds_to_string_time(seconds, showzero = 0):
723
+ msg = ''
724
+ s = m = h = 0
725
+ if seconds > 0:
726
+ s = seconds%60 # seconds (0-59)
727
+ minutes = int(seconds/60) # total minutes
728
+ m = minutes%60 # minutes (0-59)
729
+ if minutes >= 60:
730
+ hours = int(minutes/60) # total hours
731
+ h = hours%24 # hours (0-23)
732
+ if hours >= 24:
733
+ msg += "{}D".format(int(hours/24)) # days
734
+ if h: msg += "{}H".format(h)
735
+ if m: msg += "{}M".format(m)
736
+ if s:
737
+ msg += "%dS"%(s) if isinstance(s, int) else "{:.3f}S".format(s)
738
+ elif showzero:
739
+ msg = "0S"
740
+ return msg
741
+
742
+ # wrap function to call pgsystem() with a timeout control
743
+ # return self.FAILURE if error eval or time out
744
+ def tosystem(self, cmd, timeout = 0, logact = 0, cmdopt = 5, instr = None):
745
+ if logact is None: logact = self.LOGWRN
746
+ if not timeout: timeout = self.PGLOG['TIMEOUT'] # set default timeout if missed
747
+ return self.pgsystem(cmd, logact, cmdopt, instr, timeout)
748
+
749
+ # insert breaks, default to '\n', for every length, default to 1024,
750
+ # for long string; return specified number lines if mline given
751
+ @staticmethod
752
+ def break_long_string(lstr, limit = 1024, bsign = "\n", mline = 200, bchars = ' &;', minlmt = 20, eline = 0):
753
+ length = len(lstr) if lstr else 0
754
+ if length <= limit: return lstr
755
+ if bsign is None: bsign = "\n"
756
+ if bchars is None: bchars = ' &;'
757
+ addbreak = offset = 0
758
+ retstr = ""
759
+ elines = []
760
+ if eline > mline: eline = mline
761
+ mcnt = mline - eline
762
+ ecnt = 0
763
+ while offset < length:
764
+ bpos = lstr[offset:].find(bsign)
765
+ blen = bpos if bpos > -1 else (length - offset)
766
+ if blen == 0:
767
+ offset += 1
768
+ substr = "" if addbreak else bsign
769
+ addbreak = 0
770
+ elif blen <= limit:
771
+ blen += 1
772
+ substr = lstr[offset:(offset+blen)]
773
+ offset += blen
774
+ addbreak = 0
775
+ else:
776
+ substr = lstr[offset:(offset+limit)]
777
+ bpos = limit - 1
778
+ while bpos > minlmt:
779
+ char = substr[bpos]
780
+ if bchars.find(char) >= 0: break
781
+ bpos -= 1
782
+ if bpos > minlmt:
783
+ bpos += 1
784
+ substr = substr[:bpos]
785
+ offset += bpos
786
+ else:
787
+ offset += limit
788
+ addbreak = 1
789
+ substr += bsign
790
+ if mcnt:
791
+ retstr += substr
792
+ mcnt -= 1
793
+ if mcnt == 0 and eline == 0: break
794
+ elif eline > 0:
795
+ elines.append(substr)
796
+ ecnt += 1
797
+ else:
798
+ break
799
+ if ecnt > 0:
800
+ if ecnt > eline:
801
+ retstr += "..." + bsign
802
+ mcnt = ecnt - eline
803
+ else:
804
+ mcnt = 0
805
+ while mcnt < ecnt:
806
+ retstr += elines[mcnt]
807
+ mcnt += 1
808
+ return retstr
809
+
810
+ # join two paths by remove overlapping directories
811
+ # diff = 0: join given pathes
812
+ # 1: remove path1 from path2
813
+ @staticmethod
814
+ def join_paths(path1, path2, diff = 0):
815
+ if not path2: return path1
816
+ if not path1 or not diff and re.match('/', path2): return path2
817
+ if diff:
818
+ ms = re.match(r'{}/(.*)'.format(path1), path2)
819
+ if ms: return ms.group(1)
820
+ adir1 = path1.split('/')
821
+ adir2 = path2.split('/')
822
+ while adir2 and not adir2[0]: adir2.pop(0)
823
+ while adir1 and adir2 and adir2[0] == "..":
824
+ adir2.pop(0)
825
+ adir1.pop()
826
+ while adir2 and adir2[0] == ".": adir2.pop(0)
827
+ if adir1 and adir2:
828
+ len1 = len(adir1)
829
+ len2 = len(adir2)
830
+ idx1 = len1-1
831
+ idx2 = mcnt = 0
832
+ while idx2 < len1 and idx2 < len2:
833
+ if adir1[idx1] == adir2[idx2]:
834
+ mcnt = 1
835
+ break
836
+ idx2 += 1
837
+ if mcnt > 0:
838
+ while mcnt <= idx2:
839
+ if adir1[idx1-mcnt] != adir2[idx2-mcnt]: break
840
+ mcnt += 1
841
+ if mcnt > idx2: # remove mcnt matching directories
842
+ while mcnt > 0:
843
+ adir2.pop(0)
844
+ mcnt -= 1
845
+ if diff:
846
+ return '/'.join(adir2)
847
+ else:
848
+ return '/'.join(adir1 + adir2)
849
+
850
+ # validate if a command for a given BATCH host is accessable and executable
851
+ # Return self.SUCCESS if valid; self.FAILURE if not
852
+ def valid_batch_host(self, host, logact = 0):
853
+ HOST = host.upper()
854
+ return self.SUCCESS if HOST in self.BCHCMDS and self.valid_command(self.BCHCMDS[HOST], logact) else self.FAILURE
855
+
856
+ # validate if a given command is accessable and executable
857
+ # Return the full command path if valid; '' if not
858
+ def valid_command(self, cmd, logact = 0):
859
+ ms = re.match(r'^(\S+)( .*)$', cmd)
860
+ if ms:
861
+ option = ms.group(2)
862
+ cmd = ms.group(1)
863
+ else:
864
+ option = ''
865
+ if cmd not in self.COMMANDS:
866
+ buf = shutil.which(cmd)
867
+ if buf is None:
868
+ if logact: self.pglog(cmd + ": executable command not found", logact)
869
+ buf = ''
870
+ elif option:
871
+ buf += option
872
+ self.COMMANDS[cmd] = buf
873
+ return self.COMMANDS[cmd]
874
+
875
+ # add carbon copies to self.PGLOG['CCDADDR']
876
+ def add_carbon_copy(self, cc = None, isstr = None, exclude = 0, specialist = None):
877
+
878
+ if not cc:
879
+ if cc is None and isstr is None: self.PGLOG['CCDADDR'] = ''
880
+ else:
881
+ emails = re.split(r'[,\s]+', cc) if isstr else cc
882
+ for email in emails:
883
+ if not email or email.find('/') >= 0 or email == 'N': continue
884
+ if email == "S":
885
+ if not specialist: continue
886
+ email = specialist
887
+ if email.find('@') == -1: email += "@ucar.edu"
888
+ if exclude and exclude.find(email) > -1: continue
889
+ if self.PGLOG['CCDADDR']:
890
+ if self.PGLOG['CCDADDR'].find(email) > -1: continue # email Cc'd already
891
+ self.PGLOG['CCDADDR'] += ", "
892
+ self.PGLOG['CCDADDR'] += email
893
+
894
+ # get the current host name; or batch sever name if getbatch is 1
895
+ def get_host(self, getbatch = 0):
896
+
897
+ if getbatch and self.PGLOG['CURBID'] != 0:
898
+ host = self.PGLOG['PGBATCH']
899
+ elif self.PGLOG['HOSTNAME']:
900
+ return self.PGLOG['HOSTNAME']
901
+ else:
902
+ host = socket.gethostname()
903
+
904
+ return self.get_short_host(host)
905
+
906
+ #
907
+ # strip domain names and retrun the server name itself
908
+ #
909
+ def get_short_host(self, host):
910
+
911
+ if not host: return ''
912
+ ms = re.match(r'^([^\.]+)\.', host)
913
+ if ms: host = ms.group(1)
914
+ if self.PGLOG['HOSTNAME'] and (host == 'localhost' or host == self.PGLOG['HOSTNAME']): return self.PGLOG['HOSTNAME']
915
+ HOST = host.upper()
916
+ if HOST in self.BCHCMDS: return HOST
917
+
918
+ return host
919
+
920
+ # get a live PBS host name
921
+ def get_pbs_host(self):
922
+ if not self.PBSSTATS and self.PGLOG['PBSHOSTS']:
923
+ self.PBSHOSTS = self.PGLOG['PBSHOSTS'].split(':')
924
+ for host in self.PBSHOSTS:
925
+ self.PBSSTATS[host] = 1
926
+ for host in self.PBSHOSTS:
927
+ if host in self.PBSSTATS and self.PBSSTATS[host]: return host
928
+ return None
929
+
930
+ # set host status, 0 dead & 1 live, for one or all avalaible pbs hosts
931
+ def set_pbs_host(self, host = None, stat = 0):
932
+ if host:
933
+ self.PBSSTATS[host] = stat
934
+ else:
935
+ if not self.PBSHOSTS and self.PGLOG['PBSHOSTS']:
936
+ self.PBSHOSTS = self.PGLOG['PBSHOSTS'].split(':')
937
+ for host in self.PBSHOSTS:
938
+ self.PBSSTATS[host] = stat
939
+
940
+ # reset the batch host name in case was not set properly
941
+ def reset_batch_host(self, bhost, logact = None):
942
+ if logact is None: logact = self.LOGWRN
943
+ bchhost = bhost.upper()
944
+ if bchhost != self.PGLOG['PGBATCH']:
945
+ if self.PGLOG['CURBID'] > 0:
946
+ self.pglog("{}-{}: Batch ID is set, cannot change Batch host to {}".format(self.PGLOG['PGBATCH'], self.PGLOG['CURBID'], bchhost) , logact)
947
+ else:
948
+ ms = re.search(r'(^|:){}(:|$)'.format(bchhost), self.PGLOG['BCHHOSTS'])
949
+ if ms:
950
+ self.PGLOG['PGBATCH'] = bchhost
951
+ if self.PGLOG['CURBID'] == 0: self.PGLOG['CURBID'] = -1
952
+ elif self.PGLOG['PGBATCH']:
953
+ self.PGLOG['PGBATCH'] = ''
954
+ self.PGLOG['CURBID'] = 0
955
+
956
+ # return the base command name of the current process
957
+ @staticmethod
958
+ def get_command(cmdstr = None):
959
+ if not cmdstr: cmdstr = sys.argv[0]
960
+ cmdstr = op.basename(cmdstr)
961
+ ms = re.match(r'^(.+)\.(py|pl)$', cmdstr)
962
+ if ms:
963
+ return ms.group(1)
964
+ else:
965
+ return cmdstr
966
+
967
+ # wrap a given command cmd for either sudo or setuid wrapper pgstart_['username']
968
+ # to run as user asuser
969
+ def get_local_command(self, cmd, asuser = None):
970
+ cuser = self.PGLOG['SETUID'] if self.PGLOG['SETUID'] else self.PGLOG['CURUID']
971
+ if not asuser or cuser == asuser: return cmd
972
+ if cuser == self.PGLOG['GDEXUSER']:
973
+ wrapper = "pgstart_" + asuser
974
+ if self.valid_command(wrapper): return "{} {}".format(wrapper, cmd)
975
+ elif self.PGLOG['SUDOGDEX'] and asuser == self.PGLOG['GDEXUSER']:
976
+ return "sudo -u {} {}".format(self.PGLOG['GDEXUSER'], cmd) # sudo as user gdexdata
977
+ return cmd
978
+
979
+ # wrap a given command cmd for either sudo or setuid wrapper pgstart_['username']
980
+ # to run as user asuser on a given remote host
981
+ def get_remote_command(self, cmd, host, asuser = None):
982
+ return self.get_local_command(cmd, asuser)
983
+
984
+ # wrap a given sync command for given host name with/without sudo
985
+ def get_sync_command(self, host, asuser = None):
986
+ host = self.get_short_host(host)
987
+ if (not (self.PGLOG['SETUID'] and self.PGLOG['SETUID'] == self.PGLOG['GDEXUSER']) and
988
+ (not asuser or asuser == self.PGLOG['GDEXUSER'])):
989
+ return "sync" + host
990
+ return host + "-sync"
991
+
992
+ # set self.PGLOG['SETUID'] as needed
993
+ def set_suid(self, cuid = 0):
994
+ if not cuid: cuid = self.PGLOG['EUID']
995
+ if cuid != self.PGLOG['EUID'] or cuid != self.PGLOG['RUID']:
996
+ os.setreuid(cuid, cuid)
997
+ self.PGLOG['SETUID'] = pwd.getpwuid(cuid).pw_name
998
+ if not (self.PGLOG['SETUID'] == self.PGLOG['GDEXUSER'] or cuid == self.PGLOG['RUID']):
999
+ self.set_specialist_environments(self.PGLOG['SETUID'])
1000
+ self.PGLOG['CURUID'] == self.PGLOG['SETUID'] # set CURUID to a specific specialist
1001
+
1002
+ # set comman pglog
1003
+ def set_common_pglog(self):
1004
+ self.PGLOG['CURDIR'] = os.getcwd()
1005
+ # set current user id
1006
+ self.PGLOG['RUID'] = os.getuid()
1007
+ self.PGLOG['EUID'] = os.geteuid()
1008
+ self.PGLOG['CURUID'] = pwd.getpwuid(self.PGLOG['RUID']).pw_name
1009
+ try:
1010
+ self.PGLOG['RDAUID'] = self.PGLOG['GDEXUID'] = pwd.getpwnam(self.PGLOG['GDEXUSER']).pw_uid
1011
+ self.PGLOG['RDAGID'] = self.PGLOG['GDEXGID'] = grp.getgrnam(self.PGLOG['GDEXGRP']).gr_gid
1012
+ except:
1013
+ self.PGLOG['RDAUID'] = self.PGLOG['GDEXUID'] = 0
1014
+ self.PGLOG['RDAGID'] = self.PGLOG['GDEXGID'] = 0
1015
+ if self.PGLOG['CURUID'] == self.PGLOG['GDEXUSER']: self.PGLOG['SETUID'] = self.PGLOG['GDEXUSER']
1016
+ self.PGLOG['HOSTNAME'] = self.get_host()
1017
+ for htype in self.HOSTTYPES:
1018
+ ms = re.match(r'^{}(-|\d|$)'.format(htype), self.PGLOG['HOSTNAME'])
1019
+ if ms:
1020
+ self.PGLOG['HOSTTYPE'] = self.HOSTTYPES[htype]
1021
+ break
1022
+ self.PGLOG['DEFDSID'] = 'd000000' if self.PGLOG['NEWDSID'] else 'ds000.0'
1023
+ self.SETPGLOG("USRHOME", "/glade/u/home")
1024
+ self.SETPGLOG("DSSHOME", "/glade/u/home/gdexdata")
1025
+ self.SETPGLOG("GDEXHOME", "/data/local")
1026
+ self.SETPGLOG("ADDPATH", "")
1027
+ self.SETPGLOG("ADDLIB", "")
1028
+ self.SETPGLOG("OTHPATH", "")
1029
+ self.SETPGLOG("PSQLHOME", "")
1030
+ self.SETPGLOG("DSGHOSTS", "")
1031
+ self.SETPGLOG("DSIDCHRS", "d")
1032
+ if not os.getenv('HOME'): os.environ['HOME'] = "{}/{}".format(self.PGLOG['USRHOME'], self.PGLOG['CURUID'])
1033
+ self.SETPGLOG("HOMEBIN", os.environ.get('HOME') + "/bin")
1034
+ if 'PBS_JOBID' in os.environ:
1035
+ sbid = os.getenv('PBS_JOBID')
1036
+ ms = re.match(r'^(\d+)', sbid)
1037
+ self.PGLOG['CURBID'] = int(ms.group(1)) if ms else -1
1038
+ self.PGLOG['PGBATCH'] = self.PGLOG['PBSNAME']
1039
+ else:
1040
+ self.PGLOG['CURBID'] = 0
1041
+ self.PGLOG['PGBATCH'] = ''
1042
+ pgpath = self.PGLOG['HOMEBIN']
1043
+ self.PGLOG['LOCHOME'] = "/ncar/rda/setuid"
1044
+ if not op.isdir(self.PGLOG['LOCHOME']): self.PGLOG['LOCHOME'] = "/usr/local/decs"
1045
+ pgpath += ":{}/bin".format(self.PGLOG['LOCHOME'])
1046
+ locpath = "{}/bin/{}".format(self.PGLOG['DSSHOME'], self.PGLOG['HOSTTYPE'])
1047
+ if op.isdir(locpath): pgpath += ":" + locpath
1048
+ pgpath = self.add_local_path("{}/bin".format(self.PGLOG['DSSHOME']), pgpath, 1)
1049
+ if self.PGLOG['PSQLHOME']:
1050
+ locpath = self.PGLOG['PSQLHOME'] + "/bin"
1051
+ if op.isdir(locpath): pgpath += ":" + locpath
1052
+ pgpath = self.add_local_path(os.getenv('PATH'), pgpath, 1)
1053
+ if self.PGLOG['HOSTTYPE'] == 'dav': pgpath = self.add_local_path('/glade/u/apps/opt/qstat-cache/bin:/opt/pbs/bin', pgpath, 1)
1054
+ if 'OTHPATH' in self.PGLOG and self.PGLOG['OTHPATH']:
1055
+ pgpath = self.add_local_path(self.PGLOG['OTHPATH'], pgpath, 1)
1056
+ if self.PGLOG['ADDPATH']: pgpath = self.add_local_path(self.PGLOG['ADDPATH'], pgpath, 1)
1057
+ pgpath = self.add_local_path("/bin:/usr/bin:/usr/local/bin:/usr/sbin", pgpath, 1)
1058
+ os.environ['PATH'] = pgpath
1059
+ os.environ['SHELL'] = '/bin/sh'
1060
+ # set self.PGLOG values with environments and defaults
1061
+ self.SETPGLOG("DSSDBHM", self.PGLOG['DSSHOME']+"/dssdb") # dssdb home dir
1062
+ self.SETPGLOG("LOGPATH", self.PGLOG['DSSDBHM']+"/log") # path to log file
1063
+ self.SETPGLOG("LOGFILE", "pgdss.log") # log file name
1064
+ self.SETPGLOG("EMLFILE", "pgemail.log") # email log file name
1065
+ self.SETPGLOG("ERRFILE", '') # error file name
1066
+ sm = "/usr/sbin/sendmail"
1067
+ if self.valid_command(sm): self.SETPGLOG("EMLSEND", f"{sm} -t") # send email command
1068
+ self.SETPGLOG("DBGLEVEL", '') # debug level
1069
+ self.SETPGLOG("BAOTOKEN", 's.lh2t2kDjrqs3V8y2BU2zOocT') # OpenBao token
1070
+ self.SETPGLOG("DBGPATH", self.PGLOG['DSSDBHM']+"/log") # path to debug log file
1071
+ self.SETPGLOG("OBJCTBKT", "gdex-data") # default Bucket on Object Store
1072
+ self.SETPGLOG("BACKUPEP", "gdex-quasar") # default Globus Endpoint on Quasar
1073
+ self.SETPGLOG("DRDATAEP", "gdex-quasar-drdata") # DRDATA Globus Endpoint on Quasar
1074
+ self.SETPGLOG("DBGFILE", "pgdss.dbg") # debug file name
1075
+ self.SETPGLOG("CNFPATH", self.PGLOG['DSSHOME']+"/config") # path to configuration files
1076
+ self.SETPGLOG("DSSURL", "https://gdex.ucar.edu") # current dss web URL
1077
+ self.SETPGLOG("RQSTURL", "/datasets/request") # request URL path
1078
+ self.SETPGLOG("WEBSERVERS", "") # webserver names for Web server
1079
+ self.PGLOG['WEBHOSTS'] = self.PGLOG['WEBSERVERS'].split(':') if self.PGLOG['WEBSERVERS'] else []
1080
+ self.SETPGLOG("DBMODULE", '')
1081
+ self.SETPGLOG("LOCDATA", "/data")
1082
+ # set dss web homedir
1083
+ self.SETPGLOG("DSSWEB", self.PGLOG['LOCDATA']+"/web")
1084
+ self.SETPGLOG("DSWHOME", self.PGLOG['DSSWEB']+"/datasets") # datast web root path
1085
+ self.PGLOG['HOMEROOTS'] = "{}|{}".format(self.PGLOG['DSSHOME'], self.PGLOG['DSWHOME'])
1086
+ self.SETPGLOG("DSSDATA", "/glade/campaign/collections/gdex") # dss data root path
1087
+ self.SETPGLOG("DSDHOME", self.PGLOG['DSSDATA']+"/data") # dataset data root path
1088
+ self.SETPGLOG("DECSHOME", self.PGLOG['DSSDATA']+"/decsdata") # dataset decsdata root path
1089
+ self.SETPGLOG("DSHHOME", self.PGLOG['DECSHOME']+"/helpfiles") # dataset help root path
1090
+ self.SETPGLOG("GDEXWORK", "/lustre/desc1/gdex/work") # gdex work path
1091
+ self.SETPGLOG("UPDTWKP", self.PGLOG['GDEXWORK']) # dsupdt work root path
1092
+ self.SETPGLOG("TRANSFER", "/lustre/desc1/gdex/transfer") # gdex transfer path
1093
+ self.SETPGLOG("RQSTHOME", self.PGLOG['TRANSFER']+"/dsrqst") # dsrqst home
1094
+ self.SETPGLOG("DSAHOME", "") # dataset data alternate root path
1095
+ self.SETPGLOG("RQSTALTH", "") # alternate dsrqst path
1096
+ self.SETPGLOG("GPFSHOST", "") # empty if writable to glade
1097
+ self.SETPGLOG("PSQLHOST", "rda-db.ucar.edu") # host name for postgresql server
1098
+ self.SETPGLOG("SLMHOSTS", "cheyenne:casper") # host names for SLURM server
1099
+ self.SETPGLOG("PBSHOSTS", "cron:casper") # host names for PBS server
1100
+ self.SETPGLOG("CHKHOSTS", "") # host names for dscheck daemon
1101
+ self.SETPGLOG("PVIEWHOST", "pgdb02.k8s.ucar.edu") # host name for view only postgresql server
1102
+ self.SETPGLOG("PMISCHOST", "pgdb03.k8s.ucar.edu") # host name for misc postgresql server
1103
+ self.SETPGLOG("FTPUPLD", self.PGLOG['TRANSFER']+"/rossby") # ftp upload path
1104
+ self.PGLOG['GPFSROOTS'] = "{}|{}|{}".format(self.PGLOG['DSDHOME'], self.PGLOG['UPDTWKP'], self.PGLOG['RQSTHOME'])
1105
+ if 'ECCODES_DEFINITION_PATH' not in os.environ:
1106
+ os.environ['ECCODES_DEFINITION_PATH'] = "/usr/local/share/eccodes/definitions"
1107
+ os.environ['history'] = '0'
1108
+ # set tmp dir
1109
+ self.SETPGLOG("TMPPATH", self.PGLOG['GDEXWORK'] + "/ptmp")
1110
+ if not self.PGLOG['TMPPATH']: self.PGLOG['TMPPATH'] = "/data/ptmp"
1111
+ self.SETPGLOG("TMPDIR", '')
1112
+ if not self.PGLOG['TMPDIR']:
1113
+ self.PGLOG['TMPDIR'] = "/lustre/desc1/scratch/" + self.PGLOG['CURUID']
1114
+ os.environ['TMPDIR'] = self.PGLOG['TMPDIR']
1115
+ # empty diretory for HOST-sync
1116
+ self.PGLOG['TMPSYNC'] = self.PGLOG['DSSDBHM'] + "/tmp/.syncdir"
1117
+ os.umask(2)
1118
+
1119
+ # check and return TMPSYNC path, and add it if not exists
1120
+ def get_tmpsync_path(self):
1121
+ if 'DSSHOME' in self.PGLOG and self.PGLOG['DSSHOME'] and not op.exists(self.PGLOG['TMPSYNC']):
1122
+ self.pgsystem("mkdir " + self.PGLOG['TMPSYNC'], 0, self.LGWNEX, 4)
1123
+ self.pgsystem("chmod 775 " + self.PGLOG['TMPSYNC'], self.LOGWRN, 4)
1124
+ return self.PGLOG['TMPSYNC']
1125
+
1126
+ # append or prepend locpath to pgpath
1127
+ def add_local_path(self, locpath, pgpath, append = 0):
1128
+ if not locpath:
1129
+ return pgpath
1130
+ elif not pgpath:
1131
+ return locpath
1132
+ paths = locpath.split(':')
1133
+ for path in paths:
1134
+ if re.match(r'^\./*$', path): continue
1135
+ path = path.rstrip('\\')
1136
+ ms = re.search(r'(^|:){}(:|$)'.format(path), pgpath)
1137
+ if ms: continue
1138
+ if append:
1139
+ pgpath += ":" + path
1140
+ else:
1141
+ pgpath = path + ":" + pgpath
1142
+ return pgpath
1143
+
1144
+ # set self.PGLOG value; return a string or an array reference if sep is not emty
1145
+ def SETPGLOG(self, name, value = ''):
1146
+ oval = self.PGLOG[name] if name in self.PGLOG else ''
1147
+ nval = self.get_environment(name, ('' if re.match('PG', value) else value))
1148
+ self.PGLOG[name] = nval if nval else oval
1149
+
1150
+ # set specialist home and return the default shell
1151
+ def set_specialist_home(self, specialist):
1152
+ if specialist == self.PGLOG['CURUID']: return # no need reset
1153
+ if 'MAIL' in os.environ and re.search(self.PGLOG['CURUID'], os.environ['MAIL']):
1154
+ os.environ['MAIL'] = re.sub(self.PGLOG['CURUID'], specialist, os.environ['MAIL'])
1155
+ home = "{}/{}".format(self.PGLOG['USRHOME'], specialist)
1156
+ shell = "tcsh"
1157
+ buf = self.pgsystem("grep ^{}: /etc/passwd".format(specialist), self.LOGWRN, 20)
1158
+ if buf:
1159
+ lines = buf.split('\n')
1160
+ for line in lines:
1161
+ ms = re.search(r':(/.+):(/.+)', line)
1162
+ if ms:
1163
+ home = ms.group(1)
1164
+ shell = op.basename(ms.group(2))
1165
+ break
1166
+ if home != os.environ['HOME'] and op.exists(home):
1167
+ os.environ['HOME'] = home
1168
+ return shell
1169
+
1170
+ # set environments for a specified specialist
1171
+ def set_specialist_environments(self, specialist):
1172
+ shell = self.set_specialist_home(specialist)
1173
+ resource = os.environ['HOME'] + "/.tcshrc"
1174
+ checkif = 0 # 0 outside of if; 1 start if, 2 check envs, -1 checked already
1175
+ missthen = 0
1176
+ try:
1177
+ rf = open(resource, 'r')
1178
+ except:
1179
+ return # skip if cannot open
1180
+ nline = rf.readline()
1181
+ while nline:
1182
+ line = self.pgtrim(nline)
1183
+ nline = rf.readline()
1184
+ if not line: continue
1185
+ if checkif == 0:
1186
+ ms = re.match(r'^if(\s|\()', line)
1187
+ if ms: checkif = 1 # start if
1188
+ elif missthen:
1189
+ missthen = 0
1190
+ if re.match(r'^then$', line): continue # then on next line
1191
+ checkif = 0 # end of inline if
1192
+ elif re.match(r'^endif', line):
1193
+ checkif = 0 # end of if
1194
+ continue
1195
+ elif checkif == -1: # skip the line
1196
+ continue
1197
+ elif checkif == 2 and re.match(r'^else', line):
1198
+ checkif = -1 # done check envs in if
1199
+ continue
1200
+ if checkif == 1:
1201
+ if line == 'else':
1202
+ checkif = 2
1203
+ continue
1204
+ elif re.search(r'if\W', line):
1205
+ if(re.search(r'host.*!', line, re.I) and not re.search(self.PGLOG['HOSTNAME'], line) or
1206
+ re.search(r'host.*=', line, re.I) and re.search(self.PGLOG['HOSTNAME'], line)):
1207
+ checkif = 2
1208
+ if re.search(r'\sthen$', line):
1209
+ continue
1210
+ else:
1211
+ missthen = 1
1212
+ if checkif == 1: continue
1213
+ else:
1214
+ continue
1215
+ ms = re.match(r'^setenv\s+(.*)', line)
1216
+ if ms: self.one_specialist_environment(ms.group(1))
1217
+ rf.close()
1218
+ self.SETPGLOG("HOMEBIN", self.PGLOG['PGBINDIR'])
1219
+ os.environ['PATH'] = self.add_local_path(self.PGLOG['HOMEBIN'], os.environ['PATH'], 0)
1220
+
1221
+ # set one environment for specialist
1222
+ def one_specialist_environment(self, line):
1223
+ ms = re.match(r'^(\w+)[=\s]+(.+)$', line)
1224
+ if not ms: return
1225
+ (var, val) = ms.groups()
1226
+ if re.match(r'^(PATH|SHELL|IFS|CDPATH|)$', var): return
1227
+ if val.find('$') > -1: val = self.replace_environments(val)
1228
+ ms = re.match(r'^(\"|\')(.*)(\"|\')$', val)
1229
+ if ms: val = ms.group(2) # remove quotes
1230
+ os.environ[var] = val
1231
+
1232
+ # get and repalce environment variables in ginve string; defaults to the values in self.PGLOG
1233
+ def replace_environments(self, envstr, default = '', logact = 0):
1234
+ ishash = isinstance(default, dict)
1235
+ ms = re.search(r'(^|.)\$({*)(\w+)(}*)', envstr)
1236
+ if ms:
1237
+ lead = ms.group(1)
1238
+ name = ms.group(3)
1239
+ rep = ms.group(2) + name + ms.group(4)
1240
+ env = self.get_environment(name, (self.PGLOG[name] if name in self.PGLOG else (default[name] if ishash else default)), logact)
1241
+ pre = (lead if (env or lead != ":") else '')
1242
+ envstr = re.sub(r'{}\${}'.format(lead, rep), (pre+env), envstr)
1243
+ return envstr
1244
+
1245
+ # validate if the current host is a valid host to process
1246
+ def check_process_host(self, hosts, chost = None, mflag = None, pinfo = None, logact = None):
1247
+ ret = 1
1248
+ error = ''
1249
+ if not mflag: mflag = 'G'
1250
+ if not chost: chost = self.get_host(1)
1251
+ if mflag == 'M': # exact match
1252
+ if not hosts or hosts != chost:
1253
+ ret = 0
1254
+ if pinfo: error = "not matched exactly"
1255
+ elif mflag == 'I': # inclusive match
1256
+ if not hosts or hosts.find('!') == 0 or hosts.find(chost) < 0:
1257
+ ret = 0
1258
+ if pinfo: error = "not matched inclusively"
1259
+ elif hosts:
1260
+ if hosts.find(chost) >= 0:
1261
+ if hosts.find('!') == 0:
1262
+ ret = 0
1263
+ if pinfo: error = "matched exclusively"
1264
+ elif hosts.find('!') != 0:
1265
+ ret = 0
1266
+ if pinfo: error = "not matched"
1267
+ if error:
1268
+ if logact is None: logact = self.LOGERR
1269
+ self.pglog("{}: CANNOT be processed on {} for hosthame {}".format(pinfo, chost, error), logact)
1270
+ return ret
1271
+
1272
+ # convert special foreign characters into ascii characters
1273
+ @staticmethod
1274
+ def convert_chars(name, default = 'X'):
1275
+ if not name: return default
1276
+ if re.match(r'^[a-zA-Z0-9]+$', name): return name # conversion not needed
1277
+ decoded_name = unidecode(name).strip()
1278
+ # remove any non-alphanumeric and non-underscore characters
1279
+ cleaned_name = re.sub(r'[^a-zA-Z0-9_]', '', decoded_name)
1280
+ if cleaned_name:
1281
+ return cleaned_name
1282
+ else:
1283
+ return default
1284
+
1285
+ # Retrieve host and process id
1286
+ def current_process_info(self, realpid = 0):
1287
+ if realpid or self.PGLOG['CURBID'] < 1:
1288
+ return [self.PGLOG['HOSTNAME'], os.getpid()]
1289
+ else:
1290
+ return [self.PGLOG['PGBATCH'], self.PGLOG['CURBID']]
1291
+
1292
+ # convert given @ARGV to string. quote the entries with spaces
1293
+ def argv_to_string(self, argv = None, quote = 1, action = None):
1294
+ argstr = ''
1295
+ if argv is None: argv = sys.argv[1:]
1296
+ for arg in argv:
1297
+ if argstr: argstr += ' '
1298
+ ms = re.search(r'([<>\|\s])', arg)
1299
+ if ms:
1300
+ if action:
1301
+ self.pglog("{}: Cannot {} for special character '{}' in argument value".format(arg, action, ms.group(1)), self.LGEREX)
1302
+ if quote:
1303
+ if re.search(r"\'", arg):
1304
+ arg = "\"{}\"".format(arg)
1305
+ else:
1306
+ arg = "'{}'".format(arg)
1307
+ argstr += arg
1308
+ return argstr
1309
+
1310
+ # convert an integer to non-10 based string
1311
+ @staticmethod
1312
+ def int2base(x, base):
1313
+ if x == 0: return '0'
1314
+ negative = 0
1315
+ if x < 0:
1316
+ negative = 1
1317
+ x = -x
1318
+ dgts = []
1319
+ while x:
1320
+ dgts.append(str(int(x%base)))
1321
+ x = int(x/base)
1322
+ if negative: dgts.append('-')
1323
+ dgts.reverse()
1324
+ return ''.join(dgts)
1325
+
1326
+ # convert a non-10 based string to an integer
1327
+ @staticmethod
1328
+ def base2int(x, base):
1329
+ if not isinstance(x, int): x = int(x)
1330
+ if x == 0: return 0
1331
+ negative = 0
1332
+ if x < 0:
1333
+ negative = 1
1334
+ x = -x
1335
+ num = 0
1336
+ fact = 1
1337
+ while x:
1338
+ num += (x%10)*fact
1339
+ fact *= base
1340
+ x = int(x/10)
1341
+ if negative: num = -num
1342
+ return num
1343
+
1344
+ # convert integer to ordinal string
1345
+ @staticmethod
1346
+ def int2order(num):
1347
+ ordstr = ['th', 'st', 'nd', 'rd']
1348
+ snum = str(num)
1349
+ num %= 100
1350
+ if num > 19: num %= 10
1351
+ if num > 3: num = 0
1352
+ return snum + ordstr[num]