From 99d3be4befc0a1907e783abf0d6f51e3989f0134 Mon Sep 17 00:00:00 2001 From: RhinosF1 Date: Tue, 18 Mar 2025 20:38:50 +0000 Subject: [PATCH 1/6] Create upgrade-cp-service.py --- miraheze/puppet/upgrade-cp-service.py | 140 ++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 miraheze/puppet/upgrade-cp-service.py diff --git a/miraheze/puppet/upgrade-cp-service.py b/miraheze/puppet/upgrade-cp-service.py new file mode 100644 index 0000000..7c22f90 --- /dev/null +++ b/miraheze/puppet/upgrade-cp-service.py @@ -0,0 +1,140 @@ +import subprocess +import time +import os + +class Environment(TypedDict): + wikidbname: str + wikiurl: str + servers: list + + +class EnvironmentList(TypedDict): + beta: Environment + prod: Environment + + +beta: Environment = { + 'wikidbname': 'metawikibeta', + 'wikiurl': 'meta.mirabeta.org', + 'servers': ['test151'], +} + + +prod: Environment = { + 'wikidbname': 'testwiki', + 'wikiurl': 'publictestwiki.com', + 'servers': [ + 'mw151', + 'mw152', + 'mw161', + 'mw162', + 'mw171', + 'mw172', + 'mw181', + 'mw182', + 'mwtask171', + 'mwtask181', + ], +} +ENVIRONMENTS: EnvironmentList = { + 'beta': beta, + 'prod': prod, +} +del beta +del prod +HOSTNAME = socket.gethostname().split('.')[0] + +class ServersAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): # noqa: U100 + input_servers = values.split(',') + valid_servers = get_environment_info()['servers'] + if 'all' in input_servers: + input_servers = valid_servers + invalid_servers = set(input_servers) - set(valid_servers) + if invalid_servers: + parser.error(f'invalid server choice(s): {", ".join(invalid_servers)}') + setattr(namespace, self.dest, input_servers) + +def run_command(command): + """Runs a shell command and returns the output.""" + try: + result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error executing {command}: {e}") + return None + +def wait_for_ping(server, timeout=300, interval=5): + """Waits for the server to respond to ping.""" + print(f"Waiting for {server} to come back online...") + start_time = time.time() + while time.time() - start_time < timeout: + response = os.system(f"ping -c 1 {server} > /dev/null 2>&1") + if response == 0: + print(f"{server} is back online!") + return True + time.sleep(interval) + print(f"Timeout waiting for {server} to come back online.") + return False + +def check_up(Debug: str, domain: str = 'meta.miraheze.org', verify: bool = True) -> bool: + if verify is False: + os.environ['PYTHONWARNINGS'] = 'ignore:Unverified HTTPS request' + + headers = {} + if Debug: + server = f'{Debug}.wikitide.net' + headers['X-WikiTide-Debug'] = server + location = f'{domain}@{server}' + + debug_access_key = os.getenv('DEBUG_ACCESS_KEY') + + # Check if DEBUG_ACCESS_KEY is set and add it to headers + if debug_access_key: + headers['X-WikiTide-Debug-Access-Key'] = debug_access_key + up = False + req = requests.get(f'https://{domain}:{port}/w/api.php?action=query&meta=siteinfo&formatversion=2&format=json', headers=headers, verify=verify) + if req.status_code == 200 and 'miraheze' in req.text and (Debug is None or Debug in req.headers['X-Served-By']): + up = True + if not up: + print(f'Status: {req.status_code}') + print(f'Text: {"miraheze" in req.text} \n {req.text}') + if 'X-Served-By' not in req.headers: + req.headers['X-Served-By'] = 'None' + print(f'Debug: {(Debug is None or Debug in req.headers["X-Served-By"])}') + return up + +def process_server(server): + """Performs the sequence of commands for a given server.""" + print(f"Processing {server}...") + + # Mark backend as sick + run_command(f"salt-ssh -E \"cp.*\" cmd.run \"sudo varnishadm backend.set_health {server} sick\"") + + # Upgrade packages + run_command(f"sudo upgrade-packages -a -s {server}") + + # Reboot the server + run_command(f"sudo salt-ssh \"{server}.wikitide.net\" cmd.run 'reboot now'") + + # Wait for the server to come back online + if wait_for_ping(server): + + # Perform health check + if check_up(server): + print(f"Health check passed for {server}") + + # Restore backend health to auto + run_command(f"salt-ssh -E \"cp.*\" cmd.run \"sudo varnishadm backend.set_health {server} auto\"") + else: + print(f"Health check failed for {server}") + input('Press enter to continue') + else: + print(f"Skipping health check as {server} did not respond to ping.") + input('Press enter to continue') + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Process some integers.') + parser.add_argument('--servers', dest='servers', action=ServersAction, required=True, help='server(s) to deploy to') + for server in servers: + process_server(server) From 29d70550bd731edfce6eb881800599de03e3e894 Mon Sep 17 00:00:00 2001 From: RhinosF1 Date: Tue, 18 Mar 2025 20:40:12 +0000 Subject: [PATCH 2/6] Update upgrade-cp-service.py --- miraheze/puppet/upgrade-cp-service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/miraheze/puppet/upgrade-cp-service.py b/miraheze/puppet/upgrade-cp-service.py index 7c22f90..55d46f3 100644 --- a/miraheze/puppet/upgrade-cp-service.py +++ b/miraheze/puppet/upgrade-cp-service.py @@ -136,5 +136,6 @@ def process_server(server): if __name__ == "__main__": parser = argparse.ArgumentParser(description='Process some integers.') parser.add_argument('--servers', dest='servers', action=ServersAction, required=True, help='server(s) to deploy to') + args = parser.parse_args() for server in servers: - process_server(server) + process_server(args.server) From 42486e1c12f94ea7bcc392bd10738446a748ff0b Mon Sep 17 00:00:00 2001 From: RhinosF1 Date: Tue, 18 Mar 2025 20:50:23 +0000 Subject: [PATCH 3/6] Update miraheze/puppet/upgrade-cp-service.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- miraheze/puppet/upgrade-cp-service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miraheze/puppet/upgrade-cp-service.py b/miraheze/puppet/upgrade-cp-service.py index 55d46f3..efdec1a 100644 --- a/miraheze/puppet/upgrade-cp-service.py +++ b/miraheze/puppet/upgrade-cp-service.py @@ -137,5 +137,5 @@ def process_server(server): parser = argparse.ArgumentParser(description='Process some integers.') parser.add_argument('--servers', dest='servers', action=ServersAction, required=True, help='server(s) to deploy to') args = parser.parse_args() - for server in servers: + for server in args.servers: process_server(args.server) From f5c97f1344bb27510ac3a02b950c5e8248bb9483 Mon Sep 17 00:00:00 2001 From: RhinosF1 Date: Tue, 18 Mar 2025 20:51:03 +0000 Subject: [PATCH 4/6] Update miraheze/puppet/upgrade-cp-service.py Co-authored-by: Claire --- miraheze/puppet/upgrade-cp-service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miraheze/puppet/upgrade-cp-service.py b/miraheze/puppet/upgrade-cp-service.py index efdec1a..889db24 100644 --- a/miraheze/puppet/upgrade-cp-service.py +++ b/miraheze/puppet/upgrade-cp-service.py @@ -42,7 +42,7 @@ class EnvironmentList(TypedDict): } del beta del prod -HOSTNAME = socket.gethostname().split('.')[0] +HOSTNAME = socket.gethostname().partition('.')[0] class ServersAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): # noqa: U100 From 86e3fae7e9dbaea033560771bba94ea1898fa3d3 Mon Sep 17 00:00:00 2001 From: RhinosF1 Date: Tue, 18 Mar 2025 20:51:22 +0000 Subject: [PATCH 5/6] Update miraheze/puppet/upgrade-cp-service.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- miraheze/puppet/upgrade-cp-service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miraheze/puppet/upgrade-cp-service.py b/miraheze/puppet/upgrade-cp-service.py index 889db24..531be0b 100644 --- a/miraheze/puppet/upgrade-cp-service.py +++ b/miraheze/puppet/upgrade-cp-service.py @@ -138,4 +138,4 @@ def process_server(server): parser.add_argument('--servers', dest='servers', action=ServersAction, required=True, help='server(s) to deploy to') args = parser.parse_args() for server in args.servers: - process_server(args.server) + process_server(server) From 3887e3587e25e497a8fd7f05c597ec236b8e7204 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:52:55 +0000 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`Rhi?= =?UTF-8?q?nosF1-patch-1`=20(#131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @RhinosF1. * https://github.com/miraheze/python-functions/pull/130#issuecomment-2734671180 The following files were modified: * `miraheze/puppet/upgrade-cp-service.py` Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: RhinosF1 --- miraheze/puppet/upgrade-cp-service.py | 72 +++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/miraheze/puppet/upgrade-cp-service.py b/miraheze/puppet/upgrade-cp-service.py index 531be0b..f371660 100644 --- a/miraheze/puppet/upgrade-cp-service.py +++ b/miraheze/puppet/upgrade-cp-service.py @@ -46,6 +46,13 @@ class EnvironmentList(TypedDict): class ServersAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): # noqa: U100 + """ + Parse and validate server choices from a comma-separated input string. + + Splits the input into individual server names and verifies each against the current + environment's valid servers. If the keyword "all" is present, it replaces the list with + all available servers. Triggers a parser error if any invalid server names are found. + """ input_servers = values.split(',') valid_servers = get_environment_info()['servers'] if 'all' in input_servers: @@ -56,7 +63,20 @@ def __call__(self, parser, namespace, values, option_string=None): # noqa: U100 setattr(namespace, self.dest, input_servers) def run_command(command): - """Runs a shell command and returns the output.""" + """ + Executes a shell command and returns its output. + + This function runs the given command in a subshell using the subprocess module. + It captures the command's standard output and returns it after stripping any + extraneous whitespace. If the command fails, it prints an error message and + returns None. + + Args: + command (str): The shell command to execute. + + Returns: + str or None: The command's output if successful; otherwise, None. + """ try: result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True) return result.stdout.strip() @@ -65,7 +85,21 @@ def run_command(command): return None def wait_for_ping(server, timeout=300, interval=5): - """Waits for the server to respond to ping.""" + """ + Waits for a server to become reachable via ping. + + Repeatedly pings the specified server at regular intervals until it responds or the timeout is reached. + Prints status messages indicating whether the server has come back online, and returns a boolean + value signifying the server's availability. + + Args: + server: The address or hostname of the server to ping. + timeout: The maximum time in seconds to wait for a response (default is 300). + interval: The interval in seconds between consecutive ping attempts (default is 5). + + Returns: + True if the server responds within the timeout period, otherwise False. + """ print(f"Waiting for {server} to come back online...") start_time = time.time() while time.time() - start_time < timeout: @@ -78,6 +112,25 @@ def wait_for_ping(server, timeout=300, interval=5): return False def check_up(Debug: str, domain: str = 'meta.miraheze.org', verify: bool = True) -> bool: + """ + Checks whether the target service is operational by querying its MediaWiki API. + + This function sends a GET request over HTTPS to the API endpoint at the given domain. + If a debug identifier is provided, it attaches custom headers and verifies that the + response header includes the expected debug context. The service is considered up if the + response has a 200 status code, contains the 'miraheze' marker in its body, and, when + debugging is active, the debug identifier is present in the 'X-Served-By' header. + SSL verification is controlled by the verify flag (with warnings suppressed when disabled). + Diagnostic messages are printed if the service does not meet these checks. + + Parameters: + Debug: A debug identifier used to insert and validate custom headers. + domain: The service domain to query (default "meta.miraheze.org"). + verify: Flag to enforce SSL certificate verification (default True). + + Returns: + True if the service is healthy; otherwise, False. + """ if verify is False: os.environ['PYTHONWARNINGS'] = 'ignore:Unverified HTTPS request' @@ -105,7 +158,18 @@ def check_up(Debug: str, domain: str = 'meta.miraheze.org', verify: bool = True) return up def process_server(server): - """Performs the sequence of commands for a given server.""" + """ + Processes the upgrade workflow for the specified server. + + This function orchestrates the server upgrade steps by marking the backend as sick, + upgrading packages, rebooting the server, and then verifying that the server is back online. + If the server responds to a ping, it conducts a health check and, if successful, restores + the backend health to auto. If the health check fails or the server does not respond to ping, + the function pauses until the user confirms to continue. + + Parameters: + server: The hostname or identifier of the server to process. + """ print(f"Processing {server}...") # Mark backend as sick @@ -138,4 +202,4 @@ def process_server(server): parser.add_argument('--servers', dest='servers', action=ServersAction, required=True, help='server(s) to deploy to') args = parser.parse_args() for server in args.servers: - process_server(server) + process_server(args.servers)