From 3b2c485c4018df5b4fc6026c194aa45f659a527a Mon Sep 17 00:00:00 2001 From: server Date: Wed, 22 Sep 2021 14:38:06 +0300 Subject: [PATCH] init repo --- .gitignore | 2 + SourceManager.py | 587 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 590 insertions(+) create mode 100644 .gitignore create mode 100755 SourceManager.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f1c7d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +old_version/ +servers.txt \ No newline at end of file diff --git a/SourceManager.py b/SourceManager.py new file mode 100755 index 0000000..c99968a --- /dev/null +++ b/SourceManager.py @@ -0,0 +1,587 @@ +#!/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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2c1a0de --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python_valve==0.2.1 \ No newline at end of file