-
-
Notifications
You must be signed in to change notification settings - Fork 3
Create upgrade-cp-service.py #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
99d3be4
29d7055
42486e1
f5c97f1
86e3fae
3887e35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,205 @@ | ||||||||||||||||||||
| 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 | ||||||||||||||||||||
|
Comment on lines
+16
to
+44
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we didn't use intermediary variables for |
||||||||||||||||||||
| HOSTNAME = socket.gethostname().partition('.')[0] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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'] | ||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
| 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): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| 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) | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a fan of |
||||||||||||||||||||
| 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 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: | ||||||||||||||||||||
| response = os.system(f"ping -c 1 {server} > /dev/null 2>&1") | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||||||||||||||||||||
| 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: | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the variable called Debug and not debug |
||||||||||||||||||||
| """ | ||||||||||||||||||||
| 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: | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. False? But Debug is a string :p |
||||||||||||||||||||
| 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) | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Provide a default value for The code references -def check_up(Debug: str, domain: str = 'meta.miraheze.org', verify: bool = True) -> bool:
+def check_up(Debug: str, domain: str = 'meta.miraheze.org', verify: bool = True, port: int = 443) -> bool:
...📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.8.2)96-96: Undefined name (F821) 96-96: Undefined name (F821) 🪛 GitHub Actions: Check Python[error] 96-96: F821 undefined name 'requests' [error] 96-96: F821 undefined name 'port' |
||||||||||||||||||||
| 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): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| 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 | ||||||||||||||||||||
| 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.') | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. funny description :3 |
||||||||||||||||||||
| 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.servers) | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing imports to fix NameErrors.
The script makes use of
TypedDict,requests,argparse, andsocketbut fails to import them, causing NameErrors. Please add these imports:📝 Committable suggestion