source: trunk/src/allmydata/scripts/tahoe_status.py

Last change on this file was 0e5b6da, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-27T23:53:49Z

remove more Python2: unicode -> str, long -> int

  • Property mode set to 100644
File size: 10.9 KB
Line 
1"""
2Ported to Python 3.
3"""
4
5import os
6from sys import stdout as _sys_stdout
7from urllib.parse import urlencode
8
9import json
10
11from .common import BaseOptions
12from allmydata.scripts.common import get_default_nodedir
13from allmydata.scripts.common_http import BadResponse
14from allmydata.util.abbreviate import abbreviate_space, abbreviate_time
15from allmydata.util.encodingutil import argv_to_abspath
16
17_print = print
18def 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
41def _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
68def _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
86def 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
118OP_MAP = {
119    'upload': ' put ',
120    'download': ' get ',
121    'retrieve': 'retr ',
122    'publish': ' pub ',
123    'mapupdate': 'mapup',
124    'unknown': ' ??? ',
125}
126
127def _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
141def _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
150def _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
159active_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
169def 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
209def _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
217def _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
225recent_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
234def 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
276def 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
333class 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
354subCommands = [
355    ["status", None, TahoeStatusCommand,
356     "Status."],
357]
Note: See TracBrowser for help on using the repository browser.