Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@

from __future__ import annotations

from pathlib import Path

import pytest


Expand Down
1,588 changes: 783 additions & 805 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "virl2_client"
version = "2.9.0"
version = "2.9.1"
description = "VIRL2 Client Library"
authors = ["Simon Knight <[email protected]>", "Ralph Schmieder <[email protected]>"]
license = "Apache-2.0"
Expand Down
689 changes: 339 additions & 350 deletions tests/requirements.txt

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions tests/test_client_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,3 +768,38 @@ def test_get_diagnostics_paths(
if not valid:
data = {"error": f"Failed to fetch {category.value} diagnostics"}
assert diagnostics_data[category.value] == data


@respx.mock
def test_system_management_controller_triggers_compute_load(
client_library_server_current,
):
respx.post("https://localhost/api/v0/authenticate").respond(json="fake_token")
respx.get("https://localhost/api/v0/authok").respond(200)

compute_hosts_response = [
{
"id": "controller-123",
"hostname": "controller-host",
"server_address": "192.168.1.100",
"is_connector": True,
"is_simulator": False,
"is_connected": True,
"is_synced": True,
"admission_state": "approved",
"node_counts": {"deployed": 0, "running": 0, "orphans": 0},
}
]

respx.get("https://localhost/api/v0/system/compute_hosts").respond(
json=compute_hosts_response
)

client_library = ClientLibrary(
"https://localhost", "user", "pass", ssl_verify=False
)

controller = client_library.system_management.controller

assert controller.is_connector is True
assert controller.hostname == "controller-host"
2 changes: 1 addition & 1 deletion tests/test_image_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

import contextlib
import pathlib
from collections.abc import Iterator
from io import BufferedReader
from typing import Iterator
from unittest.mock import ANY, MagicMock

import pytest
Expand Down
4 changes: 2 additions & 2 deletions virl2_client/event_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import logging
from abc import ABC, abstractmethod
from os import name as os_name
from typing import TYPE_CHECKING, Any, Union
from typing import TYPE_CHECKING, Any

from .exceptions import ElementNotFound, LabNotFound

Expand Down Expand Up @@ -56,7 +56,7 @@ def __init__(self, event_dict: dict[str, Any]):
self.element_id: str = event_dict.get("element_id", "")
self.data: dict | None = event_dict.get("data")
self.lab: Lab | None = None
self.element: Union[Node, Interface, Link, None] = None
self.element: Node | Interface | Link | None = None

def __str__(self):
return (
Expand Down
3 changes: 2 additions & 1 deletion virl2_client/models/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

import json
import logging
from typing import TYPE_CHECKING, Generator
from collections.abc import Generator
from typing import TYPE_CHECKING
from uuid import uuid4

import httpx
Expand Down
3 changes: 2 additions & 1 deletion virl2_client/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ def group_labs(self, group_id: str) -> list[str]:
:returns: A list of labs associated with this group.
"""
warnings.warn(
"'GroupManagement.group_labs()' is deprecated.Use '.associations' instead.",
"'GroupManagement.group_labs()' is deprecated."
"Use '.associations' instead.",
)
return [lab["id"] for lab in self.get_group(group_id)["labs"]]

Expand Down
2 changes: 1 addition & 1 deletion virl2_client/models/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
from .group import * # noqa

warnings.warn(
"The module name 'virl2_client.models.groups' is deprecated."
"The module name 'virl2_client.models.groups' is deprecated. "
"Use 'virl2_client.models.group' instead.",
)
17 changes: 12 additions & 5 deletions virl2_client/models/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(
"""
self._id = iid
self._node = node
self._lab = node._lab
self._type = iface_type
self._label = label
self._slot = slot
Expand All @@ -83,7 +84,7 @@ def __init__(
"ipv4": None,
"ipv6": None,
}
self._deployed_mac_address = None
self._operational: dict[str, Any] = {}

def __eq__(self, other):
if not isinstance(other, Interface):
Expand Down Expand Up @@ -255,8 +256,14 @@ def discovered_ipv6(self) -> str | None:
@property
def deployed_mac_address(self) -> str | None:
"""Return the deployed MAC address of the interface."""
self.node.sync_interface_operational()
return self._deployed_mac_address
self._lab.sync_operational_if_outdated()
return self._operational.get("mac_address")

@property
def operational(self) -> dict[str, Any]:
"""Return the operational data for this interface."""
self._lab.sync_operational_if_outdated()
return self._operational.copy()

@property
def is_physical(self) -> bool:
Expand Down Expand Up @@ -346,8 +353,8 @@ def peer_interfaces(self):
Return the peer interface connected to this interface in a set.
"""
warnings.warn(
"'Interface.peer_interfaces()' is deprecated, "
"use '.peer_interface' instead.",
"'Interface.peer_interfaces()' is deprecated. "
"Use '.peer_interface' instead.",
)
return {self.peer_interface}

Expand Down
7 changes: 4 additions & 3 deletions virl2_client/models/lab.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import logging
import time
import warnings
from typing import TYPE_CHECKING, Any, Iterable
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any

from httpx import HTTPStatusError

Expand Down Expand Up @@ -2129,8 +2130,8 @@ def update_lab_groups(
:returns: Updated objects consisting of group ID and permissions.
"""
warnings.warn(
"'Lab.update_lab_groups()' is deprecated. Use '.update_associations()'"
" instead.",
"'Lab.update_lab_groups()' is deprecated. "
"Use '.update_associations()' instead.",
)
url = self._url_for("lab")
data = {"groups": group_list}
Expand Down
83 changes: 39 additions & 44 deletions virl2_client/models/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,12 @@ def __init__(
self._boot_disk_size: int | None = kwargs.get("boot_disk_size")
self._hide_links: bool = kwargs.get("hide_links", False)
self._tags: list[str] = kwargs.get("tags", [])
self._resource_pool: str | None = kwargs.get("resource_pool")
self._parameters: dict = kwargs.get("parameters", {})
self._pinned_compute_id: str | None = kwargs.get("pinned_compute_id")
self._operational: dict[str, Any] = kwargs.get("operational", {})

self._state: str | None = None
self._session: httpx.Client = lab._session
self._compute_id: str | None = kwargs.get("compute_id")
self._stale = False
self._last_sync_l3_address_time = 0.0
self._last_sync_interface_operational_time = 0.0
Expand Down Expand Up @@ -429,19 +428,19 @@ def _set_configuration(self, value: str | list | dict | None) -> None:
self._configuration[0]["content"] = value
else:
self._configuration.append({"name": "Main", "content": value})
return
if not value:
self._configuration = []
return
new_configs = value if isinstance(value, list) else [value]
current_configs = {
config["name"]: idx for idx, config in enumerate(self._configuration)
}
for config in new_configs:
if config["name"] in current_configs:
self._configuration[current_configs[config["name"]]] = config
elif isinstance(value, list):
self._configuration = value
elif isinstance(value, dict):
for configuration in self._configuration:
if configuration["name"] == value["name"]:
configuration["content"] = value["content"]
break
else:
self._configuration.append(config)
self._configuration.append(value)
elif value is None:
self._configuration = []
else:
raise TypeError(f"Unhandled type: {type(value)}")

@property
def configuration_files(self) -> list[dict[str, str]] | None:
Expand Down Expand Up @@ -516,29 +515,40 @@ def node_definition(self) -> str:
self._lab.sync_topology_if_outdated()
return self._node_definition

@property
def pinned_compute_id(self) -> str | None:
"""Return the ID of the compute this node is pinned to."""
return self._pinned_compute_id

@pinned_compute_id.setter
def pinned_compute_id(self, value) -> None:
"""Set the ID of the compute this node should be pinned to."""
self._set_node_property("pinned_compute_id", value)
self._pinned_compute_id = value

@property
def smart_annotations(self) -> dict[str, SmartAnnotation]:
"""Return the tags on this node and their corresponding smart annotations."""
self._lab.sync_topology_if_outdated()
return {tag: self._lab.get_smart_annotation_by_tag(tag) for tag in self._tags}

@property
def compute_id(self):
"""Return the ID of the compute this node is assigned to."""
self._lab.sync_operational_if_outdated()
return self._compute_id
return self._operational.get("compute_id")

@property
def resource_pool(self) -> str:
"""Return the ID of the resource pool if the node is part of a resource pool."""
self._lab.sync_operational_if_outdated()
return self._resource_pool
return self._operational.get("resource_pool")

@property
def pinned_compute_id(self) -> str | None:
"""Return the ID of the compute this node is pinned to."""
def operational(self) -> dict[str, Any]:
"""Return the full operational data as a dictionary."""
self._lab.sync_operational_if_outdated()
return self._pinned_compute_id

@pinned_compute_id.setter
def pinned_compute_id(self, value) -> None:
"""Set the ID of the compute this node should be pinned to."""
self._set_node_property("pinned_compute_id", value)
self._pinned_compute_id = value
return self._operational.copy()

@property
def cpu_usage(self) -> int | float:
Expand All @@ -558,12 +568,6 @@ def disk_write(self) -> int:
self._lab.sync_statistics_if_outdated()
return round(self.statistics["disk_write"] / 1048576)

@property
def smart_annotations(self) -> dict[str, SmartAnnotation]:
"""Return the tags on this node and their corresponding smart annotations."""
self._lab.sync_topology_if_outdated()
return {tag: self._lab.get_smart_annotation_by_tag(tag) for tag in self._tags}

@locked
def get_interface_by_label(self, label: str) -> Interface:
"""
Expand Down Expand Up @@ -895,9 +899,7 @@ def map_l3_addresses_to_interfaces(

@check_stale
@locked
def sync_operational(
self, response: dict[str, Any] | None = None
) -> dict[str, Any]:
def sync_operational(self, response: dict[str, Any] | None = None) -> None:
"""
Synchronize the operational state of the node.

Expand All @@ -908,25 +910,18 @@ def sync_operational(
if response is None:
url = self._url_for("operational")
response = self._session.get(url).json()
if response is None:
return {}
self._pinned_compute_id = response.get("pinned_compute_id")
operational = response.get("operational", {})
self._compute_id = operational.get("compute_id")
self._resource_pool = operational.get("resource_pool")
return operational
self._operational = response.get("operational")

@check_stale
@locked
def sync_interface_operational(self):
def sync_interface_operational(self) -> None:
"""Synchronize the operational state of the node's interfaces."""
url = self._url_for("inteface_operational")
response = self._session.get(url).json()
self._lab.sync_topology_if_outdated()
for interface_data in response:
interface = self._lab._interfaces[interface_data["id"]]
operational = interface_data.get("operational", {})
interface._deployed_mac_address = operational.get("mac_address")
interface._operational = interface_data.get("operational")
self._last_sync_interface_operational_time = time.time()

def update(
Expand Down
3 changes: 2 additions & 1 deletion virl2_client/models/node_image_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import pathlib
import time
import warnings
from typing import TYPE_CHECKING, BinaryIO, Callable
from collections.abc import Callable
from typing import TYPE_CHECKING, BinaryIO

from ..exceptions import InvalidContentType, InvalidImageFile
from ..utils import get_url_from_template
Expand Down
2 changes: 1 addition & 1 deletion virl2_client/models/node_image_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
from .node_image_definition import * # noqa

warnings.warn(
"The module name 'virl2_client.models.node_image_definitions' is deprecated."
"The module name 'virl2_client.models.node_image_definitions' is deprecated. "
"Use 'virl2_client.models.node_image_definition' instead.",
)
5 changes: 3 additions & 2 deletions virl2_client/models/resource_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@

import logging
import time
from collections.abc import Iterable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, Iterable
from typing import TYPE_CHECKING, Any

from ..exceptions import InvalidProperty
from ..utils import _deprecated_argument, get_url_from_template
Expand Down Expand Up @@ -457,7 +458,7 @@ def _set_resource_pool_properties(self, resource_pool_data: dict[str, Any]) -> N
self._session.patch(url, json=resource_pool_data_post)


ResourcePools = Dict[str, ResourcePool]
ResourcePools = dict[str, ResourcePool]


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion virl2_client/models/resource_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
from .resource_pool import * # noqa

warnings.warn(
"The module name 'virl2_client.models.resource_pools' is deprecated."
"The module name 'virl2_client.models.resource_pools' is deprecated. "
"Use 'virl2_client.models.resource_pool' instead.",
)
1 change: 1 addition & 0 deletions virl2_client/models/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def controller(self) -> ComputeHost:
(should never be the case).
:returns: The controller object.
"""
self.sync_compute_hosts_if_outdated()
for compute_host in self._compute_hosts.values():
if compute_host.is_connector:
return compute_host
Expand Down
2 changes: 1 addition & 1 deletion virl2_client/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
from .user import * # noqa

warnings.warn(
"The module name 'virl2_client.models.users' is deprecated."
"The module name 'virl2_client.models.users' is deprecated. "
"Use 'virl2_client.models.user' instead.",
)
Loading