Skip to content
Snippets Groups Projects
Commit 4037dc1d authored by geant-release-service's avatar geant-release-service
Browse files

Finished release 2.11.

parents bbf10779 c5cc6710
No related branches found
No related tags found
No related merge requests found
Pipeline #88546 passed
# Changelog
All notable changes to this project will be documented in this file.
## [2.11] - 2024-08-19
- (fix) Make LibreNMS retry when a request times out
- (fix) Adjust the mechanics of the minimum amount of links when creating an IP trunk
## [2.10] - 2024-08-06
- Update map API endpoint
......
......@@ -3,10 +3,11 @@
import logging
from http import HTTPStatus
from importlib import metadata
from typing import Any
from typing import Any, Literal
import requests
from requests import HTTPError
from requests import HTTPError, Response
from requests.adapters import HTTPAdapter
from gso.settings import load_oss_params
from gso.utils.helpers import SNMPVersion
......@@ -25,12 +26,24 @@ class LibreNMSClient:
self.base_url = config.LIBRENMS.base_url
self.snmp_config = config.SNMP
self.headers = {
self.session = requests.Session()
self.session.mount("https://", HTTPAdapter(max_retries=5))
self.session.headers.update({
"User-Agent": f"geant-service-orchestrator/{metadata.version("geant-service-orchestrator")}",
"Accept": "application/json",
"Content-Type": "application/json",
"X-Auth-Token": token,
}
})
def _send_request(
self, method: Literal["GET", "POST", "PUT", "DELETE"], endpoint: str, data: dict[str, Any] | None = None
) -> Response:
url = self.base_url + endpoint
logger.debug("LibreNMS - Sending request", extra={"method": method, "endpoint": url, "form_data": data})
result = self.session.request(method, url, json=data, timeout=(0.5, 75))
logger.debug("LibreNMS - Received response", extra=result.__dict__)
return result
def get_device(self, fqdn: str) -> dict[str, Any]:
"""Get an existing device from LibreNMS.
......@@ -39,7 +52,7 @@ class LibreNMSClient:
:return dict[str, Any]: A :term:`JSON` formatted list of devices that match the queried :term:`FQDN`.
:raises HTTPError: Raises an HTTP error 404 when the device is not found
"""
response = requests.get(f"{self.base_url}/devices/{fqdn}", headers=self.headers, timeout=(0.5, 75))
response = self._send_request("GET", f"/devices/{fqdn}")
response.raise_for_status()
return response.json()
......@@ -74,7 +87,7 @@ class LibreNMSClient:
}
device_data.update(getattr(self.snmp_config, snmp_version))
device = requests.post(f"{self.base_url}/devices", headers=self.headers, json=device_data, timeout=(0.5, 75))
device = self._send_request("POST", "/devices", device_data)
device.raise_for_status()
return device.json()
......@@ -86,7 +99,7 @@ class LibreNMSClient:
:return dict[str, Any]: A JSON representation of the device that got removed.
:raises HTTPError: Raises an exception if the request did not succeed.
"""
device = requests.delete(f"{self.base_url}/devices/{fqdn}", headers=self.headers, timeout=(0.5, 75))
device = self._send_request("DELETE", f"/devices/{fqdn}")
device.raise_for_status()
return device.json()
......
......@@ -12,7 +12,7 @@ from pydantic_core.core_schema import ValidationInfo
from pydantic_forms.validators import Choice
from gso import settings
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock
from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, PhysicalPortCapacity
from gso.products.product_blocks.router import RouterRole
from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate, SiteTier
from gso.products.product_types.router import Router
......@@ -332,3 +332,18 @@ def generate_inventory_for_active_routers(
}
}
}
def calculate_recommended_minimum_links(iptrunk_number_of_members: int, iptrunk_speed: PhysicalPortCapacity) -> int:
"""Calculate the recommended minimum number of links for an IP trunk based on the number of members and speed.
If the IP trunk speed is 400G, the recommended minimum number of links is the number of members minus 1.
Otherwise, the recommended minimum number of links is the number of members.
:param int iptrunk_number_of_members: The number of members in the IP trunk.
:param PhysicalPortCapacity iptrunk_speed: The speed of the IP trunk.
:return: The recommended minimum number of links for the IP trunk.
"""
if iptrunk_speed == PhysicalPortCapacity.FOUR_HUNDRED_GIGABIT_PER_SECOND:
return iptrunk_number_of_members - 1
return iptrunk_number_of_members
......@@ -35,6 +35,7 @@ from gso.utils.helpers import (
LAGMember,
available_interfaces_choices,
available_lags_choices,
calculate_recommended_minimum_links,
get_router_vendor,
validate_interface_name_list,
validate_iptrunk_unique_interface,
......@@ -70,12 +71,13 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
return validate_tt_number(tt_number)
initial_user_input = yield CreateIptrunkForm
recommended_minimum_links = calculate_recommended_minimum_links(
initial_user_input.iptrunk_number_of_members, initial_user_input.iptrunk_speed
)
class VerifyMinimumLinksForm(FormPage):
info_label: Label = (
f"This is the calculated minimum-links for this LAG: " f"{initial_user_input.iptrunk_number_of_members - 1}"
)
iptrunk_minimum_links: int = initial_user_input.iptrunk_number_of_members - 1
info_label: Label = f"This is the calculated minimum-links for this LAG: {recommended_minimum_links}"
iptrunk_minimum_links: int = recommended_minimum_links
info_label2: Label = "Please confirm or modify."
verify_minimum_links = yield VerifyMinimumLinksForm
......
......@@ -28,6 +28,7 @@ from gso.utils.helpers import (
LAGMember,
available_interfaces_choices,
available_interfaces_choices_including_current_members,
calculate_recommended_minimum_links,
get_router_vendor,
validate_interface_name_list,
validate_iptrunk_unique_interface,
......@@ -93,7 +94,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
"You will need to add the new AE members in the next steps."
)
iptrunk_speed: PhysicalPortCapacity = subscription.iptrunk.iptrunk_speed
iptrunk_number_of_members: int = subscription.iptrunk.iptrunk_minimum_links + 1
iptrunk_number_of_members: int = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members)
iptrunk_isis_metric: ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric, default_type=int) # type: ignore[valid-type]
iptrunk_ipv4_network: ReadOnlyField( # type: ignore[valid-type]
str(subscription.iptrunk.iptrunk_ipv4_network), default_type=IPv4AddressType
......@@ -108,12 +109,15 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
initial_user_input = yield ModifyIptrunkForm
recommended_minimum_links = calculate_recommended_minimum_links(
initial_user_input.iptrunk_number_of_members, initial_user_input.iptrunk_speed
)
class VerifyMinimumLinksForm(FormPage):
info_label: Label = (
f"This is the calculated minimum-links for this LAG: " f"{initial_user_input.iptrunk_number_of_members - 1}"
)
iptrunk_minimum_links: int = initial_user_input.iptrunk_number_of_members - 1
info_label2: Label = "Please confirm or modify."
info_label: Label = f"Current value of minimum-links : {subscription.iptrunk.iptrunk_minimum_links}"
info_label1: Label = f"Recommended minimum-links for this LAG: {recommended_minimum_links}"
iptrunk_minimum_links: int = recommended_minimum_links
info_label2: Label = "Please review the recommended value and adjust if necessary."
verify_minimum_links = yield VerifyMinimumLinksForm
ae_members_side_a = initialize_ae_members(subscription, initial_user_input.model_dump(), 0)
......
......@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
setup(
name="geant-service-orchestrator",
version="2.10",
version="2.11",
author="GÉANT Orchestration and Automation Team",
author_email="goat@geant.org",
description="GÉANT Service Orchestrator",
......
......@@ -10,7 +10,7 @@ from gso.utils.helpers import SNMPVersion
@pytest.fixture()
def mock_get_device_success(faker):
with patch("gso.services.librenms_client.requests.get") as mock_get_device:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_get_device:
mock_get_device().status_code = HTTPStatus.OK
mock_get_device().json.return_value = {
"status": "ok",
......@@ -81,14 +81,14 @@ def mock_get_device_success(faker):
@pytest.fixture()
def mock_get_device_not_found():
with patch("gso.services.librenms_client.requests.get") as mock_get_not_found:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_get_not_found:
mock_get_not_found().status_code = HTTPStatus.NOT_FOUND
mock_get_not_found().json.return_value = {
"status": "error",
"message": "Device non-existent-url does not exist",
}
mock_get_not_found().raise_for_status.side_effect = HTTPError(
"404 Client Error: Not Found for url: http://librenms/devices/non-existent-url",
"404 Client Error: Not Found for url: https://librenms/devices/non-existent-url",
response=mock_get_not_found(),
)
......@@ -97,7 +97,7 @@ def mock_get_device_not_found():
@pytest.fixture()
def mock_get_device_misconfigured(faker):
with patch("gso.services.librenms_client.requests.get") as mock_get_device:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_get_device:
mock_get_device().status_code = HTTPStatus.OK
mock_get_device().json.return_value = {
"status": "ok",
......@@ -169,7 +169,7 @@ def mock_get_device_misconfigured(faker):
@pytest.fixture()
def mock_get_device_unauthenticated():
with (
patch("gso.services.librenms_client.requests.get") as mock_get_unauthorized,
patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_get_unauthorized,
patch(
"gso.services.librenms_client.LibreNMSClient.get_device",
) as mock_get_device,
......@@ -177,7 +177,7 @@ def mock_get_device_unauthenticated():
mock_get_unauthorized().status_code = HTTPStatus.UNAUTHORIZED
mock_get_unauthorized().json.return_value = {"message": "Unauthenticated."}
mock_get_device.side_effect = HTTPError(
"401 Client Error: Unauthorized for url: http://librenms/devices/naughty-url",
"401 Client Error: Unauthorized for url: https://librenms/devices/naughty-url",
response=mock_get_unauthorized(),
)
......@@ -186,7 +186,7 @@ def mock_get_device_unauthenticated():
@pytest.fixture()
def mock_add_device_success():
with patch("gso.services.librenms_client.requests.post") as mock_post_device:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_post_device:
mock_post_device().status_code = HTTPStatus.OK
mock_post_device().json.return_value = {
"status": "ok",
......@@ -222,14 +222,14 @@ def mock_add_device_success():
@pytest.fixture()
def mock_add_device_bad_url():
with patch("gso.services.librenms_client.requests.post") as mock_post_device:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_post_device:
mock_post_device().status_code = HTTPStatus.INTERNAL_SERVER_ERROR
mock_post_device().json.return_value = {
"status": "error",
"message": "Could not ping non-existent-url (Hostname did not resolve to IP)",
}
mock_post_device().raise_for_status.side_effect = HTTPError(
"500 Server Error: Internal server error for url: http://librenms/devices",
"500 Server Error: Internal server error for url: https://librenms/devices",
response=mock_post_device(),
)
......@@ -238,14 +238,14 @@ def mock_add_device_bad_url():
@pytest.fixture()
def mock_add_device_unreachable():
with patch("gso.services.librenms_client.requests.post") as mock_post_device:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_post_device:
mock_post_device().status_code = HTTPStatus.INTERNAL_SERVER_ERROR
mock_post_device().json.return_value = {
"status": "error",
"message": "Could not connect to non-existent-url, please check the snmp details and snmp reachability",
}
mock_post_device().raise_for_status.side_effect = HTTPError(
"500 Server Error: Internal server error for url: http://librenms/devices",
"500 Server Error: Internal server error for url: https://librenms/devices",
response=mock_post_device(),
)
......@@ -254,7 +254,7 @@ def mock_add_device_unreachable():
@pytest.fixture()
def mock_remove_device_success(faker):
with patch("gso.services.librenms_client.requests.delete") as mock_remove_device:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_remove_device:
mock_remove_device().status_code = HTTPStatus.OK
mock_remove_device().json.return_value = {
"status": "ok",
......@@ -326,11 +326,11 @@ def mock_remove_device_success(faker):
@pytest.fixture()
def mock_remove_device_non_existent(faker):
with patch("gso.services.librenms_client.requests.delete") as mock_remove_device:
with patch("gso.services.librenms_client.LibreNMSClient._send_request") as mock_remove_device:
mock_remove_device().status_code = HTTPStatus.NOT_FOUND
mock_remove_device().json.return_value = {"status": "error", "message": "Device non-existent-url not found"}
mock_remove_device().raise_for_status.side_effect = HTTPError(
"404 Client Error: Not Found for url: http://librenms/devices/non-existent-url",
"404 Client Error: Not Found for url: https://librenms/devices/non-existent-url",
response=mock_remove_device(),
)
......@@ -353,7 +353,7 @@ def test_get_device_not_found(mock_get_device_not_found):
assert e.value.response.status_code == HTTPStatus.NOT_FOUND
assert e.value.response.json() == {"status": "error", "message": "Device non-existent-url does not exist"}
assert e.value.args[0] == "404 Client Error: Not Found for url: http://librenms/devices/non-existent-url"
assert e.value.args[0] == "404 Client Error: Not Found for url: https://librenms/devices/non-existent-url"
def test_device_exists_true(mock_get_device_success):
......@@ -376,7 +376,7 @@ def test_device_exists_bad_request(mock_get_device_unauthenticated):
assert e.value.response.status_code == HTTPStatus.UNAUTHORIZED
assert e.value.response.json() == {"message": "Unauthenticated."}
assert e.value.args[0] == "401 Client Error: Unauthorized for url: http://librenms/devices/naughty-url"
assert e.value.args[0] == "401 Client Error: Unauthorized for url: https://librenms/devices/naughty-url"
def test_add_device_success(mock_add_device_success):
......@@ -401,7 +401,7 @@ def test_add_device_bad_fqdn(mock_add_device_bad_url):
"status": "error",
"message": "Could not ping non-existent-url (Hostname did not resolve to IP)",
}
assert e.value.args[0] == "500 Server Error: Internal server error for url: http://librenms/devices"
assert e.value.args[0] == "500 Server Error: Internal server error for url: https://librenms/devices"
def test_add_device_no_ping(mock_add_device_unreachable):
......@@ -416,7 +416,7 @@ def test_add_device_no_ping(mock_add_device_unreachable):
"status": "error",
"message": "Could not connect to non-existent-url, please check the snmp details and snmp reachability",
}
assert e.value.args[0] == "500 Server Error: Internal server error for url: http://librenms/devices"
assert e.value.args[0] == "500 Server Error: Internal server error for url: https://librenms/devices"
def test_remove_device_success(mock_remove_device_success):
......@@ -435,7 +435,7 @@ def test_remove_non_existent_device(mock_remove_device_non_existent):
assert e.value.response.status_code == HTTPStatus.NOT_FOUND
assert e.value.response.json() == {"status": "error", "message": "Device non-existent-url not found"}
assert e.value.args[0] == "404 Client Error: Not Found for url: http://librenms/devices/non-existent-url"
assert e.value.args[0] == "404 Client Error: Not Found for url: https://librenms/devices/non-existent-url"
def test_validate_device_success(mock_get_device_success):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment