diff --git a/.github/workflows/ci-on_pr_main_bash.yml b/.github/workflows/ci-on_pr_main_bash.yml index bc48168..09782e3 100644 --- a/.github/workflows/ci-on_pr_main_bash.yml +++ b/.github/workflows/ci-on_pr_main_bash.yml @@ -19,6 +19,9 @@ env: TBTT_SLACK_SIGNING_SECRET: tbtt_slack_signing_secret TBTT_SLACK_USER_TOKEN: tbtt_slack_user_token TBTT_GITHUB_WEBHOOK_SECRET: tbtt_github_webhook_secret + TBTT_OVH_APP_KEY: a + TBTT_OVH_APP_SECRET: b + TBTT_OVH_CONSUMER_KEY: c jobs: lint-and-test: diff --git a/requirements.txt b/requirements.txt index 57ddf65..297bcbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,7 @@ lazy-object-proxy==1.9.0 MarkupSafe==2.1.3 mccabe==0.7.0 multidict==6.0.4 +ovh==1.1.0 packaging==23.1 platformdirs==3.9.1 pluggy==1.2.0 diff --git a/sources/TechTeamBot.py b/sources/TechTeamBot.py index bdf0184..238d047 100644 --- a/sources/TechTeamBot.py +++ b/sources/TechTeamBot.py @@ -42,6 +42,9 @@ def __setup_keys(self): self.__load_key("TBTT_SLACK_USER_TOKEN", cst.APP_CONFIG_TOKEN_SLACK_USER_TOKEN) self.__load_key("TBTT_GITHUB_ACCESS_TOKEN", cst.APP_CONFIG_TOKEN_GITHUB_ACCESS_TOKEN) self.__load_key("TBTT_GITHUB_WEBHOOK_SECRET", cst.APP_CONFIG_TOKEN_GITHUB_WEBHOOK_SECRET) + self.__load_key("TBTT_OVH_APP_KEY", cst.APP_CONFIG_TOKEN_OVH_APP_KEY) + self.__load_key("TBTT_OVH_APP_SECRET", cst.APP_CONFIG_TOKEN_OVH_APP_SECRET) + self.__load_key("TBTT_OVH_CONSUMER_KEY", cst.APP_CONFIG_TOKEN_OVH_CONSUMER_KEY) def __setup_slack_interaction_endpoint(self): """ diff --git a/sources/factories/OvhApiFactory.py b/sources/factories/OvhApiFactory.py new file mode 100644 index 0000000..a69dbc8 --- /dev/null +++ b/sources/factories/OvhApiFactory.py @@ -0,0 +1,64 @@ +""" + This module defines the factory for OVH API +""" +from flask import current_app +import sources.utils.IpAddress as IpAddress +import sources.utils.Constants as cst +import ovh + + +class OvhApiFactory(): + """ + Class managing the API for OVH + + """ + def __init__(self): + """ + The factory instanciates the objects it needed to complete the processing of the request. + """ + self.client = None + + def _get_ovh_client(self, app_context): + """ + Return the ovh client and creates it if needed + """ + if self.client is None: + app_context.push() + self.client = ovh.Client( + endpoint='ovh-eu', # Endpoint of API OVH Europe (List of available endpoints) + application_key=current_app.config[cst.APP_CONFIG_TOKEN_OVH_APP_KEY], + application_secret=current_app.config[cst.APP_CONFIG_TOKEN_OVH_APP_SECRET], + consumer_key=current_app.config[cst.APP_CONFIG_TOKEN_OVH_CONSUMER_KEY], + ) + return self.client + + def get_dedicated_servers(self, app_context): + """ + Retrieves the list of dedicated servers available + """ + client = self._get_ovh_client(app_context) + result = client.get('/dedicated/server', iamTags=None) + return result + + def get_dedicated_server_display_name(self, app_context, server_name): + """ + Returns display_name of the dedicated server. + """ + client = self._get_ovh_client(app_context) + service_info = client.get(f'/dedicated/server/{server_name}/serviceInfos') + service_id = service_info["serviceId"] + service = client.get(f'/service/{service_id}') + display_name = service["resource"]["displayName"] + return display_name + + def get_dedicated_server_ips(self, app_context, server_name): + """ + Return the IPv6 and IPv4 of a dedicated server + """ + client = self._get_ovh_client(app_context) + raw_result = client.get(f'/dedicated/server/{server_name}/ips') + result = dict() + for ip in raw_result: + ip_split = ip.split("/") + result[IpAddress.validIPAddress(ip_split[0])] = ip + return result diff --git a/sources/handlers/ServerListHandler.py b/sources/handlers/ServerListHandler.py new file mode 100644 index 0000000..09335e7 --- /dev/null +++ b/sources/handlers/ServerListHandler.py @@ -0,0 +1,61 @@ +""" + This module defines the handler for logic related to listing server IPs. +""" +from sources.factories.SlackMessageFactory import SlackMessageFactory +from sources.factories.OvhApiFactory import OvhApiFactory +import sources.utils.IpAddress as IpAddress + + +class ServerListHandler(): + """ + Class managing the business logic related to listing servers WP Media uses. + + """ + def __init__(self): + """ + The handler instanciates the objects it needed to complete the processing of the request. + """ + self.slack_message_factory = SlackMessageFactory() + self.ovh_api_factory = OvhApiFactory() + + def send_wp_rocket_ips(self, app_context, slack_user): + """ + List all IPs used for WP Rocket and sends it in a Slack DM + """ + text = "List of IPs used for WP Rocket:\n\n" + + text += "License validation/activation, update check, plugin information:\n" + # Defined in https://gitlab.one.com/systems/group.one-authdns/-/blob/main/octodns/wp-rocket.me.yaml?ref_type=heads + text += "https://wp-rocket.me / 185.10.9.101\n" + text += "\n" + + text += "Load CSS Asynchronously:\n" + # Defined in https://gitlab.one.com/systems/group.one-authdns/-/blob/main/octodns/wp-rocket.me.yaml?ref_type=heads + text += "https://cpcss.wp-rocket.me / 46.30.212.116\n" + # Defined in k8s_sips: https://gitlab.one.com/systems/chef-repo/-/blob/master/roles/onecom-global-firewall-macros.json#L173 + text += "46.30.212.64\n46.30.212.65\n46.30.212.66\n46.30.212.67\n46.30.212.68\n46.30.212.69\n46.30.211.85\n" + text += "\n" + + text += "Remove Unused CSS:\n" + # Defined in k8s_sips: https://gitlab.one.com/systems/chef-repo/-/blob/master/roles/onecom-global-firewall-macros.json#L173 + text += "46.30.212.64\n46.30.212.65\n46.30.212.66\n46.30.212.67\n46.30.212.68\n46.30.212.69\n46.30.211.85\n" + # OVH servers + all_server_list = self.ovh_api_factory.get_dedicated_servers(app_context) + for server_name in all_server_list: + display_name = self.ovh_api_factory.get_dedicated_server_display_name(app_context, server_name) + if 'worker' in display_name: + server_ips = self.ovh_api_factory.get_dedicated_server_ips(app_context, server_name) + text += server_ips[IpAddress.IP_ADDRESS_IPV4] + " / " + server_ips[IpAddress.IP_ADDRESS_IPV6] + "\n" + text += "\n" + + text += "Dynamic exclusions and inclusions:\n" + # Defined in https://gitlab.one.com/systems/group.one-authdns/-/blob/main/octodns/wp-rocket.me.yaml?ref_type=heads + text += "https://b.rucss.wp-rocket.me / 46.30.212.116\n" + text += "\n" + + text += "RocketCDN subscription:\n" + text += "https://rocketcdn.me/api/\n" + # Defined in k8s_sips: https://gitlab.one.com/systems/chef-repo/-/blob/master/roles/onecom-global-firewall-macros.json#L173 + text += "46.30.212.64\n46.30.212.65\n46.30.212.66\n46.30.212.67\n46.30.212.68\n46.30.212.69\n46.30.211.85\n" + + self.slack_message_factory.post_message(app_context, slack_user, text) diff --git a/sources/handlers/SlackCommandHandler.py b/sources/handlers/SlackCommandHandler.py index 056aa29..a012756 100644 --- a/sources/handlers/SlackCommandHandler.py +++ b/sources/handlers/SlackCommandHandler.py @@ -6,6 +6,7 @@ from threading import Thread from flask import current_app from sources.factories.SlackModalFactory import SlackModalFactory +from sources.handlers.ServerListHandler import ServerListHandler class SlackCommandHandler(): @@ -18,6 +19,7 @@ def __init__(self): The handler instanciates the objects it needed to complete the processing of the request. """ self.slack_modal_factory = SlackModalFactory() + self.server_list_handler = ServerListHandler() def process(self, payload_json): """ @@ -27,10 +29,11 @@ def process(self, payload_json): # Retrieve the shortcut callback command = payload_json['command'] - # Process the paylaod according to the callback if '/dev-team-escalation' == command: self.dev_team_escalation_command_callback(payload_json) + elif '/wprocket-ips' == command: + self.wp_rocket_ips_command_callback(payload_json) else: raise ValueError('Unknown command.') return {} @@ -46,3 +49,14 @@ def dev_team_escalation_command_callback(self, payload_json): target=self.slack_modal_factory.dev_team_escalation_modal, kwargs={ "app_context": current_app.app_context(), "trigger_id": trigger_id}) thread.start() + + def wp_rocket_ips_command_callback(self, payload_json): + """ + Callback method to process the Slack command "/wprocket-ips" + """ + initiator = payload_json["user_id"] + + thread = Thread( + target=self.server_list_handler.send_wp_rocket_ips, kwargs={ + "app_context": current_app.app_context(), "slack_user": initiator}) + thread.start() diff --git a/sources/utils/Constants.py b/sources/utils/Constants.py index 2834e2f..e41254d 100644 --- a/sources/utils/Constants.py +++ b/sources/utils/Constants.py @@ -7,3 +7,6 @@ APP_CONFIG_TOKEN_SLACK_USER_TOKEN = 'SLACK_USER_TOKEN' APP_CONFIG_TOKEN_GITHUB_ACCESS_TOKEN = 'GITHUB_ACCESS_TOKEN' APP_CONFIG_TOKEN_GITHUB_WEBHOOK_SECRET = 'GITHUB_WEBHOOK_SECRET' +APP_CONFIG_TOKEN_OVH_APP_KEY = 'OVH_APP_KEY' +APP_CONFIG_TOKEN_OVH_APP_SECRET = 'OVH_APP_SECRET' +APP_CONFIG_TOKEN_OVH_CONSUMER_KEY = 'OVH_CONSUMER_KEY' diff --git a/sources/utils/IpAddress.py b/sources/utils/IpAddress.py new file mode 100644 index 0000000..f3897e9 --- /dev/null +++ b/sources/utils/IpAddress.py @@ -0,0 +1,17 @@ +""" + Utility functions to handle IP Addresses. +""" +from ipaddress import ip_address, IPv4Address + +IP_ADDRESS_IPV4 = "IPv4" +IP_ADDRESS_IPV6 = "IPv6" + + +def validIPAddress(IP: str) -> str: + """ + Checks the IP address provided to identify if it is IPv4 or IPv6 or invalid. + """ + try: + return IP_ADDRESS_IPV4 if type(ip_address(IP)) is IPv4Address else IP_ADDRESS_IPV6 + except ValueError: + return "Invalid"