Skip to content

Commit

Permalink
SIMPLE-6883 Support for smart annotations (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-valent authored Sep 18, 2024
1 parent 290599e commit 1936abf
Show file tree
Hide file tree
Showing 10 changed files with 860 additions and 116 deletions.
58 changes: 51 additions & 7 deletions tests/test_client_library_labs.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ def test_topology_creation_and_removal():
lnk2 = lab._create_link_local(i3, i4, "1")

assert set(lab.nodes()) == {node_a, node_b, node_c}
assert lab.statistics == {"annotations": 0, "nodes": 3, "links": 2, "interfaces": 4}
assert lab.statistics == {
"annotations": 0,
"nodes": 3,
"links": 2,
"interfaces": 4,
"smart_annotations": 0,
}
assert node_a.degree() == 1
assert node_b.degree() == 2
assert node_c.degree() == 1
Expand Down Expand Up @@ -82,22 +88,58 @@ def test_topology_creation_and_removal():
assert lnk2.interfaces == (i3, i4)

lab.remove_link(lnk2)
assert lab.statistics == {"annotations": 0, "nodes": 3, "links": 1, "interfaces": 4}
assert lab.statistics == {
"annotations": 0,
"nodes": 3,
"links": 1,
"interfaces": 4,
"smart_annotations": 0,
}

lab.remove_node(node_b)
assert lab.statistics == {"annotations": 0, "nodes": 2, "links": 0, "interfaces": 2}
assert lab.statistics == {
"annotations": 0,
"nodes": 2,
"links": 0,
"interfaces": 2,
"smart_annotations": 0,
}

lab.remove_interface(i4)
assert lab.statistics == {"annotations": 0, "nodes": 2, "links": 0, "interfaces": 1}
assert lab.statistics == {
"annotations": 0,
"nodes": 2,
"links": 0,
"interfaces": 1,
"smart_annotations": 0,
}

lab.remove_interface(i1)
assert lab.statistics == {"annotations": 0, "nodes": 2, "links": 0, "interfaces": 0}
assert lab.statistics == {
"annotations": 0,
"nodes": 2,
"links": 0,
"interfaces": 0,
"smart_annotations": 0,
}

lab.remove_node(node_a)
assert lab.statistics == {"annotations": 0, "nodes": 1, "links": 0, "interfaces": 0}
assert lab.statistics == {
"annotations": 0,
"nodes": 1,
"links": 0,
"interfaces": 0,
"smart_annotations": 0,
}

lab.remove_node(node_c)
assert lab.statistics == {"annotations": 0, "nodes": 0, "links": 0, "interfaces": 0}
assert lab.statistics == {
"annotations": 0,
"nodes": 0,
"links": 0,
"interfaces": 0,
"smart_annotations": 0,
}


def test_need_to_wait1():
Expand Down Expand Up @@ -250,6 +292,7 @@ def test_tags():
auto_sync=0,
resource_pool_manager=RESOURCE_POOL_MANAGER,
)
lab.get_smart_annotation_by_tag = MagicMock()
node_a = lab._create_node_local("0", "node A", "nd", "im", "cfg", 0, 0)
node_b = lab._create_node_local("1", "node B", "nd", "im", "cfg", 0, 0)
node_c = lab._create_node_local("2", "node C", "nd", "im", "cfg", 0, 0)
Expand Down Expand Up @@ -338,6 +381,7 @@ def test_join_existing_lab(client_library):
"nodes": 7,
"links": 8,
"interfaces": 24,
"smart_annotations": 0,
}


Expand Down
4 changes: 4 additions & 0 deletions virl2_client/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class AnnotationNotFound(ElementNotFound):
pass


class SmartAnnotationNotFound(ElementNotFound):
pass


class NodeNotFound(ElementNotFound):
pass

Expand Down
2 changes: 2 additions & 0 deletions virl2_client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from .node import Node
from .node_image_definitions import NodeImageDefinitions
from .resource_pools import ResourcePoolManagement
from .smart_annotation import SmartAnnotation
from .system import SystemManagement
from .users import UserManagement

Expand All @@ -50,4 +51,5 @@
"ResourcePoolManagement",
"AuthManagement",
"Annotation",
"SmartAnnotation",
)
53 changes: 24 additions & 29 deletions virl2_client/models/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
# --X-: ellipse
# -X--: line
# X---: text
ANNOTATION_MAP = {
"text": 0b1000,
"line": 0b0100,
"ellipse": 0b0010,
"rectangle": 0b0001,
}
ANNOTATION_PROPERTY_MAP = {
"border_color": 0b1111,
"border_radius": 0b0001,
Expand Down Expand Up @@ -131,7 +137,7 @@ def __init__(
self._id = annotation_id
self._lab = lab
self._session: httpx.Client = lab._session
# When the annotationis removed on the server, this annotation object is marked
# When the annotation is removed on the server, this annotation object is marked
# stale and can no longer be interacted with - the user should discard it
self._stale = False

Expand Down Expand Up @@ -167,6 +173,18 @@ def __eq__(self, other: object):
def __hash__(self):
return hash(self._id)

def _url_for(self, endpoint: str, **kwargs) -> str:
"""
Generate the URL for a given API endpoint.
:param endpoint: The desired endpoint.
:param **kwargs: Keyword arguments used to format the URL.
:returns: The formatted URL.
"""
kwargs["lab_id"] = self._lab._id
kwargs["annotation_id"] = self._id
return get_url_from_template(endpoint, self._URL_TEMPLATES, kwargs)

@property
def id(self) -> str:
"""Return ID of the annotation."""
Expand Down Expand Up @@ -269,35 +287,17 @@ def z_index(self, value: int) -> None:
self._set_annotation_property("z_index", value)
self._z_index = value

def _url_for(self, endpoint: str, **kwargs) -> str:
"""
Generate the URL for a given API endpoint.
:param endpoint: The desired endpoint.
:param **kwargs: Keyword arguments used to format the URL.
:returns: The formatted URL.
"""
kwargs["lab_id"] = self._lab._id
kwargs["annotation_id"] = self._id
return get_url_from_template(endpoint, self._URL_TEMPLATES, kwargs)

@classmethod
def get_default_property_values(cls, annotation_type: str) -> dict[str, Any]:
"""
Return a list of all valid properties set to default values for the selected
annotation type.
"""
default_values = {}
annotation_map = {
"text": 0b1000,
"line": 0b0100,
"ellipse": 0b0010,
"rectangle": 0b0001,
}
for ppty in ANNOTATION_PROPERTY_MAP:
if ppty == "type":
continue
if not annotation_map[annotation_type] & ANNOTATION_PROPERTY_MAP[ppty]:
if not ANNOTATION_MAP[annotation_type] & ANNOTATION_PROPERTY_MAP[ppty]:
continue
ppty_default = ANNOTATION_PROPERTIES_DEFAULTS[ppty]
if isinstance(ppty_default, dict):
Expand All @@ -318,13 +318,7 @@ def is_valid_property(
assert _property in ANNOTATION_PROPERTY_MAP
except AssertionError:
return False
annotation_map = {
"text": 0b1000,
"line": 0b0100,
"ellipse": 0b0010,
"rectangle": 0b0001,
}
return annotation_map[annotation_type] & ANNOTATION_PROPERTY_MAP[_property] > 0
return ANNOTATION_MAP[annotation_type] & ANNOTATION_PROPERTY_MAP[_property] > 0

@locked
def as_dict(self) -> dict[str, Any]:
Expand Down Expand Up @@ -377,8 +371,9 @@ def _update(self, annotation_data: dict[str, Any], push_to_server: bool) -> None
raise ValueError("Can't change annotation type.")

# make sure all properties we want to update are valid
for key, value in annotation_data.items():
if key not in dir(self):
existing_keys = dir(self)
for key in annotation_data:
if key not in existing_keys:
raise InvalidProperty(f"Invalid annotation property: {key}")

if push_to_server:
Expand Down
24 changes: 12 additions & 12 deletions virl2_client/models/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(
self._slot = slot
self._mac_address = mac_address
self._state: str | None = None
self._session: httpx.Client = node.lab._session
self._session: httpx.Client = node._lab._session
self._stale = False
self.statistics = {
"readbytes": 0,
Expand Down Expand Up @@ -114,7 +114,7 @@ def _url_for(self, endpoint, **kwargs):
:param **kwargs: Keyword arguments used to format the URL.
:returns: The formatted URL.
"""
kwargs["lab"] = self.node.lab._url_for("lab")
kwargs["lab"] = self.node._lab._url_for("lab")
kwargs["id"] = self.id
return get_url_from_template(endpoint, self._URL_TEMPLATES, kwargs)

Expand Down Expand Up @@ -152,7 +152,7 @@ def physical(self) -> bool:
def mac_address(self) -> str | None:
"""Return the MAC address set to the interface.
This is the address that will be used when the device is started."""
self.node.lab.sync_topology_if_outdated()
self.node._lab.sync_topology_if_outdated()
return self._mac_address

@mac_address.setter
Expand All @@ -170,7 +170,7 @@ def connected(self) -> bool:
@property
def state(self) -> str | None:
"""Return the state of the interface."""
self.node.lab.sync_states_if_outdated()
self.node._lab.sync_states_if_outdated()
if self._state is None:
url = self._url_for("state")
self._state = self._session.get(url).json()["state"]
Expand All @@ -179,8 +179,8 @@ def state(self) -> str | None:
@property
def link(self) -> Link | None:
"""Get the link if the interface is connected, otherwise None."""
self.node.lab.sync_topology_if_outdated()
for link in self.node.lab.links():
self.node._lab.sync_topology_if_outdated()
for link in self.node._lab.links():
if self in link.interfaces:
return link
return None
Expand All @@ -205,25 +205,25 @@ def peer_node(self) -> Node | None:
@property
def readbytes(self) -> int:
"""Return the number of bytes read by the interface."""
self.node.lab.sync_statistics_if_outdated()
self.node._lab.sync_statistics_if_outdated()
return int(self.statistics["readbytes"])

@property
def readpackets(self) -> int:
"""Return the number of packets read by the interface."""
self.node.lab.sync_statistics_if_outdated()
self.node._lab.sync_statistics_if_outdated()
return int(self.statistics["readpackets"])

@property
def writebytes(self) -> int:
"""Return the number of bytes written by the interface."""
self.node.lab.sync_statistics_if_outdated()
self.node._lab.sync_statistics_if_outdated()
return int(self.statistics["writebytes"])

@property
def writepackets(self) -> int:
"""Return the number of packets written by the interface."""
self.node.lab.sync_statistics_if_outdated()
self.node._lab.sync_statistics_if_outdated()
return int(self.statistics["writepackets"])

@property
Expand Down Expand Up @@ -279,7 +279,7 @@ def as_dict(self) -> dict[str, str]:
"id": self.id,
"node": self.node.id,
"data": {
"lab_id": self.node.lab.id,
"lab_id": self.node._lab.id,
"label": self.label,
"slot": self.slot,
"type": self.type,
Expand All @@ -303,7 +303,7 @@ def get_link_to(self, other_interface: Interface) -> Link | None:

def remove(self) -> None:
"""Remove the interface from the node."""
self.node.lab.remove_interface(self)
self.node._lab.remove_interface(self)

@check_stale
def _remove_on_server(self) -> None:
Expand Down
Loading

0 comments on commit 1936abf

Please sign in to comment.