def setup_nodes(cp: ConfigParser) -> None: print('==== Nodes') print('To produce alerts, the alerter needs something to monitor! The list ' 'of nodes to be included in the monitoring will now be set up. This ' 'includes validators, sentries, and any full nodes that can be used ' 'as data sources to monitor from the network\'s perspective, together' ' with the Node Exporter URL to be used to monitor the system. You ' 'may include nodes from multiple networks in any order; PANIC ' 'will figure out which network they belong to when you run it. Node ' 'names must be set identical to the ones previously set in the API ' 'Server/s!') # Check if list already set up if len(cp.sections()) > 0 and \ not yn_prompt('The list of nodes is already set up. Do you wish to ' 'clear this list? You will then be asked to set up a ' 'new list of nodes, if you wish to do so (Y/n)\n'): return # Clear config and initialise new list cp.clear() nodes = [] # Ask if they want to set it up if not yn_prompt('Do you wish to set up the list of nodes? (Y/n)\n'): return # Get node details and append them to the list of nodes while True: # Check that API is running by retrieving some data which will be used. oasis_api_data_wrapper = OasisApiWrapper(DUMMY_LOGGER) node = get_node(nodes, oasis_api_data_wrapper) if node is not None: nodes.append(node) if node.node_is_validator: print('Successfully added validator node.') else: print('Successfully added full node.') if not yn_prompt('Do you want to add another node? (Y/n)\n'): break # Add nodes to config for i, node in enumerate(nodes): section = 'node_' + str(i) cp.add_section(section) cp[section]['node_name'] = node.node_name cp[section]['chain_name'] = node.chain_name cp[section]['node_api_url'] = node.node_api_url cp[section]['node_public_key'] = node.node_public_key cp[section]['node_is_validator'] = \ 'true' if node.node_is_validator else 'false' cp[section]['node_exporter_url'] = node.node_exporter_url cp[section]['monitor_node'] = \ 'true' if node.monitor_node else 'false' cp[section]['is_archive_node'] = \ 'true' if node.is_archive_node else 'false' cp[section]['use_as_data_source'] = \ 'true' if node.use_as_data_source else 'false'
def get_node(nodes_so_far: List[NodeConfig]) -> Optional[NodeConfig]: # Get node's name node_names_so_far = [n.node_name for n in nodes_so_far] while True: node_name = input('Unique node name:\n') if node_name in node_names_so_far: print('Node name must be unique.') else: break # Get node's RPC url while True: rpc_url = input('Node\'s RPC url (typically http://NODE_IP:26657):\n') print('Trying to connect to endpoint {}/health'.format(rpc_url)) try: get_json(rpc_url + '/health', DUMMY_LOGGER) print('Success.') break except Exception: if not yn_prompt('Failed to connect to endpoint. Do ' 'you want to try again? (Y/n)\n'): return None # Ask if node is a validator node_is_validator = yn_prompt('Is this node a validator? (Y/n)\n') # Return node return NodeConfig(node_name, rpc_url, node_is_validator, True, True)
def get_repo() -> Optional[RepoConfig]: # Get repo name repo_name = input('GitHub repository name (to know which repository ' 'triggered an alert, example: Substrate):\n') # Get repo page while True: repo_page = input('Official GitHub repository (example: ' 'w3f/substrate/):\n') if not repo_page.endswith('/'): repo_page = repo_page + '/' releases_page = InternalConf.github_releases_template.format(repo_page) print('Trying to connect to {}'.format(releases_page)) try: releases = get_json(releases_page, DUMMY_LOGGER) if 'message' in releases and releases['message'] == 'Not Found': if not yn_prompt('Connection successful, but URL is not valid. ' 'Do you want to try again? (Y/n)\n'): return None else: break # success message left to setup_repos function except Exception: if not yn_prompt('Failed to connect to page. Do ' 'you want to try again? (Y/n)\n'): return None # Return node return RepoConfig(repo_name, repo_page, True)
def get_node(nodes_so_far: List[NodeConfig], polkadot_api_data_wrapper: PolkadotApiWrapper, web_sockets_connected_to_api: List) -> Optional[NodeConfig]: # Get node's name node_names_so_far = [n.node_name for n in nodes_so_far] while True: node_name = input('Unique node name:\n') if node_name in node_names_so_far: print('Node name must be unique.') else: break # Get node's WS url while True: ws_url = input('Node\'s WS url (typically ws://NODE_IP:9944):\n') if ws_url in web_sockets_connected_to_api: print('Testing connection with node {}'.format(ws_url)) try: polkadot_api_data_wrapper.ping_node(ws_url) print('Success.') break except Exception: if not yn_prompt('Failed to connect with node {}. Do you want ' 'to try again? (Y/n)\n'.format(ws_url)): return None else: if not yn_prompt( 'Could not connect with the API Server at web socket ' '{}. Please make sure that the node was added in the ' 'API\'s config. Do you want to try again? (Y/n)\n'.format( ws_url, polkadot_api_data_wrapper.api_endpoint, polkadot_api_data_wrapper.api_endpoint)): return None # Ask if node is a validator node_is_validator = yn_prompt('Is this node a validator? (Y/n)\n') # Ask if node is an archive node. # Note: if the node is a validator, it must also be an archive node. # However, it was done this way in case of changes in future updates. node_is_archive_node = yn_prompt('Is this node an archive node? (Y/n)\n') # Get validator's stash account address. if node_is_validator: while True: stash_account_address = input('Please enter the validator\'s stash ' 'account address:\n') if not stash_account_address.strip(): if not yn_prompt('You cannot leave the stash_account_address ' 'field empty for a validator. Do you want to ' 'try again? (Y/n)\n'): return None else: break else: stash_account_address = '' # Return node return NodeConfig(node_name, ws_url, node_is_validator, node_is_archive_node, True, True, stash_account_address)
def setup_redis(cp: ConfigParser) -> None: print('==== Redis') print('Redis is used by the alerter to persist data every now and then, ' 'so that it can continue where it left off if it is restarted. It ' 'is also used to be able to get the status of the alerter and to ' 'have some control over it, such as to snooze Twilio phone calls.') if is_already_set_up(cp, 'redis') and \ not yn_prompt('Redis is already set up. Do you wish ' 'to clear the current config? (Y/n)\n'): return reset_section('redis', cp) cp['redis']['enabled'] = str(False) cp['redis']['host'] = '' cp['redis']['port'] = '' cp['redis']['password'] = '' if not yn_prompt('Do you wish to set up Redis? (Y/n)\n'): return while True: print('You will now be asked to input the IP of the Redis server.\n' 'If you will be running PANIC using Docker, do not use ' 'localhost, instead use the full IP address (local or external) ' 'of the machine that the Redis container will be running on.') host = input('Please insert the Redis host IP: (default: localhost)\n') host = 'localhost' if host == '' else host print( 'You will now be asked to input the port of the Redis server.\n' 'If you will be running PANIC using Docker, you should leave the ' 'port as the default. Otherwise, you must run the Redis Docker ' 'using -p <port>:6379.') port = input('Please insert the Redis host port: (default: 6379)\n') port = '6379' if port == '' else port print('Note if you are going to be running PANIC in docker, this is ' 'mandatory.') password = input('Please insert the Redis password:\n') if yn_prompt('Do you wish to test Redis now? (Y/n)\n'): test = test_redis(host, port, password) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['redis']['enabled'] = str(True) cp['redis']['host'] = host cp['redis']['port'] = port cp['redis']['password'] = password
def setup_telegram_commands(cp: ConfigParser) -> None: print('---- Telegram Commands') print( 'Telegram is also used as a two-way interface with the alerter and ' 'as an assistant, allowing you to do things such as snooze phone ' 'call alerts and to get the alerter\'s current status from Telegram. ' 'Once again, this requires you to set up a Telegram bot, which is ' 'free and easy. You can reuse the Telegram bot set up for alerts.') already_set_up = is_already_set_up(cp, 'telegram_commands') if already_set_up and \ not yn_prompt('Telegram commands are already set up. Do you ' 'wish to clear the current config? (Y/n)\n'): return print('NOTE: If you are running more than one instance of the P.A.N.I.C. ' 'alerter, do not use the same telegram bot as the other instance/s.') reset_section('telegram_commands', cp) cp['telegram_commands']['enabled'] = str(False) cp['telegram_commands']['bot_token'] = '' cp['telegram_commands']['bot_chat_id'] = '' if not already_set_up and \ not yn_prompt('Do you wish to set up Telegram commands? (Y/n)\n'): return while True: while True: bot_token = input( 'Please insert your Telegram bot\'s API token:\n') bot_api = TelegramBotApi(bot_token, None) confirmation = bot_api.get_me() if not confirmation['ok']: print(str(confirmation)) else: print('Successfully connected to Telegram bot.') break bot_chat_id = input('Please insert the authorised chat ID:\n') if yn_prompt('Do you wish to test Telegram commands now? (Y/n)\n'): test = test_telegram_commands(bot_token, bot_chat_id) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['telegram_commands']['enabled'] = str(True) cp['telegram_commands']['bot_token'] = bot_token cp['telegram_commands']['bot_chat_id'] = bot_chat_id
def setup_nodes(cp: ConfigParser) -> None: print('==== Nodes') print('To produce alerts, the alerter needs something to monitor! The list ' 'of nodes to be included in the monitoring will now be set up. This ' 'includes validators, sentries, and any full nodes that can be used ' 'as a data source to monitor from the network\'s perspective. You ' 'may include nodes from multiple networks in any order; P.A.N.I.C. ' 'will figure out which network they belong to when you run it. Node ' 'names must be unique!') # Check if list already set up already_set_up = len(cp.sections()) > 0 if already_set_up: if not yn_prompt( 'The list of nodes is already set up. Do you wish ' 'to replace this list with a new one? (Y/n)\n'): return # Otherwise ask if they want to set it up if not already_set_up and \ not yn_prompt('Do you wish to set up the list of nodes? (Y/n)\n'): return # Clear config and initialise new list cp.clear() nodes = [] # Get node details and append them to the list of nodes while True: node = get_node(nodes) if node is not None: nodes.append(node) if node.node_is_validator: print('Successfully added validator node.') else: print('Successfully added full node.') if not yn_prompt('Do you want to add another node? (Y/n)\n'): break # Add nodes to config cp.clear() for i, node in enumerate(nodes): section = 'node_' + str(i) cp.add_section(section) cp[section]['node_name'] = node.node_name cp[section]['node_rpc_url'] = node.node_rpc_url cp[section]['node_is_validator'] = \ 'true' if node.node_is_validator else 'false' cp[section]['include_in_node_monitor'] = \ 'true' if node.include_in_node_monitor else 'false' cp[section]['include_in_network_monitor'] = \ 'true' if node.include_in_network_monitor else 'false'
def test_telegram_commands(bot_token: str, bot_chat_id: str) -> TestOutcome: cmd_handler = TelegramCommandHandler(bot_token, bot_chat_id, None) cmd_handler.start_handling(run_in_background=True) print('Go ahead and send /ping to the Telegram bot.') input('Press ENTER once you are done sending commands...') print('Stopping the Telegram bot...') cmd_handler.stop() if yn_prompt('Was the testing successful? (Y/n)\n'): return TestOutcome.OK elif yn_prompt('Retry Telegram commands setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup
def setup_telegram_alerts(cp: ConfigParser) -> None: print('---- Telegram Alerts') print('Alerts sent via Telegram are a fast and reliable means of alerting ' 'that we highly recommend setting up. This requires you to have a ' 'Telegram bot set up, which is a free and quick procedure.') already_set_up = is_already_set_up(cp, 'telegram_alerts') if already_set_up and \ not yn_prompt('Telegram alerts are already set up. Do you ' 'wish to clear the current config? (Y/n)\n'): return reset_section('telegram_alerts', cp) cp['telegram_alerts']['enabled'] = str(False) cp['telegram_alerts']['bot_token'] = '' cp['telegram_alerts']['bot_chat_id'] = '' if not already_set_up and \ not yn_prompt('Do you wish to set up Telegram alerts? (Y/n)\n'): return while True: while True: bot_token = input( 'Please insert your Telegram bot\'s API token:\n') bot_api = TelegramBotApi(bot_token, None) confirmation = bot_api.get_me() if not confirmation['ok']: print(str(confirmation)) else: print('Successfully connected to Telegram bot.') break bot_chat_id = input('Please insert the chat ID for Telegram alerts:\n') bot_api = TelegramBotApi(bot_token, bot_chat_id) if yn_prompt('Do you wish to test Telegram alerts now? (Y/n)\n'): test = test_telegram_alerts(bot_api) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['telegram_alerts']['enabled'] = str(True) cp['telegram_alerts']['bot_token'] = bot_token cp['telegram_alerts']['bot_chat_id'] = bot_chat_id
def test_telegram_alerts(bot_api: TelegramBotApi) -> TestOutcome: response = bot_api.send_message('*Test Alert*') if response['ok']: print('Test alert sent successfully.') if yn_prompt('Was the testing successful? (Y/n)\n'): return TestOutcome.OK elif yn_prompt('Retry Telegram alerts setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup else: print('Something went wrong: {}'.format(response)) if yn_prompt('Retry Telegram alerts setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup
def setup_general(cp: ConfigParser) -> None: print('==== General') print('The first step is to set a unique identifier for the alerter. This ' 'can be any word that uniquely describes the setup being monitored. ' 'Uniqueness is very important if you are running multiple instances ' 'of the PANIC alerter, to avoid any possible namespace clashes. The ' 'name will only be used internally and will not show up in alerts.') if is_already_set_up(cp, 'general'): identifier = cp['general']['unique_alerter_identifier'] if not yn_prompt( 'A unique alerter identifier \'{}\' is already set. Do you ' 'wish to change this identifier? (Y/n)\n'.format(identifier)): return reset_section('general', cp) cp['general']['unique_alerter_identifier'] = '' while True: identifier = input('Please insert the unique identifier:\n') if len(identifier) != 0: break else: print('The unique identifier cannot be blank.') cp['general']['unique_alerter_identifier'] = identifier
def get_repo(repos_so_far: List[RepoConfig]) -> Optional[RepoConfig]: # Get repo's name repo_names_so_far = [r.repo_name for r in repos_so_far] while True: repo_name = input( 'Unique GitHub repository name (to know which repository ' 'triggered an alert, example: oasis-core):\n') if repo_name in repo_names_so_far: print('Repo name must be unique.') elif len(repo_name) == 0: print('Repo name cannot be empty.') else: break # Get repo page while True: repo_page = input('Official GitHub repository (example: ' 'oasislabs/oasis-core/):\n') if not repo_page.endswith('/'): repo_page = repo_page + '/' releases_page = InternalConf.github_releases_template.format(repo_page) print('Trying to connect to {}'.format(releases_page)) try: releases = get_json(releases_page, DUMMY_LOGGER) if 'message' in releases and releases['message'] == 'Not Found': if not yn_prompt( 'Connection successful, but URL is not valid. ' 'Do you want to try again? (Y/n)\n'): if not yn_prompt( 'Do you still want to add the repo? (Y/n)\n'): return None else: break else: break # success message left to setup_repos function except Exception: if not yn_prompt('Failed to connect to page. Do ' 'you want to try again? (Y/n)\n'): if not yn_prompt('Do you still want to add the repo? (Y/n)\n'): return None else: break # Return node return RepoConfig(repo_name, repo_page, True)
def setup_email_alerts(cp: ConfigParser) -> None: print('---- Email Alerts') print('Email alerts are more useful as a backup alerting channel rather ' 'than the main one, given that one is much more likely to notice a ' 'a message on Telegram or a phone call. Email alerts also require ' 'an SMTP server to be set up for the alerter to be able to send.') already_set_up = is_already_set_up(cp, 'email_alerts') if already_set_up and \ not yn_prompt('Email alerts are already set up. Do you ' 'wish to clear the current config? (Y/n)\n'): return reset_section('email_alerts', cp) cp['email_alerts']['enabled'] = str(False) cp['email_alerts']['smtp'] = '' cp['email_alerts']['from'] = '' cp['email_alerts']['to'] = '' if not already_set_up and \ not yn_prompt('Do you wish to set up email alerts? (Y/n)\n'): return while True: email_smtp = input('Please insert the SMTP server\'s address:\n') email_from = input('Please specify the details of the sender in the ' 'format shown below:\n\t' '[email protected]\n') email_to = input('Please specify the email address where you wish to ' 'receive email alerts:\n\t' '[email protected]\n') if yn_prompt('Do you wish to test email alerts now? (Y/n)\n'): test = test_email_alerts(email_smtp, email_from, email_to) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['email_alerts']['enabled'] = str(True) cp['email_alerts']['smtp'] = email_smtp cp['email_alerts']['from'] = email_from cp['email_alerts']['to'] = email_to
def setup_redis(cp: ConfigParser) -> None: print('==== Redis') print('Redis is used by the alerter to persist data every now and then, ' 'so that it can continue where it left off if it is restarted. It ' 'is also used to be able to get the status of the alerter and to ' 'have some control over it, such as to snooze Twilio phone calls.') already_set_up = is_already_set_up(cp, 'redis') if already_set_up and \ not yn_prompt('Redis is already set up. Do you wish ' 'to clear the current config? (Y/n)\n'): return reset_section('redis', cp) cp['redis']['enabled'] = str(False) cp['redis']['host'] = '' cp['redis']['port'] = '' cp['redis']['password'] = '' if not already_set_up and \ not yn_prompt('Do you wish to set up Redis? (Y/n)\n'): return while True: host = input('Please insert the Redis host IP: (default: localhost)\n') host = 'localhost' if host == '' else host port = input('Please insert the Redis host port: (default: 6379)\n') port = '6379' if port == '' else port password = input('Please insert the Redis password:\n') if yn_prompt('Do you wish to test Redis now? (Y/n)\n'): test = test_redis(host, port, password) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['redis']['enabled'] = str(True) cp['redis']['host'] = host cp['redis']['port'] = port cp['redis']['password'] = password
def test_email_alerts(email_smtp: str, email_from: str, email_to: str) \ -> TestOutcome: email_sender = EmailSender(email_smtp, email_from) try: email_sender.send_email('Test Alert', 'Test Alert', email_to) print('Test email sent.') if yn_prompt('Was the testing successful? (Y/n)\n'): return TestOutcome.OK elif yn_prompt('Retry email alerts setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup except Exception as e: print('Something went wrong: {}'.format(e)) if yn_prompt('Retry email alerts setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup
def setup_repos(cp: ConfigParser) -> None: print('==== GitHub Repositories') print( 'The GitHub monitor alerts on new releases in repositories. The list ' 'of GitHub repositories to monitor will now be set up.') # Check if list already set up already_set_up = len(cp.sections()) > 0 if already_set_up: if not yn_prompt('The list of repositories is already set up. Do you ' 'wish to replace this list with a new one? (Y/n)\n'): return # Otherwise ask if they want to set it up if not already_set_up and \ not yn_prompt('Do you wish to set up the list of repos? (Y/n)\n'): return # Clear config and initialise new list cp.clear() repos = [] # Get repository details and append them to the list of repositories while True: repo = get_repo() if repo is not None: repos.append(repo) print('Successfully added repository.') if not yn_prompt('Do you want to add another repo? (Y/n)\n'): break # Add repos to config cp.clear() for i, repo in enumerate(repos): section = 'repo_' + str(i) cp.add_section(section) cp[section]['repo_name'] = repo.repo_name cp[section]['repo_page'] = repo.repo_page cp[section]['include_in_github_monitor'] = \ 'true' if repo.include_in_github_monitor else 'false'
def test_twilio_alerts(twilio_no: str, to_dial: str, twilio_api: TwilioApi, internal_conf: InternalConfig = InternalConf) \ -> TestOutcome: try: twilio_api.dial_number(twilio_no, to_dial, internal_conf.twiml_instructions_url) print('Test phone call requested successfully. Please wait a bit.') if yn_prompt('Was the testing successful? (Y/n)\n'): return TestOutcome.OK elif yn_prompt('Retry Twilio alerts setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup except Exception as e: print('Something went wrong: {}'.format(e)) print('The Twilio details you provided might be incorrect.') if yn_prompt('Retry Twilio alerts setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup
def setup_api(cp: ConfigParser) -> None: print('==== Polkadot API Server') print( 'The Polkadot API Server is used by the alerter to get data from the ' 'nodes. It is important that before running both the alerter and ' 'this setup, the Polkadot API Server is set up and running.') if is_already_set_up(cp, 'api') and \ not yn_prompt('The Polkadot API Server is already set up. Do you ' 'wish to replace the current config? (Y/n)\n'): return reset_section('api', cp) cp['api']['polkadot_api_endpoint'] = '' while True: print('You will now be asked to input the API Server\'s address\n' 'If you will be running PANIC using Docker, do not use ' 'localhost, instead use the full IP address (local or external) ' 'of the machine that the API container will be running on.\n' 'You should also set the port to 3000. Otherwise, you must run ' 'the API Docker using -p <port>:3000.') polkadot_api_endpoint = input( 'Please insert the API Server\'s address:' ' (default: http://localhost:3000)\n') polkadot_api_endpoint = 'http://localhost:3000' if \ polkadot_api_endpoint == '' else polkadot_api_endpoint polkadot_api = PolkadotApiWrapper(DUMMY_LOGGER, polkadot_api_endpoint) print('Testing connection with endpoint {}'.format( polkadot_api_endpoint)) try: polkadot_api.ping_api() print('Success.') break except Exception: if not yn_prompt('Failed to connect to endpoint. Do ' 'you want to try again? (Y/n)\n'): return None cp['api']['polkadot_api_endpoint'] = polkadot_api_endpoint
def run() -> None: if not cp_ui.has_section('authentication'): raise InitialisationException( 'You cannot change your UI password because UI authentication is ' 'not set up yet. Please set it up using the run_ui_setup.py script.' ) if not cp_ui.has_option('authentication', 'hashed_password'): raise InitialisationException( 'Missing key authentication.hashed_password in the ' 'config/user_config_ui.ini config file. Please set up UI ' 'authentication again using the run_ui_setup.py script as your ' 'config is invalid.') while True: new_pass = input('Please insert your new password.\n') # The salt is the first 64 characters salt = bytes.fromhex(cp_ui['authentication']['hashed_password'][:64]) hashed_new_pass = hashlib.pbkdf2_hmac('sha256', new_pass.encode('utf-8'), salt, 100000) # Ask user if he really wants to change the password if the new password # is the same as the old one. if hashed_new_pass == \ bytes.fromhex( cp_ui['authentication']['hashed_password'][64:128]): if not yn_prompt('The new password is exactly the same as the old ' 'password. Do you want to insert a different ' 'password instead? (Y/n)\n'): print("Password did not change.") return else: # Generate a new salt to avoid situations where the user is suddenly # logged in without authenticating himself. This can happen if the # stored password matches the new password and the user is currently # not authenticated. salt = os.urandom(32) # Generate another hash with the new salt hashed_new_pass = hashlib.pbkdf2_hmac('sha256', new_pass.encode('utf-8'), salt, 100000) # First 64 characters are the salt, and the remaining characters # are the hashed password. cp_ui['authentication']['hashed_password'] = \ (salt + hashed_new_pass).hex() break print("Password changed successfully")
def test_redis(host: str, port: str, password: str) -> TestOutcome: redis = RedisApi(DUMMY_LOGGER, 0, host, int(port), password=password, namespace='') try: redis.ping_unsafe() print('Test completed successfully.') return TestOutcome.OK except Exception as e: print('Something went wrong: {}'.format(e)) if yn_prompt('Retry Redis setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup
def setup_repos(cp: ConfigParser) -> None: print('==== GitHub Repositories') print( 'The GitHub monitor alerts on new releases in repositories. The list ' 'of GitHub repositories to monitor will now be set up.') # Check if list already set up if len(cp.sections()) > 0 and \ not yn_prompt('The list of repositories is already set up. Do you ' 'wish to clear this list? You will then be asked to ' 'set up a new list, if you wish to do so (Y/n)\n'): return # Clear config and initialise new list cp.clear() repos = [] # Ask if they want to set it up if not yn_prompt('Do you wish to set up the list of repos? (Y/n)\n'): return # Get repository details and append them to the list of repositories while True: repo = get_repo(repos) if repo is not None: repos.append(repo) print('Successfully added repository.') if not yn_prompt('Do you want to add another repo? (Y/n)\n'): break # Add repos to config for i, repo in enumerate(repos): section = 'repo_' + str(i) cp.add_section(section) cp[section]['repo_name'] = repo.repo_name cp[section]['repo_page'] = repo.repo_page cp[section]['monitor_repo'] = str(repo.monitor_repo)
def test_mongo(host: str, port: str, username: str, password: str) \ -> TestOutcome: mongo = MongoApi(DUMMY_LOGGER, 'dummy_db_name', host, int(port), username=username, password=password) try: mongo.ping_unsafe() if len(username) != 0: mongo.ping_auth(username, password) print('Test completed successfully.') return TestOutcome.OK except Exception as e: print('Something went wrong: {}'.format(e)) if yn_prompt('Retry Mongo setup? (Y/n)\n'): return TestOutcome.RestartSetup else: return TestOutcome.SkipSetup
def setup_authentication(cp: ConfigParser) -> None: print('==== Authentication') print('For connections with the Web UI server to be secure, session-based ' 'authentication is used. You will now be asked to input a username, ' 'password, and a cookie secret (used to sign the cookie stored in ' 'the browser to avoid tampering). The inputted password will be ' 'hashed inside the config, so any future password changes should be ' 'made using the run_util_change_ui_auth_pass.py util. Note that if ' 'the authentication credentials are not inputted in the config, the ' 'UI server won\'t start. In addition to this, since the UI server is ' 'an HTTPS server, please also make sure that the key.pem and ' 'cert.pem files are included as presented in the documentation. This ' 'is important for maximum security.') if is_already_set_up(cp, 'authentication') and \ not yn_prompt('Authentication for the UI is already set up. Do you ' 'wish to replace the current config? (Y/n)\n'): return reset_section('authentication', cp) cp['authentication']['username'] = '' cp['authentication']['hashed_password'] = '' cp['authentication']['cookie_secret'] = '' auth_username = input('Please insert a username \n') print('Please insert a password') auth_password = getpass() salt = os.urandom(32) hashed_pass = hashlib.pbkdf2_hmac('sha256', auth_password.encode('utf-8'), salt, 100000) cookie_secret = input('Please insert a cookie secret. This should be ' 'a string.\n') cp['authentication']['username'] = auth_username # First 64 characters are the salt, and the remaining characters # are the hashed password. cp['authentication']['hashed_password'] = (salt + hashed_pass).hex() cp['authentication']['cookie_secret'] = cookie_secret
def test_yn_prompt_returns_false_for_uppercase_n(self, _): self.assertFalse( yn_prompt(TestUserInput.DUMMY_INPUT))
def test_yn_prompt_keeps_looping_until_valid_input(self, _): self.assertTrue( yn_prompt(TestUserInput.DUMMY_INPUT))
def test_yn_prompt_returns_true_for_uppercase_y(self, _): self.assertTrue( yn_prompt(TestUserInput.DUMMY_INPUT))
def setup_periodic_alive_reminder(cp: ConfigParser) -> None: print('---- Periodic alive reminder') print('The periodic alive reminder is a way for the alerter to inform its ' 'users that it is still running.') if is_already_set_up(cp, 'periodic_alive_reminder') and \ not yn_prompt('The periodic alive reminder is already set up. ' 'Do you wish to clear the current config? (Y/n)\n'): return reset_section('periodic_alive_reminder', cp) cp['periodic_alive_reminder']['enabled'] = str(False) cp['periodic_alive_reminder']['interval_seconds'] = '' cp['periodic_alive_reminder']['email_enabled'] = '' cp['periodic_alive_reminder']['telegram_enabled'] = '' cp['periodic_alive_reminder']['mongo_enabled'] = '' if not yn_prompt('Do you wish to set up the periodic alive reminder? ' '(Y/n)\n'): return interval = input("Please enter the amount of seconds you want to " "pass for the periodic alive reminder. Make sure that " "you insert a positive integer.\n") while True: try: interval_number_rep = int(interval) except ValueError: interval = input("Input is not a valid integer. Please enter " "another value:\n") continue if interval_number_rep > 0: time = timedelta(seconds=int(interval_number_rep)) time = strfdelta(time, "{hours}h {minutes}m {seconds}s") if yn_prompt( 'You will be reminded that the alerter is still running ' 'every {}. Is this correct (Y/n) \n'.format(time)): break else: interval = input( "Please enter the amount of seconds you want to " "pass for the periodic alive reminder. Make sure that " "you insert a positive integer.\n") else: interval = input("Input is not a positive integer. Please enter " "another value:\n") if is_already_set_up(cp, 'email_alerts') and \ cp['email_alerts']['enabled'] and \ yn_prompt('Would you like the periodic alive reminder ' 'to send alerts via e-mail? (Y/n)\n'): email_enabled = str(True) else: email_enabled = str(False) if is_already_set_up(cp, 'telegram_alerts') and \ cp['telegram_alerts']['enabled'] and \ yn_prompt('Would you like the periodic alive reminder ' 'to send alerts via Telegram? (Y/n)\n'): telegram_enabled = str(True) else: telegram_enabled = str(False) if is_already_set_up(cp, 'mongo') and cp['mongo']['enabled'] and \ yn_prompt('Would you like the periodic alive reminder ' 'to save alerts to Mongo? (Y/n)\n'): mongo_enabled = str(True) else: mongo_enabled = str(False) cp['periodic_alive_reminder']['enabled'] = str(True) cp['periodic_alive_reminder']['interval_seconds'] = interval cp['periodic_alive_reminder']['email_enabled'] = email_enabled cp['periodic_alive_reminder']['telegram_enabled'] = telegram_enabled cp['periodic_alive_reminder']['mongo_enabled'] = mongo_enabled
def setup_mongo(cp: ConfigParser) -> None: print('==== MongoDB') print('Mongo can be set up to persist any alert in a MongoDB collection.') if is_already_set_up(cp, 'mongo') and \ not yn_prompt('Mongo is already set up. Do you wish ' 'to clear the current config? (Y/n)\n'): return reset_section('mongo', cp) cp['mongo']['enabled'] = str(False) cp['mongo']['host'] = '' cp['mongo']['port'] = '' cp['mongo']['db_name'] = '' cp['mongo']['user'] = '' cp['mongo']['pass'] = '' if not yn_prompt('Do you wish to set up Mongo? (Y/n)\n'): return while True: print('You will now be asked to input the IP of the Mongo server.\n' 'If you will be running PANIC using Docker, do not use ' 'localhost, instead use the full IP address (local or external) ' 'of the machine that the Mongo container will be running on.') host = input('Please insert the Mongo host IP: (default: localhost)\n') host = 'localhost' if host == '' else host print('You will now be asked to input the port of the Mongo server.\n' 'If you will be running PANIC using Docker, you should leave the ' 'port as the default. If you wish to run the Mongo container on ' 'another port, please input this port number here and change the ' '`MONGO_HOST_PORT` value inside the `panic_polkadot/.env` file.') port = input('Please insert the Mongo host port: (default: 27017)\n') port = '27017' if port == '' else port print('You will now be asked which database you wish to use to store ' 'the alerts. This will be auto-created if it does not exist. ' 'You can re-use the same database if another PANIC is installed.') while True: db_name = input('Please insert database name: (default: panic)\n') db_name = 'panic' if db_name == '' else db_name if ' ' in db_name: print('Database name cannot contain spaces.') else: break username = input('Please insert the username for authentication ' '(blank for no authentication):\n') if len(username) != 0: password = input('Please insert the password for authentication:\n') else: password = '' if yn_prompt('Do you wish to test Mongo now? (Y/n)\n'): test = test_mongo(host, port, username, password) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['mongo']['enabled'] = str(True) cp['mongo']['host'] = host cp['mongo']['port'] = port cp['mongo']['db_name'] = db_name cp['mongo']['user'] = username cp['mongo']['pass'] = password
def setup_email_alerts(cp: ConfigParser) -> None: print('---- Email Alerts') print('Email alerts are more useful as a backup alerting channel rather ' 'than the main one, given that one is much more likely to notice a ' 'a message on Telegram or a phone call. Email alerts also require ' 'an SMTP server to be set up for the alerter to be able to send.') if is_already_set_up(cp, 'email_alerts') and \ not yn_prompt('Email alerts are already set up. Do you ' 'wish to clear the current config? (Y/n)\n'): return reset_section('email_alerts', cp) cp['email_alerts']['enabled'] = str(False) cp['email_alerts']['smtp'] = '' cp['email_alerts']['from'] = '' cp['email_alerts']['to'] = '' cp['email_alerts']['user'] = '' cp['email_alerts']['pass'] = '' if not yn_prompt('Do you wish to set up email alerts? (Y/n)\n'): return while True: email_smtp = input('Please insert the SMTP server\'s address:\n') email_user = input('Please insert the username for SMTP authentication ' '(blank for no authentication):\n') if len(email_user) != 0: email_pass = input('Please insert the password for SMTP ' 'authentication:\n') else: email_pass = '' email_from = input('Please specify the details of the sender in the ' 'format shown below:\n\t' '[email protected]\n') email_to = input('Please specify the email address where you wish to ' 'receive email alerts:\n\t' '[email protected]\n') while yn_prompt('Do you wish to add another email address? (Y/n)\n'): email_to += ';' + input('Please insert the email address:\n') if yn_prompt('Do you wish to test email alerts now? The first ' 'email address inserted will be used. (Y/n)\n'): test = test_email_alerts(email_smtp, email_from, email_to.split(';')[0], email_user, email_pass) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['email_alerts']['enabled'] = str(True) cp['email_alerts']['smtp'] = email_smtp cp['email_alerts']['from'] = email_from cp['email_alerts']['to'] = email_to cp['email_alerts']['user'] = email_user cp['email_alerts']['pass'] = email_pass
def setup_twilio_alerts(cp: ConfigParser) -> None: print('---- Twilio Alerts') print('Twilio phone-call alerts are the most important alerts since they ' 'are the best at grabbing your attention, especially when you\'re ' 'asleep! To set these up, you have to have a Twilio account set up, ' 'with a registered Twilio phone number and a verified phone number.' 'The timed trial version of Twilio is free.') if is_already_set_up(cp, 'twilio_alerts') and \ not yn_prompt('Twilio alerts are already set up. Do you ' 'wish to clear the current config? (Y/n)\n'): return reset_section('twilio_alerts', cp) cp['twilio_alerts']['enabled'] = str(False) cp['twilio_alerts']['account_sid'] = '' cp['twilio_alerts']['auth_token'] = '' cp['twilio_alerts']['twilio_phone_number'] = '' cp['twilio_alerts']['phone_numbers_to_dial'] = '' if not yn_prompt('Do you wish to set up Twilio alerts? (Y/n)\n'): return while True: while True: account_sid = input('Please insert your Twilio account SID:\n') auth_token = input('Please insert your Twilio account AuthToken:\n') try: twilio_api = TwilioApi(account_sid, auth_token) print('Successfully connected to Twilio.') break except Exception as e: print('Something went wrong: {}'.format(e)) twilio_no = input('Please insert your registered Twilio phone number ' 'in the format shown below:\n\t' 'E.164 format, for example: +12025551234\n') to_dial = input('Please insert the first phone number to dial for ' 'alerting purposes in the format shown below:\n\t' 'E.164 format, for example: +12025551234\n') while yn_prompt('Do you wish to add another number? (Y/n)\n'): to_dial += ';' + input('Please insert the phone number:\n') if yn_prompt('Do you wish to test Twilio alerts now? The first ' 'phone number inserted will be called. (Y/n)\n'): test = test_twilio_alerts(twilio_no, to_dial.split(';')[0], twilio_api) if test == TestOutcome.RestartSetup: continue elif test == TestOutcome.SkipSetup: return break cp['twilio_alerts']['enabled'] = str(True) cp['twilio_alerts']['account_sid'] = account_sid cp['twilio_alerts']['auth_token'] = auth_token cp['twilio_alerts']['twilio_phone_number'] = twilio_no cp['twilio_alerts']['phone_numbers_to_dial'] = to_dial