Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
remove usage of Device from attrs, include remote server as option fo…
Browse files Browse the repository at this point in the history
…r install
  • Loading branch information
cicharka committed Feb 27, 2024
1 parent 3ad0e51 commit ab847c4
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 64 deletions.
8 changes: 5 additions & 3 deletions catalystwan/api/partition_manager_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from catalystwan.api.task_status_api import Task
from catalystwan.api.versions_utils import DeviceVersions, RepositoryAPI
from catalystwan.dataclasses import Device
from catalystwan.endpoints.configuration_device_actions import (
PartitionActionPayload,
RemovePartitionActionPayload,
RemovePartitionDevice,
)
from catalystwan.endpoints.configuration_device_inventory import DeviceDetailsResponse
from catalystwan.exceptions import EmptyVersionPayloadError
from catalystwan.typed_list import DataSequence
from catalystwan.utils.upgrades_helper import get_install_specification, validate_personality_homogeneity
Expand Down Expand Up @@ -48,7 +48,9 @@ def __init__(self, session: ManagerSession) -> None:
self.repository = RepositoryAPI(self.session)
self.device_version = DeviceVersions(self.session)

def set_default_partition(self, devices: DataSequence[Device], partition: Optional[str] = None) -> Task:
def set_default_partition(
self, devices: DataSequence[DeviceDetailsResponse], partition: Optional[str] = None
) -> Task:
"""
Set default software versions for devices
Expand Down Expand Up @@ -82,7 +84,7 @@ def set_default_partition(self, devices: DataSequence[Device], partition: Option
return Task(self.session, partition_action.id)

def remove_partition(
self, devices: DataSequence[Device], partition: Optional[str] = None, force: bool = False
self, devices: DataSequence[DeviceDetailsResponse], partition: Optional[str] = None, force: bool = False
) -> Task:
"""
Remove chosen software version from device
Expand Down
96 changes: 74 additions & 22 deletions catalystwan/api/software_action_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

from catalystwan.api.task_status_api import Task
from catalystwan.api.versions_utils import DeviceVersions, RepositoryAPI
from catalystwan.dataclasses import Device
from catalystwan.endpoints.configuration_device_actions import (
InstallActionPayload,
InstallData,
InstallDevice,
InstallInput,
PartitionActionPayload,
)
from catalystwan.exceptions import EmptyVersionPayloadError, VersionDeclarationError # type: ignore
from catalystwan.endpoints.configuration_device_inventory import DeviceDetailsResponse
from catalystwan.exceptions import EmptyVersionPayloadError, ImageNotInRepositoryError # type: ignore
from catalystwan.typed_list import DataSequence
from catalystwan.utils.personality import Personality
from catalystwan.utils.upgrades_helper import get_install_specification, validate_personality_homogeneity
Expand Down Expand Up @@ -54,7 +55,7 @@ def __init__(self, session: ManagerSession) -> None:

def activate(
self,
devices: DataSequence[Device],
devices: DataSequence[DeviceDetailsResponse],
version_to_activate: Optional[str] = "",
image: Optional[str] = "",
) -> Task:
Expand All @@ -78,7 +79,7 @@ def activate(
elif version_to_activate and not image:
version = cast(str, version_to_activate)
else:
raise VersionDeclarationError("You can not provide software_image and image version at the same time!")
raise ValueError("You can not provide software_image and image version at the same time!")

payload_devices = self.device_versions.get_device_available(version, devices)
for device in payload_devices:
Expand All @@ -98,12 +99,16 @@ def activate(

def install(
self,
devices: DataSequence[Device],
devices: DataSequence[DeviceDetailsResponse],
reboot: bool = False,
sync: bool = True,
image: str = "",
image_version: str = "",
v_edge_vpn: int = 0,
v_smart_vpn: int = 0,
image: Optional[str] = None,
image_version: Optional[str] = None,
downgrade_check: bool = True,
remote_server_name: Optional[str] = None,
remote_image_filename: Optional[str] = None,
) -> Task:
"""
Method to install new software
Expand All @@ -126,35 +131,82 @@ def install(
Task: Task object representing started install process
"""
validate_personality_homogeneity(devices)
if image and not image_version:

if (
sum(
[
image is not None,
image_version is not None,
all([remote_server_name is not None, remote_image_filename is not None]),
]
)
!= 1
):
raise ValueError(
"Please provide one option to detect software to install."
"Pick either 'image', 'image_version', or both 'remote_server_name' and 'remote_image_filename'."
)

# FIXME downgrade_check will be supported when software images from Remote Server will have versions fields
if remote_server_name and remote_image_filename and downgrade_check:
raise ValueError("Downgrade check is not supported for install action for images from Remote Server.")

version, remote_image_details = None, None
if image:
version = cast(str, self.repository.get_image_version(image))
elif image_version and not image:
if image_version:
version = cast(str, image_version)
else:
raise VersionDeclarationError("You can not provide image and image version at the same time")
if remote_server_name and remote_image_filename:
remote_image_details = self.repository.get_remote_image(remote_image_filename, remote_server_name)

install_specification = get_install_specification(devices.first())
if not any([version, remote_image_details]):
raise ImageNotInRepositoryError(
"Based on provided arguments, software version to install on device(s) cannot be detected."
)

install_specification = get_install_specification(devices.first(), remote=bool(remote_image_details))
install_devices = [
InstallDevice(**device.model_dump(by_alias=True))
for device in self.device_versions.get_device_list(devices)
]
input = InstallInput(
v_edge_vpn=0,
v_smart_vpn=0,
family=install_specification.family.value,
version=version,
version_type=install_specification.version_type.value,
reboot=reboot,
sync=sync,
)

if version:
input = InstallInput(
v_edge_vpn=v_edge_vpn,
v_smart_vpn=v_smart_vpn,
family=install_specification.family.value,
version=version,
version_type=install_specification.version_type.value,
reboot=reboot,
sync=sync,
)
else:
input = InstallInput(
v_edge_vpn=v_edge_vpn,
v_smart_vpn=v_smart_vpn,
data=[
InstallData(
family=install_specification.family.value,
version=remote_image_details.version_id, # type: ignore
remote_server_id=remote_image_details.remote_server_id, # type: ignore
version_id=remote_image_details.version_id, # type: ignore
)
],
version_type=install_specification.version_type.value,
reboot=reboot,
sync=sync,
)

device_type = install_specification.device_type.value
install_payload = InstallActionPayload(
action="install", input=input, devices=install_devices, device_type=device_type
)

if downgrade_check and devices.first().personality in (Personality.VMANAGE, Personality.EDGE):
self._downgrade_check(
install_payload.devices, install_payload.input.version, install_specification.family.value
install_payload.devices,
install_payload.input.version, # type: ignore
install_specification.family.value, # type: ignore
)

install_action = self.session.endpoints.configuration_device_actions.process_install_operation(
Expand Down
89 changes: 72 additions & 17 deletions catalystwan/api/versions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

from pydantic import BaseModel, ConfigDict, Field

from catalystwan.dataclasses import Device
from catalystwan.endpoints.configuration.software_actions import SoftwareImageDetails
from catalystwan.endpoints.configuration_device_actions import PartitionDevice
from catalystwan.endpoints.configuration_device_inventory import DeviceDetailsResponse
from catalystwan.exceptions import ImageNotInRepositoryError
from catalystwan.typed_list import DataSequence
from catalystwan.utils.upgrades_helper import SoftwarePackageUploadPayload
Expand Down Expand Up @@ -112,6 +112,41 @@ def get_image_version(self, software_image: str) -> Union[str, None]:
logger.error(f"Software image {image_name} is not in available images")
return None

def get_remote_image(
self, remote_image_filename: str, remote_server_name: str
) -> Union[SoftwareImageDetails, None]:
"""
Get remote software image details, based on name in available files and remote server name.
Args:
remote_image_filename (str): path to software image on remote server
remote_server_name (str): remote server name
Returns:
Union[SoftwareImageDetails, None]: remote image image details
"""

image_name = PurePath(remote_image_filename).name
software_images = self.get_all_software_images()
for image_details in software_images:
if (
image_details.available_files
and image_details.version_type
and image_name in image_details.available_files
and remote_server_name in image_details.version_type
):
if not (image_details.remote_server_id and image_details.version_id):
raise ValueError(
f"Requested image: '{image_name}' does not include include required fields for this operation:"
f"image_details.remote_server_id - (current value: {image_details.remote_server_id})"
f"image_details.version_id - (current value: {image_details.version_id})"
)
return image_details
logger.error(
f"Software image {image_name} is not in available in images from remote server {remote_server_name}"
)
return None

def upload_image(self, image_path: str) -> None:
"""
Upload software image ('tar.gz' or 'SPA.bin') to vManage software repository
Expand Down Expand Up @@ -157,24 +192,34 @@ class DeviceVersions:
def __init__(self, session: ManagerSession):
self.repository = RepositoryAPI(session)

def _validate_devices_required_fields(self, devices: DataSequence[DeviceDetailsResponse]):
for device in devices:
if not device.uuid or not device.device_ip:
raise ValueError(
f"Provided device '{device.host_name}' doesn't include required fields for this operation:"
f"device.uuid (current value: {device.uuid})"
f"device.device_ip (current value: {device.device_ip})"
)

def _get_device_list_in(
self, version_to_set_up: str, devices: DataSequence[Device], version_type: str
self, version_to_set_up: str, devices: DataSequence[DeviceDetailsResponse], version_type: str
) -> DataSequence[PartitionDevice]:
"""
Create devices payload list included requested version, if requested version
is in specified version type
Args:
version_to_set_up (str): requested version
devices List[Device]: list of Device dataclass instances
devices List[DeviceDetailsResponse]: list of Device dataclass instances
version_type: type of version (installed, available, etc.)
Returns:
list : list of devices
"""
self._validate_devices_required_fields(devices)
devices_payload = DataSequence(
PartitionDevice,
[PartitionDevice(device_id=device.uuid, device_ip=device.id) for device in devices],
[PartitionDevice(device_id=device.uuid, device_ip=device.device_ip) for device in devices], # type: ignore
)
all_dev_versions = self.repository.get_devices_versions_repository()
for device in devices_payload:
Expand All @@ -192,31 +237,31 @@ def _get_device_list_in(
return devices_payload

def get_device_list_in_installed(
self, version_to_set_up: str, devices: DataSequence[Device]
self, version_to_set_up: str, devices: DataSequence[DeviceDetailsResponse]
) -> DataSequence[PartitionDevice]:
"""
Create devices payload list included requested version, if requested version
is in installed versions
Args:
version_to_set_up (str): requested version
devices (List[Device]): devices on which action going to be performed
devices (List[DeviceDetailsResponse]): devices on which action going to be performed
Returns:
list : list of devices
"""
return self._get_device_list_in(version_to_set_up, devices, "installed_versions")

def get_device_available(
self, version_to_set_up: str, devices: DataSequence[Device]
self, version_to_set_up: str, devices: DataSequence[DeviceDetailsResponse]
) -> DataSequence[PartitionDevice]:
"""
Create devices payload list included requested, if requested version
is in available versions
Args:
version_to_set_up (str): requested version
devices (List[Device]): devices on which action going to be performed
devices (List[DeviceDetailsResponse]): devices on which action going to be performed
Returns:
Expand All @@ -225,56 +270,66 @@ def get_device_available(
return self._get_device_list_in(version_to_set_up, devices, "available_versions")

def _get_devices_chosen_version(
self, devices: DataSequence[Device], version_type: str
self, devices: DataSequence[DeviceDetailsResponse], version_type: str
) -> DataSequence[PartitionDevice]:
"""
Create devices payload list included software version key
for every device in devices list
Args:
version_to_set_up (str): requested version
devices (List[Device]): devices on which action going to be performed
devices (List[DeviceDetailsResponse]): devices on which action going to be performed
Returns:
list : list of devices
"""
self._validate_devices_required_fields(devices)

devices_payload = DataSequence(
PartitionDevice,
[PartitionDevice(device_id=device.uuid, device_ip=device.id) for device in devices],
[PartitionDevice(device_id=device.uuid, device_ip=device.device_ip) for device in devices], # type: ignore
)
all_dev_versions = self.repository.get_devices_versions_repository()
for device in devices_payload:
device.version = getattr(all_dev_versions[device.device_id], version_type)
return devices_payload

def get_devices_current_version(self, devices: DataSequence[Device]) -> DataSequence[PartitionDevice]:
def get_devices_current_version(
self, devices: DataSequence[DeviceDetailsResponse]
) -> DataSequence[PartitionDevice]:
"""
Create devices payload list included current software version key
for every device in devices list
Args:
version_to_set_up (str): requested version
devices (List[Device]): devices on which action going to be performed
devices (List[DeviceDetailsResponse]): devices on which action going to be performed
Returns:
list : list of devices
"""

return self._get_devices_chosen_version(devices, "current_version")

def get_devices_available_versions(self, devices: DataSequence[Device]) -> DataSequence[PartitionDevice]:
def get_devices_available_versions(
self, devices: DataSequence[DeviceDetailsResponse]
) -> DataSequence[PartitionDevice]:
"""
Create devices payload list included available software versions key
for every device in devices list
Args:
devices (List[Device]): devices on which action going to be performed
devices (List[DeviceDetailsResponse]): devices on which action going to be performed
Returns:
list : list of devices
"""

return self._get_devices_chosen_version(devices, "available_versions")

def get_device_list(self, devices: DataSequence[Device]) -> List[PartitionDevice]:
return [PartitionDevice(device_id=device.uuid, device_ip=device.id) for device in devices] # type: ignore
def get_device_list(self, devices: DataSequence[DeviceDetailsResponse]) -> List[PartitionDevice]:
self._validate_devices_required_fields(devices)

return [
PartitionDevice(device_id=device.uuid, device_ip=device.device_ip) for device in devices # type: ignore
]
1 change: 1 addition & 0 deletions catalystwan/endpoints/configuration/software_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class SoftwareImageDetails(BaseModel):
vnf_properties_json: Optional[str] = Field(
default=None, serialization_alias="vnfPropertiesJson", validation_alias="vnfPropertiesJson"
)
remote_server_id: str = Field(default=None, serialization_alias="remoteServerId", validation_alias="remoteServerId")


class ConfigurationSoftwareActions(APIEndpoints):
Expand Down
Loading

0 comments on commit ab847c4

Please sign in to comment.