Skip to content

Commit d1c70e6

Browse files
Martin RevajJozef Volak
Martin Revaj
authored and
Jozef Volak
committed
workers added
- refactored code - bugfixes - validation improvements
1 parent 9d67c1c commit d1c70e6

File tree

7 files changed

+1016
-3
lines changed

7 files changed

+1016
-3
lines changed

resource-manager/python/frinx_worker/resource_manager/__init__.py

+780
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import os
2+
3+
RESOURCE_MANAGER_URL_BASE = os.getenv('RESOURCE_MANAGER_URL_BASE', 'http://localhost/api/resource')
4+
HELPDESK_URL = os.getenv('HELPDESK_URL', 'https://frinxhelpdesk.atlassian.net/servicedesk/customer/portals')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from frinx_api.resource_manager import ID
2+
3+
from frinx_worker.resource_manager.env import HELPDESK_URL
4+
5+
6+
class ResourceManagerWorkerError(Exception):
7+
"""Exception class for resource-manager workers."""
8+
9+
10+
class PoolNotFoundError(ResourceManagerWorkerError, KeyError):
11+
"""KeyError"""
12+
def __init__(self, pool_id: ID, *args):
13+
super().__init__(f'Pool with ID "{pool_id}" was not founded.', *args)
14+
15+
16+
class ResourceNotFoundError(ResourceManagerWorkerError, KeyError):
17+
"""KeyError"""
18+
def __init__(self, resource: str, *args):
19+
super().__init__(f'Resource "{resource}" was not founded.', *args)
20+
21+
22+
class ResourceManagerWorkerRuntimeError(ResourceManagerWorkerError, RuntimeError):
23+
"""RuntimeError"""
24+
def __init__(self):
25+
super().__init__(f'{self.__class__.__name__}, please report this issue on {HELPDESK_URL}. Thank you!')
26+
27+
28+
class InvalidQueryError(ResourceManagerWorkerRuntimeError):
29+
"""422 Client Error: Unprocessable Entity for url: ..."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from enum import Enum
2+
from typing import Any
3+
from typing import Optional
4+
5+
from frinx.common.worker.task_def import TaskInput
6+
from pydantic import BaseModel
7+
from pydantic import ConfigDict
8+
from pydantic import Field
9+
from pydantic import IPvAnyAddress
10+
from pydantic import NonNegativeInt
11+
from pydantic import RootModel
12+
from pydantic.alias_generators import to_camel
13+
14+
from frinx_worker.resource_manager.type_aliases import IPv4PrefixTypeAlias
15+
16+
Data = RootModel[Optional[Any]]
17+
18+
19+
class Result(BaseModel):
20+
data: Optional[Any]
21+
22+
23+
class CustomConfiguredTaskInput(TaskInput):
24+
model_config = ConfigDict(alias_generator=to_camel)
25+
26+
27+
# FIXME: refactor, each pool has own properties, this one is probably for ipv pools
28+
class PoolProperties(BaseModel):
29+
address: IPvAnyAddress
30+
prefix: int
31+
subnet: Optional[bool] = False
32+
33+
34+
class IPv4NetMaskSize(RootModel):
35+
root: NonNegativeInt = Field(le=2**32)
36+
37+
38+
class Report(RootModel):
39+
root: dict[IPv4PrefixTypeAlias, IPv4NetMaskSize]
40+
41+
42+
class ResourceTypeEnum(Enum):
43+
RANDOM_SIGNED_INT32 = 'random_signed_int32'
44+
ROUTE_DISTINGUISHER = 'route_distinguisher'
45+
IPV6_PREFIX = 'ipv6_prefix'
46+
IPV4_PREFIX = 'ipv4_prefix'
47+
VLAN_RANGE = 'vlan_range'
48+
UNIQUE_ID = 'unique_id'
49+
IPV6 = 'ipv6'
50+
IPV4 = 'ipv4'
51+
VLAN = 'vlan'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from ipaddress import IPv4Address
2+
from ipaddress import IPv6Address
3+
from typing import Literal
4+
from typing import Optional
5+
from typing import TypeAlias
6+
from typing import Union
7+
8+
from graphql_pydantic_converter.graphql_types import Mutation
9+
from graphql_pydantic_converter.graphql_types import Query
10+
from pydantic.v1.errors import IPv4AddressError
11+
from pydantic.v1.errors import IPv6AddressError
12+
13+
GraphQueryRenderer: TypeAlias = Union[Query, Mutation]
14+
IPvAddress: TypeAlias = Union[IPv4Address, IPv6Address]
15+
IPAddressError: TypeAlias = Union[IPv4AddressError, IPv6AddressError]
16+
IPAddressDict: TypeAlias = dict[str, IPvAddress]
17+
OptionalIPAddressDict: TypeAlias = dict[str, Optional[IPvAddress]]
18+
ISODateTimeString: TypeAlias = str # not validated datetime (string) format YYYY-MM-DD-hh
19+
CursorID: TypeAlias = str # not validated cursor ID, probably (16 alphabet string)
20+
# TODO: define prefixes the "smarter way"
21+
IPv4PrefixTypeAlias: TypeAlias = Literal[
22+
'/0', '/1', '/2', '/3', '/4', '/5', '/6', '/7', '/8', '/9', '/10', '/11', '/12', '/13', '/14', '/15', '/16',
23+
'/17','/18', '/19', '/20', '/21', '/22', '/23', '/24', '/25', '/26', '/27', '/28', '/29', '/30', '/31', '/32'
24+
]
25+
IPv6PrefixTypeAlias: TypeAlias = Literal[
26+
'/0', '/1', '/2', '/3', '/4', '/5', '/6', '/7', '/8', '/9', '/10', '/11', '/12', '/13', '/14', '/15', '/16',
27+
'/17', '/18', '/19', '/20', '/21', '/22', '/23', '/24', '/25', '/26', '/27', '/28', '/29', '/30', '/31',
28+
'/32', '/33', '/34', '/35', '/36', '/37', '/38', '/39', '/40', '/41', '/42', '/43', '/44', '/45', '/46',
29+
'/47', '/48', '/49', '/50', '/51', '/52', '/53', '/54', '/55', '/56', '/57', '/58', '/59', '/60', '/61',
30+
'/62', '/63', '/64', '/65', '/66', '/67', '/68', '/69', '/70', '/71', '/72', '/73', '/74', '/75', '/76',
31+
'/77', '/78', '/79', '/80', '/81', '/82', '/83', '/84', '/85', '/86', '/87', '/88', '/89', '/90', '/91',
32+
'/92', '/93', '/94', '/95', '/96', '/97', '/98', '/99', '/100', '/101', '/102', '/103', '/104', '/105',
33+
'/106', '/107', '/108', '/109', '/110', '/111', '/112', '/113', '/114', '/115', '/116', '/117', '/118',
34+
'/119', '/120', '/121', '/122', '/123', '/124', '/125', '/126', '/127', '/128'
35+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from collections import namedtuple
2+
from collections.abc import Generator
3+
from contextlib import contextmanager
4+
from http import HTTPStatus
5+
from operator import itemgetter
6+
from typing import Any
7+
from typing import Literal
8+
from typing import Optional
9+
from typing import Union
10+
11+
from frinx.common.conductor_enums import TaskResultStatus
12+
from frinx.common.type_aliases import DictAny
13+
from frinx.common.type_aliases import DictStr
14+
from frinx.common.worker.task_def import TaskOutput
15+
from frinx.common.worker.task_result import TaskResult
16+
from frinx_api.resource_manager import ID
17+
from frinx_api.resource_manager import AllocationStrategy
18+
from frinx_api.resource_manager import PoolCapacityPayload
19+
from frinx_api.resource_manager import QueryAllocationStrategiesQuery
20+
from frinx_api.resource_manager import QueryAllocationStrategiesQueryResponse
21+
from frinx_api.resource_manager import QueryPoolCapacityQuery
22+
from frinx_api.resource_manager import QueryPoolCapacityQueryResponse
23+
from frinx_api.resource_manager import QueryResourceTypesQuery
24+
from frinx_api.resource_manager import QueryResourceTypesQueryResponse
25+
from frinx_api.resource_manager import ResourceType
26+
from graphql_pydantic_converter.graphql_types import concatenate_queries
27+
from pydantic import IPvAnyAddress
28+
from python_graphql_client import GraphqlClient
29+
from requests.exceptions import HTTPError
30+
31+
from frinx_worker.resource_manager.env import RESOURCE_MANAGER_URL_BASE
32+
from frinx_worker.resource_manager.errors import InvalidQueryError
33+
from frinx_worker.resource_manager.errors import PoolNotFoundError
34+
from frinx_worker.resource_manager.models_and_enums import ResourceTypeEnum
35+
from frinx_worker.resource_manager.type_aliases import GraphQueryRenderer
36+
from frinx_worker.resource_manager.type_aliases import IPvAddress
37+
38+
39+
@contextmanager
40+
def qraphql_client_manager(endpoint: str, headers: Optional[DictStr] = None,
41+
**kwargs: Any) -> Generator[GraphqlClient, None, None]:
42+
client = GraphqlClient(endpoint=endpoint, headers=headers or {}, **kwargs)
43+
try:
44+
yield client
45+
except HTTPError as error:
46+
match error.response.status_code:
47+
case HTTPStatus.UNPROCESSABLE_ENTITY:
48+
raise InvalidQueryError()
49+
# TODO: Enhance the error handling.
50+
case _:
51+
raise error
52+
53+
54+
def execute_query(graph_query: Union[GraphQueryRenderer, str], variables: Optional[DictAny] = None,
55+
operation_name: Optional[str] = None, headers: Optional[DictStr] = None, **kwargs) -> DictAny:
56+
with qraphql_client_manager(endpoint=RESOURCE_MANAGER_URL_BASE) as client:
57+
return client.execute(
58+
query=graph_query if isinstance(graph_query, str) else graph_query.render(),
59+
variables=variables or {},
60+
operation_name=operation_name,
61+
headers=headers or {},
62+
**kwargs)
63+
64+
65+
def get_resource_type_and_allocation_strategy_id(resource_type: ResourceTypeEnum) -> tuple[ID, ID]:
66+
query_resource = QueryResourceTypesQuery(byName=resource_type, payload=ResourceType(id=True))
67+
query_allocation = QueryAllocationStrategiesQuery(byName=resource_type, payload=AllocationStrategy(id=True))
68+
data = execute_query(concatenate_queries([query_resource, query_allocation]))
69+
resource, allocation = QueryResourceTypesQueryResponse(**data), QueryAllocationStrategiesQueryResponse(**data)
70+
return resource.data.query_resource_types[0].id, allocation.data.query_allocation_strategies[0].id
71+
72+
73+
def get_free_and_utilized_capacity_of_pool(pool_id: ID) -> Optional[tuple[int, int]]:
74+
query = QueryPoolCapacityQuery(
75+
poolId=pool_id, payload=PoolCapacityPayload(freeCapacity=True, utilizedCapacity=True))
76+
response_model = QueryPoolCapacityQueryResponse(**execute_query(query))
77+
try:
78+
return (int(response_model.data.query_pool_capacity.free_capacity),
79+
int(response_model.data.query_pool_capacity.utilized_capacity))
80+
except AttributeError:
81+
raise PoolNotFoundError(pool_id)
82+
83+
84+
def get_max_prefix_len(ipv: Literal[ResourceTypeEnum.IPV4, ResourceTypeEnum.IPV6]) -> int:
85+
return 128 if ipv is ResourceTypeEnum.IPV6 else 32
86+
87+
88+
def calculate_available_prefixes(free_capacity: int,
89+
ip_version: Literal[ResourceTypeEnum.IPV4, ResourceTypeEnum.IPV6]) -> DictStr:
90+
calculated_prefixes = {}
91+
max_bit_size = get_max_prefix_len(ip_version)
92+
93+
for prefix in range(1, max_bit_size + 1):
94+
prefix_capacity = pow(2, max_bit_size - prefix)
95+
if prefix_capacity <= free_capacity:
96+
result = free_capacity // prefix_capacity
97+
calculated_prefixes[f'/{prefix}'] = str(result)
98+
99+
return calculated_prefixes
100+
101+
102+
IPAddressOwner = namedtuple('IPAddressOwner', ['owner', 'ip_address'])
103+
104+
105+
def sorted_owners_by_ip_addresses(*, reverse: bool = False,
106+
**ip_addresses: Optional[Union[IPvAddress, IPvAnyAddress]]) -> list[IPAddressOwner]:
107+
return [IPAddressOwner(*_) for _ in sorted(ip_addresses.items(), key=itemgetter(1), reverse=reverse)]
108+
109+
110+
def execute_graph_query__return_task_result(task_output: type[TaskOutput],
111+
graph_query: Union[GraphQueryRenderer, str]) -> TaskResult:
112+
result = execute_query(graph_query)
113+
status = TaskResultStatus.FAILED if 'errors' in result else TaskResultStatus.COMPLETED
114+
return TaskResult(status=status, output=task_output(result=result))

resource-manager/python/poetry.lock

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)