Package gavo :: Package utils :: Module ostricks
[frames] | no frames]

Source Code for Module gavo.utils.ostricks

  1  """ 
  2  OS abstractions and related. 
  3   
  4  This module contains, in partiular, the interface for having "easy subcommands" 
  5  using argparse.  The idea is to use the exposedFunction decorator on functions 
  6  that should be callable from the command line as subcommands; the functions 
  7  must all have the same signature. For example, if they all took the stuff 
  8  returned by argparse, you could say in the module containing them:: 
  9   
 10    args = makeCLIParser(globals()).parse_args() 
 11    args.subAction(args) 
 12   
 13  To specify the command line arguments to the function, use Args.  See 
 14  admin.py for an example. 
 15  """ 
 16   
 17  #c Copyright 2008-2019, the GAVO project 
 18  #c 
 19  #c This program is free software, covered by the GNU GPL.  See the 
 20  #c COPYING file in the source distribution. 
 21   
 22   
 23  import argparse 
 24  import contextlib 
 25  import os 
 26  import tempfile 
 27  import urllib2 
 28   
 29  from . import codetricks 
 30  from . import excs 
 31  from . import misctricks 
32 33 34 -def safeclose(f):
35 """syncs and closes the python file f. 36 37 You generally want to use this rather than a plain close() before 38 overwriting a file with a new version. 39 """ 40 f.flush() 41 os.fsync(f.fileno()) 42 f.close()
43
44 45 @contextlib.contextmanager 46 -def safeReplaced(fName):
47 """opens fName for "safe replacement". 48 49 Safe replacement means that you can write to the object returned, and 50 when everything works out all right, what you have written replaces 51 the old content of fName, where the old mode is preserved if possible. 52 When there are errors, however, the old content remains. 53 """ 54 targetDir = os.path.abspath(os.path.dirname(fName)) 55 try: 56 oldMode = os.stat(fName)[0] 57 except os.error: 58 oldMode = None 59 60 handle, tempName = tempfile.mkstemp(".temp", "", dir=targetDir) 61 targetFile = os.fdopen(handle, "w") 62 63 try: 64 yield targetFile 65 except: 66 try: 67 os.unlink(tempName) 68 except os.error: 69 pass 70 raise 71 72 else: 73 safeclose(targetFile) 74 os.rename(tempName, fName) 75 if oldMode is not None: 76 try: 77 os.chmod(fName, oldMode) 78 except os.error: 79 pass
80
81 82 -class _UrlopenRemotePasswordMgr(urllib2.HTTPPasswordMgr):
83 """A password manager that grabs credentials from upwards in 84 its call stack. 85 86 This is for cooperation with urlopenRemote, which defines a name 87 _temp_credentials. If this is non-None, it's supposed to be 88 a pair of user password presented to *any* realm. This means 89 that, at least with http basic auth, password stealing is 90 almost trivial. 91 """
92 - def find_user_password(self, realm, authuri):
93 creds = codetricks.stealVar("_temp_credentials") 94 if creds is not None: 95 return creds
96 97 98 try: 99 import ssl
100 101 - class HTTPSHandler(urllib2.HTTPSHandler):
102 - def __init__(self, debuglevel=0, context=None):
103 if context is None: 104 context = ssl.create_default_context( 105 purpose=ssl.Purpose.SERVER_AUTH, 106 cafile="/etc/ssl/certs/ca-certificates.crt") 107 context.check_hostname = False 108 context.verify_mode = ssl.CERT_NONE 109 urllib2.HTTPSHandler.__init__(self, debuglevel, context)
110 except (ImportError, IOError): 111 # probably the ssl bundle isn't where I think it is; just use 112 # the normal, certificate-checking https handler. 113 from urllib2 import HTTPSHandler 114 115 116 _restrictedURLOpener = urllib2.OpenerDirector() 117 _restrictedURLOpener.add_handler(urllib2.HTTPRedirectHandler()) 118 _restrictedURLOpener.add_handler(urllib2.HTTPHandler()) 119 _restrictedURLOpener.add_handler(HTTPSHandler()) 120 _restrictedURLOpener.add_handler(urllib2.HTTPErrorProcessor()) 121 _restrictedURLOpener.add_handler( 122 urllib2.HTTPBasicAuthHandler(_UrlopenRemotePasswordMgr())) 123 _restrictedURLOpener.add_handler(urllib2.FTPHandler()) 124 _restrictedURLOpener.add_handler(urllib2.UnknownHandler()) 125 _restrictedURLOpener.addheaders = [("user-agent", 126 "GAVO DaCHS HTTP client")]
127 128 -def urlopenRemote(url, data=None, creds=(None, None), timeout=100):
129 """works like urllib2.urlopen, except only http, https, and ftp URLs 130 are handled. 131 132 The function also massages the error messages of urllib2 a bit. urllib2 133 errors always become IOErrors (which is more convenient within the DC). 134 135 creds may be a pair of username and password. Those credentials 136 will be presented in http basic authentication to any server 137 that cares to ask. For both reasons, don't use any valuable credentials 138 here. 139 """ 140 # The name in the next line is used in _UrlopenRemotePasswrodMgr 141 _temp_credentials = creds #noflake: Picked up from down the call chain 142 try: 143 res = _restrictedURLOpener.open(url, data, timeout=timeout) 144 if res is None: 145 raise IOError("Could not open URL %s -- does the resource exist?"% 146 url) 147 return res 148 except (urllib2.URLError, ValueError) as msg: 149 msgStr = str(msg) 150 try: 151 msgStr = msg.args[0] 152 if isinstance(msgStr, Exception): 153 try: # maybe it's an os/socket type error 154 msgStr = msgStr.args[1] 155 except IndexError: # maybe not... 156 pass 157 if not isinstance(msgStr, basestring): 158 msgStr = str(msg) 159 except: 160 # there's going to be an error message, albeit maybe a weird one 161 pass 162 raise IOError("Could not open URL %s: %s"%(url, msgStr))
163
164 165 -def fgetmtime(fileobj):
166 """returns the mtime of the file below fileobj. 167 168 This raises an os.error if that file cannot be fstated. 169 """ 170 try: 171 return os.fstat(fileobj.fileno()).st_mtime 172 except AttributeError: 173 raise misctricks.logOldExc(os.error("Not a file: %s"%repr(fileobj)))
174
175 176 -def cat(srcF, destF, chunkSize=1<<20):
177 """reads srcF into destF in chunks. 178 """ 179 while True: 180 data = srcF.read(chunkSize) 181 if not data: 182 break 183 destF.write(data)
184
185 186 -def ensureDir(dirPath, mode=None, setGroupTo=None):
187 """makes sure that dirPath exists and is a directory. 188 189 If dirPath does not exist, it is created, and its permissions are 190 set to mode with group ownership setGroupTo if those are given. 191 192 setGroupTo must be a numerical gid if given. 193 194 This function may raise all kinds of os.errors if something goes 195 wrong. These probably should be handed through all the way to the 196 user since when something fails here, there's usually little 197 the program can safely do to recover. 198 """ 199 if os.path.exists(dirPath): 200 return 201 os.mkdir(dirPath) 202 if mode is not None: 203 os.chmod(dirPath, mode) 204 if setGroupTo: 205 os.chown(dirPath, -1, setGroupTo)
206
207 208 -class Arg(object):
209 """an argument/option to a subcommand. 210 211 These are constructed with positional and keyword parameters to 212 the argparse's add_argument. 213 """
214 - def __init__(self, *args, **kwargs):
215 self.args, self.kwargs = args, kwargs
216
217 - def add(self, parser):
218 parser.add_argument(*self.args, **self.kwargs)
219
220 221 -def exposedFunction(argSpecs=(), help=None):
222 """a decorator exposing a function to parseArgs. 223 224 argSpecs is a sequence of Arg objects. This defines the command line 225 interface to the function. 226 227 The decorated function itself must accept a single argument, 228 the args object returned by argparse's parse_args. 229 """ 230 def deco(func): 231 func.subparseArgs = argSpecs 232 func.subparseHelp = help 233 return func
234 return deco 235
236 237 -class _PrefixMatchDict(dict):
238 """quick hack to teach argparse to match actions based on unique 239 prefixes. 240 """
241 - def __getitem__(self, key):
242 matches = [s for s in self.keys() if s.startswith(key)] 243 if len(matches)==0: 244 raise KeyError(key) 245 elif len(matches)==1: 246 return dict.__getitem__(self, matches[0]) 247 else: 248 raise excs.ReportableError("Ambiguous subcommand specification;" 249 " choose between %s."%repr(matches))
250
251 - def __contains__(self, key):
252 for s in self.keys(): 253 if s.startswith(key): 254 return True 255 return False
256
257 258 -def makeCLIParser(functions):
259 """returns a command line parser parsing subcommands from functions. 260 261 functions is a dictionary (as returned from globals()). Subcommands 262 will be generated from all objects that have a subparseArgs attribute; 263 furnish them using the commandWithArgs decorator. 264 265 This attribute must contain a sequence of Arg items (see above). 266 """ 267 parser = argparse.ArgumentParser() 268 subparsers = parser.add_subparsers() 269 for name, val in functions.iteritems(): 270 args = getattr(val, "subparseArgs", None) 271 if args is not None: 272 subForName = subparsers.add_parser(name, help=val.subparseHelp) 273 for arg in args: 274 arg.add(subForName) 275 subForName.set_defaults(subAction=val) 276 277 # Now monkeypatch matching of unique prefixes into argparse guts. 278 # If the guts change, don't fail hard, just turn off prefix matching 279 try: 280 for action in parser._actions: 281 if isinstance(action, argparse._SubParsersAction): 282 action.choices = action._name_parser_map = \ 283 _PrefixMatchDict(action._name_parser_map) 284 break 285 except Exception as msg: 286 # no prefix matching, then 287 misctricks.sendUIEvent("Warning", 288 "Couldn't teach prefix matching to argparse: %s"%repr(msg)) 289 290 return parser
291