import telegram # Notifications import asyncio # Telegram library is async import requests # HTTP import os # Unix socket file handling import subprocess # Ping import socket # Hostname, unix socket import yaml # Config file import logging # Log messages logger = logging.getLogger('server_status') logger.setLevel(level=logging.DEBUG) with open("config.yml", "r") as file: config = yaml.safe_load(file) # Set the path for the Unix socket # without reinitialising entire script # Used for SystemD timer to invoke status check, socket_path = "/tmp/server_status.sock" bot = telegram.Bot(config["telegram"]["token"]) hostname = socket.gethostname() # Used to only notify on state change # Initialise data stucture based on targets in config file state = {} for host, host_config in config["hosts"].items(): state[host] = {"online": None, "open_ports": []} if 'urls' in host_config: state[host]["http_status"] = {} for url in host_config['urls']: state[host]['http_status'][url] = None # Send message from non async function def send_message(text): logger.debug(f"Sending message: {text}") asyncio.run(async_send_message(text)) async def async_send_message(text): async with bot: await bot.send_message(text=text, chat_id=config["telegram"]["chat_id"], parse_mode=telegram.constants.ParseMode.MARKDOWN) def ping(host: str) -> bool: response = subprocess.run(["ping", "-c", "1", host]) return response.returncode == 0 def check_target(host: str, host_config: dict): # Ping online = ping(host) # Default to desire host online, if not specified in config file desired_online = host_config['desired_online'] if 'desired_online' in host_config else True host_is_desired_state = desired_online == online first_test = state[host]['online'] == None changed_state = not first_test and state[host]['online'] != online logger.debug(f''' host_is_desired_state: {host_is_desired_state} first_test: {first_test} changed_state: {changed_state} ''') # notify if the host online state changed, or on fresh boot, if it isn't the desired online state if changed_state or (first_test and not host_is_desired_state): message = f"`{host}` is {'online' if online else 'offline'}" logger.warning(message) send_message(message) state[host]['online'] = online # Save current state if not online: return messages = [] for url in host_config['urls']: http_state = None # HTTP Code try: # print(f''' # ---=== Getting: {url} ===--- # ''') r = requests.get(url) if r.status_code != 200: http_state = r.status_code except requests.exceptions.SSLError as exception: try: reason = exception.args[0].reason.args[0].verify_message except Exception as exception: logger.warning("Unable to get reason") raise http_state = "SSL error" except Exception as exception: raise if http_state: if state[host]['http_status'][url] is None or state[host]['http_status'][url] != http_state: messages.append(f'[{url}]({url}) {http_state}') state[host]['http_status'][url] = http_state if len(messages) > 0: send_message('\n'.join( messages)) def check_status(): try: logger.info("Checking status") for host, host_config in config["hosts"].items(): check_target(host, host_config) except Exception as ex: send_message("Error getting status") raise ex def listen(): # remove the socket file if it already exists try: os.unlink(socket_path) except OSError: if os.path.exists(socket_path): raise try: # Create the Unix socket server server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) # Bind the socket to the path server.bind(socket_path) # Listen for incoming connections server.listen(1) while True: try: # accept connections logger.info("Server is listening for incoming connections...") connection, client_address = server.accept() logger.debug("Connection from", str(connection).split(", ")[0][-4:]) # receive data from the client while True: data = connection.recv(1024) if not data: break logger.debug("Received data:", data.decode()) # Send a response back to the client response = "OK\n" connection.sendall(response.encode()) check_status() finally: # Always close the connection if it"s been initialised if "connection" in locals(): connection.close() finally: # remove the socket file os.unlink(socket_path) def main(): # send_message(f"`{hostname}` online") check_status() listen() if __name__ == "__main__": asyncio.run(main())