#!/usr/bin/python3 VERSION = 1.0 #SYS IMPORTS import sys import os import types import hashlib import shutil from os.path import isfile as check_file from os.path import isdir as check_dir from os import remove as remove_file from glob import glob from json import dumps import argparse from subprocess import call as SCall from time import time FNULL = open(os.devnull,'w') #VOLVO import a2s from valve.rcon import RCON from valve.rcon import log as RCON_LOGGING RCON_LOGGING.setLevel(40) import traceback DEBUG = 0 ALL_YES = 0 class colors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' class new_o: def __init__(self, server_name): self.prefix = "[{:^25}]".format(server_name) def info(self, text, end = "\n"): print(self.prefix + "[ {}INFO{}]{}".format(colors.OKBLUE, colors.ENDC, text), end = end) return True def error(self, text): print(self.prefix + "[{}ERROR{}]{}".format(colors.FAIL, colors.ENDC, text)) return True def warning(self, text): print(self.prefix + "[ {}WARN{}]{}".format(colors.WARNING, colors.ENDC, text)) return True def debug(self, text): if DEBUG: print(self.prefix + "[{}DEBUG{}]{}".format(colors.HEADER, colors.ENDC, text)) return True class o: def info(text): print("[ {}INFO{}]{}".format(colors.OKBLUE, colors.ENDC, text)) return True def error(text): print("[{}ERROR{}]{}".format(colors.FAIL, colors.ENDC, text)) return True def warning(text): print("[ {}WARN{}]{}".format(colors.WARNING, colors.ENDC, text)) return True def debug(text): if DEBUG: print("[{}DEBUG{}]{}".format(colors.HEADER, colors.ENDC, text)) return True def hashfile(filepath, blocksize = 65536): file_hash = hashlib.sha256() with open(filepath, "rb") as f: fb = f.read(blocksize) while len(fb) > 0: file_hash.update(fb) fb = f.read(blocksize) return file_hash.hexdigest() def create_symbolic_file(source_file, symbolic_file): cmd = f"ln -s {source_file} {symbolic_file}" print(f"Execute system cmd: {cmd}") SCall(cmd.split()) class Manager: servers = [] max_lines = 4 current_server = None server_choices = [] def __init__(self, servers_file = "./servers.txt"): self.load_servers(servers_file) def select_server(self, choice): for server in self.servers: if server.select(choice): self.current_server = server o.debug("Selected: {}".format(self.current_server)) break if not self.current_server: o.error("Cannot choice server! Choice is invalid!") sys.exit(1) def load_servers(self, server_list_path): o.debug("Load servers file: {}".format(server_list_path)) ################################################################################ if check_file(server_list_path): with open(server_list_path, "r") as f_servers: readed_lines = 0 line = f_servers.readline() while line: if line and not line[0] == "#": if readed_lines == self.max_lines: readed_lines = 0 line = line.split("\n")[0] if not readed_lines: name = line elif readed_lines == 1: ip, port = line.split(":") elif readed_lines == 2: rcon_password = line elif readed_lines == 3: root_directory = line self.servers.append(Server(ip, port, rcon_password, name, root_directory)) ################################################################ readed_lines += 1 line = f_servers.readline() ############################################################################ if not self.servers: o.error("Servers not found! Insert server in {}".format(server_list_path)) sys.exit(2) else: o.error("Cannot find server file. App create him. Insert values!") with open(server_list_path, "w") as f_servers: f_servers.write(""" #////////////////////// #//PLACE HERE #////////////////////// #//ServerName #//IP:PORT #//RCON PASSWORD #//tf root folder #... """) sys.exit(1) ################################################################################ def execute(self, func, *args): """if self.current_server: getattr(self.current_server, func, None)(*args) else: for server in self.servers: for choice in self.server_choices: if server.choice(choice): getattr(server, func, None)(*args) break """ if self.current_server: getattr(self.current_server, func, None)(*args) elif self.server_choices: for server in self.servers: for choice in self.server_choices: if server.select(choice): getattr(server, func, None)(*args) break else: for server in self.servers: getattr(server, func, None)(*args) class Server: obj = None def __init__(self, ip, port, rcon_pass, name, root_directory): self.address = (ip, int(port)) self.password = rcon_pass self.name = name self.o = new_o(name) self.root = root_directory o.debug(self) def __str__(self): return "{:^25} on {}".format(self.name, self.address) def ln_vpk_files(self, vpk_directory): vpk_files = glob(f"{self.root}/*.vpk") vpk_directory_files = glob(f"{vpk_directory}/*.vpk") for server_vpk_file in vpk_files: for other_vpk_file in vpk_directory_files: if server_vpk_file.split("/")[-1] == other_vpk_file.split("/")[-1]: if hashfile(server_vpk_file) == hashfile(other_vpk_file): print(f"Create symbolic link from {other_vpk_file} to {server_vpk_file}") if self.wait_input(): remove_file(server_vpk_file) create_symbolic_file(other_vpk_file, server_vpk_file) return True def upgrade_metamod(self): pass def remove_plugin(self, plugin_name, need_reloads_plugins = True): plugins_list = glob(f"{self.root}/addons/sourcemod/plugins/*.smx") + glob(f"{self.root}/addons/sourcemod/plugins/*/*.smx") for plugin in plugins_list: if plugin.split("/")[-1] == plugin_name + ".smx": self.o.info(f"Remove plugin: {plugin}?") if self.wait_input(): remove_file(plugin) if need_reloads_plugins: self.o.info("Send sm plugins refresh command...", end = "\t") self.rcon("sm plugins refresh", hide_server_name=True) return True def upgrade_plugin(self, fresh_path, need_reloads_plugins = True): new_hash = hashfile(fresh_path) fresh_plugin_name = fresh_path.split("/")[-1:][0] plugins_list = glob(f"{self.root}/addons/sourcemod/plugins/*.smx") + glob(f"{self.root}/addons/sourcemod/plugins/*/*.smx") for plugin_path in plugins_list: if plugin_path.split("/")[-1:][0] == fresh_plugin_name: old_hash = hashfile(plugin_path) if old_hash == new_hash: self.o.info(f"Plugin {fresh_plugin_name} currenty updated!") return False else: self.o.info(f"Upgrade plugin {fresh_plugin_name}...") self.o.info(f"copy {fresh_path} to {plugin_path}") shutil.copyfile(fresh_path, plugin_path) if need_reloads_plugins: self.o.info("Send sm plugins refresh command...", end = "\t") self.rcon("sm plugins refresh", hide_server_name=True) return True self.o.info("Upgraded plugin not found in server...") return False def wait_input(self, yes = "y", no = "n"): if ALL_YES: return True while True: print("Enter \"{}\" to accept or \"{}\" to negative response:".format(yes, no) ,end="\t") response = input() if response.lower() == yes: return True if response.lower() == no: return False def select(self, request): if self.name == request: return True elif self.address == request: return True elif str(self.address[1]) == request: return True elif request in self.root: return True else: return False def status(self): player_count, max_count, ping = self.count_players() if player_count < 0: print("{} | Not responsed(((".format(self)) else: print("{} | {}/{} players | {} ms".format(self, player_count, max_count, ping)) def count_players(self): try: ping = 0.0 try: start_time = time() server = a2s.info(tuple(self.address)) count, max_count = server.player_count, server.max_players ping = server.ping except NameError: ping = time() - start_time except ValueError: return -1, -1, -1 return int(count), int(max_count), round(ping, 3) * 1000 except: #traceback.print_exc() return -1, -1, -1 def net_status_json(self): rcon_response = "" json_result = {} try: with RCON(self.address, self.password) as rcon: rcon_response = rcon.execute("net_status") response = rcon_response.body.decode("utf8","ignore") response = response.split("\n") ##################################################### gamemode, server_type, connections = response[1].split(": ")[1].split(",") connections = int(connections.split()[0]) ##################################################### client_port, server_port, hltv_port, matchmaking_port, systemlink_port, lan_port = response[2].split(": ")[1].split(",") client_port = int(client_port.split()[1]) server_port = int(server_port.split()[1]) hltv_port = int(hltv_port.split()[1]) ##################################################### try: latency_avg_out, latency_avg_in = response[3].split(": ")[1].split(",") latency_avg_out = float(latency_avg_out.split()[2][:-1]) latency_avg_in = float(latency_avg_in.split()[1][:-1]) except IndexError: print(dumps({ "config":{ "gamemode":gamemode, "server_type":server_type, "connections":connections }, "ports":{ "client":client_port, "server":server_port, "hltv":hltv_port }, "latency":{ "avg_out":0.0, "avg_in":0.0 }, "loss":{ "avg_out":0.0, "avg_in":0.0 }, "packets":{ "total":{ "out":0, "in":0 }, "client":{ "out":0, "in":0 } }, "data":{ "total":{ "out":0.0, "in":0.0 }, "client":{ "out":0.0, "in":0.0 } } })) return ##################################################### loss_avg_out, loss_avg_in = response[4].split(": ")[1].split(",") loss_avg_out = float(loss_avg_out.split()[2]) loss_avg_in = float(loss_avg_in.split()[1]) ##################################################### packets_total_out, packets_total_in = response[5].split(": ")[1].split(",") packets_total_out = float(packets_total_out.split()[3][:-2]) packets_total_in = float(packets_total_in.split()[1][:-2]) ##################################################### packets_per_client_out, packets_per_client_in = response[6].split(",") packets_per_client_out = float(packets_per_client_out.split()[3][:-2]) packets_per_client_in = float(packets_per_client_in.split()[1][:-2]) ##################################################### data_total_out = response[7].split(": ")[1].split(",")[0].split() if len(data_total_out) > 4: if data_total_out[4] == "kB/s": data_total_out = float(data_total_out[3]) * 1024 elif data_total_out[4] == "MB/s": data_total_out = float(data_total_out[3]) * 1024 * 1024 else: data_total_out = float(data_total_out[3]) # data_total_in = response[7].split(": ")[1].split(",")[1].split() if len(data_total_in) > 2: if data_total_in[2] == "kB/s": data_total_in = float(data_total_in[1]) * 1024 elif data_total_in[2] == "MB/s": data_total_in = float(data_total_in[1]) * 1024 * 1024 else: data_total_in = float(data_total_in[1]) ##################################################### data_per_client_out = response[8].split(",")[0].split() if len(data_per_client_out) > 4: if data_per_client_out[4] == "kB/s": data_per_client_out = float(data_per_client_out[3]) * 1024 elif data_per_client_out[4] == "MB/s": data_per_client_out = float(data_per_client_out[3]) * 1024 * 1024 else: data_per_client_out = float(data_per_client_out[3]) # data_per_client_in = response[8].split(",")[1].split() if len(data_per_client_in) > 2: if data_per_client_in[2] == "kB/s": data_per_client_in = float(data_per_client_in[1]) * 1024 elif data_per_client_in[2] == "MB/s": data_per_client_in = float(data_per_client_in[1]) * 1024 * 1024 else: data_per_client_in = float(data_per_client_in[1]) ###################################################### json_result = { "config":{ "gamemode":gamemode, "server_type":server_type, "connections":connections }, "ports":{ "client":client_port, "server":server_port, "hltv":hltv_port }, "latency":{ "avg_out":latency_avg_out, "avg_in":latency_avg_in }, "loss":{ "avg_out":loss_avg_out, "avg_in":loss_avg_in }, "packets":{ "total":{ "out":packets_total_out, "in":packets_total_in }, "client":{ "out":packets_per_client_out, "in":packets_per_client_in } }, "data":{ "total":{ "out":data_total_out, "in":data_total_in }, "client":{ "out":data_per_client_out, "in":data_per_client_in } } } #################################################### print(dumps(json_result)) except Exception as rcon_error: traceback.print_exc() print({}) def rcon(self, command, result = False, hide_server_name = False): if not hide_server_name: self.o.info("{obj.name:^25}: ".format(obj = self), end="\t") rcon_response = "" try: with RCON(self.address, self.password) as rcon: rcon_response = rcon.execute(command) response = rcon_response.body.decode("utf8","ignore") if not result: print(response if response else "ok") else: return response except Exception as rcon_error: rcon_response = "Rcon execute error: {}".format(rcon_error) print(rcon_response) def show_directory(self, path): full_path = "{}/{}".format(self.root, path) print("Current directory: {}".format(full_path)) if check_dir(full_path): SCall("ls -all {}".format(full_path).split()) else: print("Directory does not exist") def copy_file(self, source_file, path): full_path = "{}/{}".format(self.root, path) print("Copy in > {}".format(full_path)) new_file = source_file.split("/")[-1:][0] if check_dir(full_path): SCall("cp {0} {1}/{2}".format(source_file, full_path, new_file).split()) else: print("Destonation directory doesn't exists!") def symbolic_file(self, source_file, path): full_path = "{}/{}".format(self.root, path) print("Create link in > {}".format(full_path)) new_file = source_file.split("/")[-1:][0] if not "/" in source_file: source_file = os.getcwd() + "/" + source_file destonation_file = "{}/{}".format(full_path, new_file) if check_file(destonation_file): print("Destonation file is current created, override him?") if self.wait_input(): remove_file("{}/{}".format(full_path, new_file)) else: print("Abort operation!") return if check_dir(full_path): cmd = "ln -s {0} {1}/{2}".format(source_file, full_path, new_file) print("Execute: ", cmd) SCall(cmd.split()) else: print("Destonation directory doesn't exists!") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--serverslist", help="Path to servers list", default = "./servers.txt", type = str) parser.add_argument("--rcon", "-r", help = "Command to execute", type = str, nargs="+", default = "") parser.add_argument("--choice", "-c", help = "Choice server, aka: part name in directory", type = str, default = ["global.choice"], nargs = "+") parser.add_argument("--status", "-s", help = "Show current number players on server", default = False, action = "store_true") parser.add_argument("--yes", "-y", help = "Say YES to all response", default = False, action = "store_true") parser.add_argument("--netstatus","-ns", help = "Show json net_status", default = False, action = "store_true") ################################################################################################################################################ parser.add_argument("--CopyFile", "-cp", help = "Path of file to copy in root directory\nNeed second argument: --DestDir", type = str, default = "") parser.add_argument("--SymLinkFile", "-ln", help = "Path of file to create symbolic link in root directory\nNeed second argument: --DestDir", type = str, default = "") parser.add_argument("--DestDir", "-dd", help = "Destonation directory, aka: addons/sourcemod/plugins", type = str, default = "/") parser.add_argument("--ShowDir", "-ls", help = "Show ls command on dest directory", default = False, action = "store_true") ################################################################################################################################################ parser.add_argument("--RemovePlugin", "-rmv", help = "Name plugin to remove. Names must match.", type = str, default = "") parser.add_argument("--UpgradePlugin", "-upg", help = "Path of file to uprade. Names must match.", type = str, default = "") parser.add_argument("--NoReloadPlugins", "-nrp", help = "Upgrade plugins without send sm plugins refresh command", default = False, action = "store_true"); ################################################################################################################################################ args = parser.parse_args() ALL_YES = 1 if args.yes else 0 ################################## manager = Manager(args.serverslist) #if len(args.choice) > 0 and args.choice[0] == "global.choice": if not args.choice == ["global.choice"]: if len(args.choice) == 1: manager.select_server(args.choice[0]) else: manager.server_choices = args.choice ################################## if args.rcon: command = "" for word in args.rcon: command += word + " " command = command[:-1] manager.execute("rcon", command) sys.exit(0) ################################## if args.netstatus: manager.execute("net_status_json") sys.exit(0) ################################## if args.status: manager.execute("status") sys.exit(0) ################################## if args.ShowDir: if args.DestDir: manager.execute("show_directory", args.DestDir) sys.exit(0) else: o.error("Need --DestDir argument!") sys.exit(1) ################################## if args.CopyFile and args.DestDir: if check_file(args.CopyFile): manager.execute("copy_file", args.CopyFile, args.DestDir) sys.exit(0) else: o.error("Invalid path of source file!") sys.exit(1) ################################## if args.SymLinkFile and args.DestDir: if check_file(args.SymLinkFile): manager.execute("symbolic_file", args.SymLinkFile, args.DestDir) sys.exit(0) else: o.error("Invalid path of source file!") sys.exit(1) ################################### if args.UpgradePlugin: if check_file(args.UpgradePlugin): manager.execute("upgrade_plugin", args.UpgradePlugin, not args.NoReloadPlugins) #manager.execute("rcon", "sm plugins refresh") sys.exit(0) else: o.error("Invalid path of upgraded plugin!") sys.exit(1) ################################### if args.RemovePlugin: if True: manager.execute("remove_plugin", args.RemovePlugin, not args.NoReloadPlugins) #manager.execute("rcon", "sm plugins refresh") sys.exit(0) else: o.error("Invalid name of remove plugin!") sys.exit(1)