1 | """ |
---|
2 | Ported to Python 3. |
---|
3 | """ |
---|
4 | |
---|
5 | import os |
---|
6 | from sys import stdout as _sys_stdout |
---|
7 | from urllib.parse import urlencode |
---|
8 | |
---|
9 | import json |
---|
10 | |
---|
11 | from .common import BaseOptions |
---|
12 | from allmydata.scripts.common import get_default_nodedir |
---|
13 | from allmydata.scripts.common_http import BadResponse |
---|
14 | from allmydata.util.abbreviate import abbreviate_space, abbreviate_time |
---|
15 | from allmydata.util.encodingutil import argv_to_abspath |
---|
16 | |
---|
17 | _print = print |
---|
18 | def print(*args, **kwargs): |
---|
19 | """ |
---|
20 | Builtin ``print``-alike that will even write unicode which cannot be |
---|
21 | encoded using the specified output file's encoding. |
---|
22 | |
---|
23 | This differs from the builtin print in that it will use the "replace" |
---|
24 | encoding error handler and then write the result whereas builtin print |
---|
25 | uses the "strict" encoding error handler. |
---|
26 | """ |
---|
27 | out = kwargs.pop("file", None) |
---|
28 | if out is None: |
---|
29 | out = _sys_stdout |
---|
30 | encoding = out.encoding or "ascii" |
---|
31 | def ensafe(o): |
---|
32 | if isinstance(o, str): |
---|
33 | return o.encode(encoding, errors="replace").decode(encoding) |
---|
34 | return o |
---|
35 | return _print( |
---|
36 | *(ensafe(a) for a in args), |
---|
37 | file=out, |
---|
38 | **kwargs |
---|
39 | ) |
---|
40 | |
---|
41 | def _get_request_parameters_for_fragment(options, fragment, method, post_args): |
---|
42 | """ |
---|
43 | Get parameters for ``do_http`` for requesting the given fragment. |
---|
44 | |
---|
45 | :return dict: A dictionary suitable for use as keyword arguments to |
---|
46 | ``do_http``. |
---|
47 | """ |
---|
48 | nodeurl = options['node-url'] |
---|
49 | if nodeurl.endswith('/'): |
---|
50 | nodeurl = nodeurl[:-1] |
---|
51 | |
---|
52 | url = u'%s/%s' % (nodeurl, fragment) |
---|
53 | if method == 'POST': |
---|
54 | if post_args is None: |
---|
55 | raise ValueError("Must pass post_args= for POST method") |
---|
56 | body = urlencode(post_args) |
---|
57 | else: |
---|
58 | body = '' |
---|
59 | if post_args is not None: |
---|
60 | raise ValueError("post_args= only valid for POST method") |
---|
61 | return dict( |
---|
62 | method=method, |
---|
63 | url=url, |
---|
64 | body=body.encode("utf-8"), |
---|
65 | ) |
---|
66 | |
---|
67 | |
---|
68 | def _handle_response_for_fragment(resp, nodeurl): |
---|
69 | """ |
---|
70 | Inspect an HTTP response and return the parsed payload, if possible. |
---|
71 | """ |
---|
72 | if isinstance(resp, BadResponse): |
---|
73 | # specifically NOT using format_http_error() here because the |
---|
74 | # URL is pretty sensitive (we're doing /uri/<key>). |
---|
75 | raise RuntimeError( |
---|
76 | "Failed to get json from '%s': %s" % (nodeurl, resp.error) |
---|
77 | ) |
---|
78 | |
---|
79 | data = resp.read() |
---|
80 | parsed = json.loads(data) |
---|
81 | if parsed is None: |
---|
82 | raise RuntimeError("No data from '%s'" % (nodeurl,)) |
---|
83 | return parsed |
---|
84 | |
---|
85 | |
---|
86 | def pretty_progress(percent, size=10, output_ascii=False): |
---|
87 | """ |
---|
88 | Displays a unicode or ascii based progress bar of a certain |
---|
89 | length. Should we just depend on a library instead? |
---|
90 | |
---|
91 | (Originally from txtorcon) |
---|
92 | """ |
---|
93 | |
---|
94 | curr = int(percent / 100.0 * size) |
---|
95 | part = (percent / (100.0 / size)) - curr |
---|
96 | |
---|
97 | if output_ascii: |
---|
98 | part = int(part * 4) |
---|
99 | part = '.oO%'[part] |
---|
100 | block_chr = '#' |
---|
101 | |
---|
102 | else: |
---|
103 | block_chr = u'\u2588' |
---|
104 | # there are 8 unicode characters for vertical-bars/horiz-bars |
---|
105 | part = int(part * 8) |
---|
106 | |
---|
107 | # unicode 0x2581 -> 2589 are vertical bar chunks, like rainbarf uses |
---|
108 | # and following are narrow -> wider bars |
---|
109 | part = chr(0x258f - part) # for smooth bar |
---|
110 | # part = chr(0x2581 + part) # for neater-looking thing |
---|
111 | |
---|
112 | # hack for 100+ full so we don't print extra really-narrow/high bar |
---|
113 | if percent >= 100.0: |
---|
114 | part = '' |
---|
115 | curr = int(curr) |
---|
116 | return '%s%s%s' % ((block_chr * curr), part, (' ' * (size - curr - 1))) |
---|
117 | |
---|
118 | OP_MAP = { |
---|
119 | 'upload': ' put ', |
---|
120 | 'download': ' get ', |
---|
121 | 'retrieve': 'retr ', |
---|
122 | 'publish': ' pub ', |
---|
123 | 'mapupdate': 'mapup', |
---|
124 | 'unknown': ' ??? ', |
---|
125 | } |
---|
126 | |
---|
127 | def _render_active_upload(op): |
---|
128 | total = ( |
---|
129 | op['progress-hash'] + |
---|
130 | op['progress-ciphertext'] + |
---|
131 | op['progress-encode-push'] |
---|
132 | ) / 3.0 * 100.0 |
---|
133 | return { |
---|
134 | u"op_type": u" put ", |
---|
135 | u"total": "{:3.0f}".format(total), |
---|
136 | u"progress_bar": u"{}".format(pretty_progress(total, size=15)), |
---|
137 | u"storage-index-string": op["storage-index-string"], |
---|
138 | u"status": op["status"], |
---|
139 | } |
---|
140 | |
---|
141 | def _render_active_download(op): |
---|
142 | return { |
---|
143 | u"op_type": u" get ", |
---|
144 | u"total": op["progress"], |
---|
145 | u"progress_bar": u"{}".format(pretty_progress(op['progress'] * 100.0, size=15)), |
---|
146 | u"storage-index-string": op["storage-index-string"], |
---|
147 | u"status": op["status"], |
---|
148 | } |
---|
149 | |
---|
150 | def _render_active_generic(op): |
---|
151 | return { |
---|
152 | u"op_type": OP_MAP[op["type"]], |
---|
153 | u"progress_bar": u"", |
---|
154 | u"total": u"???", |
---|
155 | u"storage-index-string": op["storage-index-string"], |
---|
156 | u"status": op["status"], |
---|
157 | } |
---|
158 | |
---|
159 | active_renderers = { |
---|
160 | "upload": _render_active_upload, |
---|
161 | "download": _render_active_download, |
---|
162 | "publish": _render_active_generic, |
---|
163 | "retrieve": _render_active_generic, |
---|
164 | "mapupdate": _render_active_generic, |
---|
165 | "unknown": _render_active_generic, |
---|
166 | } |
---|
167 | |
---|
168 | |
---|
169 | def render_active(stdout, status_data): |
---|
170 | active = status_data.get('active', None) |
---|
171 | if not active: |
---|
172 | print(u"No active operations.", file=stdout) |
---|
173 | return |
---|
174 | |
---|
175 | header = u"\u2553 {:<5} \u2565 {:<26} \u2565 {:<22} \u2565 {}".format( |
---|
176 | "type", |
---|
177 | "storage index", |
---|
178 | "progress", |
---|
179 | "status message", |
---|
180 | ) |
---|
181 | header_bar = u"\u255f\u2500{}\u2500\u256b\u2500{}\u2500\u256b\u2500{}\u2500\u256b\u2500{}".format( |
---|
182 | u'\u2500' * 5, |
---|
183 | u'\u2500' * 26, |
---|
184 | u'\u2500' * 22, |
---|
185 | u'\u2500' * 20, |
---|
186 | ) |
---|
187 | line_template = ( |
---|
188 | u"\u2551 {op_type} " |
---|
189 | u"\u2551 {storage-index-string} " |
---|
190 | u"\u2551 {progress_bar:15} " |
---|
191 | u"({total}%) " |
---|
192 | u"\u2551 {status}" |
---|
193 | ) |
---|
194 | footer_bar = u"\u2559\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}".format( |
---|
195 | u'\u2500' * 5, |
---|
196 | u'\u2500' * 26, |
---|
197 | u'\u2500' * 22, |
---|
198 | u'\u2500' * 20, |
---|
199 | ) |
---|
200 | print(u"Active operations:", file=stdout) |
---|
201 | print(header, file=stdout) |
---|
202 | print(header_bar, file=stdout) |
---|
203 | for op in active: |
---|
204 | print(line_template.format( |
---|
205 | **active_renderers[op["type"]](op) |
---|
206 | )) |
---|
207 | print(footer_bar, file=stdout) |
---|
208 | |
---|
209 | def _render_recent_generic(op): |
---|
210 | return { |
---|
211 | u"op_type": OP_MAP[op["type"]], |
---|
212 | u"storage-index-string": op["storage-index-string"], |
---|
213 | u"nice_size": abbreviate_space(op["total-size"]), |
---|
214 | u"status": op["status"], |
---|
215 | } |
---|
216 | |
---|
217 | def _render_recent_mapupdate(op): |
---|
218 | return { |
---|
219 | u"op_type": u"mapup", |
---|
220 | u"storage-index-string": op["storage-index-string"], |
---|
221 | u"nice_size": op["mode"], |
---|
222 | u"status": op["status"], |
---|
223 | } |
---|
224 | |
---|
225 | recent_renderers = { |
---|
226 | "upload": _render_recent_generic, |
---|
227 | "download": _render_recent_generic, |
---|
228 | "publish": _render_recent_generic, |
---|
229 | "retrieve": _render_recent_generic, |
---|
230 | "mapupdate": _render_recent_mapupdate, |
---|
231 | "unknown": _render_recent_generic, |
---|
232 | } |
---|
233 | |
---|
234 | def render_recent(verbose, stdout, status_data): |
---|
235 | recent = status_data.get('recent', None) |
---|
236 | if not recent: |
---|
237 | print(u"No recent operations.", file=stdout) |
---|
238 | |
---|
239 | header = u"\u2553 {:<5} \u2565 {:<26} \u2565 {:<10} \u2565 {}".format( |
---|
240 | "type", |
---|
241 | "storage index", |
---|
242 | "size", |
---|
243 | "status message", |
---|
244 | ) |
---|
245 | line_template = ( |
---|
246 | u"\u2551 {op_type} " |
---|
247 | u"\u2551 {storage-index-string} " |
---|
248 | u"\u2551 {nice_size:<10} " |
---|
249 | u"\u2551 {status}" |
---|
250 | ) |
---|
251 | footer = u"\u2559\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}\u2500\u2568\u2500{}".format( |
---|
252 | u'\u2500' * 5, |
---|
253 | u'\u2500' * 26, |
---|
254 | u'\u2500' * 10, |
---|
255 | u'\u2500' * 20, |
---|
256 | ) |
---|
257 | non_verbose_ops = ('upload', 'download') |
---|
258 | recent = [op for op in status_data['recent'] if op['type'] in non_verbose_ops] |
---|
259 | print(u"\nRecent operations:", file=stdout) |
---|
260 | if len(recent) or verbose: |
---|
261 | print(header, file=stdout) |
---|
262 | |
---|
263 | ops_to_show = status_data['recent'] if verbose else recent |
---|
264 | for op in ops_to_show: |
---|
265 | print(line_template.format( |
---|
266 | **recent_renderers[op["type"]](op) |
---|
267 | )) |
---|
268 | if len(recent) or verbose: |
---|
269 | print(footer, file=stdout) |
---|
270 | |
---|
271 | skipped = len(status_data['recent']) - len(ops_to_show) |
---|
272 | if not verbose and skipped: |
---|
273 | print(u" Skipped {} non-upload/download operations; use --verbose to see".format(skipped), file=stdout) |
---|
274 | |
---|
275 | |
---|
276 | def do_status(options, do_http=None): |
---|
277 | if do_http is None: |
---|
278 | from allmydata.scripts.common_http import do_http |
---|
279 | |
---|
280 | nodedir = options["node-directory"] |
---|
281 | with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'r') as f: |
---|
282 | token = f.read().strip() |
---|
283 | with open(os.path.join(nodedir, u'node.url'), 'r') as f: |
---|
284 | options['node-url'] = f.read().strip() |
---|
285 | |
---|
286 | # do *all* our data-retrievals first in case there's an error |
---|
287 | try: |
---|
288 | status_data = _handle_response_for_fragment( |
---|
289 | do_http(**_get_request_parameters_for_fragment( |
---|
290 | options, |
---|
291 | 'status?t=json', |
---|
292 | method='POST', |
---|
293 | post_args=dict( |
---|
294 | t='json', |
---|
295 | token=token, |
---|
296 | ), |
---|
297 | )), |
---|
298 | options['node-url'], |
---|
299 | ) |
---|
300 | statistics_data = _handle_response_for_fragment( |
---|
301 | do_http(**_get_request_parameters_for_fragment( |
---|
302 | options, |
---|
303 | 'statistics?t=json', |
---|
304 | method='POST', |
---|
305 | post_args=dict( |
---|
306 | t='json', |
---|
307 | token=token, |
---|
308 | ), |
---|
309 | )), |
---|
310 | options['node-url'], |
---|
311 | ) |
---|
312 | except Exception as e: |
---|
313 | print(u"failed to retrieve data: %s" % str(e), file=options.stderr) |
---|
314 | return 2 |
---|
315 | |
---|
316 | downloaded_bytes = statistics_data['counters'].get('downloader.bytes_downloaded', 0) |
---|
317 | downloaded_files = statistics_data['counters'].get('downloader.files_downloaded', 0) |
---|
318 | uploaded_bytes = statistics_data['counters'].get('uploader.bytes_uploaded', 0) |
---|
319 | uploaded_files = statistics_data['counters'].get('uploader.files_uploaded', 0) |
---|
320 | print(u"Statistics (for last {}):".format(abbreviate_time(statistics_data['stats']['node.uptime'])), file=options.stdout) |
---|
321 | print(u" uploaded {} in {} files".format(abbreviate_space(uploaded_bytes), uploaded_files), file=options.stdout) |
---|
322 | print(u" downloaded {} in {} files".format(abbreviate_space(downloaded_bytes), downloaded_files), file=options.stdout) |
---|
323 | print(u"", file=options.stdout) |
---|
324 | |
---|
325 | render_active(options.stdout, status_data) |
---|
326 | render_recent(options['verbose'], options.stdout, status_data) |
---|
327 | |
---|
328 | # open question: should we return non-zero if there were no |
---|
329 | # operations at all to display? |
---|
330 | return 0 |
---|
331 | |
---|
332 | |
---|
333 | class TahoeStatusCommand(BaseOptions): |
---|
334 | |
---|
335 | optFlags = [ |
---|
336 | ["verbose", "v", "Include publish, retrieve, mapupdate in ops"], |
---|
337 | ] |
---|
338 | |
---|
339 | def postOptions(self): |
---|
340 | if self.parent['node-directory']: |
---|
341 | self['node-directory'] = argv_to_abspath(self.parent['node-directory']) |
---|
342 | else: |
---|
343 | self['node-directory'] = get_default_nodedir() |
---|
344 | |
---|
345 | def getSynopsis(self): |
---|
346 | return "Usage: tahoe [global-options] status [options]" |
---|
347 | |
---|
348 | def getUsage(self, width=None): |
---|
349 | t = BaseOptions.getUsage(self, width) |
---|
350 | t += "Various status information" |
---|
351 | return t |
---|
352 | |
---|
353 | |
---|
354 | subCommands = [ |
---|
355 | ["status", None, TahoeStatusCommand, |
---|
356 | "Status."], |
---|
357 | ] |
---|