diff --git a/api/blueprints/common/otp/timed_otp.py b/api/blueprints/common/otp/timed_otp.py index ccf78e8..fb6dd91 100644 --- a/api/blueprints/common/otp/timed_otp.py +++ b/api/blueprints/common/otp/timed_otp.py @@ -340,7 +340,6 @@ async def verify_otp( ) # If the OTP failed: - print("OTP RESPONSE:", otp_response) if not otp_response.success: return ResponseModel( status_code = StatusCodes.FAILED, http_code = HttpCodes.UNAUTHORIZED, diff --git a/controllers_v2/software/mikrotik/mikrotik_hostpot_1000.py b/controllers_v2/software/mikrotik/mikrotik_hostpot_1000.py index 418ae02..7d54e81 100644 --- a/controllers_v2/software/mikrotik/mikrotik_hostpot_1000.py +++ b/controllers_v2/software/mikrotik/mikrotik_hostpot_1000.py @@ -735,7 +735,9 @@ class MikroTikHotspot1000Controller(MikroTikController): mikrotik_client.new_action_chain(action_chain) # Try to configure the device: + roll_back_response = await self.quick_roll_back(mikrotik_client, use_https = False) config_response = await self.quick_config(mikrotik_client, mikrotik_auth, use_https = False) + config_response = roll_back_response + config_response config_response.actionChain = mikrotik_client.action_chain return config_response diff --git a/controllers_v2/software/mikrotik/mikrotik_pppoe_1000_v2.py b/controllers_v2/software/mikrotik/mikrotik_pppoe_1000_v2.py new file mode 100644 index 0000000..581bb74 --- /dev/null +++ b/controllers_v2/software/mikrotik/mikrotik_pppoe_1000_v2.py @@ -0,0 +1,532 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 3rd Mar., 2025. + + OBJECTIVE: + + To handle configuration for MikroTik servers such that they work in PPPoE mode with support for 1,000 clients. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# My async utils: +from utils_v2.string import json +from utils_v2.date_time import date_time +from utils_v2.mikrotik.controllers.async_mikrotik import AsyncMikroTik +from utils_v2.database.async_mysql_v2 import AsyncMySQL +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache + +# Controllers: +from controllers_v2.software.mikrotik.base import MikroTikController + +# To make very controlled API calls: +from utils_v2.rest.controllers.async_base import AsyncREST +from utils_v2.rest.models.api_call import ApiResponse + +# Models: +from models.software.mikrotik.auth import ( + MikroTikPPPoE1000Auth, + MikroTikHotspot1000Auth, + MikroTikAuthResponse +) +from models.software.mikrotik.configure import MikroTikConfigAttemptResponse + +# To work with datatypes: +from typing import List, Any + +# To make HTTP requests: +import httpx + +# To work with IP addresses: +import ipaddress + +# to work with MongoDB: +from bson.objectid import ObjectId + +# To make abstract classes: +from abc import ABC, abstractmethod + +# For asynchronous activities: +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class MikroTikPPPoE1000Controller(MikroTikController): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + CLIENT_NAME = "mikrotikPPPoE1000" + + # For automated configuration, and identification of automated configuration: + NAME_PREFIX = "easyfi" + CREATED_BY_NAME = NAME_PREFIX + "-pppoe-1000" + HW_INTERFACE_NAME = NAME_PREFIX + "-pppoe" + VLAN_NAME = NAME_PREFIX + "-vlan-{}" # .................... Substitute the VLAN's id here. + PRIVATE_IP_POOL_NAME = NAME_PREFIX + "-pppoe-pool-{}" # ... Substitute the VLAN's id here. + PPP_PROFILE_NAME = NAME_PREFIX + "-pppoe-prf-{}" # ........ Substitute the VLAN's id here. + PPP_SERVER_NAME = NAME_PREFIX + "-pppoe-srv-{}" # ......... Substitute the VLAN's id here. + RADIUS_SERVER_NAME = NAME_PREFIX + "-radius" # ............ Substitute the VLAN's id here. + NAT_RULE_NAME = NAME_PREFIX + "-pppoe-nat-{}" # ........... Substitute rule no. here. + SNMP_COMMUNITY_NAME = NAME_PREFIX + "-snmp" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + debug: bool = True, + debug_prefix: str = "MTik. PPPoE 1K (C) | ", + debug_only_errors: bool = True + ): + + """ + This is a special class that specifically handles configuration for MikroTik servers that work in PPPoE mode and + have support for upto 1,000 active clients. + :param cache: The object to use for caching results from database calls. + :param http_client: The HTTP client to use to make REST-ful API calls. + :param debug: Whether, or not, you would like to print debugging messages: + :param debug_prefix: The prefix to print with the debugging messages. + :param debug_only_errors: Whether you would like to print only error messages or all messages. + :return: None. + """ + + # Invoke the parent's constructor: + MikroTikController.__init__( + self, + cache = cache, + alert_url = alert_url, + http_client = http_client, + base_filter = {"client": self.CLIENT_NAME}, + debug = debug, + debug_prefix = debug_prefix, + debug_only_errors = debug_only_errors + ) + + # Init a variable in a parent: + self._client = self.CLIENT_NAME + + # ┏┓ • ┓ ┏┓ + # ┃┃┓┏┓┏┃┏ ┗┓┏┓╋┓┏┏┓ + # ┗┻┗┻┗┗┛┗ ┗┛┗ ┗┗┻┣┛ + # ┛ + + async def quick_config( + self, + mikrotik_client: AsyncMikroTik, + mikrotik_auth: MikroTikHotspot1000Auth, + use_https: bool = True + ) -> MikroTikConfigAttemptResponse: + + """ + To very quickly set up the MikroTik with just one VLAN. + :param mikrotik_client: The client to use. + :param mikrotik_auth: The set of credentials as received from the UI/API. + :param use_https: Whether to use HTTPS, or HTTP. + :return: A structured response to indicate what happened during the process. + """ + + # Start by assuming values: + config_result = MikroTikConfigAttemptResponse() + all_success = True + all_messages = ["STARTING QUICK CONFIG."] + + # Prepare the needed variables: + PPP_VLAN_ID = mikrotik_auth.vlanIds[0] + PPP_INTERFACE_NAME = self.HW_INTERFACE_NAME + PPP_VLAN_NAME = self.VLAN_NAME.format(PPP_VLAN_ID) + PPP_IP_POOL_NAME = self.PRIVATE_IP_POOL_NAME.format(PPP_VLAN_ID) + PPP_PROFILE_NAME = self.PPP_PROFILE_NAME.format(PPP_VLAN_ID) + PPP_SERVER_NAME = self.PPP_SERVER_NAME.format(PPP_VLAN_ID) + PPP_DHCP_SERVER_NAME = self.PRIVATE_IP_POOL_NAME.format(PPP_VLAN_ID) + PPP_SNMP_COMMUNITY_NAME = mikrotik_auth.snmpCommunity + # --- + PPP_PUBLIC_IP_SUBNET = mikrotik_auth.publicIpPool + PPP_FIRST_PUBLIC_IP = mikrotik_auth.firstPublicIp + PPP_LAST_PUBLIC_IP = mikrotik_auth.lastPublicIp + # --- + PPP_PRIVATE_IP_SUBNET = str(next(ipaddress.summarize_address_range( + ipaddress.IPv4Address(mikrotik_auth.firstPrivateIp), + ipaddress.IPv4Address(mikrotik_auth.lastPrivateIp) + ))) + PPP_FIRST_PRIVATE_IP = mikrotik_auth.firstPrivateIp + PPP_LAST_PRIVATE_IP = mikrotik_auth.lastPrivateIp + PPP_GATEWAY_PRIVATE_IP = str(ipaddress.IPv4Address(mikrotik_auth.firstPrivateIp) + 1) + # --- + PPP_RADIUS_SERVER_IP = mikrotik_auth.radius + PPP_RADIUS_SERVER_SECRET = mikrotik_auth.secret + # --- + PPP_COMMENT = mikrotik_client.create_comment_json() + + # STEP 1: + # Set up the interface: + api_response = await mikrotik_client.list_interfaces(use_https = use_https) + target_if_dot_id = None + for interface in api_response.data or []: + if interface["type"] == "ether": + target_if_dot_id = interface[".id"] + break + api_response = await mikrotik_client.update_interface( + dot_id = target_if_dot_id, + json_payload = { + "name": PPP_INTERFACE_NAME, + "comment": PPP_COMMENT + }, + use_https = use_https + ) + all_messages.append(f"STEP 1 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + # STEP 2: + # Create the VLAN: + api_response = await mikrotik_client.add_vlan( + json_payload = { + "name": PPP_VLAN_NAME, + "interface": PPP_INTERFACE_NAME, + "vlan-id": PPP_VLAN_ID, + "disabled": "false", + "comment": PPP_COMMENT + } + ) + all_messages.append(f"STEP 2 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + # STEP 3: + # Add a new IP Pool for the PPPoE users: + api_response = await mikrotik_client.add_ip_pool( + json_payload = { + "name": PPP_IP_POOL_NAME, + "ranges": PPP_PRIVATE_IP_SUBNET, + "comment": PPP_COMMENT + } + ) + all_messages.append(f"STEP 3 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + # STEP 4: + # Add a new PPPoE Profile: + api_response = await mikrotik_client.add_ppp_profile( + json_payload = { + "name": PPP_PROFILE_NAME, + "local-address": PPP_GATEWAY_PRIVATE_IP, + "remote-address": PPP_IP_POOL_NAME, + "comment": PPP_COMMENT + } + ) + all_messages.append(f"STEP 4 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + # STEP 5: + # Add a new PPPoE Server: + api_response = await mikrotik_client.add_ppp_server( + json_payload = { + "interface": PPP_VLAN_ID, + "profile": PPP_PROFILE_NAME, + "service-name": PPP_SERVER_NAME, + "comment": PPP_COMMENT + } + ) + all_messages.append(f"STEP 5 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + # STEP 6: + # Set up PPPoE's AAA such that it uses RADIUS: + api_response = await mikrotik_client.set_ppp_aaa( + json_payload = { + "accounting": "true", + "interim-update": "3m", + "use-circuit-id-in-nas-port-id": "false", + "use-radius": "true" + } + ) + all_messages.append(f"STEP 6 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + # STEP 7: + # Add a new RADIUS Server: + api_response = await mikrotik_client.add_radius_server( + json_payload = { + "accounting-port": "1813", + "address": PPP_RADIUS_SERVER_IP, + "authentication-port": "1812", + "disabled": "false", + "protocol": "udp", + "secret": PPP_RADIUS_SERVER_SECRET, + "service": "ppp,login,hotspot,dhcp", + "timeout": "300ms", + "comment": PPP_COMMENT + } + ) + all_messages.append(f"STEP 7 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + # STEP 8: + # Allow incoming traffic from RADIUS: + api_response = await mikrotik_client.set_radius_incoming( + json_payload = { + "accept": "yes", + "port": "3799", + # "comment": HS_COMMENT # ... NOT SUPPORTED! + } + ) + all_messages.append(f"STEP 8 ({api_response.action}): {api_response.success}") + if not api_response.success: all_success = False + + async def quick_roll_back( + self, + mikrotik_client: AsyncMikroTik, + use_https: bool = True + ) -> MikroTikConfigAttemptResponse: + + """ + To very quickly roll back the setup done with the quick config method. + :param mikrotik_client: The client to use. + :param use_https: Whether to use HTTPS, or HTTP. + :return: A structured response to indicate what happened during the process. + """ + + # Start by assuming values: + config_result = MikroTikConfigAttemptResponse() + all_success = True + all_messages = ["STARTING QUICK ROLL-BACK."] + + # ┏┓ ┓ + # ┣┫┓┏╋┣┓ + # ┛┗┗┻┗┛┗ + + async def save_auth( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + mikrotik_auth: MikroTikPPPoE1000Auth, + action_chain: str | int = None + ) -> MikroTikAuthResponse: + + """ + Checks if a particular set of incoming credentials give access to a valid server and then stores the + credentials. + :param sql_conn: The database connection to use to perform this task. + :param mongo_data_conn: The database connection to use to perform this task. + :param mikrotik_auth: The set of credentials as received from the UI/API. + :param action_chain: A custom action chain to apply for logging the steps. + :return: A structured response to indicate what happened during authorization. + """ + + # Start with a blank response: + auth_response = MikroTikAuthResponse() + + # Create a MikroTik client: + mikrotik_client = AsyncMikroTik( + config_by = self.CREATED_BY_NAME, + mikrotik_ip = mikrotik_auth.nasIp, + username = mikrotik_auth.username, + password = mikrotik_auth.password, + port = mikrotik_auth.nasPort, + use_https = True, + http_client = None, + action_log_conn = mongo_data_conn + ) + mikrotik_client.new_action_chain(action_chain) + + # Try to configure the device: + # roll_back_response = await self.quick_roll_back(mikrotik_client, use_https = False) + config_response = await self.quick_config(mikrotik_client, mikrotik_auth, use_https = False) + # config_response = roll_back_response + config_response + config_response.actionChain = mikrotik_client.action_chain + return config_response + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + async def roll_back( + self, + mikrotik_client: AsyncMikroTik, + mikrotik_auth: MikroTikPPPoE1000Auth + ) -> MikroTikConfigAttemptResponse: + + """ + The rolling-back to the original state (as best as possible) in case the configurations fails midway after + completing some no. of steps. + :param mikrotik_client: The client to use. + :param mikrotik_auth: The set of credentials as received from the UI/API. + :return: A structured response to indicate what happened during the configuration attempt. + """ + + # Start with some variables: + keep_going = True + all_messages = ["STARTING ROLLBACK."] + roll_back_response = MikroTikConfigAttemptResponse() + + # Next, we roll back all the PPPoE Servers: + if keep_going: + step_response = await self.roll_back_ppp_servers(mikrotik_client, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next, we roll back all the PPPoE Profiles: + if keep_going: + step_response = await self.roll_back_ppp_profiles(mikrotik_client, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next, we roll back all the IP pools: + if keep_going: + step_response = await self.roll_back_ip_pools(mikrotik_client, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next we roll back all the VLANs: + if keep_going: + step_response = await self.roll_back_vlans(mikrotik_client, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next, we free-up the interface: + if keep_going: + step_response = await self.roll_back_interface(mikrotik_client, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Done here: + roll_back_response.success = keep_going + roll_back_response.message = " -> ".join(all_messages) + return roll_back_response + + async def configure( + self, + mikrotik_client: AsyncMikroTik, + mikrotik_auth: MikroTikPPPoE1000Auth + ) -> MikroTikConfigAttemptResponse: + + """ + Run the configuration steps for the system. + :param mikrotik_client: The client to use. + :param mikrotik_auth: The set of credentials as received from the UI/API. + :return: A structured response to indicate what happened during the configuration attempt. + """ + + # Start with some variables: + keep_going = True + all_messages = ["STARTING CONFIG."] + config_response = MikroTikConfigAttemptResponse(actionChain = mikrotik_client.action_chain) + + # First, we arrange an interface: + if keep_going: + step_response = await self.set_up_interface(mikrotik_client, dot_id = "*3", use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next, we create the needed VLANs: + if keep_going: + await asyncio.sleep(0.25) + step_response = await self.set_up_vlans(mikrotik_client, mikrotik_auth, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next, we create the Private IP Pools: + if keep_going: + await asyncio.sleep(0.25) + step_response = await self.set_up_ip_pools(mikrotik_client, mikrotik_auth, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next, we create the PPPoE Profiles: + if keep_going: + await asyncio.sleep(0.25) + step_response = await self.set_up_ppp_profiles(mikrotik_client, mikrotik_auth, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Next, we create the PPPoE Servers: + if keep_going: + await asyncio.sleep(0.25) + step_response = await self.set_up_ppp_servers(mikrotik_client, mikrotik_auth, use_https = False) + all_messages.append(step_response.message) + keep_going = step_response.success + + # Done here: + config_response.success = keep_going + config_response.message = " -> ".join(all_messages) + return config_response + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass