From b30beadba304728349a186a474f60b612df28165 Mon Sep 17 00:00:00 2001 From: Antonio Emmanuel Guerrero Silva Date: Tue, 22 Oct 2024 22:28:08 -0600 Subject: [PATCH] refs #839 Review and correction of bugs in migrated code --- admin/Interface/Apagar.py | 9 ++ admin/Interface/CambiarAcceso.py | 58 ++++++++++++ admin/Interface/Configurar.py | 121 ++++++++++++++++++++++++++ admin/Interface/ConsolaRemota.py | 29 ++++++ admin/Interface/CrearImagen.py | 83 ++++++++++++++++++ admin/Interface/EjecutarScript.py | 63 ++++++++++++++ admin/Interface/GetConfiguration.py | 67 ++++++++++++++ admin/Interface/GetIpAddress.py | 12 +++ admin/Interface/IniciarSesion.py | 17 ++++ admin/Interface/InventarioHardware.py | 20 +++++ admin/Interface/InventarioSoftware.py | 45 ++++++++++ admin/Interface/ProcesaCache.py | 8 ++ admin/Interface/Reiniciar.py | 7 ++ admin/Interface/RestaurarImagen.py | 28 ++++++ 14 files changed, 567 insertions(+) create mode 100644 admin/Interface/Apagar.py create mode 100644 admin/Interface/CambiarAcceso.py create mode 100644 admin/Interface/Configurar.py create mode 100644 admin/Interface/ConsolaRemota.py create mode 100644 admin/Interface/CrearImagen.py create mode 100644 admin/Interface/EjecutarScript.py create mode 100644 admin/Interface/GetConfiguration.py create mode 100644 admin/Interface/GetIpAddress.py create mode 100644 admin/Interface/IniciarSesion.py create mode 100644 admin/Interface/InventarioHardware.py create mode 100644 admin/Interface/InventarioSoftware.py create mode 100644 admin/Interface/ProcesaCache.py create mode 100644 admin/Interface/Reiniciar.py create mode 100644 admin/Interface/RestaurarImagen.py diff --git a/admin/Interface/Apagar.py b/admin/Interface/Apagar.py new file mode 100644 index 0000000..4219f56 --- /dev/null +++ b/admin/Interface/Apagar.py @@ -0,0 +1,9 @@ +import os +import sys + +def main(): + os.system('poweroff') + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/admin/Interface/CambiarAcceso.py b/admin/Interface/CambiarAcceso.py new file mode 100644 index 0000000..4d507bd --- /dev/null +++ b/admin/Interface/CambiarAcceso.py @@ -0,0 +1,58 @@ +import os +import sys +import subprocess + +#!/usr/bin/env python3 + +def main(mode): + PROG = os.path.basename(__file__) + CALLER = og_get_caller() + if not og_check_string_in_group(CALLER, ["CrearImagen", "ConsolaRemota", "CrearImagenBasica", "CrearSoftIncremental"]): + og_raise_error("OG_ERR_NOTEXEC", f"{CALLER} -> {PROG}") + sys.exit(1) + + REPOIP = og_get_repo_ip() + if not REPOIP: + og_raise_error("OG_ERR_NOTFOUND", "repo no montado") + sys.exit(1) + if og_is_repo_locked(): + og_raise_error("OG_ERR_LOCKED", f"repo {REPOIP}") + sys.exit(1) + + PROTO = os.getenv("ogprotocol", "smb") + if PROTO not in ["nfs", "smb"]: + og_raise_error("OG_ERR_FORMAT", f"protocolo desconocido {PROTO}") + sys.exit(1) + + if mode == "admin": + MODE = "rw" + elif mode == "user": + MODE = "ro" + else: + og_raise_error("OG_ERR_FORMAT", f"modo desconocido {mode}") + sys.exit(1) + + OGIMG = "/path/to/ogimg" # Placeholder for actual OGIMG path + subprocess.run(["umount", OGIMG], check=True) + + ogunit = os.getenv("ogunit", "") + OGUNIT = f"/{ogunit}" if ogunit else "" + og_echo("info", f"{PROG}: Montar repositorio {REPOIP} por {PROTO} en modo {mode}") + + if PROTO == "nfs": + subprocess.run(["mount", "-t", "nfs", f"{REPOIP}:{OGIMG}{OGUNIT}", OGIMG, "-o", MODE], check=True) + elif PROTO == "smb": + with open("/scripts/ogfunctions") as f: + for line in f: + if "OPTIONS=" in line: + PASS = line.split("pass=")[1].split()[0] + break + else: + PASS = "og" + subprocess.run(["mount.cifs", f"//{REPOIP}/ogimages{OGUNIT}", OGIMG, "-o", f"{MODE},serverino,acl,username=opengnsys,password={PASS}"], check=True) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: CambiarAcceso.py ", file=sys.stderr) + sys.exit(1) + main(sys.argv[1]) \ No newline at end of file diff --git a/admin/Interface/Configurar.py b/admin/Interface/Configurar.py new file mode 100644 index 0000000..4950349 --- /dev/null +++ b/admin/Interface/Configurar.py @@ -0,0 +1,121 @@ +import os +import subprocess +import sys + +def run_command(command): + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode != 0: + print(f"Error: {result.stderr}") + sys.exit(result.returncode) + return result.stdout.strip() + +def main(): + # Load engine configurator from engine.cfg file. + og_engine_configurate = os.getenv('OGENGINECONFIGURATE') + if not og_engine_configurate: + run_command('source /opt/opengnsys/etc/engine.cfg') + + # Clear temporary file used as log track by httpdlog + for log_file in [os.getenv('OGLOGSESSION'), os.getenv('OGLOGCOMMAND'), f"{os.getenv('OGLOGCOMMAND')}.tmp"]: + with open(log_file, 'w') as f: + f.write(" ") + + # Registro de inicio de ejecución + run_command(f'ogEcho log session "$MSG_INTERFACE_START {sys.argv[0]} {" ".join(sys.argv[1:])}"') + + # Captura de parámetros (se ignora el 1er parámetro y se eliminan espacios y tabuladores). + param = "".join(sys.argv[2:]).replace(" ", "").replace("\t", "") + + # Activa navegador para ver progreso + coproc = subprocess.Popen(['/opt/opengnsys/bin/browser', '-qws', 'http://localhost/cgi-bin/httpd-log.sh']) + + # Leer los dos bloques de parámetros, separados por '!'. + pparam, sparam = param.split('!') + + # Toma valores de disco y caché, separados por "*". + disk_params = dict(item.split('=') for item in pparam.split('*')) + dis = disk_params.get('dis') + che = disk_params.get('che') + tch = disk_params.get('tch') + + # Error si no se define el parámetro de disco (dis). + if not dis: + sys.exit(os.getenv('OG_ERR_FORMAT')) + + # Toma valores de distribución de particiones, separados por "%". + partition_params = sparam.split('%') + + maxp = 0 + TBP = {} + TBF = {} + + for param in partition_params: + cfg = dict(item.split('=') for item in param.split('*')) + par = int(cfg.get('par', 0)) + cpt = cfg.get('cpt') + sfi = cfg.get('sfi') + tam = cfg.get('tam') + ope = cfg.get('ope') + + if cpt != "CACHE": + TBP[par] = f"{cpt}:{tam}" + + if ope == "1": + if run_command(f'ogCheckStringInGroup {cpt} "EMPTY EXTENDED LINUX-LVM LVM ZPOOL"') != "0": + TBF[par] = sfi + + if par > maxp: + maxp = par + + # Tamaño actual de la cache + CACHESIZE = run_command('ogGetCacheSize') + + # Desmonta todas las particiones y la caché + run_command(f'ogEcho session log "[10] $MSG_HELP_ogUnmountAll"') + run_command(f'ogUnmountAll {dis}') + run_command('ogUnmountCache') + + # Elimina la tabla de particiones + if run_command('ogGetPartitionTableType 1') != 'MSDOS': + run_command(f'ogDeletePartitionTable {dis}') + run_command(f'ogExecAndLog COMMAND ogUpdatePartitionTable {dis}') + run_command(f'ogCreatePartitionTable {dis} MSDOS') + + # Inicia la cache. + if "CACHE" in sparam: + run_command(f'ogEcho session log "[30] $MSG_HELP_ogCreateCache"') + run_command(f'ogEcho session log " initCache {tch}"') + run_command(f'ogExecAndLog COMMAND initCache {tch}') + + # Definir particionado. + run_command(f'ogEcho session log "[50] $MSG_HELP_ogCreatePartitions"') + run_command(f'ogEcho session log " ogCreatePartitions {dis} {" ".join(TBP.values())}"') + if run_command(f'ogExecAndLog COMMAND ogCreatePartitions {dis} {" ".join(TBP.values())}') != "0": + coproc.kill() + sys.exit(run_command(f'ogRaiseError session log $OG_ERR_GENERIC "ogCreatePartitions {dis} {" ".join(TBP.values())}"')) + + run_command(f'ogExecAndLog COMMAND ogUpdatePartitionTable {dis}') + + # Formatear particiones + run_command(f'ogEcho session log "[70] $MSG_HELP_ogFormat"') + + for par in range(1, maxp + 1): + if TBF.get(par) == "CACHE": + if CACHESIZE == tch: + run_command(f'ogEcho session log " ogFormatCache"') + run_command(f'ogExecAndLog COMMAND ogFormatCache') + elif TBF.get(par): + run_command(f'ogEcho session log " ogFormatFs {dis} {par} {TBF[par]}"') + if run_command(f'ogExecAndLog COMMAND ogFormatFs {dis} {par} {TBF[par]}') != "0": + coproc.kill() + sys.exit(run_command(f'ogRaiseError session log $OG_ERR_GENERIC "ogFormatFs {dis} {par} {TBF[par]}"')) + + # Registro de fin de ejecución + run_command(f'ogEcho log session "$MSG_INTERFACE_END {0}"') + + # Retorno + coproc.kill() + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/admin/Interface/ConsolaRemota.py b/admin/Interface/ConsolaRemota.py new file mode 100644 index 0000000..6790b6a --- /dev/null +++ b/admin/Interface/ConsolaRemota.py @@ -0,0 +1,29 @@ +import os +import sys +import subprocess + +def main(script_path, output_path): + try: + # Make the script executable + os.chmod(script_path, 0o755) + + # Execute the script and redirect output + with open(output_path, 'w') as output_file: + result = subprocess.run([script_path], stdout=output_file, stderr=subprocess.PIPE) + + # Check if the script execution was successful + if result.returncode != 0: + sys.exit(result.returncode) + except Exception as e: + print(f"An error occurred: {e}") + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python3 ConsolaRemota.py ") + sys.exit(1) + + script_path = sys.argv[1] + output_path = sys.argv[2] + + main(script_path, output_path) \ No newline at end of file diff --git a/admin/Interface/CrearImagen.py b/admin/Interface/CrearImagen.py new file mode 100644 index 0000000..df66d1e --- /dev/null +++ b/admin/Interface/CrearImagen.py @@ -0,0 +1,83 @@ +import os +import sys +import subprocess +import time + +#!/usr/bin/env python3 + + +# Error codes +OG_ERR_NOTEXEC = 1 +OG_ERR_LOCKED = 4 +OG_ERR_FORMAT = 1 +OG_ERR_PARTITION = 3 +OG_ERR_IMAGE = 5 +OG_ERR_NOTWRITE = 14 +OG_ERR_NOTCACHE = 15 +OG_ERR_CACHESIZE = 16 +OG_ERR_REDUCEFS = 17 +OG_ERR_EXTENDFS = 18 + +def main(): + if len(sys.argv) != 5: + og_raise_error(OG_ERR_FORMAT, "Incorrect number of arguments") + + disk_num = sys.argv[1] + partition_num = sys.argv[2] + image_name = sys.argv[3] + repo = sys.argv[4] if sys.argv[4] else "REPO" + + start_time = time.time() + + # Load engine configurator + og_engine_configurate = os.getenv('OGENGINECONFIGURATE') + if not og_engine_configurate: + exec(open("/opt/opengnsys/etc/engine.cfg").read()) + + # Clear temporary log files + with open(os.getenv('OGLOGSESSION'), 'w') as f: + f.write(" ") + with open(os.getenv('OGLOGCOMMAND'), 'w') as f: + f.write(" ") + with open(f"{os.getenv('OGLOGCOMMAND')}.tmp", 'w') as f: + f.write(" ") + + # Log start of execution + og_echo("log session", f"Start {sys.argv[0]} {' '.join(sys.argv)}") + + # Check if called by OpenGnsys Client + caller = og_get_caller() + if caller != "ogAdmClient": + og_raise_error(OG_ERR_NOTEXEC, f"{caller} -> {sys.argv[0]}") + + # Default repository value + if repo == og_get_ip_address(): + repo = "CACHE" + + if og_check_ip_address(repo) or repo == "REPO": + og_unit = os.getenv('ogunit') + if og_unit: + og_unit = og_unit + if not og_change_repo(repo, og_unit): + og_raise_error(OG_ERR_NOTFOUND, f"{repo}") + + if repo == "REPO" and os.getenv('boot') != "admin": + if cambiar_acceso("admin") > 0: + sys.exit(1) + + og_echo("createImage", f"{disk_num} {partition_num} {repo} /{image_name}") + if subprocess.call(["which", "createImageCustom"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0: + retval = create_image_custom(disk_num, partition_num, repo, image_name) + else: + retval = create_image(disk_num, partition_num, repo, image_name) + + if repo == "REPO" and os.getenv('boot') != "admin": + cambiar_acceso("user") + + # Log end of execution + og_echo("log session", f"End {retval}") + + sys.exit(retval) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/admin/Interface/EjecutarScript.py b/admin/Interface/EjecutarScript.py new file mode 100644 index 0000000..57d9aaa --- /dev/null +++ b/admin/Interface/EjecutarScript.py @@ -0,0 +1,63 @@ +import os +import time +import subprocess +import sys + +def main(script_path): + start_time = time.time() + + # Load engine configurator from engine.cfg file. + engine_config_path = '/opt/opengnsys/etc/engine.cfg' + if 'OGENGINECONFIGURATE' not in os.environ: + with open(engine_config_path) as f: + exec(f.read(), globals()) + + # Clear temporary file used as log track by httpdlog + with open(os.environ['OGLOGSESSION'], 'w') as f: + f.write("") + with open(os.environ['OGLOGCOMMAND'], 'w') as f: + f.write("") + + # Registro de inicio de ejecución + ogEcho('log session', f"{os.environ['MSG_INTERFACE_START']} {sys.argv[0]} {' '.join(sys.argv[1:])}") + + with open(os.environ['OGLOGFILE'], 'a') as log_file: + log_file.write("\n Instrucciones a ejecutar: *****************************\n") + with open(script_path) as script_file: + log_file.write(script_file.read()) + + log_file.write("\n Salida de las instrucciones: *****************************\n") + + os.chmod(script_path, 0o755) + result = subprocess.run([script_path], capture_output=True, text=True) + ret_val = result.returncode + + with open(os.environ['OGLOGCOMMAND'], 'a') as log_command_file: + log_command_file.write(result.stdout) + log_command_file.write(result.stderr) + + elapsed_time = time.time() - start_time + if ret_val == 0: + ogEcho('log session', f"[100] Duracion de la operacion {int(elapsed_time // 60)}m {int(elapsed_time % 60)}s") + else: + ogRaiseError('log session', ret_val) + ogEcho('log session', 'error "Operacion no realizada"') + + # Registro de fin de ejecución + ogEcho('log session', f"{os.environ['MSG_INTERFACE_END']} {ret_val}") + + sys.exit(ret_val) + +def ogEcho(log_type, message): + # Placeholder for the ogEcho function + print(f"{log_type}: {message}") + +def ogRaiseError(log_type, error_code): + # Placeholder for the ogRaiseError function + print(f"{log_type}: Error code {error_code}") + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python EjecutarScript.py ") + sys.exit(1) + main(sys.argv[1]) \ No newline at end of file diff --git a/admin/Interface/GetConfiguration.py b/admin/Interface/GetConfiguration.py new file mode 100644 index 0000000..a987afb --- /dev/null +++ b/admin/Interface/GetConfiguration.py @@ -0,0 +1,67 @@ +import os +import subprocess + +#!/usr/bin/env python3 + +def main(): + os.environ["DEBUG"] = "no" + + ser = ogGetSerialNumber() + cfg = "" + disks = len(ogDiskToDev().split()) + + for dsk in range(1, disks + 1): + particiones = ogGetPartitionsNumber(dsk) or 0 + ptt = ogGetPartitionTableType(dsk) + ptt_map = {"MSDOS": 1, "GPT": 2, "LVM": 3, "ZPOOL": 4} + ptt = ptt_map.get(ptt, 0) + + cfg += f"{dsk}:0:{ptt}:::{ogGetDiskSize(dsk)}:0;" + + for par in range(1, particiones + 1): + cod = ogGetPartitionId(dsk, par) + fsi = getFsType(dsk, par) or "EMPTY" + tam = ogGetPartitionSize(dsk, par) or "0" + soi = "" + uso = 0 + + if fsi not in ["", "EMPTY", "LINUX-SWAP", "LINUX-LVM", "ZVOL"]: + if ogMount(dsk, par): + soi = getOsVersion(dsk, par).split(":")[1] + if not soi: + soi = getOsVersion(dsk, par).split(":")[1] + if not soi and fsi not in ["EMPTY", "CACHE"]: + soi = "DATA" + uso = int(subprocess.getoutput(f"df {ogGetMountPoint(dsk, par)} | awk '{{getline; printf \"%d\",$5}}'") or 0) + + cfg += f"{dsk}:{par}:{cod}:{fsi}:{soi}:{tam}:{uso};" + + if not cfg: + cfg = "1:0:0:::0;" + + cfgfile = "/tmp/getconfig" + with open(cfgfile, "w") as f: + f.write(f"{ser + ';' if ser else ''}{cfg}") + + generateMenuDefault() + + with open(cfgfile, "r") as f: + data = f.read() + + for line in data.split(";"): + if line: + parts = line.split(":") + if len(parts) == 1: + print(f"ser={parts[0]}") + else: + print(f"disk={parts[0]}\tpar={parts[1]}\tcpt={parts[2]}\tfsi={parts[3]}\tsoi={parts[4]}\ttam={parts[5]}\tuso={parts[6]}") + + for root, dirs, files in os.walk("/mnt"): + for file in files: + if file.startswith("ogboot."): + os.remove(os.path.join(root, file)) + + os.environ.pop("DEBUG", None) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/admin/Interface/GetIpAddress.py b/admin/Interface/GetIpAddress.py new file mode 100644 index 0000000..4458282 --- /dev/null +++ b/admin/Interface/GetIpAddress.py @@ -0,0 +1,12 @@ +import socket + +#!/usr/bin/env python3 + + +def get_ip_address(): + hostname = socket.gethostname() + ip_address = socket.gethostbyname(hostname) + return ip_address + +if __name__ == "__main__": + print("IP Address:", get_ip_address()) \ No newline at end of file diff --git a/admin/Interface/IniciarSesion.py b/admin/Interface/IniciarSesion.py new file mode 100644 index 0000000..86968d0 --- /dev/null +++ b/admin/Interface/IniciarSesion.py @@ -0,0 +1,17 @@ +import sys +import subprocess + +def main(): + args = sys.argv[1:] + + if len(args) == 1: + disk = 1 + part = args[0] + else: + disk = args[0] + part = args[1] + + boot_os(disk, part) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/admin/Interface/InventarioHardware.py b/admin/Interface/InventarioHardware.py new file mode 100644 index 0000000..850f14b --- /dev/null +++ b/admin/Interface/InventarioHardware.py @@ -0,0 +1,20 @@ +import subprocess +import sys + +def list_hardware_info(): + # Replace this with the actual command to list hardware info + result = subprocess.run(['listHardwareInfo'], capture_output=True, text=True) + return result.stdout + +def save_hardware_info(output_file): + hardware_info = list_hardware_info() + lines = hardware_info.splitlines() + if len(lines) > 1: + with open(output_file, 'w') as f: + f.write('\n'.join(lines[1:])) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python InventarioHardware.py ") + sys.exit(1) + save_hardware_info(sys.argv[1]) \ No newline at end of file diff --git a/admin/Interface/InventarioSoftware.py b/admin/Interface/InventarioSoftware.py new file mode 100644 index 0000000..6ab4215 --- /dev/null +++ b/admin/Interface/InventarioSoftware.py @@ -0,0 +1,45 @@ +import os +import time +import shutil +import subprocess +import sys + +def main(arg1, arg2, destination): + start_time = time.time() + + # Load the engine configuration from the engine.cfg file + og_engine_configurate = os.getenv('OGENGINECONFIGURATE') + if not og_engine_configurate: + with open('/opt/opengnsys/etc/engine.cfg') as f: + exec(f.read()) + + # Clear the temporary files used as log tracking for httpdlog + og_log_session = os.getenv('OGLOGSESSION') + og_log_command = os.getenv('OGLOGCOMMAND') + if og_log_session and og_log_command: + with open(og_log_session, 'w') as f: + f.write(" ") + with open(og_log_command, 'w') as f: + f.write(" ") + with open(f"{og_log_command}.tmp", 'w') as f: + f.write(" ") + + # Log the start of execution + msg_interface_start = os.getenv('MSG_INTERFACE_START') + if msg_interface_start: + subprocess.run(['ogEcho', 'log', 'session', f"{msg_interface_start} {__file__} {arg1} {arg2}"]) + + # Get the software info file and copy it to the destination + file = list_software_info(arg1, arg2) + shutil.copy(file, destination) + + elapsed_time = time.time() - start_time + msg_scripts_time_partial = os.getenv('MSG_SCRIPTS_TIME_PARTIAL') + if msg_scripts_time_partial: + subprocess.run(['ogEcho', 'log', 'session', f" [ ] {msg_scripts_time_partial} : {int(elapsed_time // 60)}m {int(elapsed_time % 60)}s"]) + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: python InventarioSoftware.py ") + sys.exit(1) + main(sys.argv[1], sys.argv[2], sys.argv[3]) \ No newline at end of file diff --git a/admin/Interface/ProcesaCache.py b/admin/Interface/ProcesaCache.py new file mode 100644 index 0000000..c45b959 --- /dev/null +++ b/admin/Interface/ProcesaCache.py @@ -0,0 +1,8 @@ +import sys + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python ProcesaCache.py ") + sys.exit(1) + + init_cache(sys.argv[1]) \ No newline at end of file diff --git a/admin/Interface/Reiniciar.py b/admin/Interface/Reiniciar.py new file mode 100644 index 0000000..d77f9cb --- /dev/null +++ b/admin/Interface/Reiniciar.py @@ -0,0 +1,7 @@ +import os + +def reboot_system(): + os.system('reboot') + +if __name__ == "__main__": + reboot_system() \ No newline at end of file diff --git a/admin/Interface/RestaurarImagen.py b/admin/Interface/RestaurarImagen.py new file mode 100644 index 0000000..60b4af6 --- /dev/null +++ b/admin/Interface/RestaurarImagen.py @@ -0,0 +1,28 @@ +import sys +import subprocess + +def deploy_image(repo_ip, image_name, disk, partition, protocol, protocol_options, *args): + try: + result = subprocess.run( + ["deployImage", repo_ip, image_name, disk, partition, protocol, protocol_options, *args], + check=True + ) + return result.returncode + except subprocess.CalledProcessError as e: + return e.returncode + +if __name__ == "__main__": + if len(sys.argv) < 7: + print("Usage: python RestaurarImagen.py [additional_args...]") + sys.exit(1) + + disk = sys.argv[1] + partition = sys.argv[2] + image_name = sys.argv[3] + repo_ip = sys.argv[4] + protocol = sys.argv[5] + protocol_options = sys.argv[6] + additional_args = sys.argv[7:] + + exit_code = deploy_image(repo_ip, image_name, disk, partition, protocol, protocol_options, *additional_args) + sys.exit(exit_code) \ No newline at end of file