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
18
19
20
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
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
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
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 """
96
97
98 try:
99 import ssl
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
112
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
141 _temp_credentials = creds
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:
154 msgStr = msgStr.args[1]
155 except IndexError:
156 pass
157 if not isinstance(msgStr, basestring):
158 msgStr = str(msg)
159 except:
160
161 pass
162 raise IOError("Could not open URL %s: %s"%(url, msgStr))
163
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):
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 """
215 self.args, self.kwargs = args, kwargs
216
217 - def add(self, parser):
218 parser.add_argument(*self.args, **self.kwargs)
219
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
238 """quick hack to teach argparse to match actions based on unique
239 prefixes.
240 """
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
252 for s in self.keys():
253 if s.startswith(key):
254 return True
255 return False
256
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
278
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
287 misctricks.sendUIEvent("Warning",
288 "Couldn't teach prefix matching to argparse: %s"%repr(msg))
289
290 return parser
291