(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:
@@ -215,7 +215,7 @@ async def authorize_software_client(
|
||||
|
||||
# Note down the results:
|
||||
success = response.success
|
||||
message = response.message
|
||||
message = f"Action Chain '{response.actionChain}': {response.message}"
|
||||
|
||||
# ┏┓ ┳┳┓•┓ ┏┳┓•┓ ┓┏ ┓┏┓┏┓┏┓
|
||||
# ┣ ┏┓┏┓ ┃┃┃┓┃┏┏┓┏┓ ┃ ┓┃┏ ┣┫┏┓╋┏┏┓┏┓╋ ┃┃┫┃┫┃┫
|
||||
|
||||
@@ -36,6 +36,7 @@ sys.path.append(".")
|
||||
sys.path.append("..")
|
||||
|
||||
# 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_mongo_v2 import AsyncMongo
|
||||
from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache
|
||||
@@ -181,11 +182,13 @@ class AllMikroTikController(MikroTikController):
|
||||
|
||||
async def roll_back(
|
||||
self,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
|
||||
) -> MikroTikRollBackAttemptResponse:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
@@ -194,10 +197,12 @@ class AllMikroTikController(MikroTikController):
|
||||
|
||||
async def configure(
|
||||
self,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
|
||||
) -> 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.
|
||||
"""
|
||||
|
||||
@@ -37,6 +37,7 @@ 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
|
||||
@@ -179,416 +180,6 @@ class MikroTikController(CoreSoftwareController, ABC):
|
||||
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
|
||||
async def roll_back(
|
||||
self,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
|
||||
) -> MikroTikRollBackAttemptResponse:
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
@@ -635,11 +228,13 @@ class MikroTikController(CoreSoftwareController, ABC):
|
||||
@abstractmethod
|
||||
async def configure(
|
||||
self,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
|
||||
) -> 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.
|
||||
"""
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
N/A
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
# *** IMPORT ***
|
||||
@@ -37,6 +38,7 @@ 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
|
||||
@@ -54,10 +56,7 @@ from models.software.mikrotik.auth import (
|
||||
MikroTikHotspot1000Auth,
|
||||
MikroTikAuthResponse
|
||||
)
|
||||
from models.software.mikrotik.configure import (
|
||||
MikroTikConfigAttemptResponse,
|
||||
MikroTikRollBackAttemptResponse
|
||||
)
|
||||
from models.software.mikrotik.configure import MikroTikConfigAttemptResponse
|
||||
|
||||
# To work with datatypes:
|
||||
from typing import List, Any
|
||||
@@ -71,6 +70,9 @@ from bson.objectid import ObjectId
|
||||
# To make abstract classes:
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# For asynchronous activities:
|
||||
import asyncio
|
||||
|
||||
|
||||
# *****************************************************************************************************************
|
||||
# ***** ****
|
||||
@@ -118,9 +120,16 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
CLIENT_NAME = "mikrotikPPPoE1000"
|
||||
|
||||
# For automated configuration, and identification of automated configuration:
|
||||
CREATED_BY_NAME = "easyfi"
|
||||
HW_INTERFACE_NAME = "easyfi-pppoe"
|
||||
VLAN_NAME = "easyfi-vlan-{}" # ... Substitute the VLAN's id here.
|
||||
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"
|
||||
|
||||
# ┏┓
|
||||
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
|
||||
@@ -162,46 +171,21 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
# Init a variable in a parent:
|
||||
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(
|
||||
self,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
dot_id: str = None,
|
||||
use_https: bool = True
|
||||
) -> MikroTikConfigAttemptResponse:
|
||||
|
||||
"""
|
||||
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.
|
||||
:return: A structured response to indicate what happened during the process.
|
||||
"""
|
||||
@@ -210,13 +194,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
step_response = MikroTikConfigAttemptResponse()
|
||||
|
||||
# Enlist all the interfaces:
|
||||
api_response = await self.list_interfaces(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
use_https = use_https
|
||||
)
|
||||
api_response = await mikrotik_client.list_interfaces(use_https = use_https)
|
||||
|
||||
# If the listing failed:
|
||||
if not api_response.success:
|
||||
@@ -225,47 +203,51 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
step_response.exception = step_response.exception
|
||||
return step_response
|
||||
|
||||
# We find the first available interface:
|
||||
unused_if_json = None
|
||||
for if_json in await api_response.get_json():
|
||||
if str(if_json.get("running")).lower() == "false":
|
||||
unused_if_json = if_json
|
||||
# We find/select an interface:
|
||||
selected_if_json = None
|
||||
for if_json in api_response.data:
|
||||
if (
|
||||
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
|
||||
|
||||
# If we found no available interface:
|
||||
if unused_if_json is None:
|
||||
if selected_if_json is None:
|
||||
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
|
||||
return step_response
|
||||
|
||||
# Reconfigure the idle interface:
|
||||
api_response = await self.update_interface(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
dot_id = unused_if_json.get(".id"),
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
use_https = use_https,
|
||||
api_response = await mikrotik_client.update_interface(
|
||||
dot_id = selected_if_json.get(".id"),
|
||||
json_payload = {
|
||||
"name": self.HW_INTERFACE_NAME,
|
||||
"disabled": "false",
|
||||
"comment": self.create_comment_json(
|
||||
created_by = self.CREATED_BY_NAME,
|
||||
created_ts = date_time.get_current_utc_date_time(as_string = False),
|
||||
roll_back_config = unused_if_json
|
||||
)
|
||||
}
|
||||
"comment": mikrotik_client.create_comment_json(roll_back_config = selected_if_json)
|
||||
},
|
||||
use_https = use_https,
|
||||
)
|
||||
|
||||
# Check if the attempt was successful:
|
||||
if api_response.success:
|
||||
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
|
||||
else:
|
||||
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
|
||||
|
||||
# Done here:
|
||||
@@ -273,13 +255,13 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
|
||||
async def roll_back_interface(
|
||||
self,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
use_https: bool = True
|
||||
) -> MikroTikRollBackAttemptResponse:
|
||||
) -> MikroTikConfigAttemptResponse:
|
||||
|
||||
"""
|
||||
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.
|
||||
:return: A structured response to indicate what happened during the process.
|
||||
"""
|
||||
@@ -290,13 +272,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
step_response = MikroTikConfigAttemptResponse()
|
||||
|
||||
# Enlist all the interfaces:
|
||||
api_response = await self.list_interfaces(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
use_https = use_https
|
||||
)
|
||||
api_response = await mikrotik_client.list_interfaces(use_https = use_https)
|
||||
|
||||
# If the listing failed:
|
||||
if not api_response.success:
|
||||
@@ -308,35 +284,29 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
# Loop through all the interfaces,
|
||||
# identify which ones were configured by this class,
|
||||
# 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:
|
||||
created_by = None
|
||||
original_config = None
|
||||
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")
|
||||
# Extract the needed values:
|
||||
comment_json = mikrotik_client.parse_comment_json(if_json.get("comment"))
|
||||
original_config = mikrotik_client.get_original_config(comment_json)
|
||||
|
||||
# If this was indeed set up by this class,
|
||||
# and if rollback data is available:
|
||||
if created_by == self.CREATED_BY_NAME and original_config is not None:
|
||||
api_response = await self.update_interface(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
if mikrotik_client.is_my_config(
|
||||
config_json = if_json,
|
||||
name_substring = self.NAME_PREFIX
|
||||
):
|
||||
api_response = await mikrotik_client.update_interface(
|
||||
dot_id = if_json.get(".id"),
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
use_https = use_https,
|
||||
json_payload={
|
||||
"name": original_config.get("name", self.HW_INTERFACE_NAME),
|
||||
json_payload = {
|
||||
"name": original_config.get("name"),
|
||||
"disabled": "false",
|
||||
"comment": original_config.get("comment", "")
|
||||
}
|
||||
"comment": original_config.get("comment")
|
||||
},
|
||||
use_https = use_https,
|
||||
)
|
||||
total_count += 1
|
||||
if api_response.success: rolled_back_count += 1
|
||||
print("RB API RESP:", api_response)
|
||||
|
||||
# Construct the final response:
|
||||
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(
|
||||
self,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
vlan_id: int,
|
||||
vlan_list: List[dict],
|
||||
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
|
||||
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_list: The list of existing VLANs already configured in the MikroTik device. Helps us decide between
|
||||
the use of PUT and PATCH methods.
|
||||
@@ -386,22 +356,14 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
if existing_dot_id is None:
|
||||
|
||||
# We add the VLAN:
|
||||
api_response = await self.add_vlan(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
api_response = await mikrotik_client.add_vlan(
|
||||
json_payload = {
|
||||
"name": self.VLAN_NAME.format(vlan_id),
|
||||
"vlan-id": str(vlan_id),
|
||||
"interface": self.HW_INTERFACE_NAME,
|
||||
"disabled": "false",
|
||||
"comment": self.create_comment_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
|
||||
)
|
||||
"comment": mikrotik_client.create_comment_json(roll_back_config = existing_vlan_json)
|
||||
},
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
use_https = use_https
|
||||
)
|
||||
|
||||
@@ -419,22 +381,14 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
else:
|
||||
|
||||
# We update it:
|
||||
api_response = await self.update_vlan(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
api_response = await mikrotik_client.update_vlan(
|
||||
dot_id = existing_dot_id,
|
||||
json_payload = {
|
||||
"name": self.VLAN_NAME.format(vlan_id),
|
||||
"interface": self.HW_INTERFACE_NAME,
|
||||
"disabled": "false",
|
||||
"comment": self.create_comment_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
|
||||
)
|
||||
"comment": mikrotik_client.create_comment_json(roll_back_config = existing_vlan_json)
|
||||
},
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
use_https = use_https
|
||||
)
|
||||
|
||||
@@ -453,12 +407,14 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
|
||||
async def set_up_vlans(
|
||||
self,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth,
|
||||
use_https: bool = True
|
||||
) -> ApiResponse:
|
||||
) -> MikroTikConfigAttemptResponse:
|
||||
|
||||
"""
|
||||
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 use_https: Whether to use HTTPS, or HTTP.
|
||||
:return: A structured response to indicate what happened during the process.
|
||||
@@ -468,13 +424,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
step_response = MikroTikConfigAttemptResponse()
|
||||
|
||||
# Enlist all the existing VLANs:
|
||||
api_response = await self.list_vlans(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
use_https = use_https
|
||||
)
|
||||
api_response = await mikrotik_client.list_vlans(use_https = use_https)
|
||||
|
||||
# If the listing failed:
|
||||
if not api_response.success:
|
||||
@@ -484,28 +434,26 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
return step_response
|
||||
|
||||
# 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,
|
||||
# then fire them all asynchronously:
|
||||
tasks = [
|
||||
self.set_up_one_vlan(
|
||||
mikrotik_auth = mikrotik_auth,
|
||||
mikrotik_client = mikrotik_client,
|
||||
vlan_id = vlan_id,
|
||||
vlan_list = vlan_list,
|
||||
use_https = use_https
|
||||
) for vlan_id in mikrotik_auth.vlanRange
|
||||
) for vlan_id in mikrotik_auth.vlanIds
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# Assess the results:
|
||||
total_count = len(results)
|
||||
success_count = 0
|
||||
all_messages = []
|
||||
for result in results:
|
||||
if result.success: success_count += 1
|
||||
all_messages.append(result.message)
|
||||
step_response.message = " -> ".join(all_messages)
|
||||
step_response.message = f"{success_count}/{total_count} VLAN(s) configured."
|
||||
step_response.success = True if success_count == total_count else False
|
||||
|
||||
# Done here:
|
||||
@@ -513,13 +461,13 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
|
||||
async def roll_back_vlans(
|
||||
self,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth,
|
||||
mikrotik_client: AsyncMikroTik,
|
||||
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.
|
||||
: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.
|
||||
:return: A structured response to indicate what happened during the process.
|
||||
"""
|
||||
@@ -531,13 +479,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
step_response = MikroTikConfigAttemptResponse()
|
||||
|
||||
# Enlist all the existing VLANs:
|
||||
api_response = await self.list_vlans(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
use_https = use_https
|
||||
)
|
||||
api_response = await mikrotik_client.list_vlans(use_https = use_https)
|
||||
|
||||
# If the listing failed:
|
||||
if not api_response.success:
|
||||
@@ -549,14 +491,17 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
# 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 vlan_json in await api_response.get_json():
|
||||
for vlan_json in api_response.data:
|
||||
|
||||
# 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:
|
||||
comment = self.parse_comment_json(vlan_json.get("comment")) or {}
|
||||
roll_back_json = comment.get("rollbackConfig")
|
||||
comment_json = mikrotik_client.parse_comment_json(vlan_json.get("comment"))
|
||||
roll_back_json = mikrotik_client.get_original_config(comment_json)
|
||||
vlan_id = vlan_json.get("vlan-id")
|
||||
dot_id = vlan_json.get(".id")
|
||||
|
||||
@@ -564,14 +509,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
if roll_back_json is None:
|
||||
|
||||
# We delete the resource:
|
||||
await self.remove_vlan(
|
||||
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
|
||||
)
|
||||
await mikrotik_client.remove_vlan(dot_id = dot_id, use_https = use_https)
|
||||
|
||||
# Assess the result:
|
||||
total_count += 1
|
||||
@@ -584,10 +522,7 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
else:
|
||||
|
||||
# We roll back to the previous configuration:
|
||||
await self.update_vlan(
|
||||
mikrotik_ip = mikrotik_auth.nasIp,
|
||||
username = mikrotik_auth.username,
|
||||
password = mikrotik_auth.password,
|
||||
await mikrotik_client.update_vlan(
|
||||
dot_id = dot_id,
|
||||
json_payload = {
|
||||
"name": roll_back_json.get("name"),
|
||||
@@ -595,7 +530,6 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
"disabled": roll_back_json.get("disabled"),
|
||||
"comment": ""
|
||||
},
|
||||
port_no = mikrotik_auth.nasPort,
|
||||
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}")
|
||||
|
||||
# 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
|
||||
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:
|
||||
return step_response
|
||||
@@ -634,31 +794,34 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
: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:
|
||||
auth_response = MikroTikAuthResponse()
|
||||
|
||||
# Try to configure the system:
|
||||
config_response = await self.configure(mikrotik_auth)
|
||||
print("CONFIG RESPONSE:", config_response)
|
||||
auth_response = config_response
|
||||
# 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
|
||||
)
|
||||
|
||||
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:
|
||||
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)
|
||||
config_response += roll_back_response
|
||||
|
||||
# Done here:
|
||||
auth_response = config_response
|
||||
return auth_response
|
||||
|
||||
# ┏┓ ┏•
|
||||
@@ -668,30 +831,38 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
|
||||
async def roll_back(
|
||||
self,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
|
||||
) -> MikroTikRollBackAttemptResponse:
|
||||
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 = []
|
||||
all_messages = ["STARTING ROLLBACK."]
|
||||
roll_back_response = MikroTikConfigAttemptResponse()
|
||||
|
||||
# Next we remove all the VLANs:
|
||||
# Next, we roll back all the IP pools:
|
||||
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)
|
||||
keep_going = step_response.success
|
||||
|
||||
# Next, we free-up the interface:
|
||||
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)
|
||||
keep_going = step_response.success
|
||||
|
||||
@@ -702,30 +873,39 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
||||
|
||||
async def configure(
|
||||
self,
|
||||
mikrotik_auth: MikroTikPPPoE1000Auth | MikroTikHotspot1000Auth
|
||||
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 = []
|
||||
config_response = MikroTikConfigAttemptResponse()
|
||||
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_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)
|
||||
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_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)
|
||||
keep_going = step_response.success
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ class MikroTikPPPoE1000Auth(BaseModel):
|
||||
frozen = True
|
||||
)
|
||||
|
||||
vlanRange: List[int] = Field(
|
||||
vlanIds: List[int] = Field(
|
||||
description = "Don't know, and don't want to know.",
|
||||
min_length = 1,
|
||||
frozen = True
|
||||
@@ -234,7 +234,7 @@ class MikroTikPPPoE1000Auth(BaseModel):
|
||||
def validate_ip_range(cls, value):
|
||||
return ip.to_hyphen_notation(value)
|
||||
|
||||
@field_validator("vlanRange", mode = "before")
|
||||
@field_validator("vlanIds", mode = "before")
|
||||
def validate_vlans(cls, value):
|
||||
return parse_vlans(value)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
actionChain: str | int = Field(
|
||||
description = "To be able to link all the actions of one process",
|
||||
frozen = False,
|
||||
default = None
|
||||
)
|
||||
|
||||
success: bool = Field(
|
||||
description = "To indicate whether or not, the action was a success",
|
||||
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):
|
||||
|
||||
actionChain: str | int = Field(
|
||||
description = "To be able to link all the actions of one process",
|
||||
frozen = False,
|
||||
default = None
|
||||
)
|
||||
|
||||
success: bool = Field(
|
||||
description = "To indicate whether or not, the action was a success",
|
||||
frozen = False,
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
```json
|
||||
{
|
||||
@@ -80,8 +83,8 @@ Use `PUT` or `PATCH` method on the path `/ip/pool`. Consider to the following ex
|
||||
|
||||
**NOTE:** Looping needed.
|
||||
|
||||
**ROLL-BACK:** Save the original configuration as a JSON string in the `comment` field. If there was no original
|
||||
configuration, simply delete your record.
|
||||
**ROLL-BACK:** Identify your entry from the `name` field (contains 'easyfi') or from the contents of the `comment` field
|
||||
and remove it.
|
||||
|
||||
### 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:
|
||||
```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.
|
||||
"service-name": "easyfi-pppoe-srv-2001", // ... Use the VLAN id in the name.
|
||||
"comment": "{...}" // ......................... The JSON string to indicate automated config.
|
||||
|
||||
+1
-1
@@ -20,8 +20,8 @@ if [[ $SELECTION -gt 0 && $SELECTION -le ${#SERVERS[@]} ]]; then
|
||||
SELECTED_SERVER=${SERVERS[$((SELECTION - 1))]}
|
||||
|
||||
# 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 "Your username on the server ..... : " USER
|
||||
|
||||
# Run the command:
|
||||
ssh -p "$PORT" "$USER@$SELECTED_SERVER"
|
||||
|
||||
Reference in New Issue
Block a user