| 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 | ] |
|---|