(20250211) Completed documentation for MikroTik's PPPoE system as per my current understanding.
This commit is contained in:
@@ -21,8 +21,7 @@
|
|||||||
N/A
|
N/A
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
# *****************************************************************************************************************
|
# *****************************************************************************************************************
|
||||||
# ***** ****
|
# ***** ****
|
||||||
# *** IMPORT ***
|
# *** IMPORT ***
|
||||||
@@ -36,6 +35,8 @@ sys.path.append(".")
|
|||||||
sys.path.append("..")
|
sys.path.append("..")
|
||||||
|
|
||||||
# My async utils:
|
# My async utils:
|
||||||
|
from utils_v2.string import json
|
||||||
|
from utils_v2.date_time import date_time
|
||||||
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
|
||||||
@@ -185,7 +186,7 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_mikrotik_url(
|
def get_mikrotik_url(
|
||||||
nas_ip: str,
|
mikrotik_ip: str,
|
||||||
path: str,
|
path: str,
|
||||||
port_no: int | str = None,
|
port_no: int | str = None,
|
||||||
use_https: bool = True
|
use_https: bool = True
|
||||||
@@ -193,7 +194,7 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Simply creates the base URL for hitting the MikroTik server.
|
Simply creates the base URL for hitting the MikroTik server.
|
||||||
:param nas_ip: The IP address of the MikroTik device.
|
:param mikrotik_ip: The IP address of the MikroTik device.
|
||||||
:param path: The path of the REST API to hit.
|
:param path: The path of the REST API to hit.
|
||||||
:param port_no: The port no. to hit the MikroTik device on.
|
:param port_no: The port no. to hit the MikroTik device on.
|
||||||
:param use_https: Whether to use HTTPS, or HTTP.
|
:param use_https: Whether to use HTTPS, or HTTP.
|
||||||
@@ -201,46 +202,46 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
base_url = r"https://" if use_https else r"http://"
|
base_url = r"https://" if use_https else r"http://"
|
||||||
base_url += nas_ip
|
base_url += mikrotik_ip
|
||||||
if port_no is not None: base_url += f":{port_no}"
|
if port_no is not None: base_url += f":{port_no}"
|
||||||
base_url += "/rest"
|
base_url += "/rest"
|
||||||
if not path.startswith("/"): path = "/" + path
|
if not path.startswith("/"): path = "/" + path
|
||||||
return base_url + path
|
return base_url + path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def split_ipv4_range_among_vlans(
|
def split_ipv4_range_among_targets(
|
||||||
start_ip: ipaddress.IPv4Address | str,
|
start_ip: ipaddress.IPv4Address | str,
|
||||||
end_ip: ipaddress.IPv4Address | str,
|
end_ip: ipaddress.IPv4Address | str,
|
||||||
vlan_ids: List[int]
|
targets: List
|
||||||
) -> List[dict]:
|
) -> List[dict]:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Divides the IPv4 range over the list of VLAN ids.
|
Divides the IPv4 range over the list of VLAN ids.
|
||||||
:param start_ip: The first IP in the full pool (range).
|
:param start_ip: The first IP in the full pool (range).
|
||||||
:param end_ip: The last IP in the full pool (range).
|
:param end_ip: The last IP in the full pool (range).
|
||||||
:param vlan_ids: The list of VLAN ids (need not be sequential or ordered).
|
:param targets: The list of targets (need not be sequential or ordered) like VLAN ids.
|
||||||
:return: A list of dicts that describes each VLAN.
|
:return: A list of dicts that describes the subnet for each target.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Parse the inputs:
|
# Parse the inputs:
|
||||||
start_ip_obj = start_ip if isinstance(start_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(start_ip)
|
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)
|
end_ip_obj = end_ip if isinstance(end_ip, ipaddress.IPv4Address) else ipaddress.IPv4Address(end_ip)
|
||||||
vlan_count = len(vlan_ids)
|
target_count = len(targets)
|
||||||
|
|
||||||
# Calculate the total number of IPs in the range:
|
# Calculate the total number of IPs in the range:
|
||||||
total_ips = int(end_ip_obj) - int(start_ip_obj) + 1
|
total_ips = int(end_ip_obj) - int(start_ip_obj) + 1
|
||||||
|
|
||||||
# Calculate the no. of IPs each VLAN gets,
|
# Calculate the no. of IPs each VLAN gets,
|
||||||
# and add three because we need IPs for network, gateway and broadcast:
|
# and add three because we need IPs for network, gateway and broadcast:
|
||||||
ips_per_vlan = math.ceil(total_ips / vlan_count) + 3
|
ips_per_target = math.ceil(total_ips / target_count) + 3
|
||||||
|
|
||||||
# Generate subnets for each VLAN:
|
# Generate subnets for each VLAN:
|
||||||
subnets = []
|
subnets = []
|
||||||
current_ip = start_ip_obj
|
current_ip = start_ip_obj
|
||||||
for vlan_id in vlan_ids:
|
for target in targets:
|
||||||
|
|
||||||
# Calculate the network address for the current VLAN:
|
# Calculate the network address for the current VLAN:
|
||||||
subnet_network = ipaddress.IPv4Network(f"{current_ip}/{32 - (ips_per_vlan).bit_length()}", strict = False)
|
subnet_network = ipaddress.IPv4Network(f"{current_ip}/{32 - ips_per_target.bit_length()}", strict = False)
|
||||||
|
|
||||||
# Check for overlap with the parent network:
|
# Check for overlap with the parent network:
|
||||||
subnet_start_ip_obj = subnet_network.network_address
|
subnet_start_ip_obj = subnet_network.network_address
|
||||||
@@ -249,7 +250,7 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
|
|
||||||
# Add the calculated subnet to the list:
|
# Add the calculated subnet to the list:
|
||||||
subnets.append({
|
subnets.append({
|
||||||
"vlanId": vlan_id,
|
"target": target,
|
||||||
"network": str(subnet_network),
|
"network": str(subnet_network),
|
||||||
"size": int(2 ** (32 - subnet_network.prefixlen)),
|
"size": int(2 ** (32 - subnet_network.prefixlen)),
|
||||||
"startIp": str(subnet_start_ip_obj),
|
"startIp": str(subnet_start_ip_obj),
|
||||||
@@ -262,6 +263,51 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
# Done here:
|
# Done here:
|
||||||
return subnets
|
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
|
||||||
|
|
||||||
# ┏┓
|
# ┏┓
|
||||||
# ┗┓┓┏┏╋┏┓┏┳┓
|
# ┗┓┓┏┏╋┏┓┏┳┓
|
||||||
# ┗┛┗┫┛┗┗ ┛┗┗
|
# ┗┛┗┫┛┗┗ ┛┗┗
|
||||||
@@ -269,7 +315,7 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
|
|
||||||
async def get_system_resource(
|
async def get_system_resource(
|
||||||
self,
|
self,
|
||||||
nas_ip: str,
|
mikrotik_ip: str,
|
||||||
username: str,
|
username: str,
|
||||||
password: str,
|
password: str,
|
||||||
port_no: int | str = None,
|
port_no: int | str = None,
|
||||||
@@ -279,7 +325,7 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
"""
|
"""
|
||||||
To get a summary of the hardware resources available in the MikroTik device. This also becomes a great way to
|
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.
|
quickly check if any given device is valid, and up and running.
|
||||||
:param nas_ip: The IP address of 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 username: The username to get access to the MikroTik device.
|
||||||
:param password: The password 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 port_no: The port no. to hit the MikroTik device on.
|
||||||
@@ -287,10 +333,10 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
:return: A structured API response.
|
:return: A structured API response.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Make the API call and return the response:
|
# Make the API call:
|
||||||
return await self._rest.get(
|
api_response = await self._rest.get(
|
||||||
url = self.get_mikrotik_url(
|
url = self.get_mikrotik_url(
|
||||||
nas_ip = nas_ip,
|
mikrotik_ip = mikrotik_ip,
|
||||||
path = r"/system/resource",
|
path = r"/system/resource",
|
||||||
port_no = port_no,
|
port_no = port_no,
|
||||||
use_https = use_https
|
use_https = use_https
|
||||||
@@ -301,6 +347,103 @@ class MikroTikController(CoreSoftwareController, ABC):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
):
|
||||||
|
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
interface_id: str,
|
||||||
|
port_no: int | str = None,
|
||||||
|
use_https: bool = True,
|
||||||
|
name: str = None,
|
||||||
|
disabled: bool = False,
|
||||||
|
comment: str = None
|
||||||
|
):
|
||||||
|
|
||||||
|
"""
|
||||||
|
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 interface_id: The id of the interface that you'd like to modify.
|
||||||
|
:param port_no: The port no. to hit the MikroTik device on.
|
||||||
|
:param use_https: Whether to use HTTPS, or HTTP.
|
||||||
|
:param name: The new name for the interface.
|
||||||
|
:param disabled: Whether, or not, you'd like to disable the interface.
|
||||||
|
:param comment: A note that you'd like to attach to the entry. It'll then be available in the listing API.
|
||||||
|
:return: A structured API response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prepare the JSON to give to the API:
|
||||||
|
input_json = {
|
||||||
|
k: v for k, v in {
|
||||||
|
"name": name,
|
||||||
|
"disabled": disabled,
|
||||||
|
"comment": comment
|
||||||
|
}.items() if v is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make the API call:
|
||||||
|
api_response = await self._rest.patch(
|
||||||
|
url = self.get_mikrotik_url(
|
||||||
|
mikrotik_ip = mikrotik_ip,
|
||||||
|
path = f"/interface/{interface_id}",
|
||||||
|
port_no = port_no,
|
||||||
|
use_https = use_https
|
||||||
|
),
|
||||||
|
auth = httpx.BasicAuth(
|
||||||
|
username = username,
|
||||||
|
password = password
|
||||||
|
),
|
||||||
|
json = input_json
|
||||||
|
)
|
||||||
|
|
||||||
|
# Done here:
|
||||||
|
return api_response
|
||||||
|
|
||||||
# ┏┓ ┓
|
# ┏┓ ┓
|
||||||
# ┣┫┓┏╋┣┓
|
# ┣┫┓┏╋┣┓
|
||||||
# ┛┗┗┻┗┛┗
|
# ┛┗┗┻┗┛┗
|
||||||
|
|||||||
@@ -21,8 +21,7 @@
|
|||||||
N/A
|
N/A
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
# *****************************************************************************************************************
|
# *****************************************************************************************************************
|
||||||
# ***** ****
|
# ***** ****
|
||||||
# *** IMPORT ***
|
# *** IMPORT ***
|
||||||
@@ -37,6 +36,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.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
|
||||||
@@ -117,6 +117,11 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
|||||||
|
|
||||||
CLIENT_NAME = "mikrotikPPPoE1000"
|
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.
|
||||||
|
|
||||||
# ┏┓
|
# ┏┓
|
||||||
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
|
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
|
||||||
# ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛
|
# ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛
|
||||||
@@ -157,6 +162,158 @@ 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
|
||||||
|
|
||||||
|
# ┳ ┏
|
||||||
|
# ┃┏┓╋┏┓┏┓╋┏┓┏┏┓┏
|
||||||
|
# ┻┛┗┗┗ ┛ ┛┗┻┗┗ ┛
|
||||||
|
|
||||||
|
async def interface_setup(
|
||||||
|
self,
|
||||||
|
mikrotik_auth: MikroTikPPPoE1000Auth,
|
||||||
|
use_https: bool = True
|
||||||
|
) -> MikroTikConfigAttemptResponse:
|
||||||
|
|
||||||
|
"""
|
||||||
|
To find the first available interface.
|
||||||
|
: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 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
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the listing failed:
|
||||||
|
if not api_response.success:
|
||||||
|
step_response.success = False
|
||||||
|
step_response.message = "Failed to enlist h/w interfaces during configuration."
|
||||||
|
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
|
||||||
|
break
|
||||||
|
|
||||||
|
# If we found no available interface:
|
||||||
|
if unused_if_json is None:
|
||||||
|
step_response.success = False
|
||||||
|
step_response.message = "Failed to find an idle 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,
|
||||||
|
interface_id = unused_if_json.get(".id"),
|
||||||
|
port_no = mikrotik_auth.nasPort,
|
||||||
|
use_https = use_https,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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.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.exception = api_response.exception
|
||||||
|
|
||||||
|
# Done here:
|
||||||
|
return step_response
|
||||||
|
|
||||||
|
async def interface_roll_back(
|
||||||
|
self,
|
||||||
|
mikrotik_auth: MikroTikPPPoE1000Auth,
|
||||||
|
use_https: bool = True
|
||||||
|
) -> MikroTikRollBackAttemptResponse:
|
||||||
|
|
||||||
|
"""
|
||||||
|
To find the first available interface.
|
||||||
|
: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:
|
||||||
|
total_count = 0
|
||||||
|
rolled_back_count = 0
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the listing failed:
|
||||||
|
if not api_response.success:
|
||||||
|
step_response.success = False
|
||||||
|
step_response.message = "Failed to enlist h/w interfaces during rollback."
|
||||||
|
step_response.exception = step_response.exception
|
||||||
|
return step_response
|
||||||
|
|
||||||
|
# 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():
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
interface_id = if_json.get(".id"),
|
||||||
|
port_no = mikrotik_auth.nasPort,
|
||||||
|
use_https = use_https,
|
||||||
|
name = original_config.get("name", self.HW_INTERFACE_NAME),
|
||||||
|
disabled = False,
|
||||||
|
comment = original_config.get("comment", "")
|
||||||
|
)
|
||||||
|
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)."
|
||||||
|
if rolled_back_count == total_count: step_response.success = True
|
||||||
|
|
||||||
|
# Done here:
|
||||||
|
return step_response
|
||||||
|
|
||||||
# ┏┓ ┓
|
# ┏┓ ┓
|
||||||
# ┣┫┓┏╋┣┓
|
# ┣┫┓┏╋┣┓
|
||||||
# ┛┗┗┻┗┛┗
|
# ┛┗┗┻┗┛┗
|
||||||
@@ -179,10 +336,10 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
|||||||
|
|
||||||
# print("IN-AUTH:", json.to_string(mikrotik_auth.model_dump(), default = str))
|
# print("IN-AUTH:", json.to_string(mikrotik_auth.model_dump(), default = str))
|
||||||
print("VLAN SUBNETS:", json.to_string(
|
print("VLAN SUBNETS:", json.to_string(
|
||||||
self.split_ipv4_range_among_vlans(
|
self.split_ipv4_range_among_targets(
|
||||||
start_ip = mikrotik_auth.firstPrivateIp,
|
start_ip = mikrotik_auth.firstPrivateIp,
|
||||||
end_ip = mikrotik_auth.lastPrivateIp,
|
end_ip = mikrotik_auth.lastPrivateIp,
|
||||||
vlan_ids = mikrotik_auth.vlanRange
|
targets = mikrotik_auth.vlanRange
|
||||||
),
|
),
|
||||||
default = str
|
default = str
|
||||||
))
|
))
|
||||||
@@ -190,24 +347,34 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
|||||||
# Start with a blank response:
|
# Start with a blank response:
|
||||||
auth_response = MikroTikAuthResponse()
|
auth_response = MikroTikAuthResponse()
|
||||||
|
|
||||||
# Try connecting to the server to check if the credentials are valid, or not:
|
# # Try connecting to the server to check if the credentials are valid, or not:
|
||||||
api_response = await self.get_system_resource(
|
# api_response = await self.get_system_resource(
|
||||||
nas_ip = mikrotik_auth.nasIp,
|
# mikrotik_ip = mikrotik_auth.nasIp,
|
||||||
port_no = mikrotik_auth.nasPort,
|
# port_no = mikrotik_auth.nasPort,
|
||||||
username = mikrotik_auth.username,
|
# username = mikrotik_auth.username,
|
||||||
password = mikrotik_auth.password,
|
# password = mikrotik_auth.password,
|
||||||
use_https = False
|
# use_https = False
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
|
# # If the connection attempt failed:
|
||||||
|
# if not api_response.success:
|
||||||
|
# auth_response.exception = api_response.exception
|
||||||
|
# if api_response.httpCode is None:
|
||||||
|
# auth_response.message = "Exception: " + api_response.exception.__class__.__name__
|
||||||
|
# if exception_str := str(auth_response.exception): auth_response.message += f" ({exception_str})"
|
||||||
|
# elif api_response.httpCode in [401]: auth_response.message = "Invalid credentials passed."
|
||||||
|
# elif api_response.httpCode in [502]: auth_response.message = "Could not ."
|
||||||
|
# else: auth_response.message = "Unknown error."
|
||||||
|
|
||||||
# If the connection attempt failed:
|
# # Try to configure the system:
|
||||||
if not api_response.success:
|
# config_response = await self.configure(mikrotik_auth)
|
||||||
auth_response.exception = api_response.exception
|
# print("CONFIG RESPONSE:", config_response)
|
||||||
if api_response.httpCode is None:
|
#
|
||||||
auth_response.message = "Exception: " + api_response.exception.__class__.__name__
|
# await asyncio.sleep(5.0)
|
||||||
if exception_str := str(auth_response.exception): auth_response.message += f" ({exception_str})"
|
|
||||||
elif api_response.httpCode in [401]: auth_response.message = "Invalid credentials passed."
|
# Try to roll all configuration back:
|
||||||
elif api_response.httpCode in [502]: auth_response.message = "Could not ."
|
roll_back_response = await self.roll_back(mikrotik_auth)
|
||||||
else: auth_response.message = "Unknown error."
|
print("ROLL-BACK RESPONSE:", roll_back_response)
|
||||||
|
|
||||||
# Done here:
|
# Done here:
|
||||||
return auth_response
|
return auth_response
|
||||||
@@ -229,7 +396,21 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
|||||||
:return: A structured response to indicate what happened during the configuration attempt.
|
:return: A structured response to indicate what happened during the configuration attempt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError
|
# Start with some variables:
|
||||||
|
keep_going = True
|
||||||
|
all_messages = []
|
||||||
|
roll_back_response = MikroTikConfigAttemptResponse()
|
||||||
|
|
||||||
|
# First, we arrange an interface:
|
||||||
|
if keep_going:
|
||||||
|
step_response = await self.interface_roll_back(mikrotik_auth, use_https = False)
|
||||||
|
all_messages.append(step_response.message)
|
||||||
|
keep_going = step_response.success
|
||||||
|
|
||||||
|
# Done here:
|
||||||
|
roll_back_response.success = keep_going
|
||||||
|
roll_back_response.message = " -> ".join(all_messages)
|
||||||
|
return roll_back_response
|
||||||
|
|
||||||
async def configure(
|
async def configure(
|
||||||
self,
|
self,
|
||||||
@@ -242,7 +423,21 @@ class MikroTikPPPoE1000Controller(MikroTikController):
|
|||||||
:return: A structured response to indicate what happened during the configuration attempt.
|
:return: A structured response to indicate what happened during the configuration attempt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError
|
# Start with some variables:
|
||||||
|
keep_going = True
|
||||||
|
all_messages = []
|
||||||
|
config_response = MikroTikConfigAttemptResponse()
|
||||||
|
|
||||||
|
# First, we arrange an interface:
|
||||||
|
if keep_going:
|
||||||
|
step_response = await self.interface_setup(mikrotik_auth, use_https = False)
|
||||||
|
all_messages.append(step_response.message)
|
||||||
|
keep_going = step_response.success
|
||||||
|
|
||||||
|
# Done here:
|
||||||
|
config_response.success = keep_going
|
||||||
|
config_response.message = " -> ".join(all_messages)
|
||||||
|
return config_response
|
||||||
|
|
||||||
|
|
||||||
# *****************************************************************************************************************
|
# *****************************************************************************************************************
|
||||||
|
|||||||
+176
-12
@@ -1,7 +1,7 @@
|
|||||||
# MiktoTik Configuration Steps
|
# MikroTik (PPPoE) Configuration Steps for 1,000 Clients
|
||||||
#### 20250210 / Khushal P S
|
#### 20250210 / Khushal P S
|
||||||
We need to configure MikroTik servers for the end use case of "PPPoE"Here are the steps to achieve it through MikroTik's
|
We need to configure MikroTik servers for the end use case of "PPPoE". Here are the steps to achieve it through
|
||||||
REST API facility. Use the `MiktoTik (EasyFi)` Postman collection for this.
|
MikroTik's REST API facility. Use the `MiktoTik (EasyFi)` Postman collection for this.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,6 +10,24 @@ REST API facility. Use the `MiktoTik (EasyFi)` Postman collection for this.
|
|||||||
MikroTik allows access via REST API using Basic-Auth headers which take a `username` and a `password`. The base path
|
MikroTik allows access via REST API using Basic-Auth headers which take a `username` and a `password`. The base path
|
||||||
would look something like `http://<ip-addr>/rest`. You may need to mention a port no. if the default has been changed.
|
would look something like `http://<ip-addr>/rest`. You may need to mention a port no. if the default has been changed.
|
||||||
|
|
||||||
|
You must also know what are "Private" and "Public" IP addresses in the context of an ISP. For an ISP, every IP that he
|
||||||
|
gives to his client is "Private" to his network, and every IP that he uses to connect his client to the broader internet
|
||||||
|
is a "Public" IP. Typically, many private IPs use the same public IP to connect to the internet. You will configure this
|
||||||
|
ratio in the NAT-ing step.
|
||||||
|
|
||||||
|
### Broad Steps
|
||||||
|
|
||||||
|
- We first select a physical interface.
|
||||||
|
- Then we create all the needed VLANs and point them to the physical interface. Let's say this no. is 'n'.
|
||||||
|
- Then we cut up the full private IP block into 'n' pools.
|
||||||
|
- Then we create 'n' no. of PPPoE profiles such that each profile gets one private IP pool.
|
||||||
|
- Then we create 'n' no. of PPPoE servers such that each server gets one IP pool (through a profile), and one VLAN interface.
|
||||||
|
- Set up AAA such that we use RADIUS.
|
||||||
|
- Then we add the given RADIUS server's config.
|
||||||
|
- Then we allow incoming traffic from RADIUS servers.
|
||||||
|
- Then we populate the NAT table with rules.
|
||||||
|
- Then we add a new SNMP community.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Steps for PPPoE
|
## Steps for PPPoE
|
||||||
@@ -30,43 +48,189 @@ is an actual physical connectivity interface (typically ethernet) on the MikroTi
|
|||||||
You will be given a range of VLAN interfaces like `"2001-2010"`. Create on VLAN for each id such that the value of the
|
You will be given a range of VLAN interfaces like `"2001-2010"`. Create on VLAN for each id such that the value of the
|
||||||
name of the `"interface"` that you set for it is set to `"easyfi-pppoe"` (the name of the physical interface).
|
name of the `"interface"` that you set for it is set to `"easyfi-pppoe"` (the name of the physical interface).
|
||||||
|
|
||||||
Use `PUT` method on the path `/interface/vlan`. Consider to the following example JSON:
|
Use `PUT` or `PATCH` method on the path `/interface/vlan`. Consider to the following example JSON:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "easyfi-vlan-2001",
|
"name": "easyfi-vlan-2001", // .... Use the VLAN id in the name.
|
||||||
"interface": "easyfi-pppoe",
|
"interface": "easyfi-pppoe", // ... The h/w interface that you prepared in the prev. step.
|
||||||
"vlan-id": "2001",
|
"vlan-id": "2001", // ............. The VLAN id.
|
||||||
"disabled": "false",
|
"disabled": "false", // ........... To ensure it is enabled by default.
|
||||||
"comment": "easyfi"
|
"comment": "{...}" // ............. The JSON string to indicate automated config.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**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:** Save the original configuration as a JSON string in the `comment` field. If there was no original
|
||||||
configuration, simply delete your record.
|
configuration, simply delete your record.
|
||||||
|
|
||||||
### 3. Assign Private IP Subnets to the VLANs
|
### 3. Create Private IP Subnets (a.k.a. Pools/Ranges)
|
||||||
|
|
||||||
You will receive the Private IP range in either CIDR notation or as a hyphen-separated string.
|
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).
|
||||||
|
|
||||||
|
Use `PUT` or `PATCH` method on the path `/ip/pool`. Consider to the following example JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "easyfi-pppoe-pool-2001", // ... Use the VLAN id in the name.
|
||||||
|
"ranges": "100.64.0.0/24", // .......... The subnet for the associated VLAN id.
|
||||||
|
"comment": "{...}" // .................. The JSON string to indicate automated config.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### 4. Create PPPoE Profiles
|
||||||
|
|
||||||
|
Now we are ready to create PPPoE profiles. A profile is like a blueprint for the servers we will create later. It
|
||||||
|
defines the rules that the server must follow. For now, our major focus is on telling the server which private IP pool
|
||||||
|
to use. We create the same no. of PPPoE profiles as we have created VLANs.
|
||||||
|
|
||||||
|
Use `PUT` or `PATCH` method on the path `/ppp/profile`. Consider to the following example JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "easyfi-pppoe-prf-2001", // .............. Use the VLAN id in the name.
|
||||||
|
"local-address": "192.168.10.1", // .............. The IP address of the MikroTik device itself (NAS IP).
|
||||||
|
"remote-address": "easyfi-pppoe-pool-2001", // ... Use the VLAN id in the name.
|
||||||
|
"comment": "{...}" // ............................ The JSON string to indicate automated config.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### 5. Create PPPoE Servers
|
||||||
|
|
||||||
|
Here's where we tie up ALL the steps we've done so far!
|
||||||
|
|
||||||
|
In this step we spin up actual server instances (software) that handle the PPPoE traffic. On one hand we have virtual
|
||||||
|
sub-blocks of our private IP pool, and on the other we have VLANs that point to the physical interfaces. Here we spin
|
||||||
|
up servers that use one VLAN and one private IP sub-pool to actually handle the traffic.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"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.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### 6. Set-up AAA to use RADIUS
|
||||||
|
|
||||||
|
Now we need to tell the MikroTik device to use a RADIUS server for AAA.
|
||||||
|
|
||||||
|
Use `POST` method on the path `/ppp/aaa/set`. Use the following JSON as is:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accounting": "true",
|
||||||
|
"interim-update": "3m",
|
||||||
|
"use-circuit-id-in-nas-port-id": "false",
|
||||||
|
"use-radius": "true"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ROLL-BACK:** Not known.
|
||||||
|
|
||||||
|
### 7. Set up the given RADIUS Server
|
||||||
|
|
||||||
|
We configure the MikroTik to use the given Radius Server for its AAA activities.
|
||||||
|
|
||||||
|
Use `PUT` or `PATCH` method on the path `/radius`. Consider to the following example JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "easyfi-radius", // ............... Use this value.
|
||||||
|
"address": "<radius_server_ip>", // ....... The IP address of the RADIUS server.
|
||||||
|
"secret": "<radius_server_secret>", // .... The "password" of the RADIUS server.
|
||||||
|
"accounting-port": "1813", // ............. Port of the RADIUS server.
|
||||||
|
"authentication-port": "1812", // ......... Port of the RADIUS server.
|
||||||
|
"disabled": "false", // ................... Use this value.
|
||||||
|
"protocol": "udp", // ..................... Use this value.
|
||||||
|
"service": "ppp,login,hotspot,dhcp", // ... Use this value.
|
||||||
|
"timeout": "300ms", // .................... Use this value.
|
||||||
|
"comment": "{...}" // ..................... The JSON string to indicate automated config.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ROLL-BACK:** Enlist the RADIUS servers in the system and remove the one with your name (`easyfi-radius`) in it. You
|
||||||
|
may also use some other identifier like the IP address of the RADIUS server or the contents of the `comment` field.
|
||||||
|
|
||||||
|
### 8. Allow Incoming Traffic from RADIUS Servers
|
||||||
|
|
||||||
|
In some cases the RADIUS server needs to be able to initiate the communication. We allow that in this step.
|
||||||
|
|
||||||
|
Use `POST` method on the path `/radius/incoming/set`. Consider to the following example JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accept": "yes", // ... Enable incoming RADIUS handling
|
||||||
|
"port": "3799" // .... Port of the MikroTik server for RADIUS authentication.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ROLL-BACK:** Not known.
|
||||||
|
|
||||||
|
### 9. NAT Table Setup
|
||||||
|
|
||||||
|
We now add IP mapping rules to the NAT table. Any ISP is expected to have more private IPs than public IPs. The ISP will
|
||||||
|
assign private IPs to his clients such that multiple private IPs will use the same public IP to connect to the internet.
|
||||||
|
In this step we are effectively assigning one slice of the ISP's private IP to one of his public IPs. A known good
|
||||||
|
sharing ratio is 16:1, but we split it equally.
|
||||||
|
|
||||||
|
In the following example, all the traffic from all the IPs in the `src-address` will be sent out to the open internet as
|
||||||
|
if it were being sent from the IP mentioned in the `to-addresses`.
|
||||||
|
|
||||||
|
Use `PUT` method on the path `/ip/firewall/nat`. Consider to the following example JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "easyfi-pppoe-nat-0000", // ..... A name to later identify the rules created by the automated script.
|
||||||
|
"action": "src-nat", // ................. To indicate that we are translating a private IP to a public IP.
|
||||||
|
"chain": "srcnat", // ................... To indicate that we are translating a private IP to a public IP.
|
||||||
|
"disabled": "false", // ................. To enable the rule immediately.
|
||||||
|
"src-address": "100.64.0.16/28", // ..... One of your private IP subnets.
|
||||||
|
"to-addresses": "111.222.111.111", // ... One of your public IPs.
|
||||||
|
"comment": "{...}" // ................... The JSON string to indicate automated config.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ROLL-BACK:** Identify entries from their `name` field (contains 'easyfi') or from the contents of the `comment` field
|
||||||
|
and remove them.
|
||||||
|
|
||||||
|
### 10. Add the SNMP Community
|
||||||
|
|
||||||
|
This is needed by the Network Management System (NMS). I don't know much about it, but it is a very straight-forward
|
||||||
|
step.
|
||||||
|
|
||||||
|
Use `PUT` or `PATCH` method on the path `/snmp/community`. Consider to the following example JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "easyfi-snmp", // .............. A name to later identify the entry created by the automated script.
|
||||||
|
"addresses": "::/0", // ................ Use this value.
|
||||||
|
"authentication-password": "", // ...... Use this value.
|
||||||
|
"authentication-protocol": "MD5", // ... Use this value.
|
||||||
|
"disabled": "false", // ................ Use this value.
|
||||||
|
"encryption-password": "", // .......... Use this value.
|
||||||
|
"encryption-protocol": "DES", // ....... Use this value.
|
||||||
|
"read-access": "true", // .............. Use this value.
|
||||||
|
"security": "none", // ................. Use this value.
|
||||||
|
"write-access": "false", // ............ Use this value.
|
||||||
|
"comment": "{...}" // .................. The JSON string to indicate automated config.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ROLL-BACK:** Identify your entry from the `name` field (contains 'easyfi') or from the contents of the `comment` field
|
||||||
|
and remove it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*End of Document.*
|
||||||
Reference in New Issue
Block a user