github.com/swiftstack/ProxyFS@v0.0.0-20210203235616-4017c267d62f/bin/pfs_stat (about) 1 #!/usr/bin/env python 2 3 # Copyright (c) 2015-2021, NVIDIA CORPORATION. 4 # SPDX-License-Identifier: Apache-2.0 5 6 import json 7 import os.path 8 import re 9 import sys 10 import time 11 import argparse 12 import requests 13 import subprocess 14 15 # TODO: 16 # * humanize stats if humanize is installed 17 # * better print formatting; use tables if appropriate module is installed 18 19 CONFIG_FILE_PATHS = [ 20 "/opt/ss/etc/proxyfs.conf", 21 "/etc/proxyfsd/saioproxyfsd0.conf" 22 ] 23 24 PFSCONFJSON_PATHS = [ 25 "/opt/ss/bin/pfsconfjson", 26 "/home/swift/code/ProxyFS/bin/pfsconfjson" 27 ] 28 29 DEFAULT_CONFIG = { 30 "hostname": "localhost", 31 "port": 15346 32 } 33 34 35 def find_config_file_path(): 36 for file_path in CONFIG_FILE_PATHS: 37 if os.path.isfile(file_path): 38 return file_path 39 return None 40 41 42 def get_values_to_return(provided_hostname, provided_port, calculated_hostname, 43 calculated_port): 44 if provided_hostname is not None: 45 return_hostname = provided_hostname 46 else: 47 return_hostname = calculated_hostname 48 49 if provided_port is not None: 50 return_port = provided_port 51 else: 52 return_port = calculated_port 53 54 return return_hostname, return_port 55 56 57 def get_pfsconfjson_path(): 58 try: 59 path = subprocess.check_output("which pfsconfjsons", 60 stderr=subprocess.PIPE, shell=True) 61 return path.strip() 62 except: 63 pass 64 65 for file_path in PFSCONFJSON_PATHS: 66 if os.path.isfile(file_path): 67 return file_path 68 69 try: 70 paths = subprocess.check_output("find / -iname 'pfsconfjson'", 71 shell=True, 72 stderr=subprocess.PIPE).splitlines() 73 except subprocess.CalledProcessError as ex: 74 paths = [line for line in ex.output.splitlines() if 75 "Permission denied" not in line] 76 finally: 77 if len(paths) > 0: 78 return paths[0].strip() 79 80 return None 81 82 83 def get_config_hostname_and_port(config_file_path, pfsconfjson_path): 84 pfsconfjson_call = "{} {}".format(pfsconfjson_path, config_file_path) 85 config_json_str = subprocess.check_output(pfsconfjson_call, shell=True) 86 config = json.loads(config_json_str) 87 whoami = config["Cluster"]["WhoAmI"][0] 88 hostname = config[whoami]["PrivateIPAddr"][0] 89 port = int(config["HTTPServer"]["TCPPort"][0]) 90 return hostname, port 91 92 93 def get_hostname_and_port(provided_hostname, provided_port): 94 if provided_hostname is not None and provided_port is not None: 95 return provided_hostname, provided_port 96 97 config_file_path = find_config_file_path() 98 if config_file_path is None: 99 return get_values_to_return(provided_hostname, provided_port, 100 DEFAULT_CONFIG["hostname"], 101 DEFAULT_CONFIG["port"]) 102 103 pfsconfjson_path = get_pfsconfjson_path() 104 if pfsconfjson_path is None: 105 return get_values_to_return(provided_hostname, provided_port, 106 DEFAULT_CONFIG["hostname"], 107 DEFAULT_CONFIG["port"]) 108 109 try: 110 config_hostname, config_port = get_config_hostname_and_port( 111 config_file_path, pfsconfjson_path) 112 return get_values_to_return(provided_hostname, provided_port, 113 config_hostname, config_port) 114 except: 115 return get_values_to_return(provided_hostname, provided_port, 116 DEFAULT_CONFIG["hostname"], 117 DEFAULT_CONFIG["port"]) 118 119 120 def print_stats(s, exclude=[], show_all=False, diff=True): 121 exclude_re = None 122 if exclude: 123 exclude_re = '|'.join(exclude) 124 125 for k in sorted(s.keys()): 126 # Do not display the line if it is in an excluded category 127 # or if we're in diff mode and it is unchanged. 128 # Unless we specified -v; in that case, show everything. 129 hide = re.match(exclude_re, k) 130 hide = hide or (diff and not s[k]) 131 if show_all or not hide: 132 print("{: <90} {: >20}".format(k, s[k])) 133 134 135 def stat_diff(s1, s2): 136 diff = {} 137 for k in s2: 138 if k not in s1: 139 continue 140 diff[k] = s2[k] - s1[k] 141 142 return diff 143 144 145 def parse_stats(text): 146 stats = {} 147 for l in text.split("\n"): 148 if not l: 149 continue 150 151 # Are stats guaranteed to be positive integers? 152 match = re.match("(\S+)\s+(\d+)\s*$", l) 153 if match: 154 stats[match.group(1)] = int(match.group(2)) 155 else: 156 sys.stderr.write("WARNING: unparsed line: [%s]\n" % l) 157 158 return stats 159 160 161 def get_stats_from_proxyfsd(hostname, port): 162 r = requests.get("http://%s:%d/metrics" % (hostname, port)) 163 r.raise_for_status() 164 stats = r.json() 165 stats['timestamp'] = int(time.time()) 166 return stats 167 168 169 def read_stats_from_file(fn): 170 with open(fn) as f: 171 text = f.read() 172 stats = parse_stats(text) 173 return stats 174 175 176 if __name__ == '__main__': 177 parser = argparse.ArgumentParser(description='Show Proxyfs stats') 178 parser.add_argument('--verbose', '-v', action='store_true', 179 help='verbose output') 180 parser.add_argument('--hostname', '-H', type=str, default=None, 181 help='Hostname of proxyfs stat server (probably localhost)') 182 parser.add_argument('--port', '-p', type=int, default=None, 183 help='Port used by proxyfs stat server') 184 parser.add_argument('--file', '-f', type=str, 185 help='Read starting stats from a file') 186 parser.add_argument('cmd', type=str, nargs=argparse.REMAINDER, help='command to run') 187 188 args = parser.parse_args() 189 190 excludes = ['go_runtime', 'logging_level'] 191 192 hostname, port = get_hostname_and_port(args.hostname, args.port) 193 194 if args.cmd or args.file: 195 start_stats = None 196 if args.file: 197 start_stats = read_stats_from_file(args.file) 198 else: 199 start_stats = get_stats_from_proxyfsd(hostname, port) 200 201 if args.cmd: 202 subprocess.call(args.cmd) 203 204 end_stats = get_stats_from_proxyfsd(hostname, port) 205 diff = stat_diff(start_stats, end_stats) 206 207 if args.cmd: 208 print("\n--") 209 print_stats(diff, exclude=excludes, show_all=args.verbose) 210 else: 211 stats = get_stats_from_proxyfsd(hostname, port) 212 print_stats(stats, diff=False, exclude=excludes, show_all=args.verbose)