(20250212) Started a new class (util) that handles MikroTik config. in async mode, and started implementing that in the PPPoE-1000 controller. I can now configure and roll back the first three steps. Long way to go :')

This commit is contained in:
2025-02-12 19:05:29 +05:30
parent 6e65de9dfc
commit 02dd675f83
8 changed files with 409 additions and 607 deletions
+1 -1
View File
@@ -215,7 +215,7 @@ async def authorize_software_client(
# Note down the results: # Note down the results:
success = response.success success = response.success
message = response.message message = f"Action Chain '{response.actionChain}': {response.message}"
# ┏┓ ┳┳┓•┓ ┏┳┓•┓ ┓┏ ┓┏┓┏┓┏┓ # ┏┓ ┳┳┓•┓ ┏┳┓•┓ ┓┏ ┓┏┓┏┓┏┓
# ┣ ┏┓┏┓ ┃┃┃┓┃┏┏┓┏┓ ┃ ┓┃┏ ┣┫┏┓╋┏┏┓┏┓╋ ┃┃┫┃┫┃┫ # ┣ ┏┓┏┓ ┃┃┃┓┃┏┏┓┏┓ ┃ ┓┃┏ ┣┫┏┓╋┏┏┓┏┓╋ ┃┃┫┃┫┃┫
@@ -36,6 +36,7 @@ sys.path.append(".")
sys.path.append("..") sys.path.append("..")
# My async utils: # My async utils:
from utils_v2.mikrotik.controllers.async_mikrotik import AsyncMikroTik
from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
@@ -181,11 +182,13 @@ class AllMikroTikController(MikroTikController):
async def roll_back( async def roll_back(
self, self,
mikrotik_client: AsyncMikroTik,
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
) -> MikroTikRollBackAttemptResponse: ) -> MikroTikRollBackAttemptResponse:
""" """
The rolling-back to the original state (as best as possible) in case the configurations fails midway after The rolling-back to the original state (as best as possible) in case the configurations fails midway after
completing some no. of steps. 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. :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. :return: A structured response to indicate what happened during the configuration attempt.
""" """
@@ -194,10 +197,12 @@ class AllMikroTikController(MikroTikController):
async def configure( async def configure(
self, self,
mikrotik_client: AsyncMikroTik,
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
) -> MikroTikConfigAttemptResponse: ) -> MikroTikConfigAttemptResponse:
""" """
Run the configuration steps for the system. 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. :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. :return: A structured response to indicate what happened during the configuration attempt.
""" """
+5 -410
View File
@@ -37,6 +37,7 @@ sys.path.append("..")
# My async utils: # My async utils:
from utils_v2.string import json from utils_v2.string import json
from utils_v2.date_time import date_time 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_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
@@ -179,416 +180,6 @@ class MikroTikController(CoreSoftwareController, ABC):
debug_only_errors = debug_only_errors debug_only_errors = debug_only_errors
) )
# ┓┏ ┓
# ┣┫┏┓┃┏┓┏┓┏┓┏
# ┛┗┗ ┗┣┛┗ ┛ ┛
# ┛
@staticmethod
def get_mikrotik_url(
mikrotik_ip: str,
path: str,
port_no: int | str = None,
use_https: bool = True
) -> str:
"""
Simply creates the base URL for hitting the MikroTik server.
:param mikrotik_ip: The IP address of the MikroTik device.
:param path: The path of the REST API to hit.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: The Base URL string.
"""
base_url = r"https://" if use_https else r"http://"
base_url += mikrotik_ip
if port_no is not None: base_url += f":{port_no}"
base_url += "/rest"
if not path.startswith("/"): path = "/" + path
return base_url + path
@staticmethod
def split_ipv4_range_among_targets(
start_ip: ipaddress.IPv4Address | str,
end_ip: ipaddress.IPv4Address | str,
targets: List
) -> List[dict]:
"""
Divides the IPv4 range over the list of VLAN ids.
:param start_ip: The first IP in the full pool (range).
:param end_ip: The last IP in the full pool (range).
:param targets: The list of targets (need not be sequential or ordered) like VLAN ids.
:return: A list of dicts that describes the subnet for each target.
"""
# Parse the inputs:
start_ip_obj = start_ip if isinstance(start_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(start_ip)
end_ip_obj = end_ip if isinstance(end_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(end_ip)
target_count = len(targets)
# Calculate the total number of IPs in the range:
total_ips = int(end_ip_obj) - int(start_ip_obj) + 1
# Calculate the no. of IPs each VLAN gets,
# and add three because we need IPs for network, gateway and broadcast:
ips_per_target = math.ceil(total_ips / target_count) + 3
# Generate subnets for each VLAN:
subnets = []
current_ip = start_ip_obj
for target in targets:
# Calculate the network address for the current VLAN:
subnet_network = ipaddress.IPv4Network(f"{current_ip}/{32 - ips_per_target.bit_length()}", strict = False)
# Check for overlap with the parent network:
subnet_start_ip_obj = subnet_network.network_address
subnet_end_ip_obj = subnet_network.broadcast_address
if subnet_start_ip_obj <= end_ip_obj:
# Add the calculated subnet to the list:
subnets.append({
"target": target,
"network": str(subnet_network),
"size": int(2 ** (32 - subnet_network.prefixlen)),
"startIp": str(subnet_start_ip_obj),
"endIp": str(subnet_end_ip_obj)
})
# Update the current IP for the next subnet
current_ip = ipaddress.IPv4Address(int(subnet_network.broadcast_address) + 1)
# Done here:
return subnets
@staticmethod
def create_comment_json(
created_by: str,
created_ts: datetime.datetime = None,
roll_back_config: dict = None
) -> str:
"""
To create a JSON string that can be put as a comment in any step to later identify the work that was done and
have some hint about how to roll it back if needed.
:param created_by: A hint to put to identify which process/tool created this comment/config/process.
:param created_ts: The datetime (preferably UTC) when this comment was created.
:param roll_back_config: The original config that can be used when rolling back.
:return: The comment string to be used.
"""
# Create the JSON:
comment_json = {
"createdBy": created_by,
"createdTs": (created_ts or date_time.get_current_utc_date_time(as_string = False)).timestamp(),
"rollbackConfig": roll_back_config
}
# Return a string:
return json.to_string(comment_json, no_space = True)
@staticmethod
def parse_comment_json(comment: str) -> dict | list | None:
"""
Tries to parse a comment as if it were a JSON string.
:param comment: The comment string.
:return: The dict/list parsed form the JSON, or a null value of the input was not a valid JSON string.
"""
# Start with a null value:
parsed = None
# Try to parse the comment as if it were a JSON string:
try: parsed = json.from_string(comment)
except: parsed = None
# Done here:
return parsed
# ┏┓
# ┗┓┓┏┏╋┏┓┏┳┓
# ┗┛┗┫┛┗┗ ┛┗┗
# ┛
async def get_system_resource(
self,
mikrotik_ip: str,
username: str,
password: str,
port_no: int | str = None,
use_https: bool = True
) -> ApiResponse:
"""
To get a summary of the hardware resources available in the MikroTik device. This also becomes a great way to
quickly check if any given device is valid, and up and running.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.get(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = r"/system/resource",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
)
)
# Done here:
return api_response
# ┳ ┏
# ┃┏┓╋┏┓┏┓╋┏┓┏┏┓┏
# ┻┛┗┗┗ ┛ ┛┗┻┗┗ ┛
async def list_interfaces(
self,
mikrotik_ip: str,
username: str,
password: str,
port_no: int | str = None,
use_https: bool = True
) -> ApiResponse:
"""
To enlist all the physical connectivity interfaces available on the MikroTik device.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.get(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = r"/interface",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
)
)
# Done here:
return api_response
async def update_interface(
self,
mikrotik_ip: str,
username: str,
password: str,
dot_id: str,
json_payload: dict,
port_no: int | str = None,
use_https: bool = True
) -> ApiResponse:
"""
To update an interface's configuration.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param dot_id: The value of the '.id' field in the list.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:param json_payload: The JSON to send in the body pf the request.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.patch(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = f"/interface/{dot_id}",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
),
json = json_payload
)
# Done here:
return api_response
# ┓┏┓ ┏┓┳┓
# ┃┃┃ ┣┫┃┃┏
# ┗┛┗┛┛┗┛┗┛
async def list_vlans(
self,
mikrotik_ip: str,
username: str,
password: str,
port_no: int | str = None,
use_https: bool = True
) -> ApiResponse:
"""
To enlist all the physical connectivity interfaces available on the MikroTik device.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.get(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = r"/interface/vlan",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
)
)
# Done here:
return api_response
async def add_vlan(
self,
mikrotik_ip: str,
username: str,
password: str,
json_payload: dict,
port_no: int | str = None,
use_https: bool = True
) -> ApiResponse:
"""
To add a new VLAN to the MikroTik's configuration.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param json_payload: The JSON to send in the body pf the request.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.put(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = r"/interface/vlan",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
),
json = json_payload
)
# Done here:
return api_response
async def update_vlan(
self,
mikrotik_ip: str,
username: str,
password: str,
dot_id: str,
json_payload: dict,
port_no: int | str = None,
use_https: bool = True,
) -> ApiResponse:
"""
To update an existing VLAN in the MikroTik's configuration.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param dot_id: The value of the '.id' field in the list.
:param json_payload: The JSON to send in the body pf the request.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.patch(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = f"/interface/vlan/{dot_id}",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
),
json = json_payload
)
# Done here:
return api_response
async def remove_vlan(
self,
mikrotik_ip: str,
username: str,
password: str,
dot_id: str,
port_no: int | str = None,
use_https: bool = True,
):
"""
To delete an existing VLAN from the MikroTik's configuration.
:param mikrotik_ip: The IP address of the MikroTik device.
:param username: The username to get access to the MikroTik device.
:param password: The password to get access to the MikroTik device.
:param dot_id: The value of the '.id' field in the list.
:param port_no: The port no. to hit the MikroTik device on.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured API response.
"""
# Make the API call:
api_response = await self._rest.delete(
url = self.get_mikrotik_url(
mikrotik_ip = mikrotik_ip,
path = f"/interface/vlan/{dot_id}",
port_no = port_no,
use_https = use_https
),
auth = httpx.BasicAuth(
username = username,
password = password
)
)
# Done here:
return api_response
# ┏┓ ┓ # ┏┓ ┓
# ┣┫┓┏╋┣┓ # ┣┫┓┏╋┣┓
# ┛┗┗┻┗┛┗ # ┛┗┗┻┗┛┗
@@ -620,12 +211,14 @@ class MikroTikController(CoreSoftwareController, ABC):
@abstractmethod @abstractmethod
async def roll_back( async def roll_back(
self, self,
mikrotik_client: AsyncMikroTik,
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
) -> MikroTikRollBackAttemptResponse: ) -> MikroTikRollBackAttemptResponse:
""" """
The rolling-back to the original state (as best as possible) in case the configurations fails midway after The rolling-back to the original state (as best as possible) in case the configurations fails midway after
completing some no. of steps. 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. :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. :return: A structured response to indicate what happened during the configuration attempt.
""" """
@@ -635,11 +228,13 @@ class MikroTikController(CoreSoftwareController, ABC):
@abstractmethod @abstractmethod
async def configure( async def configure(
self, self,
mikrotik_client: AsyncMikroTik,
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
) -> MikroTikConfigAttemptResponse: ) -> MikroTikConfigAttemptResponse:
""" """
Run the configuration steps for the system. 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. :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. :return: A structured response to indicate what happened during the configuration attempt.
""" """
@@ -21,7 +21,8 @@
N/A N/A
""" """
import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
# *** IMPORT *** # *** IMPORT ***
@@ -37,6 +38,7 @@ sys.path.append("..")
# My async utils: # My async utils:
from utils_v2.string import json from utils_v2.string import json
from utils_v2.date_time import date_time 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_mysql_v2 import AsyncMySQL
from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.database.async_mongo_v2 import AsyncMongo
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
@@ -54,10 +56,7 @@ from models.software.mikrotik.auth import (
MikroTikHotspot1000Auth, MikroTikHotspot1000Auth,
MikroTikAuthResponse MikroTikAuthResponse
) )
from models.software.mikrotik.configure import ( from models.software.mikrotik.configure import MikroTikConfigAttemptResponse
MikroTikConfigAttemptResponse,
MikroTikRollBackAttemptResponse
)
# To work with datatypes: # To work with datatypes:
from typing import List, Any from typing import List, Any
@@ -71,6 +70,9 @@ from bson.objectid import ObjectId
# To make abstract classes: # To make abstract classes:
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
# For asynchronous activities:
import asyncio
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
@@ -118,9 +120,16 @@ class MikroTikPPPoE1000Controller(MikroTikController):
CLIENT_NAME = "mikrotikPPPoE1000" CLIENT_NAME = "mikrotikPPPoE1000"
# For automated configuration, and identification of automated configuration: # For automated configuration, and identification of automated configuration:
CREATED_BY_NAME = "easyfi" NAME_PREFIX = "easyfi"
HW_INTERFACE_NAME = "easyfi-pppoe" CREATED_BY_NAME = NAME_PREFIX + "-pppoe-1000"
VLAN_NAME = "easyfi-vlan-{}" # ... Substitute the VLAN's id here. 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"
# ┏┓ # ┏┓
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
@@ -162,46 +171,21 @@ class MikroTikPPPoE1000Controller(MikroTikController):
# Init a variable in a parent: # Init a variable in a parent:
self._client = self.CLIENT_NAME self._client = self.CLIENT_NAME
# ┓┏ ┓
# ┣┫┏┓┃┏┓┏┓┏┓┏
# ┛┗┗ ┗┣┛┗ ┛ ┛
# ┛
def created_by_easyfi(
self,
resource_json: dict = None
) -> bool:
"""
Checks if the resource was created/modified by this class.
:param resource_json; The JSON of the resource that needs to be checked.
:return: True if created by this class, else False.
"""
# Extract values:
resource_name = resource_json.get("name") or ""
comment_json = self.parse_comment_json(resource_json.get("comment")) or {}
# Perform checks:
if (
comment_json.get("createdBy", "???") == self.CREATED_BY_NAME or
resource_name.lower().find("easyfi") >= 0
): return True
else: return False
# ┳ ┏ # ┳ ┏
# ┃┏┓╋┏┓┏┓╋┏┓┏┏┓┏ # ┃┏┓╋┏┓┏┓╋┏┓┏┏┓┏
# ┻┛┗┗┗ ┛ ┛┗┻┗┗ ┛ # ┻┛┗┗┗ ┛ ┛┗┻┗┗ ┛
async def set_up_interface( async def set_up_interface(
self, self,
mikrotik_auth: MikroTikPPPoE1000Auth, mikrotik_client: AsyncMikroTik,
dot_id: str = None,
use_https: bool = True use_https: bool = True
) -> MikroTikConfigAttemptResponse: ) -> MikroTikConfigAttemptResponse:
""" """
To find the first available interface and set it up for use. To find the first available interface and set it up for use.
:param mikrotik_auth: The set of credentials as received from the UI/API. :param mikrotik_client: The client to use.
:param dot_id: If you already have an interface in mind, send its id here.
:param use_https: Whether to use HTTPS, or HTTP. :param use_https: Whether to use HTTPS, or HTTP.
:return: A structured response to indicate what happened during the process. :return: A structured response to indicate what happened during the process.
""" """
@@ -210,13 +194,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
step_response = MikroTikConfigAttemptResponse() step_response = MikroTikConfigAttemptResponse()
# Enlist all the interfaces: # Enlist all the interfaces:
api_response = await self.list_interfaces( api_response = await mikrotik_client.list_interfaces(use_https = use_https)
mikrotik_ip = mikrotik_auth.nasIp,
port_no = mikrotik_auth.nasPort,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
use_https = use_https
)
# If the listing failed: # If the listing failed:
if not api_response.success: if not api_response.success:
@@ -225,47 +203,51 @@ class MikroTikPPPoE1000Controller(MikroTikController):
step_response.exception = step_response.exception step_response.exception = step_response.exception
return step_response return step_response
# We find the first available interface: # We find/select an interface:
unused_if_json = None selected_if_json = None
for if_json in await api_response.get_json(): for if_json in api_response.data:
if str(if_json.get("running")).lower() == "false": if (
unused_if_json = if_json str(if_json.get("type")).lower() == "ether" and
(
(
dot_id is not None and
str(if_json.get(".id")).lower() == str(dot_id)
) or
(
dot_id is None and
str(if_json.get("running")).lower() == "false"
)
)
):
selected_if_json = if_json
break break
# If we found no available interface: # If we found no available interface:
if unused_if_json is None: if selected_if_json is None:
step_response.success = False step_response.success = False
step_response.message = "Failed to find an idle h/w interface." step_response.message = "Failed to select a h/w interface."
step_response.exception = None step_response.exception = None
return step_response return step_response
# Reconfigure the idle interface: # Reconfigure the idle interface:
api_response = await self.update_interface( api_response = await mikrotik_client.update_interface(
mikrotik_ip = mikrotik_auth.nasIp, dot_id = selected_if_json.get(".id"),
username = mikrotik_auth.username,
password = mikrotik_auth.password,
dot_id = unused_if_json.get(".id"),
port_no = mikrotik_auth.nasPort,
use_https = use_https,
json_payload = { json_payload = {
"name": self.HW_INTERFACE_NAME, "name": self.HW_INTERFACE_NAME,
"disabled": "false", "disabled": "false",
"comment": self.create_comment_json( "comment": mikrotik_client.create_comment_json(roll_back_config = selected_if_json)
created_by = self.CREATED_BY_NAME, },
created_ts = date_time.get_current_utc_date_time(as_string = False), use_https = use_https,
roll_back_config = unused_if_json
)
}
) )
# Check if the attempt was successful: # Check if the attempt was successful:
if api_response.success: if api_response.success:
step_response.success = True step_response.success = True
step_response.message = f"H/w interface ('{unused_if_json.get('.id')}') configured." step_response.message = f"H/w interface ('{selected_if_json.get('.id')}') configured."
step_response.exception = None step_response.exception = None
else: else:
step_response.success = False step_response.success = False
step_response.message = f"H/w interface ('{unused_if_json.get('.id')}') found, but could NOT be configured." step_response.message = f"H/w interface ('{selected_if_json.get('.id')}') found, but could NOT be configured."
step_response.exception = api_response.exception step_response.exception = api_response.exception
# Done here: # Done here:
@@ -273,13 +255,13 @@ class MikroTikPPPoE1000Controller(MikroTikController):
async def roll_back_interface( async def roll_back_interface(
self, self,
mikrotik_auth: MikroTikPPPoE1000Auth, mikrotik_client: AsyncMikroTik,
use_https: bool = True use_https: bool = True
) -> MikroTikRollBackAttemptResponse: ) -> MikroTikConfigAttemptResponse:
""" """
To find the first available interface. To find the first available interface.
:param mikrotik_auth: The set of credentials as received from the UI/API. :param mikrotik_client: The client to use.
:param use_https: Whether to use HTTPS, or HTTP. :param use_https: Whether to use HTTPS, or HTTP.
:return: A structured response to indicate what happened during the process. :return: A structured response to indicate what happened during the process.
""" """
@@ -290,13 +272,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
step_response = MikroTikConfigAttemptResponse() step_response = MikroTikConfigAttemptResponse()
# Enlist all the interfaces: # Enlist all the interfaces:
api_response = await self.list_interfaces( api_response = await mikrotik_client.list_interfaces(use_https = use_https)
mikrotik_ip = mikrotik_auth.nasIp,
port_no = mikrotik_auth.nasPort,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
use_https = use_https
)
# If the listing failed: # If the listing failed:
if not api_response.success: if not api_response.success:
@@ -308,35 +284,29 @@ class MikroTikPPPoE1000Controller(MikroTikController):
# Loop through all the interfaces, # Loop through all the interfaces,
# identify which ones were configured by this class, # identify which ones were configured by this class,
# and perform rollback on those: # and perform rollback on those:
for if_json in await api_response.get_json(): for if_json in api_response.data:
# Check if the configuration was made by this class: # Extract the needed values:
created_by = None comment_json = mikrotik_client.parse_comment_json(if_json.get("comment"))
original_config = None original_config = mikrotik_client.get_original_config(comment_json)
comment_json = self.parse_comment_json(if_json.get("comment"))
if comment_json is not None:
created_by = comment_json.get("createdBy")
original_config = comment_json.get("rollbackConfig")
# If this was indeed set up by this class, # If this was indeed set up by this class,
# and if rollback data is available: # and if rollback data is available:
if created_by == self.CREATED_BY_NAME and original_config is not None: if mikrotik_client.is_my_config(
api_response = await self.update_interface( config_json = if_json,
mikrotik_ip = mikrotik_auth.nasIp, name_substring = self.NAME_PREFIX
username = mikrotik_auth.username, ):
password = mikrotik_auth.password, api_response = await mikrotik_client.update_interface(
dot_id = if_json.get(".id"), dot_id = if_json.get(".id"),
port_no = mikrotik_auth.nasPort,
use_https = use_https,
json_payload = { json_payload = {
"name": original_config.get("name", self.HW_INTERFACE_NAME), "name": original_config.get("name"),
"disabled": "false", "disabled": "false",
"comment": original_config.get("comment", "") "comment": original_config.get("comment")
} },
use_https = use_https,
) )
total_count += 1 total_count += 1
if api_response.success: rolled_back_count += 1 if api_response.success: rolled_back_count += 1
print("RB API RESP:", api_response)
# Construct the final response: # Construct the final response:
step_response.message = f"Rolled back {rolled_back_count}/{total_count} h/w interface configuration(s)." step_response.message = f"Rolled back {rolled_back_count}/{total_count} h/w interface configuration(s)."
@@ -351,16 +321,16 @@ class MikroTikPPPoE1000Controller(MikroTikController):
async def set_up_one_vlan( async def set_up_one_vlan(
self, self,
mikrotik_auth: MikroTikPPPoE1000Auth, mikrotik_client: AsyncMikroTik,
vlan_id: int, vlan_id: int,
vlan_list: List[dict], vlan_list: List[dict],
use_https: bool = True use_https: bool = True
): ) -> MikroTikConfigAttemptResponse:
""" """
To set up just one of the needed VLANs. If the VLAN id is not taken, a new VLAN will be created, otherwise the To set up just one of the needed VLANs. If the VLAN id is not taken, a new VLAN will be created, otherwise the
existing one will be updated. existing one will be updated.
:param mikrotik_auth: The set of credentials as received from the UI/API. :param mikrotik_client: The client to use.
:param vlan_id: The id of the VLAN you want to create. :param vlan_id: The id of the VLAN you want to create.
:param vlan_list: The list of existing VLANs already configured in the MikroTik device. Helps us decide between :param vlan_list: The list of existing VLANs already configured in the MikroTik device. Helps us decide between
the use of PUT and PATCH methods. the use of PUT and PATCH methods.
@@ -386,22 +356,14 @@ class MikroTikPPPoE1000Controller(MikroTikController):
if existing_dot_id is None: if existing_dot_id is None:
# We add the VLAN: # We add the VLAN:
api_response = await self.add_vlan( api_response = await mikrotik_client.add_vlan(
mikrotik_ip = mikrotik_auth.nasIp,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
json_payload = { json_payload = {
"name": self.VLAN_NAME.format(vlan_id), "name": self.VLAN_NAME.format(vlan_id),
"vlan-id": str(vlan_id), "vlan-id": str(vlan_id),
"interface": self.HW_INTERFACE_NAME, "interface": self.HW_INTERFACE_NAME,
"disabled": "false", "disabled": "false",
"comment": self.create_comment_json( "comment": mikrotik_client.create_comment_json(roll_back_config = existing_vlan_json)
created_by = self.CREATED_BY_NAME,
created_ts = date_time.get_current_utc_date_time(as_string = False),
roll_back_config = existing_vlan_json
)
}, },
port_no = mikrotik_auth.nasPort,
use_https = use_https use_https = use_https
) )
@@ -419,22 +381,14 @@ class MikroTikPPPoE1000Controller(MikroTikController):
else: else:
# We update it: # We update it:
api_response = await self.update_vlan( api_response = await mikrotik_client.update_vlan(
mikrotik_ip = mikrotik_auth.nasIp,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
dot_id = existing_dot_id, dot_id = existing_dot_id,
json_payload = { json_payload = {
"name": self.VLAN_NAME.format(vlan_id), "name": self.VLAN_NAME.format(vlan_id),
"interface": self.HW_INTERFACE_NAME, "interface": self.HW_INTERFACE_NAME,
"disabled": "false", "disabled": "false",
"comment": self.create_comment_json( "comment": mikrotik_client.create_comment_json(roll_back_config = existing_vlan_json)
created_by = self.CREATED_BY_NAME,
created_ts = date_time.get_current_utc_date_time(as_string = False),
roll_back_config = existing_vlan_json
)
}, },
port_no = mikrotik_auth.nasPort,
use_https = use_https use_https = use_https
) )
@@ -453,12 +407,14 @@ class MikroTikPPPoE1000Controller(MikroTikController):
async def set_up_vlans( async def set_up_vlans(
self, self,
mikrotik_client: AsyncMikroTik,
mikrotik_auth: MikroTikPPPoE1000Auth, mikrotik_auth: MikroTikPPPoE1000Auth,
use_https: bool = True use_https: bool = True
) -> ApiResponse: ) -> MikroTikConfigAttemptResponse:
""" """
To set up all the needed VLANs: To set up all the needed VLANs:
:param mikrotik_client: The client to use.
:param mikrotik_auth: The set of credentials as received from the UI/API. :param mikrotik_auth: The set of credentials as received from the UI/API.
:param use_https: Whether to use HTTPS, or HTTP. :param use_https: Whether to use HTTPS, or HTTP.
:return: A structured response to indicate what happened during the process. :return: A structured response to indicate what happened during the process.
@@ -468,13 +424,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
step_response = MikroTikConfigAttemptResponse() step_response = MikroTikConfigAttemptResponse()
# Enlist all the existing VLANs: # Enlist all the existing VLANs:
api_response = await self.list_vlans( api_response = await mikrotik_client.list_vlans(use_https = use_https)
mikrotik_ip = mikrotik_auth.nasIp,
port_no = mikrotik_auth.nasPort,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
use_https = use_https
)
# If the listing failed: # If the listing failed:
if not api_response.success: if not api_response.success:
@@ -484,28 +434,26 @@ class MikroTikPPPoE1000Controller(MikroTikController):
return step_response return step_response
# Extract the list here: # Extract the list here:
vlan_list = await api_response.get_json() vlan_list = api_response.data
# Create a task for each VLAN that you need to set up, # Create a task for each VLAN that you need to set up,
# then fire them all asynchronously: # then fire them all asynchronously:
tasks = [ tasks = [
self.set_up_one_vlan( self.set_up_one_vlan(
mikrotik_auth = mikrotik_auth, mikrotik_client = mikrotik_client,
vlan_id = vlan_id, vlan_id = vlan_id,
vlan_list = vlan_list, vlan_list = vlan_list,
use_https = use_https use_https = use_https
) for vlan_id in mikrotik_auth.vlanRange ) for vlan_id in mikrotik_auth.vlanIds
] ]
results = await asyncio.gather(*tasks) results = await asyncio.gather(*tasks)
# Assess the results: # Assess the results:
total_count = len(results) total_count = len(results)
success_count = 0 success_count = 0
all_messages = []
for result in results: for result in results:
if result.success: success_count += 1 if result.success: success_count += 1
all_messages.append(result.message) step_response.message = f"{success_count}/{total_count} VLAN(s) configured."
step_response.message = " -> ".join(all_messages)
step_response.success = True if success_count == total_count else False step_response.success = True if success_count == total_count else False
# Done here: # Done here:
@@ -513,13 +461,13 @@ class MikroTikPPPoE1000Controller(MikroTikController):
async def roll_back_vlans( async def roll_back_vlans(
self, self,
mikrotik_auth: MikroTikPPPoE1000Auth, mikrotik_client: AsyncMikroTik,
use_https: bool = True use_https: bool = True
) -> MikroTikRollBackAttemptResponse: ) -> MikroTikConfigAttemptResponse:
""" """
To reset VLANs to their original state (if they already existed) or remove them if this class added them. To reset VLANs to their original state (if they already existed) or remove them if this class added them.
:param mikrotik_auth: The set of credentials as received from the UI/API. :param mikrotik_client: The client to use.
:param use_https: Whether to use HTTPS, or HTTP. :param use_https: Whether to use HTTPS, or HTTP.
:return: A structured response to indicate what happened during the process. :return: A structured response to indicate what happened during the process.
""" """
@@ -531,13 +479,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
step_response = MikroTikConfigAttemptResponse() step_response = MikroTikConfigAttemptResponse()
# Enlist all the existing VLANs: # Enlist all the existing VLANs:
api_response = await self.list_vlans( api_response = await mikrotik_client.list_vlans(use_https = use_https)
mikrotik_ip = mikrotik_auth.nasIp,
port_no = mikrotik_auth.nasPort,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
use_https = use_https
)
# If the listing failed: # If the listing failed:
if not api_response.success: if not api_response.success:
@@ -549,14 +491,17 @@ class MikroTikPPPoE1000Controller(MikroTikController):
# Loop through all the VLANs, # Loop through all the VLANs,
# and either reset them to their original state (if they already existed), # and either reset them to their original state (if they already existed),
# or remove them entirely if they didn't exist before we came: # or remove them entirely if they didn't exist before we came:
for vlan_json in await api_response.get_json(): for vlan_json in api_response.data:
# Check if the VLAN was created by this class: # Check if the VLAN was created by this class:
if not self.created_by_easyfi(vlan_json): continue if not mikrotik_client.is_my_config(
config_json = vlan_json,
name_substring = self.NAME_PREFIX
): continue
# Extract the rollback information: # Extract the rollback information:
comment = self.parse_comment_json(vlan_json.get("comment")) or {} comment_json = mikrotik_client.parse_comment_json(vlan_json.get("comment"))
roll_back_json = comment.get("rollbackConfig") roll_back_json = mikrotik_client.get_original_config(comment_json)
vlan_id = vlan_json.get("vlan-id") vlan_id = vlan_json.get("vlan-id")
dot_id = vlan_json.get(".id") dot_id = vlan_json.get(".id")
@@ -564,14 +509,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
if roll_back_json is None: if roll_back_json is None:
# We delete the resource: # We delete the resource:
await self.remove_vlan( await mikrotik_client.remove_vlan(dot_id = dot_id, use_https = use_https)
mikrotik_ip = mikrotik_auth.nasIp,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
dot_id = dot_id,
port_no = mikrotik_auth.nasPort,
use_https = use_https
)
# Assess the result: # Assess the result:
total_count += 1 total_count += 1
@@ -584,10 +522,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
else: else:
# We roll back to the previous configuration: # We roll back to the previous configuration:
await self.update_vlan( await mikrotik_client.update_vlan(
mikrotik_ip = mikrotik_auth.nasIp,
username = mikrotik_auth.username,
password = mikrotik_auth.password,
dot_id = dot_id, dot_id = dot_id,
json_payload = { json_payload = {
"name": roll_back_json.get("name"), "name": roll_back_json.get("name"),
@@ -595,7 +530,6 @@ class MikroTikPPPoE1000Controller(MikroTikController):
"disabled": roll_back_json.get("disabled"), "disabled": roll_back_json.get("disabled"),
"comment": "" "comment": ""
}, },
port_no = mikrotik_auth.nasPort,
use_https = use_https use_https = use_https
) )
@@ -607,9 +541,235 @@ class MikroTikPPPoE1000Controller(MikroTikController):
else: all_messages.append(f"Failed to roll back VLAN with id {vlan_id}") else: all_messages.append(f"Failed to roll back VLAN with id {vlan_id}")
# Assess the overall results: # Assess the overall results:
all_messages.append(f"Removed/rolled-back {success_count}/{total_count} VLANs.")
if success_count == total_count: step_response.success = True if success_count == total_count: step_response.success = True
step_response.message = " -> ".join(all_messages) step_response.message = f"Removed/rolled-back {success_count}/{total_count} VLAN(s)."
# Done here:
return step_response
# ┳┏┓ ┏┓ ┓
# ┃┃┃ ┃┃┏┓┏┓┃┏
# ┻┣┛ ┣┛┗┛┗┛┗┛
async def set_up_one_ip_pool(
self,
mikrotik_client: AsyncMikroTik,
ip_pool: dict,
existing_ip_pools: List[dict],
use_https: bool = True
) -> MikroTikConfigAttemptResponse:
"""
To set up one of the needed IP pools.
:param mikrotik_client: The client to use.
:param ip_pool: The IP pool you want configured by the end of the process.
:param existing_ip_pools: A list of the existing IP pools. Helps to decide between 'PUT' and 'PATCH' methods.
:param use_https: Whether to use HTTPS, or HTTP.
:return: A structured response to indicate what happened during the process.
"""
# Start by assuming failure:
step_response = MikroTikConfigAttemptResponse()
# Check if the configuration already exists:
existing_dot_id = None
existing_ip_pool_json = None
for existing_ip_pool in existing_ip_pools:
if existing_ip_pool.get("name", "???") == ip_pool["name"]:
existing_dot_id = existing_ip_pool[".id"]
existing_ip_pool_json = existing_ip_pool
break
# If the IP pool doesn't already exist:
if existing_dot_id is None:
# Add the configuration:
api_response = await mikrotik_client.add_ip_pool(
json_payload = {
"name": ip_pool["name"],
"ranges": ip_pool["network"],
"comment": mikrotik_client.create_comment_json()
},
use_https = use_https
)
# And assess the result:
if api_response.success:
step_response.success = True
step_response.message = f"Added new IP pool '{ip_pool['name']}'."
step_response.exception = None
else:
step_response.success = False
step_response.message = f"Failed to add new IP pool '{ip_pool['name']}'."
step_response.exception = api_response.exception
# If the IP pool already exists:
else:
# Update the configuration:
api_response = await mikrotik_client.update_ip_pool(
dot_id = existing_dot_id,
json_payload = {
"name": ip_pool["name"],
"ranges": ip_pool["network"],
"comment": mikrotik_client.create_comment_json(roll_back_config = existing_ip_pool_json)
},
use_https = use_https
)
# And assess the result:
if api_response.success:
step_response.success = True
step_response.message = f"Updated existing IP pool '{ip_pool['name']}'."
step_response.exception = None
else:
step_response.success = False
step_response.message = f"Failed to update existing IP pool '{ip_pool['name']}'."
step_response.exception = api_response.exception
# Done here:
return step_response
async def set_up_ip_pools(
self,
mikrotik_client: AsyncMikroTik,
mikrotik_auth: MikroTikPPPoE1000Auth,
use_https: bool = True
) -> MikroTikConfigAttemptResponse:
"""
To set up ALL the needed IP pools.
: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 failure:
step_response = MikroTikConfigAttemptResponse()
# Enlist all the existing VLANs:
api_response = await mikrotik_client.list_ip_pools(use_https = use_https)
# If the listing failed:
if not api_response.success:
step_response.success = False
step_response.message = "Failed to enlist existing IP pools during configuration."
step_response.exception = step_response.exception
return step_response
# Extract the list here:
existing_ip_pools = api_response.data
# First we split the whole private IP range
# into the no. of VLANs we had been asked to make:
ip_pools = mikrotik_client.split_ipv4_range_equally(
start_ip = mikrotik_auth.firstPrivateIp,
end_ip = mikrotik_auth.lastPrivateIp,
targets = mikrotik_auth.vlanIds
)
# Create tasks to create IP Pools,
# and fire them asynchronously:
tasks = [
self.set_up_one_ip_pool(
mikrotik_client = mikrotik_client,
ip_pool = {
"name": self.PRIVATE_IP_POOL_NAME.format(ip_pool["target"]),
"network": ip_pool["network"],
},
existing_ip_pools = existing_ip_pools,
use_https = use_https
) for ip_pool in ip_pools
]
results = await asyncio.gather(*tasks)
# Assess the results:
total_count = len(results)
success_count = 0
for result in results:
if result.success: success_count += 1
step_response.message = f"{success_count}/{total_count} IP pool(s) configured."
step_response.success = True if success_count == total_count else False
# Done here:
return step_response
async def roll_back_ip_pools(
self,
mikrotik_client: AsyncMikroTik,
use_https: bool = True
) -> MikroTikConfigAttemptResponse:
"""
To reset IP pools to their original state (if they already existed) or remove them if this class added them.
: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 failure:
total_count = 0
success_count = 0
step_response = MikroTikConfigAttemptResponse()
# Enlist all the existing VLANs:
api_response = await mikrotik_client.list_ip_pools(use_https = use_https)
# If the listing failed:
if not api_response.success:
step_response.success = False
step_response.message = "Failed to enlist existing IP pools during rollback."
step_response.exception = step_response.exception
return step_response
# Loop through all the VLANs,
# and either reset them to their original state (if they already existed),
# or remove them entirely if they didn't exist before we came:
for resource_json in api_response.data:
# Check if the VLAN was created by this class:
if not mikrotik_client.is_my_config(
config_json = resource_json,
name_substring = self.NAME_PREFIX
): continue
# Extract the rollback information:
comment_json = mikrotik_client.parse_comment_json(resource_json.get("comment"))
roll_back_json = mikrotik_client.get_original_config(comment_json)
dot_id = resource_json.get(".id")
# If the resource has no rollback information:
if roll_back_json is None:
# We delete the resource:
await mikrotik_client.remove_ip_pool(dot_id = dot_id, use_https = use_https)
# Assess the result:
total_count += 1
if api_response.success: success_count += 1
# If the resource has rollback information:
else:
# We roll back to the previous configuration:
await mikrotik_client.update_ip_pool(
dot_id = dot_id,
json_payload = {
"name": roll_back_json.get("name"),
"ranges": roll_back_json.get("ranges"),
"comment": ""
},
use_https = use_https
)
# Assess the result:
total_count += 1
if api_response.success: success_count += 1
# Assess the overall results:
if success_count == total_count: step_response.success = True
step_response.message = f"Removed/rolled-back {success_count}/{total_count} IP pool(s)."
# Done here: # Done here:
return step_response return step_response
@@ -634,31 +794,34 @@ class MikroTikPPPoE1000Controller(MikroTikController):
:return: A structured response to indicate what happened during authorization. :return: A structured response to indicate what happened during authorization.
""" """
# # print("IN-AUTH:", json.to_string(mikrotik_auth.model_dump(), default = str))
# print("VLAN SUBNETS:", json.to_string(
# self.split_ipv4_range_among_targets(
# start_ip = mikrotik_auth.firstPrivateIp,
# end_ip = mikrotik_auth.lastPrivateIp,
# targets = mikrotik_auth.vlanRange
# ),
# default = str
# ))
# Start with a blank response: # Start with a blank response:
auth_response = MikroTikAuthResponse() auth_response = MikroTikAuthResponse()
# Try to configure the system: # Create a MikroTik client:
config_response = await self.configure(mikrotik_auth) mikrotik_client = AsyncMikroTik(
print("CONFIG RESPONSE:", config_response) config_by = self.CREATED_BY_NAME,
auth_response = config_response 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
)
await asyncio.sleep(10.0) # Try to configure the system:
config_response = await self.configure(mikrotik_client, mikrotik_auth)
print("CONFIG RESPONSE:", config_response)
await asyncio.sleep(2.0)
# Try to roll all configuration back: # Try to roll all configuration back:
roll_back_response = await self.roll_back(mikrotik_auth) roll_back_response = await self.roll_back(mikrotik_client, mikrotik_auth)
print("ROLL-BACK RESPONSE:", roll_back_response) print("ROLL-BACK RESPONSE:", roll_back_response)
config_response += roll_back_response
# Done here: # Done here:
auth_response = config_response
return auth_response return auth_response
# ┏┓ ┏• # ┏┓ ┏•
@@ -668,30 +831,38 @@ class MikroTikPPPoE1000Controller(MikroTikController):
async def roll_back( async def roll_back(
self, self,
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth mikrotik_client: AsyncMikroTik,
) -> MikroTikRollBackAttemptResponse: mikrotik_auth: MikroTikPPPoE1000Auth
) -> MikroTikConfigAttemptResponse:
""" """
The rolling-back to the original state (as best as possible) in case the configurations fails midway after The rolling-back to the original state (as best as possible) in case the configurations fails midway after
completing some no. of steps. 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. :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. :return: A structured response to indicate what happened during the configuration attempt.
""" """
# Start with some variables: # Start with some variables:
keep_going = True keep_going = True
all_messages = [] all_messages = ["STARTING ROLLBACK."]
roll_back_response = MikroTikConfigAttemptResponse() roll_back_response = MikroTikConfigAttemptResponse()
# Next we remove all the VLANs: # Next, we roll back all the IP pools:
if keep_going: if keep_going:
step_response = await self.roll_back_vlans(mikrotik_auth, use_https = False) 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) all_messages.append(step_response.message)
keep_going = step_response.success keep_going = step_response.success
# Next, we free-up the interface: # Next, we free-up the interface:
if keep_going: if keep_going:
step_response = await self.roll_back_interface(mikrotik_auth, use_https = False) step_response = await self.roll_back_interface(mikrotik_client, use_https = False)
all_messages.append(step_response.message) all_messages.append(step_response.message)
keep_going = step_response.success keep_going = step_response.success
@@ -702,30 +873,39 @@ class MikroTikPPPoE1000Controller(MikroTikController):
async def configure( async def configure(
self, self,
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth mikrotik_client: AsyncMikroTik,
mikrotik_auth: MikroTikPPPoE1000Auth
) -> MikroTikConfigAttemptResponse: ) -> MikroTikConfigAttemptResponse:
""" """
Run the configuration steps for the system. 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. :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. :return: A structured response to indicate what happened during the configuration attempt.
""" """
# Start with some variables: # Start with some variables:
keep_going = True keep_going = True
all_messages = [] all_messages = ["STARTING CONFIG."]
config_response = MikroTikConfigAttemptResponse() config_response = MikroTikConfigAttemptResponse(actionChain = mikrotik_client.action_chain)
# First, we arrange an interface: # First, we arrange an interface:
if keep_going: if keep_going:
step_response = await self.set_up_interface(mikrotik_auth, use_https = False) step_response = await self.set_up_interface(mikrotik_client, dot_id = "*3", use_https = False)
all_messages.append(step_response.message) all_messages.append(step_response.message)
keep_going = step_response.success keep_going = step_response.success
# Next, we create the needed VLANs: # Next, we create the needed VLANs:
if keep_going: if keep_going:
await asyncio.sleep(0.25) await asyncio.sleep(0.25)
step_response = await self.set_up_vlans(mikrotik_auth, use_https = False) 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) all_messages.append(step_response.message)
keep_going = step_response.success keep_going = step_response.success
+2 -2
View File
@@ -172,7 +172,7 @@ class MikroTikPPPoE1000Auth(BaseModel):
frozen = True frozen = True
) )
vlanRange: List[int] = Field( vlanIds: List[int] = Field(
description = "Don't know, and don't want to know.", description = "Don't know, and don't want to know.",
min_length = 1, min_length = 1,
frozen = True frozen = True
@@ -234,7 +234,7 @@ class MikroTikPPPoE1000Auth(BaseModel):
def validate_ip_range(cls, value): def validate_ip_range(cls, value):
return ip.to_hyphen_notation(value) return ip.to_hyphen_notation(value)
@field_validator("vlanRange", mode = "before") @field_validator("vlanIds", mode = "before")
def validate_vlans(cls, value): def validate_vlans(cls, value):
return parse_vlans(value) return parse_vlans(value)
+20 -1
View File
@@ -77,6 +77,12 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]
class MikroTikConfigAttemptResponse(BaseModel): class MikroTikConfigAttemptResponse(BaseModel):
actionChain: str | int = Field(
description = "To be able to link all the actions of one process",
frozen = False,
default = None
)
success: bool = Field( success: bool = Field(
description = "To indicate whether or not, the action was a success", description = "To indicate whether or not, the action was a success",
frozen = False, frozen = False,
@@ -107,7 +113,14 @@ class MikroTikConfigAttemptResponse(BaseModel):
# ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏
# ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛
pass def __add__(self, other):
return MikroTikConfigAttemptResponse(
actionChain = other.actionChain or self.actionChain,
success = self.success and other.success,
message = self.message + " -> " + other.message,
exception = other.exception or self.exception
)
# ┓┏ ┓• ┓ • # ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
@@ -121,6 +134,12 @@ class MikroTikConfigAttemptResponse(BaseModel):
class MikroTikRollBackAttemptResponse(BaseModel): class MikroTikRollBackAttemptResponse(BaseModel):
actionChain: str | int = Field(
description = "To be able to link all the actions of one process",
frozen = False,
default = None
)
success: bool = Field( success: bool = Field(
description = "To indicate whether or not, the action was a success", description = "To indicate whether or not, the action was a success",
frozen = False, frozen = False,
+6 -3
View File
@@ -69,6 +69,9 @@ configuration, simply delete your record.
You will receive the Private IP range in either CIDR notation or as a hyphen-separated string. You need to cut up this You will receive the Private IP range in either CIDR notation or as a hyphen-separated string. You need to cut up this
larger IP pool into subnets that you will eventually assign to each VLAN (by way of creating PPPoE servers). larger IP pool into subnets that you will eventually assign to each VLAN (by way of creating PPPoE servers).
**WARNING:** It is possible to have IP pools in a MikroTik device with overlapping IP addresses. It is not recommended,
though, because it may lead to conflicts that could be hard to trace back.
Use `PUT` or `PATCH` method on the path `/ip/pool`. Consider to the following example JSON: Use `PUT` or `PATCH` method on the path `/ip/pool`. Consider to the following example JSON:
```json ```json
{ {
@@ -80,8 +83,8 @@ Use `PUT` or `PATCH` method on the path `/ip/pool`. Consider to the following ex
**NOTE:** Looping needed. **NOTE:** Looping needed.
**ROLL-BACK:** Save the original configuration as a JSON string in the `comment` field. If there was no original **ROLL-BACK:** Identify your entry from the `name` field (contains 'easyfi') or from the contents of the `comment` field
configuration, simply delete your record. and remove it.
### 4. Create PPPoE Profiles ### 4. Create PPPoE Profiles
@@ -115,7 +118,7 @@ up servers that use one VLAN and one private IP sub-pool to actually handle the
Use `PUT` or `PATCH` method on the path `/interface/pppoe-server/server`. Consider to the following example JSON: Use `PUT` or `PATCH` method on the path `/interface/pppoe-server/server`. Consider to the following example JSON:
```json ```json
{ {
"interface": "vlan10", // ..................... The VLAN interface you had created earlier. "interface": "easyfi-vlan-2001", // ........... The VLAN interface you had created earlier.
"profile": "easyfi-pppoe-prf-2001", // ........ The PPPoE profile you created earlier. "profile": "easyfi-pppoe-prf-2001", // ........ The PPPoE profile you created earlier.
"service-name": "easyfi-pppoe-srv-2001", // ... Use the VLAN id in the name. "service-name": "easyfi-pppoe-srv-2001", // ... Use the VLAN id in the name.
"comment": "{...}" // ......................... The JSON string to indicate automated config. "comment": "{...}" // ......................... The JSON string to indicate automated config.
+1 -1
View File
@@ -20,8 +20,8 @@ if [[ $SELECTION -gt 0 && $SELECTION -le ${#SERVERS[@]} ]]; then
SELECTED_SERVER=${SERVERS[$((SELECTION - 1))]} SELECTED_SERVER=${SERVERS[$((SELECTION - 1))]}
# Ask the username and target port no. on the server:: # Ask the username and target port no. on the server::
read -rp "Your username on the server ..... : " USER
read -rp "The target port no. ............. : " PORT read -rp "The target port no. ............. : " PORT
read -rp "Your username on the server ..... : " USER
# Run the command: # Run the command:
ssh -p "$PORT" "$USER@$SELECTED_SERVER" ssh -p "$PORT" "$USER@$SELECTED_SERVER"