From 08313f6ee9421ad12c595c1a77a970a7a49e4c0e Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 18 Sep 2024 13:19:00 +0300 Subject: [PATCH 01/72] Initial version --- .github/workflows/.gitkeep | 0 .gitignore | 6 + .pylintrc | 11 + LICENSE | 202 +++++++++ MANIFEST.in | 11 + README.md | 12 +- ci_dev.yml | 65 +++ dev/ankaios_sdk.py | 160 +++++++ dev/export_proto.sh | 9 + dev/main_ch.py | 221 ++++++++++ dev/main_ch_new.py | 192 +++++++++ dev/new_main.py | 12 + requirements.txt | 3 + requirements_dev.txt | 6 + run_tests.py | 89 ++++ setup.cfg | 21 + setup.py | 88 ++++ src/AnkaiosSDK.egg-info/PKG-INFO | 32 ++ src/AnkaiosSDK.egg-info/SOURCES.txt | 24 ++ src/AnkaiosSDK.egg-info/dependency_links.txt | 1 + src/AnkaiosSDK.egg-info/requires.txt | 3 + src/AnkaiosSDK.egg-info/top_level.txt | 1 + src/AnkaiosSDK/Ankaios.py | 314 ++++++++++++++ src/AnkaiosSDK/__init__.py | 18 + src/AnkaiosSDK/_components/CompleteState.py | 135 ++++++ src/AnkaiosSDK/_components/Request.py | 69 ++++ src/AnkaiosSDK/_components/Response.py | 127 ++++++ src/AnkaiosSDK/_components/Workload.py | 391 ++++++++++++++++++ src/AnkaiosSDK/_components/WorkloadState.py | 206 +++++++++ src/AnkaiosSDK/_components/__init__.py | 21 + src/AnkaiosSDK/_protos/.gitignore | 2 + src/AnkaiosSDK/_protos/__init__.py | 21 + src/AnkaiosSDK/_protos/ank_base.proto | 320 ++++++++++++++ src/AnkaiosSDK/_protos/control_api.proto | 48 +++ tests/WorkloadState/__init__.py | 13 + .../test_workload_execution_state.py | 39 ++ .../test_workload_instance_name.py | 28 ++ tests/WorkloadState/test_workload_state.py | 31 ++ .../test_workload_state_collection.py | 65 +++ .../WorkloadState/test_workload_state_enum.py | 22 + .../test_workload_substate_enum.py | 50 +++ tests/__init__.py | 13 + tests/test_workload.py | 164 ++++++++ 43 files changed, 3265 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/.gitkeep create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 ci_dev.yml create mode 100644 dev/ankaios_sdk.py create mode 100755 dev/export_proto.sh create mode 100644 dev/main_ch.py create mode 100644 dev/main_ch_new.py create mode 100644 dev/new_main.py create mode 100644 requirements.txt create mode 100644 requirements_dev.txt create mode 100644 run_tests.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/AnkaiosSDK.egg-info/PKG-INFO create mode 100644 src/AnkaiosSDK.egg-info/SOURCES.txt create mode 100644 src/AnkaiosSDK.egg-info/dependency_links.txt create mode 100644 src/AnkaiosSDK.egg-info/requires.txt create mode 100644 src/AnkaiosSDK.egg-info/top_level.txt create mode 100644 src/AnkaiosSDK/Ankaios.py create mode 100644 src/AnkaiosSDK/__init__.py create mode 100644 src/AnkaiosSDK/_components/CompleteState.py create mode 100644 src/AnkaiosSDK/_components/Request.py create mode 100644 src/AnkaiosSDK/_components/Response.py create mode 100644 src/AnkaiosSDK/_components/Workload.py create mode 100644 src/AnkaiosSDK/_components/WorkloadState.py create mode 100644 src/AnkaiosSDK/_components/__init__.py create mode 100644 src/AnkaiosSDK/_protos/.gitignore create mode 100644 src/AnkaiosSDK/_protos/__init__.py create mode 100644 src/AnkaiosSDK/_protos/ank_base.proto create mode 100644 src/AnkaiosSDK/_protos/control_api.proto create mode 100644 tests/WorkloadState/__init__.py create mode 100644 tests/WorkloadState/test_workload_execution_state.py create mode 100644 tests/WorkloadState/test_workload_instance_name.py create mode 100644 tests/WorkloadState/test_workload_state.py create mode 100644 tests/WorkloadState/test_workload_state_collection.py create mode 100644 tests/WorkloadState/test_workload_state_enum.py create mode 100644 tests/WorkloadState/test_workload_substate_enum.py create mode 100644 tests/__init__.py create mode 100644 tests/test_workload.py diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97a5e1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +*$py.class +reports/ +.coverage +.pytest_cache/ \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..07cc419 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,11 @@ +[MASTER] +ignore-patterns= + .*_pb2.py, + .*_pb2_grpc.py, + +[MESSAGES CONTROL] +disable= + # classes from protos are not recognized by pylint + E1101, # no-member + # module names have the same name with the classes within. They cannot be snake_case + C0103, # invalid-name diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..69c68a5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,11 @@ +# Include all .proto files in the src/protos directory +recursive-include src/protos *.proto + +# Include the README file +include README.md + +# Include the license file +include LICENSE + +# Include the requirements.txt file +include requirements.txt \ No newline at end of file diff --git a/README.md b/README.md index f5fd307..8c6f523 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ -# ank-sdk-python +# Ankaios Python SDK + Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. + +# To do +- Finish the marked TODO methods from Ankaios.py +- Fix protobuf versioning warning +- Add utest for 100% coverage (currently done Workload and WorkloadState) +- Fix Lint (currently done Workload.py) +- Improve requirements, requirements-dev and setup.py:install_requires +- Enable github workflow verifications +- Add to pip wheel \ No newline at end of file diff --git a/ci_dev.yml b/ci_dev.yml new file mode 100644 index 0000000..4271d55 --- /dev/null +++ b/ci_dev.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + name: Lint Code + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + pip install pylint + + - name: Run pylint + run: | + pylint src tests > pylint_report.txt + + - name: Upload pylint report + uses: actions/upload-artifact@v3 + with: + name: pylint-report + path: pylint_report.txt + + test: + name: Run Tests and Coverage + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + pip install pytest coverage + + - name: Run tests with coverage + run: | + coverage run -m unittest discover -s src + coverage report + coverage html + + - name: Upload coverage report + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: coverage.html diff --git a/dev/ankaios_sdk.py b/dev/ankaios_sdk.py new file mode 100644 index 0000000..c334076 --- /dev/null +++ b/dev/ankaios_sdk.py @@ -0,0 +1,160 @@ +import ank_base_pb2 as ank_base +import control_api_pb2 as control_api +from google.protobuf.internal.encoder import _VarintBytes +from google.protobuf.internal.decoder import _DecodeVarint +import threading +import time +import logging + + +"""Ankaios log levels.""" +class AnkaiosLogLevel: + FATAL = logging.FATAL + ERROR = logging.ERROR + WARN = logging.WARN + INFO = logging.INFO + DEBUG = logging.DEBUG + + +""" +Ankaios SDK for Python to interact with the Ankaios control interface. + +This SDK provides the functionality to interact with the Ankaios control interface +by sending requests to add a new workload dynamically and to request the workload states. +""" +class Ankaios: + ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" + WAITING_TIME_IN_SEC = 5 + REQUEST_ID = "dynamic_nginx@python_control_interface" + + def __init__(self) -> None: + """Initialize the Ankaios object.""" + self.logger = None + self.read_thread = None + + self.create_logger() + + def start_read(self): + """Starts a thread to read from the control interface input fifo.""" + self.read_thread = threading.Thread(target=self._read_from_control_interface) + self.read_thread.start() + + def join_read(self): + """Joins the read thread.""" + self.read_thread.join() + + def create_logger(self): + """Create a logger with custom format and default log level.""" + formatter = logging.Formatter('%(asctime)s %(message)s', datefmt="%FT%TZ") + self.logger = logging.getLogger("Ankaios logger") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + self.logger.addHandler(handler) + self.change_logger_level(AnkaiosLogLevel.INFO) + + def set_logger_level(self, level: AnkaiosLogLevel): + self.logger.setLevel(level) + + def create_request_to_add_new_workload(self): + """Create the Request containing an UpdateStateRequest + that contains the details for adding the new workload and + the update mask to add only the new workload. + """ + + return control_api.ToAnkaios( + request=ank_base.Request( + requestId=self.REQUEST_ID, + updateStateRequest=ank_base.UpdateStateRequest( + newState=ank_base.CompleteState( + desiredState=ank_base.State( + apiVersion="v0.1", + workloads=ank_base.WorkloadMap(workloads={ + "dynamic_nginx": ank_base.Workload( + agent="agent_A", + runtime="podman", + restartPolicy=ank_base.NEVER, + runtimeConfig="image: docker.io/library/nginx\ncommandOptions: [\"-p\", \"8080:80\"]") + }) + ) + ), + updateMask=["desiredState.workloads.dynamic_nginx"] + ) + ) + ) + + def create_request_for_complete_state(self): + """Create a Request to request the CompleteState + for querying the workload states. + """ + + return control_api.ToAnkaios( + request=ank_base.Request( + completeStateRequest=ank_base.CompleteStateRequest( + fieldMask=["workloadStates.agent_A.dynamic_nginx"] + ), + requestId=self.REQUEST_ID, + ) + ) + + def _read_from_control_interface(self): + """Reads from the control interface input fifo and prints the workload states.""" + + with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: + + while True: + varint_buffer = b'' # Buffer for reading in the byte size of the proto msg + while True: + next_byte = f.read(1) # Consume byte for byte + if not next_byte: + break + varint_buffer += next_byte + if next_byte[0] & 0b10000000 == 0: # Stop if the most significant bit is 0 (indicating the last byte of the varint) + break + msg_len, _ = _DecodeVarint(varint_buffer, 0) # Decode the varint and receive the proto msg length + + msg_buf = b'' # Buffer for the proto msg itself + for _ in range(msg_len): + next_byte = f.read(1) # Read exact amount of byte according to the calculated proto msg length + if not next_byte: + break + msg_buf += next_byte + + from_ankaios = control_api.FromAnkaios() + try: + from_ankaios.ParseFromString(msg_buf) # Deserialize the received proto msg + except Exception as e: + self.logger.info(f"Invalid response, parsing error: '{e}'") + continue + + request_id = from_ankaios.response.requestId + if from_ankaios.response.requestId == self.REQUEST_ID: + self.logger.info(f"Receiving Response containing the workload states of the current state:\nFromServer {{\n{from_ankaios}}}\n") + else: + self.logger.info(f"RequestId does not match. Skipping messages from requestId: {request_id}") + + def write_to_control_interface(self): + """Writes a Request into the control interface output fifo + to add the new workload dynamically and every x sec according to WAITING_TIME_IN_SEC + another Request to request the workload states. + """ + + with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: + update_workload_request = self.create_request_to_add_new_workload() + update_workload_request_byte_len = update_workload_request.ByteSize() # Length of the msg + proto_update_workload_request_msg = update_workload_request.SerializeToString() # Serialized proto msg + + self.logger.info(f'Sending Request containing details for adding the dynamic workload \"dynamic_nginx\":\nToServer {{\n{update_workload_request}}}\n') + f.write(_VarintBytes(update_workload_request_byte_len)) # Send the byte length of the proto msg + f.write(proto_update_workload_request_msg) # Send the proto msg itself + f.flush() + + request_complete_state = self.create_request_for_complete_state() + request_complete_state_byte_len = request_complete_state.ByteSize() # Length of the msg + proto_request_complete_state_msg = request_complete_state.SerializeToString() # Serialized proto msg + + while True: + self.logger.info(f"Sending Request containing details for requesting all workload states:\nToServer {{{request_complete_state}}}\n") + f.write(_VarintBytes(request_complete_state_byte_len)) # Send the byte length of the proto msg + f.write(proto_request_complete_state_msg) # Send the proto msg itself + f.flush() + time.sleep(self.WAITING_TIME_IN_SEC) # Wait according to WAITING_TIME_IN_SEC until sending the next Request to Ankaios to avoid spamming... diff --git a/dev/export_proto.sh b/dev/export_proto.sh new file mode 100755 index 0000000..5164adf --- /dev/null +++ b/dev/export_proto.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# Set the ANKAIOS project root directory +ANKAIOS_ROOT="/workspace/ankaios" + +cp $ANKAIOS_ROOT/api/proto/ank_base.proto $ANKAIOS_ROOT/python_sdk/ankaios_sdk/protos +cp $ANKAIOS_ROOT/api/proto/control_api.proto $ANKAIOS_ROOT/python_sdk/ankaios_sdk/protos + +protoc --python_out=$ANKAIOS_ROOT/python_sdk/ankaios_sdk --proto_path=$ANKAIOS_ROOT/python_sdk/ankaios_sdk/protos ank_base.proto control_api.proto diff --git a/dev/main_ch.py b/dev/main_ch.py new file mode 100644 index 0000000..fd77263 --- /dev/null +++ b/dev/main_ch.py @@ -0,0 +1,221 @@ +import os +import sys +import logging +from uuid import uuid4 +from threading import Thread +from queue import Queue +from google.protobuf.internal.encoder import _VarintBytes +from google.protobuf.internal.decoder import _DecodeVarint +import ankaios_pb2 as ank +from config import ANKAIOS_CONTROL_INTERFACE_BASE_PATH +from logger import get_logger + +# pylint: disable=no-member + +logger: logging.Logger = get_logger() + + +class Ankaios: + """ + Class for interacting with the Ankaios server + + This class access the FIFO files of the Ankaios control interface. + Hence only one object of this class should be created. + """ + + def __init__(self): + """ + Creates a new Ankaios object to interact with the control interface + :param logger: The logger to object should use for logging + :type logger : logging.Logger + """ + self._response_queues = {} + self._read_messages_thread = Thread(target=self._read_messages, daemon=True) + self._read_messages_thread.start() + + if not os.path.exists( + f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input" + ) or not os.path.exists(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output"): + logger.error("no connection to Ankaios control interface") + sys.exit(1) + + self._request_queue = Queue() + self._send_requests_thread = Thread(target=self._send_requests, daemon=True) + self._send_requests_thread.start() + logger.debug("Created object of %s", str(self.__class__.__name__)) + + def __del__(self) -> None: + logger.debug("Destroyed object of %s", str(self.__class__.__name__)) + + def _read_messages(self): + with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: + + while True: + varint_buffer = ( + b"" # Buffer for reading in the byte size of the proto msg + ) + while True: + next_byte = f.read(1) # Consume byte for byte + if not next_byte: + break + varint_buffer += next_byte + if ( + next_byte[0] & 0b10000000 == 0 + ): # Stop if the most significant bit is 0 (indicating the last byte of the varint) + break + msg_len, _ = _DecodeVarint( + varint_buffer, 0 + ) # Decode the varint and receive the proto msg length + + msg_buf = b"" # Buffer for the proto msg itself + while msg_len > 0: + next_bytes = f.read( + msg_len + ) # Read exact amount of byte according to the calculated proto msg length + if not next_bytes: + break + msg_len -= len(next_bytes) + msg_buf += next_bytes + + from_server = ank.FromServer() + try: + from_server.ParseFromString( + msg_buf + ) # Deserialize the received proto msg + except Exception as e: + logger.debug("Invalid response, parsing error: '%s'", e) + continue + + if from_server.response is not None: + response = from_server.response + request_id = response.requestId + response_queue = self._response_queues.get(request_id) + if response_queue is not None: + del self._response_queues[request_id] + response_queue.put(response) + else: + logger.debug("Response for unknown RequestId: %s", request_id) + else: + logger.debug("Received None as response message: %s", from_server) + + def _send_requests(self): + with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: + while True: + request = self._request_queue.get() + request_byte_len = request.ByteSize() # Length of the msg + proto_request = request.SerializeToString() # Serialized proto msg + logger.debug("Sending Request: %s\n", request) + f.write( + _VarintBytes(request_byte_len) + ) # Send the byte length of the proto msg + f.write(proto_request) # Send the proto msg itself + f.flush() + + def get_state(self): + """ + Get the current Ankaios state + :returns: the current Ankaios server state + :rtype: ank.CompleteState + """ + request_complete_state = ank.Request( + completeStateRequest=ank.CompleteStateRequest(fieldMask=["workloadStates"]), + ) + return self._execute_request(request_complete_state).completeState + + def delete_workload(self, workload_name): + """ + Delete a workload from Ankaios + :param workload_name: The name of the workload + :type workload_name : str + :returns: server response to SetState request + :rtype: ank.FromServer + + Example + ------- + + :: + ankaios.delete_workload("nginx") + """ + update_state_request = ank.Request( + updateStateRequest=ank.UpdateStateRequest( + newState=ank.CompleteState(desiredState=ank.State(apiVersion="v0.1")), + updateMask=[f"desiredState.workloads.{workload_name}"], + ) + ) + return self._execute_request(update_state_request) + + def add_workload( + self, + workload_name, + agent, + runtime, + runtime_config, + restart_policy, + tags, + dependencies, + ): + """Creates a new Ankaios workload + :param workload_name: The name of the workload + :type workload_name : str + :param agent: The agent the workload should run on + :type agent : str + :param runtime: The runtime to used + :type runtime_config : str + :param runtime: The runtime specific configuration + :type runtime_config : str + :returns: server response to SetState request + :rtype: ank.FromServer + + Example + ------- + + :: + ankaios.add_workload( + workload_name="nginx", + agent="agent_A", + runtime_config=yaml.dump( + { + "image": "docker.io/library/nginx:latest", + "commandOptions": ["--net=host"], + } + ), + ) + """ + update_state_request = ank.Request( + updateStateRequest=ank.UpdateStateRequest( + newState=ank.CompleteState( + desiredState=ank.State( + apiVersion="v0.1", + workloads={ + workload_name: ank.Workload( + agent=agent, + runtime=runtime, + runtimeConfig=runtime_config, + tags=[ + ank.Tag(key=key, value=value) + for key, value in tags.items() + ], + dependencies={ + name: ank.AddCondition.Value(condition) + for name, condition in dependencies.items() + }, + restartPolicy=ank.RestartPolicy.Value(restart_policy), + ) + }, + ) + ), + updateMask=[f"desiredState.workloads.{workload_name}"], + ) + ) + return self._execute_request(update_state_request) + + def _execute_request(self, request): + request_id = str(uuid4()) + response_queue = Queue() + self._response_queues[request_id] = response_queue + request.requestId = request_id + + to_server = ank.ToServer(request=request) + + self._request_queue.put(to_server) + return response_queue.get() diff --git a/dev/main_ch_new.py b/dev/main_ch_new.py new file mode 100644 index 0000000..dd49344 --- /dev/null +++ b/dev/main_ch_new.py @@ -0,0 +1,192 @@ +import ankaios_pb2 as ank +from google.protobuf.internal.encoder import _VarintBytes +from google.protobuf.internal.decoder import _DecodeVarint +from uuid import uuid4 +from threading import Thread +from queue import Queue + + +ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" + + +class Ankaios: + """ + Class for interacting with the Ankaios server + + This class access the FIFO files of the Ankaios control interface. + Hence only one object of this class should be created. + """ + + def __init__(self, logger): + """ + Creates a new Ankaios object to interact with the control interface + :param logger: The logger to object should use for logging + :type logger : logging.Logger + """ + self.logger = logger + self._response_queues = {} + self._read_messages_thread = Thread(target=self._read_messages, daemon=True) + self._read_messages_thread.start() + + self._request_queue = Queue() + self._send_requests_thread = Thread(target=self._send_requests, daemon=True) + self._send_requests_thread.start() + + def _read_messages(self): + with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: + + while True: + varint_buffer = ( + b"" # Buffer for reading in the byte size of the proto msg + ) + while True: + next_byte = f.read(1) # Consume byte for byte + if not next_byte: + break + varint_buffer += next_byte + if ( + next_byte[0] & 0b10000000 == 0 + ): # Stop if the most significant bit is 0 (indicating the last byte of the varint) + break + msg_len, _ = _DecodeVarint( + varint_buffer, 0 + ) # Decode the varint and receive the proto msg length + + msg_buf = b"" # Buffer for the proto msg itself + while msg_len > 0: + next_bytes = f.read( + msg_len + ) # Read exact amount of byte according to the calculated proto msg length + if not next_bytes: + break + msg_len -= len(next_bytes) + msg_buf += next_bytes + + from_server = ank.FromServer() + try: + from_server.ParseFromString( + msg_buf + ) # Deserialize the received proto msg + except Exception as e: + self.logger.info(f"Invalid response, parsing error: '{e}'") + continue + + if from_server.response is not None: + response = from_server.response + request_id = response.requestId + response_queue = self._response_queues.get(request_id) + if response_queue is not None: + del self._response_queues[request_id] + response_queue.put(response) + else: + self.logger.info( + f"Response for unknown RequestId: {request_id}" + ) + else: + self.logger.info( + f"Received None as response message: {from_server}" + ) + + def _send_requests(self): + with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: + while True: + request = self._request_queue.get() + request_byte_len = request.ByteSize() # Length of the msg + proto_request = request.SerializeToString() # Serialized proto msg + self.logger.info(f"Sending Request: {{{request}}}\n") + f.write( + _VarintBytes(request_byte_len) + ) # Send the byte length of the proto msg + f.write(proto_request) # Send the proto msg itself + f.flush() + + def get_state(self): + """ + Get the current Ankaios state + :returns: the current Ankaios server state + :rtype: ank.CompleteState + """ + request_complete_state = request = ank.Request( + completeStateRequest=ank.CompleteStateRequest(fieldMask=["workloadStates"]), + ) + return self._execute_request(request_complete_state).completeState + + def delete_workload(self, workload_name): + """ + Delete a workload from Ankaios + :param workload_name: The name of the workload + :type workload_name : str + :returns: server response to SetState request + :rtype: ank.FromServer + + Example + ------- + + :: + ankaios.delete_workload("nginx") + """ + update_state_request = request = ank.Request( + updateStateRequest=ank.UpdateStateRequest( + newState=ank.CompleteState(desiredState=ank.State(apiVersion="v0.1")), + updateMask=[f"desiredState.workloads.{workload_name}"], + ) + ) + return self._execute_request(update_state_request) + + def add_workload(self, workload_name, agent, runtime="podman", runtime_config=""): + """Creates a new Ankaios workload + :param workload_name: The name of the workload + :type workload_name : str + :param agent: The agent the workload should run on + :type agent : str + :param runtime: The runtime to used + :type runtime_config : str + :param runtime: The runtime specific configuration + :type runtime_config : str + :returns: server response to SetState request + :rtype: ank.FromServer + + Example + ------- + + :: + ankaios.add_workload( + workload_name="nginx", + agent="agent_A", + runtime_config=yaml.dump( + { + "image": "docker.io/library/nginx:latest", + "commandOptions": ["--net=host"], + } + ), + ) + """ + update_state_request = ank.Request( + updateStateRequest=ank.UpdateStateRequest( + newState=ank.CompleteState( + desiredState=ank.State( + apiVersion="v0.1", + workloads={ + workload_name: ank.Workload( + agent=agent, + runtime=runtime, + runtimeConfig=runtime_config, + ) + }, + ) + ), + updateMask=[f"desiredState.workloads.{workload_name}"], + ) + ) + return self._execute_request(update_state_request) + + def _execute_request(self, request): + request_id = str(uuid4()) + response_queue = Queue() + self._response_queues[request_id] = response_queue + request.requestId = request_id + + to_server = ank.ToServer(request=request) + + self._request_queue.put(to_server) + return response_queue.get() diff --git a/dev/new_main.py b/dev/new_main.py new file mode 100644 index 0000000..c58ade8 --- /dev/null +++ b/dev/new_main.py @@ -0,0 +1,12 @@ +from ankaios_sdk import Ankaios, AnkaiosLogLevel + + +if __name__ == "__main__": + ank = Ankaios() + ank.set_logger_level(AnkaiosLogLevel.INFO) + + ank.start_read() + ank.write_to_control_interface() + ank.join_read() + + exit(0) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..666ae4d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +setuptools # For setting up the package +protobuf # Core protobuf library +grpcio-tools # For generating .pb2 files \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..fe4967d --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,6 @@ +setuptools # For setting up the package +protobuf # Core protobuf library +grpcio-tools # For generating .pb2 files +pytest # For testing +pytest-cov # For coverage +pylint # For linting diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..2347df7 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,89 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import os +import pytest +import argparse +import subprocess + + +REPORT_DIR = "reports" +COVERAGE_DIR = os.path.join(REPORT_DIR, "coverage") +UTEST_DIR = os.path.join(REPORT_DIR, "utest") +PYLINT_DIR = os.path.join(REPORT_DIR, "pylint") + + +def run_pytest_utest(): + os.makedirs(UTEST_DIR, exist_ok=True) + pytest.main([ + '--junitxml={}'.format(os.path.join(UTEST_DIR, 'utest_report.xml')), + 'tests', + '-vv' + ]) + + +def run_pytest_cov(): + os.makedirs(COVERAGE_DIR, exist_ok=True) + pytest.main([ + '--cov=src', + # '--cov-config=.coveragerc', + '--cov-report=html:{}'.format(os.path.join(COVERAGE_DIR, 'html')), + '--cov-report=xml:{}'.format(os.path.join(COVERAGE_DIR, 'cov_report.xml')), + '--cov-report=term', + 'tests', + '-vv' + ]) + + +def run_pylint(): + os.makedirs(PYLINT_DIR, exist_ok=True) + result = subprocess.run([ + 'pylint', 'src', 'tests', '--rcfile=.pylintrc', '--output-format=parseable' + ], capture_output=True, text=True) + + pylint_output = result.stdout + rating_line = None + output_lines = pylint_output.split('\n') + + for line in output_lines: + if 'Your code has been rated at' in line: + rating_line = line + break + + if rating_line: + print(rating_line) + + with open(os.path.join(PYLINT_DIR, 'pylint_report.txt'), 'w') as f: + f.write('\n'.join(output_lines)) + + +if __name__ == "__main__": + os.makedirs(REPORT_DIR, exist_ok=True) + parser = argparse.ArgumentParser(description='Run tests for AnkaiosSDK Python package') + parser.add_argument('-c', '--cov', action='store_true', help='Run coverage') + parser.add_argument('-u', '--utest', action='store_true', help='Run unit tests') + parser.add_argument('-l', '--lint', action='store_true', help='Run pylint') + parser.add_argument('-a', '--all', action='store_true', help='Run all tests') + + args = parser.parse_args() + if not any([args.cov, args.utest, args.lint, args.all]): + parser.print_help() + exit(0) + + if args.cov or args.all: + run_pytest_cov() + if args.utest or args.all: + run_pytest_utest() + if args.lint or args.all: + run_pylint() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c736525 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,21 @@ +[tool:pytest] +testpaths = tests + +[coverage:run] +source = src +omit = + */__init__.py + */_protos/*_pb2.py + */_protos/*_pb2_grpc.py + */tests/* + +[coverage:report] +exclude_lines = + pragma: no cover + if __name__ == "__main__": + +[coverage:html] +directory = reports/coverage/html + +[coverage:xml] +output = reports/coverage/cov_report.xml \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4d9e0f7 --- /dev/null +++ b/setup.py @@ -0,0 +1,88 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import os +from setuptools import setup, find_packages +from grpc_tools import protoc + + +PROJECT_NAME = "AnkaiosSDK" + + +def generate_protos(): + """Generate python protobuf files from the proto files.""" + protos_dir = f"src/{PROJECT_NAME}/_protos" + proto_files = ["ank_base.proto", "control_api.proto"] + + for proto_file in proto_files: + proto_path = os.path.join(protos_dir, proto_file) + output_file = proto_path.replace('.proto', '_pb2.py') + + if not os.path.exists(output_file) or os.path.getmtime(proto_path) > os.path.getmtime(output_file): + print(f"Compiling {proto_path}...") + command = [ + 'grpc_tools.protoc', + f'-I={protos_dir}', + f'--python_out={protos_dir}', + f'--grpc_python_out={protos_dir}', + proto_path + ] + if protoc.main(command) != 0: + raise Exception(f"Error: {proto_file} compilation failed") + + # Fix the import path in the generated control_api_pb2 + # https://github.com/protocolbuffers/protobuf/issues/1491#issuecomment-261914766 + if "control_api" in proto_file: + with open(output_file, 'r') as file: + filedata = file.read() + newdata = filedata.replace( + "import ank_base_pb2 as ank__base__pb2", + "from . import ank_base_pb2 as ank__base__pb2") + with open(output_file, 'w') as file: + file.write(newdata) + + +generate_protos() + + +setup( + name=PROJECT_NAME, + version="0.1.0", + license="Apache-2.0", + author="Elektrobit Automotive GmbH and Ankaios contributors", + # author_email="", + description="Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform.", + long_description=open('README.md').read(), + long_description_content_type="text/markdown", + url="https://eclipse-ankaios.github.io/ankaios/latest/", + python_requires='>=3.6', + package_dir={'': 'src'}, + packages=find_packages(where="src"), + include_package_data=True, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + ], + project_urls={ + "Documentation": "https://eclipse-ankaios.github.io/ankaios/latest/", + "Source": "https://github.com/eclipse-ankaios/ank-sdk-python", + "Bug Tracker": "https://github.com/eclipse-ankaios/ank-sdk-python/issues", + }, + install_requires=[ + "setuptools", + "protobuf", + "grpcio-tools", + ], +) diff --git a/src/AnkaiosSDK.egg-info/PKG-INFO b/src/AnkaiosSDK.egg-info/PKG-INFO new file mode 100644 index 0000000..c791182 --- /dev/null +++ b/src/AnkaiosSDK.egg-info/PKG-INFO @@ -0,0 +1,32 @@ +Metadata-Version: 2.1 +Name: AnkaiosSDK +Version: 0.1.0 +Summary: Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. +Home-page: https://eclipse-ankaios.github.io/ankaios/latest/ +Author: Elektrobit Automotive GmbH and Ankaios contributors +License: Apache-2.0 +Project-URL: Documentation, https://eclipse-ankaios.github.io/ankaios/latest/ +Project-URL: Source, https://github.com/eclipse-ankaios/ank-sdk-python +Project-URL: Bug Tracker, https://github.com/eclipse-ankaios/ank-sdk-python/issues +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +License-File: LICENSE + +# Ankaios Python SDK + +Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. + +# To do +- Finish the marked TODO methods from Ankaios.py +- Fix protobuf versioning warning +- Improve the WorkloadState handling +- Add utest for 100% coverage (currently done Workload and WorkloadState) +- Fix Lint (currently done Workload.py) +- Improve requirements, requirements-dev and setup.py:install_requires +- Enable github workflow verifications +- Add to pip wheel + diff --git a/src/AnkaiosSDK.egg-info/SOURCES.txt b/src/AnkaiosSDK.egg-info/SOURCES.txt new file mode 100644 index 0000000..df6abe5 --- /dev/null +++ b/src/AnkaiosSDK.egg-info/SOURCES.txt @@ -0,0 +1,24 @@ +LICENSE +MANIFEST.in +README.md +requirements.txt +setup.cfg +setup.py +src/AnkaiosSDK/Ankaios.py +src/AnkaiosSDK/__init__.py +src/AnkaiosSDK.egg-info/PKG-INFO +src/AnkaiosSDK.egg-info/SOURCES.txt +src/AnkaiosSDK.egg-info/dependency_links.txt +src/AnkaiosSDK.egg-info/requires.txt +src/AnkaiosSDK.egg-info/top_level.txt +src/AnkaiosSDK/_components/CompleteState.py +src/AnkaiosSDK/_components/Request.py +src/AnkaiosSDK/_components/Response.py +src/AnkaiosSDK/_components/Workload.py +src/AnkaiosSDK/_components/WorkloadState.py +src/AnkaiosSDK/_components/__init__.py +src/AnkaiosSDK/_protos/__init__.py +src/AnkaiosSDK/_protos/ank_base_pb2.py +src/AnkaiosSDK/_protos/ank_base_pb2_grpc.py +src/AnkaiosSDK/_protos/control_api_pb2.py +src/AnkaiosSDK/_protos/control_api_pb2_grpc.py \ No newline at end of file diff --git a/src/AnkaiosSDK.egg-info/dependency_links.txt b/src/AnkaiosSDK.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/AnkaiosSDK.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/AnkaiosSDK.egg-info/requires.txt b/src/AnkaiosSDK.egg-info/requires.txt new file mode 100644 index 0000000..141c816 --- /dev/null +++ b/src/AnkaiosSDK.egg-info/requires.txt @@ -0,0 +1,3 @@ +grpcio-tools +protobuf +setuptools diff --git a/src/AnkaiosSDK.egg-info/top_level.txt b/src/AnkaiosSDK.egg-info/top_level.txt new file mode 100644 index 0000000..467f9bb --- /dev/null +++ b/src/AnkaiosSDK.egg-info/top_level.txt @@ -0,0 +1 @@ +AnkaiosSDK diff --git a/src/AnkaiosSDK/Ankaios.py b/src/AnkaiosSDK/Ankaios.py new file mode 100644 index 0000000..ed31447 --- /dev/null +++ b/src/AnkaiosSDK/Ankaios.py @@ -0,0 +1,314 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import logging +from threading import Thread, Lock +from google.protobuf.internal.encoder import _VarintBytes +from google.protobuf.internal.decoder import _DecodeVarint + +from ._protos import _control_api +from ._components import Workload, CompleteState, Request, Response, ResponseEvent, WorkloadStateCollection + + +__all__ = ["Ankaios", "AnkaiosLogLevel"] + + +"""Ankaios log levels.""" +class AnkaiosLogLevel: + FATAL = logging.FATAL + ERROR = logging.ERROR + WARN = logging.WARN + INFO = logging.INFO + DEBUG = logging.DEBUG + + +""" +Ankaios SDK for Python to interact with the Ankaios control interface. + +This SDK provides the functionality to interact with the Ankaios control interface +by sending requests to add a new workload dynamically and to request the workload states. +""" +class Ankaios: + ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" + WAITING_TIME_IN_SEC = 5 + + def __init__(self) -> None: + """Initialize the Ankaios object.""" + self.logger = None + self.path = self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH + + self._read_thread = None + self._read = False + self._responses_lock = Lock() + self._responses: dict[str, ResponseEvent] = {} + + self._create_logger() + + def __enter__(self) -> "Ankaios": + """Connect to the control interface.""" + self.connect() + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + """Disconnect from the control interface.""" + self.disconnect() + pass + + def __del__(self) -> None: + """Destroy the Ankaios object.""" + self.logger.debug("Destroyed object of %s", str(self.__class__.__name__)) + + def _create_logger(self) -> None: + """Create a logger with custom format and default log level.""" + formatter = logging.Formatter('%(asctime)s %(message)s', datefmt="%FT%TZ") + self.logger = logging.getLogger("Ankaios logger") + handler = logging.StreamHandler() + handler.setFormatter(formatter) + self.logger.addHandler(handler) + self.set_logger_level(AnkaiosLogLevel.INFO) + + def _read_from_control_interface(self) -> None: + """Reads from the control interface input fifo and saves the response.""" + + with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: + + while self._read: + # Buffer for reading in the byte size of the proto msg + varint_buffer = b'' + while True: + # Consume byte for byte + next_byte = f.read(1) + if not next_byte: + break + varint_buffer += next_byte + # Stop if the most significant bit is 0 (indicating the last byte of the varint) + if next_byte[0] & 0b10000000 == 0: + break + # Decode the varint and receive the proto msg length + msg_len, _ = _DecodeVarint(varint_buffer, 0) + + # Buffer for the proto msg itself + msg_buf = b'' + for _ in range(msg_len): + # Read exact amount of byte according to the calculated proto msg length + next_byte = f.read(1) + if not next_byte: + break + msg_buf += next_byte + + try: + response = Response(msg_buf) + except ValueError as e: + print(f"{e}") + continue + + request_id = response.get_request_id() + with self._responses_lock: + if request_id in self._responses: + self._responses[request_id].set_response(response) + else: + self._responses[request_id] = ResponseEvent(response) + self._responses[request_id].set() + + def _get_response_by_id(self, request_id: str, timeout: int = 10) -> Response: + """Returns the response by the request id.""" + if not self._read: + raise ValueError("Reading from the control interface is not started.") + + with self._responses_lock: + if request_id in self._responses: + return self._responses.pop(request_id).get_response() + self._responses[request_id] = ResponseEvent() + + return self._responses[request_id].wait_for_response(timeout) + + def _write_to_pipe(self, request: Request) -> None: + """Writes the request into the control interface output fifo""" + with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: + request_to_ankaios = _control_api.ToAnkaios(request=request._get()) + # Send the byte length of the proto msg + f.write(_VarintBytes(request_to_ankaios.ByteSize())) + # Send the proto msg itself + f.write(request_to_ankaios.SerializeToString()) + f.flush() + + def _send_request(self, request: Request, timeout: int = 10) -> Response: + """Send a request and wait for the response.""" + if not self._read: + raise ValueError("Cannot request if not connected.") + self._write_to_pipe(request) + + try: + response = self._get_response_by_id(request.get_id(), timeout) + except TimeoutError as e: + raise e + return response + + def set_logger_level(self, level: AnkaiosLogLevel) -> None: + """Set the log level of the logger.""" + self.logger.setLevel(level) + + def connect(self) -> None: + """Connect to the control interface by starting to read from the input fifo.""" + if self._read: + raise ValueError("Reading from the control interface is already started.") + self._read_thread = Thread(target=self._read_from_control_interface) + self._read_thread.start() + self._read = True + + def disconnect(self) -> None: + """Disconnect from the control interface by stopping to read from the input fifo.""" + if not self._read: + raise ValueError("Reading from the control interface is not started.") + self._read = False + self._read_thread.join() + + def apply_manifest(self, manifest: dict) -> None: + # TODO apply_manifest - ank apply + pass + + def delete_manifest(self, manifest: dict) -> None: + # TODO delete_manifest + pass + + def run_workload(self, workload_name: str, workload: Workload) -> None: + """Send a request to run a workload.""" + complete_state = CompleteState() + complete_state.set_workload(workload_name, workload) + + # Create the request + request = Request(request_type="update_state") + request.set_complete_state(complete_state) + request.add_mask(f"desiredState.workloads.{workload_name}") + + # Send request + try: + response = self._send_request(request) + except TimeoutError as e: + self.logger.error(f"{e}") + return + + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error(f"Error while trying to run workload: {content}") + elif content_type == "update_state_success": + self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". + format(content["added_workloads"], content["deleted_workloads"])) + + def delete_workload(self, workload_name: str) -> None: + """Send a request to delete a workload.""" + request = Request(request_type="update_state") + request.set_complete_state(CompleteState()) + request.add_mask(f"desiredState.workloads.{workload_name}") + + try: + response = self._send_request(request) + except TimeoutError as e: + self.logger.error(f"{e}") + return + + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error(f"Error while trying to delete workload: {content}") + elif content_type == "update_state_success": + self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". + format(content["added_workloads"], content["deleted_workloads"])) + + def get_workload(self, workload_name: str, timeout: int = 10) -> Workload: + """Get the workload from the requested complete state.""" + state = self.get_state(timeout, [f"desiredState.workloads.{workload_name}"]) + if state is not None: + return state.get_workload(workload_name) + + def set_config_from_file(self, name: str, config_path: str) -> None: + """Set the config from a file.""" + with open(config_path, "r") as f: + config = f.read() + self.set_config(name, config) + + def set_config(self, name: str, config: dict) -> None: + # TODO set_config - not yet implemented + raise NotImplementedError("set_config is not implemented yet.") + + def get_config(self, name: str) -> dict: + # TODO get_config - not yet implemented + raise NotImplementedError("get_config is not implemented yet.") + + def delete_config(self, name: str) -> None: + # TODO delete_config - not yet implemented + raise NotImplementedError("delete_config is not implemented yet.") + + def get_state(self, timeout: int = 10, field_mask: list[str] = list()) -> CompleteState: + """Send a request to get the complete state""" + request = Request(request_type="get_state") + for mask in field_mask: + request.add_mask(mask) + try: + response = self._send_request(request, timeout) + except TimeoutError as e: + self.logger.error(f"{e}") + return None + + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error(f"Error while trying to get the state: {content}") + return None + + complete_state = CompleteState(content) + return complete_state + + def get_agents(self, timeout: int = 10) -> list[str]: + """Get the agents from the requested complete state.""" + state = self.get_state(timeout) + if state is not None: + return state.get_agents() + + def get_workload_states(self, timeout: int = 10) -> WorkloadStateCollection: + state = self.get_state(timeout) + if state is not None: + return state.get_workload_states() + + def get_workload_states_on_agent(self, agent_name: str, timeout: int = 10) -> WorkloadStateCollection: + state = self.get_state(timeout, ["workloadStates." + agent_name]) + if state is not None: + return state.get_workload_states() + + def get_workload_states_on_workload_name(self, workload_name: str, timeout: int = 10) -> WorkloadStateCollection: + state = self.get_state(timeout, ["workloadStates." + workload_name]) + if state is not None: + return state.get_workload_states() + + +if __name__ == "__main__": + with Ankaios() as ankaios: + # Create workload + workload = Workload( + agent_name="agent_A", + runtime="podman", + restart_policy="NEVER", + runtime_config="image: docker.io/library/nginx\ncommandOptions: [\"-p\", \"8080:80\"]" + ) + + # Run workload + ankaios.run_workload("dynamic_nginx", workload) + + # Get state + complete_state = ankaios.get_state(field_mask=["workloadStates.agent_A.dynamic_nginx"]) + print(complete_state) + + # Delete workload + ankaios.delete_workload("dynamic_nginx") diff --git a/src/AnkaiosSDK/__init__.py b/src/AnkaiosSDK/__init__.py new file mode 100644 index 0000000..177beb0 --- /dev/null +++ b/src/AnkaiosSDK/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from .Ankaios import * +from ._components import * + +__all__ = [name for name in globals() if not name.startswith('_')] diff --git a/src/AnkaiosSDK/_components/CompleteState.py b/src/AnkaiosSDK/_components/CompleteState.py new file mode 100644 index 0000000..f632e47 --- /dev/null +++ b/src/AnkaiosSDK/_components/CompleteState.py @@ -0,0 +1,135 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from .._protos import _ank_base +from .Workload import Workload +from .WorkloadState import WorkloadStateCollection + + +__all__ = ["CompleteState"] +DEFAULT_API_VERSION = "v0.1" + + +class CompleteState: + """ + A class to represent the complete state + """ + def __init__(self, api_version: str = DEFAULT_API_VERSION) -> None: + self._complete_state = _ank_base.CompleteState() + self._set_api_version(api_version) + self._workloads: dict[str, Workload] = {} + self._workload_state_collection = WorkloadStateCollection() + + def __str__(self) -> str: + return str(self._to_proto()) + + def _set_api_version(self, version: str) -> None: + """Set the API version for the complete state.""" + self._complete_state.desiredState.apiVersion = version + + def set_workload(self, name: str, workload: Workload) -> None: + """Add a workload to the complete state.""" + self._workloads[name] = workload + + def get_workload(self, name: str) -> Workload: + """Get a workload from the complete state by it's name.""" + return self._workloads.get(name) + + def get_workloads(self) -> dict[str, Workload]: + """Get a workloads from the complete state.""" + return self._workloads + + def get_workload_states(self) -> WorkloadStateCollection: + """Get the workload states.""" + return self._workload_state_collection + + def get_agents(self) -> list[str]: + """Get the connected agents.""" + # Return keys because the value "AgentAttributes" is not yet implemented + return self._complete_state.agents.keys() + + def _to_proto(self) -> _ank_base.CompleteState: + """Convert the CompleteState object to a proto message.""" + # Clear previous workloads + for name, workload in self._workloads.items(): + self._complete_state.desiredState.workloads.workloads[name].CopyFrom(workload._to_proto()) + return self._complete_state + + def _from_proto(self, proto: _ank_base.CompleteState) -> None: + """Convert the proto message to a CompleteState object.""" + self._complete_state = proto + self._workloads = {} + for name, workload in self._complete_state.desiredState.workloads.workloads.items(): + self._workloads[name] = Workload() + self._workloads[name]._from_proto(workload) + self._workload_state_collection._from_proto(self._complete_state.workloadStates) + + +if __name__ == "__main__": + complete_state = CompleteState() + + # Create workload + workload = Workload() + workload.update_agent_name("agent_A") + + # Add workload to complete state + complete_state.set_workload("dynamic_nginx", workload) + complete_state.set_workload("dynamic_nginx2", workload) + + print(complete_state) + + new_complete_state = CompleteState() + new_complete_state._from_proto(complete_state._to_proto()) + print(new_complete_state) + + complete_state_workload_states = CompleteState() + complete_state_workload_states._from_proto(_ank_base.CompleteState( + workloadStates=_ank_base.WorkloadStatesMap(agentStateMap={ + "agent_A": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ + "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ + "1234": _ank_base.ExecutionState( + additionalInfo="Random info", + succeeded=_ank_base.SUCCEEDED_OK, + ) + }) + }), + "agent_B": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ + "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ + "5678": _ank_base.ExecutionState( + additionalInfo="Random info", + pending=_ank_base.PENDING_WAITING_TO_START, + ) + }), + "dyn_nginx": _ank_base.ExecutionsStatesForId(idStateMap={ + "9012": _ank_base.ExecutionState( + additionalInfo="Random info", + stopping=_ank_base.STOPPING_WAITING_TO_STOP, + ) + }) + }) + }) + ) + ) + + print(complete_state_workload_states._complete_state.workloadStates) + + print("\nFor agent_B:") + workload_states_by_agent = complete_state_workload_states.get_workload_states_on_agent("agent_B") + for key in workload_states_by_agent: + print(f"Workload ID: {key}, workload name: {workload_states_by_agent[key][0]}, state: {workload_states_by_agent[key][1]}") + + print("\nFor nginx workloads:") + workload_states_by_name = complete_state_workload_states.get_workload_states_on_workload_name("nginx") + for key in workload_states_by_name: + print(f"Workload ID: {key}, agent name: {workload_states_by_name[key][0]}, state: {workload_states_by_name[key][1]}") diff --git a/src/AnkaiosSDK/_components/Request.py b/src/AnkaiosSDK/_components/Request.py new file mode 100644 index 0000000..73d3609 --- /dev/null +++ b/src/AnkaiosSDK/_components/Request.py @@ -0,0 +1,69 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import uuid +from .._protos import _ank_base +from .CompleteState import CompleteState + + +__all__ = ["Request"] + + +class Request: + def __init__(self, request_type: str) -> None: + self._request = _ank_base.Request() + self._request.requestId = str(uuid.uuid4()) + self._request_type = request_type + + if request_type not in ["update_state", "get_state"]: + raise ValueError("Invalid request type. Supported values: 'update_state', 'get_state'.") + + def __str__(self) -> str: + return str(self._to_proto()) + + def _get_id(self) -> str: + """Get the request ID.""" + return self._request.requestId + + def set_complete_state(self, complete_state: CompleteState) -> None: + """Set the complete state for the request.""" + if self._request_type != "update_state": + raise ValueError("Complete state can only be set for an update state request.") + + self._request.updateStateRequest.newState.CopyFrom(complete_state._to_proto()) + + def add_mask(self, mask: str) -> None: + """Set the update mask for the request.""" + if self._request_type == "update_state": + self._request.updateStateRequest.updateMask.append(mask) + elif self._request_type == "get_state": + self._request.completeStateRequest.fieldMask.append(mask) + else: + raise ValueError("Invalid request type.") + + def _to_proto(self) -> _ank_base.Request: + """Convert the Request object to a proto message.""" + return self._request + + +if __name__ == "__main__": + request_update = Request(request_type="update_state") + + # Create the CompleteState object + complete_state = CompleteState() + request_update.set_complete_state(complete_state) + print(request_update) + + request_get = Request(request_type="get_state") + print(request_get) diff --git a/src/AnkaiosSDK/_components/Response.py b/src/AnkaiosSDK/_components/Response.py new file mode 100644 index 0000000..d9a53eb --- /dev/null +++ b/src/AnkaiosSDK/_components/Response.py @@ -0,0 +1,127 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Union +from threading import Event +from .._protos import _ank_base, _control_api +from .CompleteState import CompleteState +from .Workload import Workload + + +__all__ = ["Response", "ResponseEvent"] + + +class Response: + def __init__(self, message_buffer: bytes) -> None: + """Initialize the Response object with the received message buffer.""" + self.buffer = message_buffer + self._response = None + self.content_type = None + self.content = None + + self._parse_response() + self._from_proto() + + def _parse_response(self) -> None: + from_ankaios = _control_api.FromAnkaios() + try: + # Deserialize the received proto msg + from_ankaios.ParseFromString(self.buffer) + except Exception as e: + raise ValueError(f"Invalid response, parsing error: '{e}'") + self._response = from_ankaios.response + + def _from_proto(self) -> None: + """ + Convert the proto message to a Response object. + This can be either an error, a complete state, or an update state success. + """ + if self._response.HasField("error"): + self.content_type = "error" + self.content = self._response.error.message + elif self._response.HasField("completeState"): + self.content_type = "complete_state" + self.content = CompleteState() + self.content._from_proto(self._response.completeState) + elif self._response.HasField("UpdateStateSuccess"): + self.content_type = "update_state_success" + self.content = { + "added_workloads": self._response.UpdateStateSuccess.addedWorkloads, + "deleted_workloads": self._response.UpdateStateSuccess.deletedWorkloads, + } + else: + raise ValueError("Invalid response type.") + + def get_request_id(self) -> str: + """Get the request_id of the response.""" + return self._response.requestId + + def check_request_id(self, request_id: str) -> bool: + """Check if the request_id of the response matches the given request_id.""" + return self._response.requestId == request_id + + def get_content(self) -> tuple[str, Union[str, CompleteState, dict]]: + """Get the content of the response.""" + return (self.content_type, self.content) + + +class ResponseEvent(Event): + def __init__(self, response: Response = None) -> None: + super().__init__() + self._response = response + + def set_response(self, response: Response) -> None: + """Set the response.""" + self._response = response + self.set() + + def get_response(self) -> Response: + """Get the response.""" + return self._response + + def wait_for_response(self, timeout: int) -> Response: + """Wait for the response.""" + if not self.wait(timeout): + raise TimeoutError("Timeout while waiting for the response.") + return self.get_response() + + +if __name__ == "__main__": + complete_state = CompleteState() + + # Create workload + workload = Workload( + agent_name="agent_A" + ) + + # Add workload to complete state + complete_state.set_workload("dynamic_nginx", workload) + complete_state.set_workload("dynamic_nginx2", workload) + + + from_ankaios = _control_api.FromAnkaios( + response=_ank_base.Response( + requestId="1234", + completeState=complete_state._to_proto() + ) + ) + + response = Response(from_ankaios.SerializeToString()) + print(response.get_request_id()) + (content_type, content) = response.get_content() + print(content_type) + print(content) + + if response.check_request_id("1234"): + print("Request ID matches") \ No newline at end of file diff --git a/src/AnkaiosSDK/_components/Workload.py b/src/AnkaiosSDK/_components/Workload.py new file mode 100644 index 0000000..a616d8f --- /dev/null +++ b/src/AnkaiosSDK/_components/Workload.py @@ -0,0 +1,391 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This script defines the Workload and WorkloadBuilder classes for creating and managing workloads. + +Classes: + Workload: Represents a workload with various attributes and methods to update them. + WorkloadBuilder: A builder class to create a Workload object with a fluent interface. + +Usage: + - Create a workload using the WorkloadBuilder: + workload = Workload.builder() \ + .agent_name("agent_A") \ + .runtime("podman") \ + .restart_policy("NEVER") \ + .runtime_config("image: docker.io/library/nginx\n" + + "commandOptions: [\"-p\", \"8080:80\"]") \ + .add_dependency("other_workload", "RUNNING") \ + .add_tag("key1", "value1") \ + .add_tag("key2", "value2") \ + .build() + + - Update fields of the workload: + workload.update_agent_name("agent_B") + + - Update dependencies: + deps = workload.get_dependencies() + deps["other_workload"] = "SUCCEEDED" + workload.update_dependencies(deps) + + - Update tags: + tags = workload.get_tags() + tags.pop("key1") + workload.update_tags(tags) + + - Print the updated workload: + print(workload) +""" + + +from .._protos import _ank_base + + +__all__ = ["Workload", "WorkloadBuilder"] + + +class Workload: + """ + A class to represent a workload. + """ + def __init__(self) -> None: + """ + Initialize a Workload object. + """ + self._workload = _ank_base.Workload() + + def __str__(self) -> str: + """ + Return a string representation of the Workload object. + + Returns: + str: String representation of the Workload object. + """ + return str(self._to_proto()) + + @staticmethod + def builder() -> "WorkloadBuilder": + """ + Return a WorkloadBuilder object. + + Returns: + WorkloadBuilder: A builder object to create a Workload. + """ + return WorkloadBuilder() + + def update_agent_name(self, agent_name: str) -> None: + """ + Set the agent name for the workload. + + Args: + agent_name (str): The agent name to update. + """ + self._workload.agent = agent_name + + def update_runtime(self, runtime: str) -> None: + """ + Set the runtime for the workload. + + Args: + runtime (str): The runtime to update. + """ + self._workload.runtime = runtime + + def update_runtime_config(self, config: str) -> None: + """ + Set the runtime-specific configuration for the workload. + + Args: + config (str): The runtime configuration to update. + """ + self._workload.runtimeConfig = config + + def update_runtime_config_from_file(self, config_file: str) -> None: + """ + Set the runtime-specific configuration for the workload from a file. + + Args: + config_file (str): The path to the configuration file. + """ + with open(config_file, "r", encoding="utf-8") as file: + self._workload.runtimeConfig = file.read() + + def update_restart_policy(self, policy: str) -> None: + """ + Set the restart policy for the workload. + Supported values: 'NEVER', 'ON_FAILURE', 'ALWAYS'. + + Args: + policy (str): The restart policy to update. + + Raises: + ValueError: If an invalid restart policy is provided. + """ + policy_map = { + "NEVER": _ank_base.NEVER, + "ON_FAILURE": _ank_base.ON_FAILURE, + "ALWAYS": _ank_base.ALWAYS + } + + if policy not in policy_map: + raise ValueError("Invalid restart policy. Supported values " + + "'NEVER', 'ON_FAILURE', 'ALWAYS'.") + self._workload.restartPolicy = policy_map[policy] + + def add_dependency(self, workload_name: str, condition: str) -> None: + """ + Add a dependency to the workload. + Supported values: 'RUNNING', 'SUCCEEDED', 'FAILED'. + + Args: + workload_name (str): The name of the dependent workload. + condition (str): The condition for the dependency. + + Raises: + ValueError: If an invalid condition is provided. + """ + condition_map = { + "RUNNING": _ank_base.ADD_COND_RUNNING, + "SUCCEEDED": _ank_base.ADD_COND_SUCCEEDED, + "FAILED": _ank_base.ADD_COND_FAILED + } + + if condition not in condition_map: + raise ValueError("Invalid condition. Supported values: " + + "'RUNNING', 'SUCCEEDED', 'FAILED'.") + self._workload.dependencies.dependencies[workload_name] = condition_map[condition] + + def get_dependencies(self) -> dict: + """ + Return the dependencies of the workload. + + Returns: + dict: A dictionary of dependencies with workload names as keys and conditions as values. + """ + deps = dict(self._workload.dependencies.dependencies) + for dep in deps: + if deps[dep] == _ank_base.ADD_COND_RUNNING: + deps[dep] = "RUNNING" + elif deps[dep] == _ank_base.ADD_COND_SUCCEEDED: + deps[dep] = "SUCCEEDED" + elif deps[dep] == _ank_base.ADD_COND_FAILED: + deps[dep] = "FAILED" + return deps + + def update_dependencies(self, dependencies: dict) -> None: + """ + Update the dependencies of the workload. + + Args: + dependencies (dict): A dictionary of dependencies with workload names and values. + """ + self._workload.dependencies.dependencies.clear() + for workload_name, condition in dependencies.items(): + self.add_dependency(workload_name, condition) + + def add_tag(self, key: str, value: str) -> None: + """ + Add a tag to the workload. + + Args: + key (str): The key of the tag. + value (str): The value of the tag. + """ + tag = _ank_base.Tag(key=key, value=value) + self._workload.tags.tags.append(tag) + + def get_tags(self) -> list[tuple[str, str]]: + """ + Return the tags of the workload. + + Returns: + list: A list of tuples containing tag keys and values. + """ + tags = [] + for tag in self._workload.tags.tags: + tags.append((tag.key, tag.value)) + return tags + + def update_tags(self, tags: list) -> None: + """ + Update the tags of the workload. + + Args: + tags (list): A list of tuples containing tag keys and values. + """ + while len(self._workload.tags.tags) > 0: + self._workload.tags.tags.pop() + for key, value in tags: + self.add_tag(key, value) + + def _to_proto(self) -> _ank_base.Workload: + """ + Convert the Workload object to a proto message. + + Returns: + _ank_base.Workload: The proto message representation of the Workload object. + """ + return self._workload + + def _from_proto(self, proto: _ank_base.Workload) -> None: + """ + Convert the proto message to a Workload object. + + Args: + proto (_ank_base.Workload): The proto message to convert. + """ + self._workload = proto + + +class WorkloadBuilder: + """ + A builder class to create a Workload object. + """ + def __init__(self) -> None: + """ + Initialize a WorkloadBuilder object. + """ + self.wl_agent_name = None + self.wl_runtime = None + self.wl_runtime_config = None + self.wl_restart_policy = None + self.dependencies = {} + self.tags = [] + + def agent_name(self, agent_name: str) -> "WorkloadBuilder": + """ + Set the agent name. + + Args: + agent_name (str): The agent name to set. + + Returns: + WorkloadBuilder: The builder object. + """ + self.wl_agent_name = agent_name + return self + + def runtime(self, runtime: str) -> "WorkloadBuilder": + """ + Set the runtime. + + Args: + runtime (str): The runtime to set. + + Returns: + WorkloadBuilder: The builder object. + """ + self.wl_runtime = runtime + return self + + def runtime_config(self, runtime_config: str) -> "WorkloadBuilder": + """ + Set the runtime configuration. + + Args: + runtime_config (str): The runtime configuration to set. + + Returns: + WorkloadBuilder: The builder object. + """ + self.wl_runtime_config = runtime_config + return self + + def runtime_config_from_file(self, runtime_config_path: str) -> "WorkloadBuilder": + """ + Set the runtime configuration using a file. + + Args: + runtime_config_path (str): The path to the configuration file. + + Returns: + WorkloadBuilder: The builder object. + """ + with open(runtime_config_path, "r", encoding="utf-8") as file: + self.wl_runtime_config = file.read() + return self + + def restart_policy(self, restart_policy: str) -> "WorkloadBuilder": + """ + Set the restart policy. + + Args: + restart_policy (str): The restart policy to set. + + Returns: + WorkloadBuilder: The builder object. + """ + self.wl_restart_policy = restart_policy + return self + + def add_dependency(self, workload_name: str, condition: str) -> "WorkloadBuilder": + """ + Add a dependency. + + Args: + workload_name (str): The name of the dependent workload. + condition (str): The condition for the dependency. + + Returns: + WorkloadBuilder: The builder object. + """ + self.dependencies[workload_name] = condition + return self + + def add_tag(self, key: str, value: str) -> "WorkloadBuilder": + """ + Add a tag. + + Args: + key (str): The key of the tag. + value (str): The value of the tag. + + Returns: + WorkloadBuilder: The builder object. + """ + self.tags.append((key, value)) + return self + + def build(self) -> Workload: + """ + Build the Workload object. + Required fields: agent name, runtime and runtime configuration. + + Returns: + Workload: The built Workload object. + + Raises: + ValueError: If required fields are not set. + """ + workload = Workload() + + if self.wl_agent_name is None: + raise ValueError("Workload can not be built without an agent name.") + if self.wl_runtime is None: + raise ValueError("Workload can not be built without a runtime.") + if self.wl_runtime_config is None: + raise ValueError("Workload can not be built without a runtime configuration.") + + workload.update_agent_name(self.wl_agent_name) + workload.update_runtime(self.wl_runtime) + workload.update_runtime_config(self.wl_runtime_config) + + if self.wl_restart_policy is not None: + workload.update_restart_policy(self.wl_restart_policy) + if len(self.dependencies) > 0: + workload.update_dependencies(self.dependencies) + if len(self.tags) > 0: + workload.update_tags(self.tags) + return workload diff --git a/src/AnkaiosSDK/_components/WorkloadState.py b/src/AnkaiosSDK/_components/WorkloadState.py new file mode 100644 index 0000000..918ff6a --- /dev/null +++ b/src/AnkaiosSDK/_components/WorkloadState.py @@ -0,0 +1,206 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import TypeAlias +from enum import Enum +from .._protos import _ank_base + + +__all__ = ["WorkloadStateCollection", "WorkloadState", "WorkloadInstanceName", + "WorkloadExecutionState", "WorkloadStateEnum", "WorkloadSubStateEnum"] + + +class WorkloadStateEnum(Enum): + AgentDisconnected: int = 0 + Pending: int = 1 + Running: int = 2 + Stopping: int = 3 + Succeeded: int = 4 + Failed: int = 5 + NotScheduled: int = 6 + Removed: int = 7 + + def __str__(self) -> str: + return self.name + + @staticmethod + def _get(field: str) -> "WorkloadStateEnum": + field = field[0].upper() + field[1:] # Capitalize the first letter + return WorkloadStateEnum[field] + + +class WorkloadSubStateEnum(Enum): + AGENT_DISCONNECTED: int = 0 + PENDING_INITIAL: int = 1 + PENDING_WAITING_TO_START: int = 2 + PENDING_STARTING: int = 3 + PENDING_STARTING_FAILED: int = 4 + RUNNING_OK: int = 5 + STOPPING: int = 6 + STOPPING_WAITING_TO_STOP: int = 7 + STOPPING_REQUESTED_AT_RUNTIME: int = 8 + STOPPING_DELETE_FAILED: int = 9 + SUCCEEDED_OK: int = 10 + FAILED_EXEC_FAILED: int = 11 + FAILED_UNKNOWN: int = 12 + FAILED_LOST: int = 13 + NOT_SCHEDULED: int = 14 + REMOVED: int = 15 + + def __str__(self) -> str: + return self.name + + @staticmethod + def _get(state: WorkloadStateEnum, field: _ank_base) -> "WorkloadSubStateEnum": + proto_mapper = {} + if state == WorkloadStateEnum.AgentDisconnected: + proto_mapper = { + _ank_base.AGENT_DISCONNECTED: WorkloadSubStateEnum.AGENT_DISCONNECTED + } + elif state == WorkloadStateEnum.Pending: + proto_mapper = { + _ank_base.PENDING_INITIAL: WorkloadSubStateEnum.PENDING_INITIAL, + _ank_base.PENDING_WAITING_TO_START: WorkloadSubStateEnum.PENDING_WAITING_TO_START, + _ank_base.PENDING_STARTING: WorkloadSubStateEnum.PENDING_STARTING, + _ank_base.PENDING_STARTING_FAILED: WorkloadSubStateEnum.PENDING_STARTING_FAILED + } + elif state == WorkloadStateEnum.Running: + proto_mapper = { + _ank_base.RUNNING_OK: WorkloadSubStateEnum.RUNNING_OK + } + elif state == WorkloadStateEnum.Stopping: + proto_mapper = { + _ank_base.STOPPING: WorkloadSubStateEnum.STOPPING, + _ank_base.STOPPING_WAITING_TO_STOP: WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP, + _ank_base.STOPPING_REQUESTED_AT_RUNTIME: WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME, + _ank_base.STOPPING_DELETE_FAILED: WorkloadSubStateEnum.STOPPING_DELETE_FAILED + } + elif state == WorkloadStateEnum.Succeeded: + proto_mapper = { + _ank_base.SUCCEEDED_OK: WorkloadSubStateEnum.SUCCEEDED_OK + } + elif state == WorkloadStateEnum.Failed: + proto_mapper = { + _ank_base.FAILED_EXEC_FAILED: WorkloadSubStateEnum.FAILED_EXEC_FAILED, + _ank_base.FAILED_UNKNOWN: WorkloadSubStateEnum.FAILED_UNKNOWN, + _ank_base.FAILED_LOST: WorkloadSubStateEnum.FAILED_LOST + } + elif state == WorkloadStateEnum.NotScheduled: + proto_mapper = { + _ank_base.NOT_SCHEDULED: WorkloadSubStateEnum.NOT_SCHEDULED + } + elif state == WorkloadStateEnum.Removed: + proto_mapper = { + _ank_base.REMOVED: WorkloadSubStateEnum.REMOVED + } + if field not in proto_mapper: + raise ValueError(f"No corresponding WorkloadSubStateEnum value for enum: {field}") + return proto_mapper[field] + + def _sub_state2ank_base(self) -> _ank_base: + try: + return getattr(_ank_base, self.name) + except AttributeError: # pragma: no cover + raise ValueError(f"No corresponding ank_base value for enum: {self.name}") + + +class WorkloadExecutionState: + def __init__(self, state: _ank_base.ExecutionState) -> None: + self.state: WorkloadStateEnum = None + self.substate: WorkloadSubStateEnum = None + self.info: str = None + + self._interpret_state(state) + + def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: + self.info = str(exec_state.additionalInfo) + + field = exec_state.WhichOneof("ExecutionStateEnum") + if field is None: + raise ValueError("Invalid state for workload.") + + self.state = WorkloadStateEnum._get(field) + self.substate = WorkloadSubStateEnum._get(self.state, exec_state.__getattribute__(field)) + + +class WorkloadInstanceName: + def __init__(self, agent_name: str, workload_name: str, workload_id: str) -> None: + self.agent_name = agent_name + self.workload_name = workload_name + self.workload_id = workload_id + + def __str__(self) -> str: + return f"{self.agent_name}.{self.workload_name}.{self.workload_id}" + + +class WorkloadState: + def __init__(self, agent_name: str, workload_name: str, workload_id: str, state: _ank_base.ExecutionState) -> None: + self.execution_state = WorkloadExecutionState(state) + self.workload_instance_name = WorkloadInstanceName(agent_name, workload_name, workload_id) + + +class WorkloadStateCollection: + ExecutionsStatesForId: TypeAlias = dict[str, WorkloadExecutionState] + ExecutionsStatesOfWorkload: TypeAlias = dict[str, ExecutionsStatesForId] + WorkloadStatesMap: TypeAlias = dict[str, ExecutionsStatesOfWorkload] + + def __init__(self) -> None: + self._workload_states: list[WorkloadState] = [] + + def add_workload_state(self, state: WorkloadState) -> None: + self._workload_states.append(state) + + def get_as_dict(self) -> WorkloadStatesMap: + return_dict = self.WorkloadStatesMap() + for state in self._workload_states: + + agent_name = state.workload_instance_name.agent_name + if agent_name not in return_dict: + return_dict[agent_name] = self.ExecutionsStatesOfWorkload() + + workload_name = state.workload_instance_name.workload_name + if workload_name not in return_dict[agent_name]: + return_dict[agent_name][workload_name] = self.ExecutionsStatesForId() + + workload_id = state.workload_instance_name.workload_id + return_dict[agent_name][workload_name][workload_id] = state.execution_state + return return_dict + + def get_as_list(self) -> list[WorkloadState]: + return self._workload_states + + def _from_proto(self, state: _ank_base.WorkloadStatesMap) -> None: + for agent_name in state.agentStateMap: + for workload_name in state.agentStateMap[agent_name].wlNameStateMap: + for workload_id in state.agentStateMap[agent_name].wlNameStateMap[workload_name].idStateMap: + self.add_workload_state(WorkloadState( + agent_name, + workload_name, + workload_id, + state.agentStateMap[agent_name].wlNameStateMap[workload_name].idStateMap[workload_id] + )) + + +# Example usage +if __name__ == "__main__": + # Create a WorkloadState object + workload_state = WorkloadExecutionState(_ank_base.ExecutionState( + additionalInfo="Info about pending", + pending=_ank_base.PENDING_STARTING + )) + + # Print the state, substate, and info + print(workload_state.state) # Will get a WorkloadStateEnum object + print(workload_state.substate) # Will get a WorkloadSubStateEnum object + print(workload_state.info) diff --git a/src/AnkaiosSDK/_components/__init__.py b/src/AnkaiosSDK/_components/__init__.py new file mode 100644 index 0000000..cb49a62 --- /dev/null +++ b/src/AnkaiosSDK/_components/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from .Workload import * +from .WorkloadState import * +from .CompleteState import * +from .Request import * +from .Response import * + +__all__ = [name for name in globals() if not name.startswith('_')] diff --git a/src/AnkaiosSDK/_protos/.gitignore b/src/AnkaiosSDK/_protos/.gitignore new file mode 100644 index 0000000..6c182e7 --- /dev/null +++ b/src/AnkaiosSDK/_protos/.gitignore @@ -0,0 +1,2 @@ +*pb2.py +*pb2_grpc.py \ No newline at end of file diff --git a/src/AnkaiosSDK/_protos/__init__.py b/src/AnkaiosSDK/_protos/__init__.py new file mode 100644 index 0000000..7410f15 --- /dev/null +++ b/src/AnkaiosSDK/_protos/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +try: + import AnkaiosSDK._protos.ank_base_pb2 as _ank_base + import AnkaiosSDK._protos.control_api_pb2 as _control_api +except ImportError as r: + raise r + +__all__ = ["_ank_base", "_control_api"] diff --git a/src/AnkaiosSDK/_protos/ank_base.proto b/src/AnkaiosSDK/_protos/ank_base.proto new file mode 100644 index 0000000..e7120d8 --- /dev/null +++ b/src/AnkaiosSDK/_protos/ank_base.proto @@ -0,0 +1,320 @@ +// Copyright (c) 2023 Elektrobit Automotive GmbH +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// [impl->swdd~ank-base-provides-object-definitions~1] + +syntax = "proto3"; +package ank_base; + +/** +* A message containing a request to the Ankaios server to update the state or to request the complete state of the Ankaios system. +*/ +message Request { + string requestId = 1; + oneof RequestContent { + UpdateStateRequest updateStateRequest = 2; /// A message to Ankaios server to update the state of one or more agent(s). + CompleteStateRequest completeStateRequest = 3; /// A message to Ankaios server to request the complete state by the given request id and the optional field mask. + } +} + +/** +* A message containing a response from the Ankaios server to a particular request. +* The response content depends on the request content previously sent to the Ankaios server. +*/ +message Response { + string requestId = 1; + oneof ResponseContent { + Error error = 3; + CompleteState completeState = 4; + UpdateStateSuccess UpdateStateSuccess = 5; + } +} + +/** +* An enum type describing the expected workload state. Used for dependency management. +*/ +enum AddCondition { + ADD_COND_RUNNING = 0; /// The workload is operational. + ADD_COND_SUCCEEDED = 1; /// The workload has successfully exited. + ADD_COND_FAILED = 2; /// The workload has exited with an error or could not be started. +} + +/** +* A message containing a request for the complete/partial state of the Ankaios system. +* This is usually answered with a [CompleteState](#completestate) message. +*/ +message CompleteStateRequest { + repeated string fieldMask = 1; /// A list of symbolic field paths within the State message structure e.g. 'desiredState.workloads.nginx'. +} + +/** +* A message containing a request to update the state of the Ankaios system. +* The new state is provided as state object. +* To specify which part(s) of the new state object should be updated +* a list of update mask (same as field mask) paths needs to be provided. +*/ +message UpdateStateRequest { + CompleteState newState = 1; /// The new state of the Ankaios system. + repeated string updateMask = 2; /// A list of symbolic field paths within the state message structure e.g. 'desiredState.workloads.nginx' to specify what to be updated. +} + +/** +* A message from the server containing the ids of the workloads that have been started and stopped in response to a previously sent UpdateStateRequest. +*/ +message UpdateStateSuccess { + repeated string addedWorkloads = 1; /// Workload istance names of workloads which will be started + repeated string deletedWorkloads = 2; /// Workload instance names of workloads which will be stopped +} + +/** +* A message containing the complete state of the Ankaios system. +* This is a response to the [CompleteStateRequest](#completestaterequest) message. +*/ +message CompleteState { + State desiredState = 1; /// The state the user wants to reach. + WorkloadStatesMap workloadStates = 2; /// The current execution states of the workloads. + AgentMap agents = 3; /// The agents currently connected to the Ankaios cluster. +} + +/** +* A nested map that provides the execution state of a workload in a structured way. +* The first level allows searches by agent. +*/ +message WorkloadStatesMap { + map agentStateMap = 1; +} + +/** +* A map providing the execution state of a workload for a given name. +*/ +message ExecutionsStatesOfWorkload { + map wlNameStateMap = 1; +} + +/** +* A map providing the execution state of a specific workload for a given id. +* This level is needed as a workload could be running more than once on one agent in different versions. +*/ +message ExecutionsStatesForId { + map idStateMap = 1; +} + +/** +* A message containing information about the detailed state of a workload in the Ankaios system. +*/ +message ExecutionState { + string additionalInfo = 1; /// The additional info contains more detailed information from the runtime regarding the execution state. + oneof ExecutionStateEnum { + AgentDisconnected agentDisconnected = 2; /// The exact state of the workload cannot be determined, e.g., because of a broken connection to the responsible agent. + Pending pending = 3; /// The workload is going to be started eventually. + Running running = 4; /// The workload is operational. + Stopping stopping = 5; /// The workload is scheduled for stopping. + Succeeded succeeded = 6; /// The workload has successfully finished its operation. + Failed failed = 7; /// The workload has failed or is in a degraded state. + NotScheduled notScheduled = 8; /// The workload is not scheduled to run at any agent. This is signalized with an empty agent in the workload specification. + Removed removed = 9; /// The workload was removed from Ankaios. This state is used only internally in Ankaios. The outside world removed states are just not there. + } +} + +/** +* The workload was removed from Ankaios. This state is used only internally in Ankaios. The outside world removed states are just not there. +*/ +enum Removed { + REMOVED = 0; +} + +/** +* The exact state of the workload cannot be determined, e.g., because of a broken connection to the responsible agent. +*/ +enum AgentDisconnected { + AGENT_DISCONNECTED = 0; +} + +/** +* The workload is not scheduled to run at any agent. This is signalized with an empty agent in the workload specification. +*/ +enum NotScheduled { + NOT_SCHEDULED = 0; +} + +/** +* The workload is going to be started eventually. +*/ +enum Pending { + PENDING_INITIAL = 0; /// The workload specification has not yet being scheduled + PENDING_WAITING_TO_START = 1; /// The start of the workload will be triggered once all its dependencies are met. + PENDING_STARTING = 2; /// Starting the workload was scheduled at the corresponding runtime. + PENDING_STARTING_FAILED = 8; /// The starting of the workload by the runtime failed. +} + +/** +* The workload is operational. +*/ +enum Running { + RUNNING_OK = 0; /// The workload is operational. +} +/** +* The workload is scheduled for stopping. +*/ +enum Stopping { + STOPPING = 0; /// The workload is being stopped. + STOPPING_WAITING_TO_STOP = 1; /// The deletion of the workload will be triggered once neither 'pending' nor 'running' workload depending on it exists. + STOPPING_REQUESTED_AT_RUNTIME = 2; /// This is an Ankaios generated state returned when the stopping was explicitly trigged by the user and the request was sent to the runtime. + STOPPING_DELETE_FAILED = 8; /// The deletion of the workload by the runtime failed. +} + +/** +* The workload has successfully finished operation. +*/ +enum Succeeded { + SUCCEEDED_OK = 0; /// The workload has successfully finished operation. +} + +/** +* The workload has failed or is in a degraded state. +*/ +enum Failed { + FAILED_EXEC_FAILED = 0; /// The workload has failed during operation + FAILED_UNKNOWN = 1; /// The workload is in an unsupported by Ankaios runtime state. The workload was possibly altered outside of Ankaios. + FAILED_LOST = 2; /// The workload cannot be found anymore. The workload was possibly altered outside of Ankaios or was auto-removed by the runtime. +} + +/** +* A nested map that provides the names of the connected agents and their optional attributes. +* The first level allows searches by agent name. +*/ +message AgentMap { + map agents = 1; +} + +/** +* A message that has not yet been implemented but will contain attributes of the agent in the future. +*/ +message AgentAttributes {} + +/** +* A message containing the information about the workload state. +*/ +message WorkloadState { + WorkloadInstanceName instanceName = 1; + ExecutionState executionState = 2; /// The workload execution state. +} + +message WorkloadInstanceName { + string workloadName = 1; /// The name of the workload. + string agentName = 2; /// The name of the owning Agent. + string id = 3; // A unique identifier of the workload. +} + +/** +* A message containing the state information. +*/ +message State { + string apiVersion = 1; /// The current version of the API. + WorkloadMap workloads = 2; /// A mapping from workload names to workload configurations. +} + +/** +* This is a workaround for proto not supporing optional maps +*/ +message WorkloadMap { + map workloads = 1; +} + +/** +* A message containing the configuration of a workload. +*/ +message Workload { + optional string agent = 1; /// The name of the owning Agent. + optional RestartPolicy restartPolicy = 2; /// An enum value that defines the condition under which a workload is restarted. + Dependencies dependencies = 3; /// A map of workload names and expected states to enable a synchronized start of the workload. + Tags tags = 4; /// A list of tag names. + optional string runtime = 5; /// The name of the runtime e.g. podman. + optional string runtimeConfig = 6; /// The configuration information specific to the runtime. + ControlInterfaceAccess controlInterfaceAccess = 7; +} + +/** +* This is a workaround for proto not supporing optional repeated values +*/ +message Tags { + repeated Tag tags = 1; +} + +/** +* This is a workaround for proto not supporing optional maps +*/ +message Dependencies { + map dependencies = 1; +} + +/** +* A message to store a tag. +*/ +message Tag { + string key = 1; /// The key of the tag. + string value = 2; /// The value of the tag. +} + +/** +* An enum type describing the restart behavior of a workload. +*/ +enum RestartPolicy { + NEVER = 0; /// The workload is never restarted. Once the workload exits, it remains in the exited state. + ON_FAILURE = 1; /// If the workload exits with a non-zero exit code, it will be restarted. + ALWAYS = 2; /// The workload is restarted upon termination, regardless of the exit code. +} + +message Error { + string message = 1; +} + +/** +* A message containing the parts of the control interface the workload as authorized to access. +* By default, all access is denied. +* Only if a matching allow rule is found, and no matching deny rules is found, the access is allowed. +*/ +message ControlInterfaceAccess { + repeated AccessRightsRule allowRules = 1; // Rules allow the access + repeated AccessRightsRule denyRules = 2; // Rules denying the access +} + +/** +* A message containing an allow or deny rule. +**/ +message AccessRightsRule { + oneof AccessRightsRuleEnum { + StateRule stateRule = 1; // Rule for getting or setting the state + } +} + +/** +* Message containing a rule for getting or setting the state +**/ +message StateRule { + ReadWriteEnum operation = 1; // Defines which actions are allowed + repeated string filterMasks = 2; // Pathes definind what can be accessed. Segements of path can be a wildcare "*". +} + + +/** +* An enum type describing which action is allowed. +*/ +enum ReadWriteEnum { + RW_NOTHING = 0; // Allow nothing + RW_READ = 1; // Allow read + RW_WRITE = 2; // Allow write + RW_READ_WRITE = 5; // Allow read and write +} + diff --git a/src/AnkaiosSDK/_protos/control_api.proto b/src/AnkaiosSDK/_protos/control_api.proto new file mode 100644 index 0000000..8fd93e9 --- /dev/null +++ b/src/AnkaiosSDK/_protos/control_api.proto @@ -0,0 +1,48 @@ +// Copyright (c) 2024 Elektrobit Automotive GmbH +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// [impl->swdd~control-api-provides-control-interface-definitions~1] + +/** +* The Ankaios Control Interface is used in the communcation between a workload and Ankaios +* +* The protocol consists of the following top-level message types: +* +* 1. [ToAnkaios](#toankaios): workload -> ankaios +* +* 2. [FromAnkaios](#fromankaios): ankaios -> workload +* +*/ +syntax = "proto3"; +package control_api; + +import "ank_base.proto"; + +/** +* Messages to the Ankaios server. +*/ +message ToAnkaios { + oneof ToAnkaiosEnum { + ank_base.Request request = 3; + } +} + +/** +* Messages from the Ankaios server to e.g. the Ankaios agent. +*/ +message FromAnkaios { + oneof FromAnkaiosEnum { + ank_base.Response response = 3; /// A message containing a response to a previous request. + } +} diff --git a/tests/WorkloadState/__init__.py b/tests/WorkloadState/__init__.py new file mode 100644 index 0000000..094cebe --- /dev/null +++ b/tests/WorkloadState/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tests/WorkloadState/test_workload_execution_state.py b/tests/WorkloadState/test_workload_execution_state.py new file mode 100644 index 0000000..4dc6979 --- /dev/null +++ b/tests/WorkloadState/test_workload_execution_state.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from src.AnkaiosSDK import WorkloadExecutionState, WorkloadStateEnum, WorkloadSubStateEnum +from src.AnkaiosSDK._protos import _ank_base + + +def test_interpret_state(): + workload_state = WorkloadExecutionState( + _ank_base.ExecutionState( + additionalInfo="Dummy information", + pending=_ank_base.PENDING_WAITING_TO_START + ) + ) + + assert workload_state.state == WorkloadStateEnum.Pending + assert workload_state.substate == WorkloadSubStateEnum.PENDING_WAITING_TO_START + assert workload_state.info == "Dummy information" + + +def test_interpret_state_error(): + with pytest.raises(ValueError, match="Invalid state for workload."): + WorkloadExecutionState( + _ank_base.ExecutionState( + additionalInfo="No state present" + ) + ) diff --git a/tests/WorkloadState/test_workload_instance_name.py b/tests/WorkloadState/test_workload_instance_name.py new file mode 100644 index 0000000..e64374d --- /dev/null +++ b/tests/WorkloadState/test_workload_instance_name.py @@ -0,0 +1,28 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from src.AnkaiosSDK import WorkloadInstanceName + + +def test_creation(): + workload_instance_name = WorkloadInstanceName( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234" + ) + assert workload_instance_name is not None + assert workload_instance_name.agent_name == "agent_Test" + assert workload_instance_name.workload_name == "workload_Test" + assert workload_instance_name.workload_id == "1234" + assert str(workload_instance_name) == "agent_Test.workload_Test.1234" diff --git a/tests/WorkloadState/test_workload_state.py b/tests/WorkloadState/test_workload_state.py new file mode 100644 index 0000000..484ae7d --- /dev/null +++ b/tests/WorkloadState/test_workload_state.py @@ -0,0 +1,31 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from src.AnkaiosSDK import WorkloadState +from src.AnkaiosSDK._protos import _ank_base + + +def test_creation(): + workload_state = WorkloadState( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234", + state=_ank_base.ExecutionState( + additionalInfo="Dummy information", + pending=_ank_base.PENDING_WAITING_TO_START + ) + ) + assert workload_state is not None + assert workload_state.execution_state is not None + assert workload_state.workload_instance_name is not None diff --git a/tests/WorkloadState/test_workload_state_collection.py b/tests/WorkloadState/test_workload_state_collection.py new file mode 100644 index 0000000..f47089a --- /dev/null +++ b/tests/WorkloadState/test_workload_state_collection.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from src.AnkaiosSDK import WorkloadStateCollection, WorkloadState, WorkloadExecutionState +from src.AnkaiosSDK._protos import _ank_base + + +def test_get(): + workload_state_collection = WorkloadStateCollection() + assert workload_state_collection is not None + assert len(workload_state_collection._workload_states) == 0 + + execution_state = _ank_base.ExecutionState( + additionalInfo="Dummy information", + pending=_ank_base.PENDING_WAITING_TO_START + ) + + workload_state = WorkloadState( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234", + state=execution_state + ) + + workload_state_collection.add_workload_state(workload_state) + assert len(workload_state_collection._workload_states) == 1 + assert workload_state_collection.get_as_list() == [workload_state] + + workload_states_dict = workload_state_collection.get_as_dict() + assert len(workload_states_dict) == 1 + assert "agent_Test" in workload_states_dict.keys() + assert len(workload_states_dict["agent_Test"]) == 1 + assert "workload_Test" in workload_states_dict["agent_Test"].keys() + assert len(workload_states_dict["agent_Test"]["workload_Test"]) == 1 + assert "1234" in workload_states_dict["agent_Test"]["workload_Test"].keys() + assert type(workload_states_dict["agent_Test"]["workload_Test"]["1234"]) == WorkloadExecutionState + +def test_from_proto(): + ank_workload_state = _ank_base.WorkloadStatesMap( + agentStateMap={"agent_Test": _ank_base.ExecutionsStatesOfWorkload( + wlNameStateMap={"workload_Test": _ank_base.ExecutionsStatesForId( + idStateMap={"1234": _ank_base.ExecutionState( + additionalInfo="Dummy information", + pending=_ank_base.PENDING_WAITING_TO_START + )} + )} + )} + ) + + workload_state_collection = WorkloadStateCollection() + workload_state_collection._from_proto(ank_workload_state) + assert len(workload_state_collection._workload_states) == 1 + assert len(workload_state_collection.get_as_list()) == 1 diff --git a/tests/WorkloadState/test_workload_state_enum.py b/tests/WorkloadState/test_workload_state_enum.py new file mode 100644 index 0000000..83502da --- /dev/null +++ b/tests/WorkloadState/test_workload_state_enum.py @@ -0,0 +1,22 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from src.AnkaiosSDK import WorkloadStateEnum + + +def test_get(): + field = "agentDisconnected" + workload_state = WorkloadStateEnum._get(field) + assert workload_state == WorkloadStateEnum.AgentDisconnected + assert str(workload_state) == "AgentDisconnected" diff --git a/tests/WorkloadState/test_workload_substate_enum.py b/tests/WorkloadState/test_workload_substate_enum.py new file mode 100644 index 0000000..7bf258d --- /dev/null +++ b/tests/WorkloadState/test_workload_substate_enum.py @@ -0,0 +1,50 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from src.AnkaiosSDK import WorkloadStateEnum, WorkloadSubStateEnum +from src.AnkaiosSDK._protos import _ank_base + + +@pytest.mark.parametrize("state, field, expected", [ + (WorkloadStateEnum.AgentDisconnected, _ank_base.AGENT_DISCONNECTED, WorkloadSubStateEnum.AGENT_DISCONNECTED), + (WorkloadStateEnum.Pending, _ank_base.PENDING_INITIAL, WorkloadSubStateEnum.PENDING_INITIAL), + (WorkloadStateEnum.Pending, _ank_base.PENDING_WAITING_TO_START, WorkloadSubStateEnum.PENDING_WAITING_TO_START), + (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING, WorkloadSubStateEnum.PENDING_STARTING), + (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING_FAILED, WorkloadSubStateEnum.PENDING_STARTING_FAILED), + (WorkloadStateEnum.Running, _ank_base.RUNNING_OK, WorkloadSubStateEnum.RUNNING_OK), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING, WorkloadSubStateEnum.STOPPING), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_WAITING_TO_STOP, WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_REQUESTED_AT_RUNTIME, WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_DELETE_FAILED, WorkloadSubStateEnum.STOPPING_DELETE_FAILED), + (WorkloadStateEnum.Succeeded, _ank_base.SUCCEEDED_OK, WorkloadSubStateEnum.SUCCEEDED_OK), + (WorkloadStateEnum.Failed, _ank_base.FAILED_EXEC_FAILED, WorkloadSubStateEnum.FAILED_EXEC_FAILED), + (WorkloadStateEnum.Failed, _ank_base.FAILED_UNKNOWN, WorkloadSubStateEnum.FAILED_UNKNOWN), + (WorkloadStateEnum.Failed, _ank_base.FAILED_LOST, WorkloadSubStateEnum.FAILED_LOST), + (WorkloadStateEnum.NotScheduled, _ank_base.NOT_SCHEDULED, WorkloadSubStateEnum.NOT_SCHEDULED), + (WorkloadStateEnum.Removed, _ank_base.REMOVED, WorkloadSubStateEnum.REMOVED) +]) +def test_get(state: WorkloadStateEnum, field: _ank_base, expected: WorkloadSubStateEnum): + assert WorkloadSubStateEnum._get(state, field) == expected + + +def test_get_error(): + with pytest.raises(ValueError): + WorkloadSubStateEnum._get(WorkloadStateEnum.AgentDisconnected, _ank_base.PENDING_WAITING_TO_START) + + +def test_sub_state2ank_base(): + substate = WorkloadSubStateEnum.FAILED_UNKNOWN + assert substate._sub_state2ank_base() == _ank_base.FAILED_UNKNOWN + assert str(substate) == "FAILED_UNKNOWN" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..561306d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/test_workload.py b/tests/test_workload.py new file mode 100644 index 0000000..e468e87 --- /dev/null +++ b/tests/test_workload.py @@ -0,0 +1,164 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from unittest.mock import patch, mock_open +from src.AnkaiosSDK import Workload, WorkloadBuilder +from src.AnkaiosSDK._protos import _ank_base + +@pytest.fixture +def workload(): + return Workload.builder() \ + .agent_name("agent_Test") \ + .runtime("runtime_test") \ + .restart_policy("NEVER") \ + .runtime_config("config_test") \ + .add_dependency("workload_test", "RUNNING") \ + .add_tag("key1", "value1") \ + .add_tag("key2", "value2") \ + .build() + +def test_builder(workload): + builder = workload.builder() + assert builder is not None + assert isinstance(builder, WorkloadBuilder) + +def test_update_fields(workload): + workload.update_agent_name("new_agent_Test") + assert workload._workload.agent == "new_agent_Test" + + workload.update_runtime("new_runtime_test") + assert workload._workload.runtime == "new_runtime_test" + + workload.update_runtime_config("new_config_test") + assert workload._workload.runtimeConfig == "new_config_test" + + with patch("builtins.open", mock_open(read_data="new_config_test_from_file")): + workload.update_runtime_config_from_file("new_config_test_from_file") + assert workload._workload.runtimeConfig == "new_config_test_from_file" + + with pytest.raises(ValueError): + workload.update_restart_policy("INVALID_POLICY") + workload.update_restart_policy("ON_FAILURE") + assert workload._workload.restartPolicy == _ank_base.ON_FAILURE + +def test_dependencies(workload): + assert len(workload.get_dependencies()) == 1 + + with pytest.raises(ValueError): + workload.add_dependency("other_workload_test", "DANCING") + + workload.add_dependency("other_workload_test", "SUCCEEDED") + assert len(workload.get_dependencies()) == 2 + + workload.add_dependency("another_workload_test", "FAILED") + + deps = workload.get_dependencies() + assert len(deps) == 3 + deps.pop("other_workload_test") + + workload.update_dependencies(deps) + assert len(workload.get_dependencies()) == 2 + +def test_tags(workload): + assert len(workload.get_tags()) == 2 + + # Allow duplicate tags + workload.add_tag("key1", "new_value1") + assert len(workload.get_tags()) == 3 + + tags = workload.get_tags() + tags = tags[1:] + workload.update_tags(tags) + + assert len(workload.get_tags()) == 2 + +def test_proto(workload): + proto = workload._to_proto() + assert proto is not None + assert proto.agent == "agent_Test" + assert proto.runtime == "runtime_test" + assert proto.restartPolicy == _ank_base.NEVER + assert proto.runtimeConfig == "config_test" + assert proto.dependencies.dependencies == {"workload_test": _ank_base.ADD_COND_RUNNING} + assert proto.tags == _ank_base.Tags(tags=[ + _ank_base.Tag(key="key1", value="value1"), + _ank_base.Tag(key="key2", value="value2") + ]) + + new_workload = Workload() + new_workload._from_proto(proto) + assert new_workload is not None + assert str(workload) == str(new_workload) + +@pytest.fixture +def builder(): + return WorkloadBuilder() + +def test_workload_fields(builder): + assert builder.agent_name("agent_Test") == builder + assert builder.wl_agent_name == "agent_Test" + + assert builder.runtime("runtime_test") == builder + assert builder.wl_runtime == "runtime_test" + + assert builder.runtime_config("config_test") == builder + assert builder.wl_runtime_config == "config_test" + + with patch("builtins.open", mock_open(read_data="config_test_from_file")): + assert builder.runtime_config_from_file("config_test_from_file") == builder + assert builder.wl_runtime_config == "config_test_from_file" + + assert builder.restart_policy("NEVER") == builder + assert builder.wl_restart_policy == "NEVER" + +def test_add_dependency(builder): + assert len(builder.dependencies) == 0 + + assert builder.add_dependency("workload_test", "RUNNING") == builder + assert builder.dependencies == {"workload_test": "RUNNING"} + + assert builder.add_dependency("workload_test_other", "RUNNING") == builder + assert builder.dependencies == {"workload_test": "RUNNING", "workload_test_other": "RUNNING"} + +def test_add_tag(builder): + assert len(builder.tags) == 0 + + assert builder.add_tag("key_test", "abc") == builder + assert builder.tags == [("key_test", "abc")] + + assert builder.add_tag("key_test", "bcd") == builder + assert builder.tags == [("key_test", "abc"), ("key_test", "bcd")] + +def test_build(builder): + with pytest.raises(ValueError, match="Workload can not be built without an agent name."): + builder.build() + builder.agent_name("agent_Test") + + with pytest.raises(ValueError, match="Workload can not be built without a runtime."): + builder.build() + builder.runtime("runtime_test") + + with pytest.raises(ValueError, match="Workload can not be built without a runtime configuration."): + builder.build() + builder.runtime_config("config_test") + + builder.restart_policy("NEVER") + builder.add_dependency("workload_test", "RUNNING") + builder.add_tag("key_test", "abc") + + workload = builder.build() + + assert workload is not None + assert isinstance(workload, Workload) \ No newline at end of file From b1245343b06f31123c02cc42c9e1b1e032078924 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 18 Sep 2024 13:24:36 +0300 Subject: [PATCH 02/72] Remove unnecessary files --- .gitignore | 10 +- dev/ankaios_sdk.py | 160 -------------- dev/export_proto.sh | 9 - dev/main_ch.py | 221 ------------------- dev/main_ch_new.py | 192 ---------------- dev/new_main.py | 12 - src/AnkaiosSDK.egg-info/PKG-INFO | 32 --- src/AnkaiosSDK.egg-info/SOURCES.txt | 24 -- src/AnkaiosSDK.egg-info/dependency_links.txt | 1 - src/AnkaiosSDK.egg-info/requires.txt | 3 - src/AnkaiosSDK.egg-info/top_level.txt | 1 - 11 files changed, 9 insertions(+), 656 deletions(-) delete mode 100644 dev/ankaios_sdk.py delete mode 100755 dev/export_proto.sh delete mode 100644 dev/main_ch.py delete mode 100644 dev/main_ch_new.py delete mode 100644 dev/new_main.py delete mode 100644 src/AnkaiosSDK.egg-info/PKG-INFO delete mode 100644 src/AnkaiosSDK.egg-info/SOURCES.txt delete mode 100644 src/AnkaiosSDK.egg-info/dependency_links.txt delete mode 100644 src/AnkaiosSDK.egg-info/requires.txt delete mode 100644 src/AnkaiosSDK.egg-info/top_level.txt diff --git a/.gitignore b/.gitignore index 97a5e1a..145128e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ +# Python ignores __pycache__/ *.py[cod] *$py.class + +# Reports directory reports/ + +# Tools caches .coverage -.pytest_cache/ \ No newline at end of file +.pytest_cache/ + +# Build directory +src/AnkaiosSDK.egg-info \ No newline at end of file diff --git a/dev/ankaios_sdk.py b/dev/ankaios_sdk.py deleted file mode 100644 index c334076..0000000 --- a/dev/ankaios_sdk.py +++ /dev/null @@ -1,160 +0,0 @@ -import ank_base_pb2 as ank_base -import control_api_pb2 as control_api -from google.protobuf.internal.encoder import _VarintBytes -from google.protobuf.internal.decoder import _DecodeVarint -import threading -import time -import logging - - -"""Ankaios log levels.""" -class AnkaiosLogLevel: - FATAL = logging.FATAL - ERROR = logging.ERROR - WARN = logging.WARN - INFO = logging.INFO - DEBUG = logging.DEBUG - - -""" -Ankaios SDK for Python to interact with the Ankaios control interface. - -This SDK provides the functionality to interact with the Ankaios control interface -by sending requests to add a new workload dynamically and to request the workload states. -""" -class Ankaios: - ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" - WAITING_TIME_IN_SEC = 5 - REQUEST_ID = "dynamic_nginx@python_control_interface" - - def __init__(self) -> None: - """Initialize the Ankaios object.""" - self.logger = None - self.read_thread = None - - self.create_logger() - - def start_read(self): - """Starts a thread to read from the control interface input fifo.""" - self.read_thread = threading.Thread(target=self._read_from_control_interface) - self.read_thread.start() - - def join_read(self): - """Joins the read thread.""" - self.read_thread.join() - - def create_logger(self): - """Create a logger with custom format and default log level.""" - formatter = logging.Formatter('%(asctime)s %(message)s', datefmt="%FT%TZ") - self.logger = logging.getLogger("Ankaios logger") - handler = logging.StreamHandler() - handler.setFormatter(formatter) - self.logger.addHandler(handler) - self.change_logger_level(AnkaiosLogLevel.INFO) - - def set_logger_level(self, level: AnkaiosLogLevel): - self.logger.setLevel(level) - - def create_request_to_add_new_workload(self): - """Create the Request containing an UpdateStateRequest - that contains the details for adding the new workload and - the update mask to add only the new workload. - """ - - return control_api.ToAnkaios( - request=ank_base.Request( - requestId=self.REQUEST_ID, - updateStateRequest=ank_base.UpdateStateRequest( - newState=ank_base.CompleteState( - desiredState=ank_base.State( - apiVersion="v0.1", - workloads=ank_base.WorkloadMap(workloads={ - "dynamic_nginx": ank_base.Workload( - agent="agent_A", - runtime="podman", - restartPolicy=ank_base.NEVER, - runtimeConfig="image: docker.io/library/nginx\ncommandOptions: [\"-p\", \"8080:80\"]") - }) - ) - ), - updateMask=["desiredState.workloads.dynamic_nginx"] - ) - ) - ) - - def create_request_for_complete_state(self): - """Create a Request to request the CompleteState - for querying the workload states. - """ - - return control_api.ToAnkaios( - request=ank_base.Request( - completeStateRequest=ank_base.CompleteStateRequest( - fieldMask=["workloadStates.agent_A.dynamic_nginx"] - ), - requestId=self.REQUEST_ID, - ) - ) - - def _read_from_control_interface(self): - """Reads from the control interface input fifo and prints the workload states.""" - - with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: - - while True: - varint_buffer = b'' # Buffer for reading in the byte size of the proto msg - while True: - next_byte = f.read(1) # Consume byte for byte - if not next_byte: - break - varint_buffer += next_byte - if next_byte[0] & 0b10000000 == 0: # Stop if the most significant bit is 0 (indicating the last byte of the varint) - break - msg_len, _ = _DecodeVarint(varint_buffer, 0) # Decode the varint and receive the proto msg length - - msg_buf = b'' # Buffer for the proto msg itself - for _ in range(msg_len): - next_byte = f.read(1) # Read exact amount of byte according to the calculated proto msg length - if not next_byte: - break - msg_buf += next_byte - - from_ankaios = control_api.FromAnkaios() - try: - from_ankaios.ParseFromString(msg_buf) # Deserialize the received proto msg - except Exception as e: - self.logger.info(f"Invalid response, parsing error: '{e}'") - continue - - request_id = from_ankaios.response.requestId - if from_ankaios.response.requestId == self.REQUEST_ID: - self.logger.info(f"Receiving Response containing the workload states of the current state:\nFromServer {{\n{from_ankaios}}}\n") - else: - self.logger.info(f"RequestId does not match. Skipping messages from requestId: {request_id}") - - def write_to_control_interface(self): - """Writes a Request into the control interface output fifo - to add the new workload dynamically and every x sec according to WAITING_TIME_IN_SEC - another Request to request the workload states. - """ - - with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: - update_workload_request = self.create_request_to_add_new_workload() - update_workload_request_byte_len = update_workload_request.ByteSize() # Length of the msg - proto_update_workload_request_msg = update_workload_request.SerializeToString() # Serialized proto msg - - self.logger.info(f'Sending Request containing details for adding the dynamic workload \"dynamic_nginx\":\nToServer {{\n{update_workload_request}}}\n') - f.write(_VarintBytes(update_workload_request_byte_len)) # Send the byte length of the proto msg - f.write(proto_update_workload_request_msg) # Send the proto msg itself - f.flush() - - request_complete_state = self.create_request_for_complete_state() - request_complete_state_byte_len = request_complete_state.ByteSize() # Length of the msg - proto_request_complete_state_msg = request_complete_state.SerializeToString() # Serialized proto msg - - while True: - self.logger.info(f"Sending Request containing details for requesting all workload states:\nToServer {{{request_complete_state}}}\n") - f.write(_VarintBytes(request_complete_state_byte_len)) # Send the byte length of the proto msg - f.write(proto_request_complete_state_msg) # Send the proto msg itself - f.flush() - time.sleep(self.WAITING_TIME_IN_SEC) # Wait according to WAITING_TIME_IN_SEC until sending the next Request to Ankaios to avoid spamming... diff --git a/dev/export_proto.sh b/dev/export_proto.sh deleted file mode 100755 index 5164adf..0000000 --- a/dev/export_proto.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -# Set the ANKAIOS project root directory -ANKAIOS_ROOT="/workspace/ankaios" - -cp $ANKAIOS_ROOT/api/proto/ank_base.proto $ANKAIOS_ROOT/python_sdk/ankaios_sdk/protos -cp $ANKAIOS_ROOT/api/proto/control_api.proto $ANKAIOS_ROOT/python_sdk/ankaios_sdk/protos - -protoc --python_out=$ANKAIOS_ROOT/python_sdk/ankaios_sdk --proto_path=$ANKAIOS_ROOT/python_sdk/ankaios_sdk/protos ank_base.proto control_api.proto diff --git a/dev/main_ch.py b/dev/main_ch.py deleted file mode 100644 index fd77263..0000000 --- a/dev/main_ch.py +++ /dev/null @@ -1,221 +0,0 @@ -import os -import sys -import logging -from uuid import uuid4 -from threading import Thread -from queue import Queue -from google.protobuf.internal.encoder import _VarintBytes -from google.protobuf.internal.decoder import _DecodeVarint -import ankaios_pb2 as ank -from config import ANKAIOS_CONTROL_INTERFACE_BASE_PATH -from logger import get_logger - -# pylint: disable=no-member - -logger: logging.Logger = get_logger() - - -class Ankaios: - """ - Class for interacting with the Ankaios server - - This class access the FIFO files of the Ankaios control interface. - Hence only one object of this class should be created. - """ - - def __init__(self): - """ - Creates a new Ankaios object to interact with the control interface - :param logger: The logger to object should use for logging - :type logger : logging.Logger - """ - self._response_queues = {} - self._read_messages_thread = Thread(target=self._read_messages, daemon=True) - self._read_messages_thread.start() - - if not os.path.exists( - f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input" - ) or not os.path.exists(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output"): - logger.error("no connection to Ankaios control interface") - sys.exit(1) - - self._request_queue = Queue() - self._send_requests_thread = Thread(target=self._send_requests, daemon=True) - self._send_requests_thread.start() - logger.debug("Created object of %s", str(self.__class__.__name__)) - - def __del__(self) -> None: - logger.debug("Destroyed object of %s", str(self.__class__.__name__)) - - def _read_messages(self): - with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: - - while True: - varint_buffer = ( - b"" # Buffer for reading in the byte size of the proto msg - ) - while True: - next_byte = f.read(1) # Consume byte for byte - if not next_byte: - break - varint_buffer += next_byte - if ( - next_byte[0] & 0b10000000 == 0 - ): # Stop if the most significant bit is 0 (indicating the last byte of the varint) - break - msg_len, _ = _DecodeVarint( - varint_buffer, 0 - ) # Decode the varint and receive the proto msg length - - msg_buf = b"" # Buffer for the proto msg itself - while msg_len > 0: - next_bytes = f.read( - msg_len - ) # Read exact amount of byte according to the calculated proto msg length - if not next_bytes: - break - msg_len -= len(next_bytes) - msg_buf += next_bytes - - from_server = ank.FromServer() - try: - from_server.ParseFromString( - msg_buf - ) # Deserialize the received proto msg - except Exception as e: - logger.debug("Invalid response, parsing error: '%s'", e) - continue - - if from_server.response is not None: - response = from_server.response - request_id = response.requestId - response_queue = self._response_queues.get(request_id) - if response_queue is not None: - del self._response_queues[request_id] - response_queue.put(response) - else: - logger.debug("Response for unknown RequestId: %s", request_id) - else: - logger.debug("Received None as response message: %s", from_server) - - def _send_requests(self): - with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: - while True: - request = self._request_queue.get() - request_byte_len = request.ByteSize() # Length of the msg - proto_request = request.SerializeToString() # Serialized proto msg - logger.debug("Sending Request: %s\n", request) - f.write( - _VarintBytes(request_byte_len) - ) # Send the byte length of the proto msg - f.write(proto_request) # Send the proto msg itself - f.flush() - - def get_state(self): - """ - Get the current Ankaios state - :returns: the current Ankaios server state - :rtype: ank.CompleteState - """ - request_complete_state = ank.Request( - completeStateRequest=ank.CompleteStateRequest(fieldMask=["workloadStates"]), - ) - return self._execute_request(request_complete_state).completeState - - def delete_workload(self, workload_name): - """ - Delete a workload from Ankaios - :param workload_name: The name of the workload - :type workload_name : str - :returns: server response to SetState request - :rtype: ank.FromServer - - Example - ------- - - :: - ankaios.delete_workload("nginx") - """ - update_state_request = ank.Request( - updateStateRequest=ank.UpdateStateRequest( - newState=ank.CompleteState(desiredState=ank.State(apiVersion="v0.1")), - updateMask=[f"desiredState.workloads.{workload_name}"], - ) - ) - return self._execute_request(update_state_request) - - def add_workload( - self, - workload_name, - agent, - runtime, - runtime_config, - restart_policy, - tags, - dependencies, - ): - """Creates a new Ankaios workload - :param workload_name: The name of the workload - :type workload_name : str - :param agent: The agent the workload should run on - :type agent : str - :param runtime: The runtime to used - :type runtime_config : str - :param runtime: The runtime specific configuration - :type runtime_config : str - :returns: server response to SetState request - :rtype: ank.FromServer - - Example - ------- - - :: - ankaios.add_workload( - workload_name="nginx", - agent="agent_A", - runtime_config=yaml.dump( - { - "image": "docker.io/library/nginx:latest", - "commandOptions": ["--net=host"], - } - ), - ) - """ - update_state_request = ank.Request( - updateStateRequest=ank.UpdateStateRequest( - newState=ank.CompleteState( - desiredState=ank.State( - apiVersion="v0.1", - workloads={ - workload_name: ank.Workload( - agent=agent, - runtime=runtime, - runtimeConfig=runtime_config, - tags=[ - ank.Tag(key=key, value=value) - for key, value in tags.items() - ], - dependencies={ - name: ank.AddCondition.Value(condition) - for name, condition in dependencies.items() - }, - restartPolicy=ank.RestartPolicy.Value(restart_policy), - ) - }, - ) - ), - updateMask=[f"desiredState.workloads.{workload_name}"], - ) - ) - return self._execute_request(update_state_request) - - def _execute_request(self, request): - request_id = str(uuid4()) - response_queue = Queue() - self._response_queues[request_id] = response_queue - request.requestId = request_id - - to_server = ank.ToServer(request=request) - - self._request_queue.put(to_server) - return response_queue.get() diff --git a/dev/main_ch_new.py b/dev/main_ch_new.py deleted file mode 100644 index dd49344..0000000 --- a/dev/main_ch_new.py +++ /dev/null @@ -1,192 +0,0 @@ -import ankaios_pb2 as ank -from google.protobuf.internal.encoder import _VarintBytes -from google.protobuf.internal.decoder import _DecodeVarint -from uuid import uuid4 -from threading import Thread -from queue import Queue - - -ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" - - -class Ankaios: - """ - Class for interacting with the Ankaios server - - This class access the FIFO files of the Ankaios control interface. - Hence only one object of this class should be created. - """ - - def __init__(self, logger): - """ - Creates a new Ankaios object to interact with the control interface - :param logger: The logger to object should use for logging - :type logger : logging.Logger - """ - self.logger = logger - self._response_queues = {} - self._read_messages_thread = Thread(target=self._read_messages, daemon=True) - self._read_messages_thread.start() - - self._request_queue = Queue() - self._send_requests_thread = Thread(target=self._send_requests, daemon=True) - self._send_requests_thread.start() - - def _read_messages(self): - with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: - - while True: - varint_buffer = ( - b"" # Buffer for reading in the byte size of the proto msg - ) - while True: - next_byte = f.read(1) # Consume byte for byte - if not next_byte: - break - varint_buffer += next_byte - if ( - next_byte[0] & 0b10000000 == 0 - ): # Stop if the most significant bit is 0 (indicating the last byte of the varint) - break - msg_len, _ = _DecodeVarint( - varint_buffer, 0 - ) # Decode the varint and receive the proto msg length - - msg_buf = b"" # Buffer for the proto msg itself - while msg_len > 0: - next_bytes = f.read( - msg_len - ) # Read exact amount of byte according to the calculated proto msg length - if not next_bytes: - break - msg_len -= len(next_bytes) - msg_buf += next_bytes - - from_server = ank.FromServer() - try: - from_server.ParseFromString( - msg_buf - ) # Deserialize the received proto msg - except Exception as e: - self.logger.info(f"Invalid response, parsing error: '{e}'") - continue - - if from_server.response is not None: - response = from_server.response - request_id = response.requestId - response_queue = self._response_queues.get(request_id) - if response_queue is not None: - del self._response_queues[request_id] - response_queue.put(response) - else: - self.logger.info( - f"Response for unknown RequestId: {request_id}" - ) - else: - self.logger.info( - f"Received None as response message: {from_server}" - ) - - def _send_requests(self): - with open(f"{ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: - while True: - request = self._request_queue.get() - request_byte_len = request.ByteSize() # Length of the msg - proto_request = request.SerializeToString() # Serialized proto msg - self.logger.info(f"Sending Request: {{{request}}}\n") - f.write( - _VarintBytes(request_byte_len) - ) # Send the byte length of the proto msg - f.write(proto_request) # Send the proto msg itself - f.flush() - - def get_state(self): - """ - Get the current Ankaios state - :returns: the current Ankaios server state - :rtype: ank.CompleteState - """ - request_complete_state = request = ank.Request( - completeStateRequest=ank.CompleteStateRequest(fieldMask=["workloadStates"]), - ) - return self._execute_request(request_complete_state).completeState - - def delete_workload(self, workload_name): - """ - Delete a workload from Ankaios - :param workload_name: The name of the workload - :type workload_name : str - :returns: server response to SetState request - :rtype: ank.FromServer - - Example - ------- - - :: - ankaios.delete_workload("nginx") - """ - update_state_request = request = ank.Request( - updateStateRequest=ank.UpdateStateRequest( - newState=ank.CompleteState(desiredState=ank.State(apiVersion="v0.1")), - updateMask=[f"desiredState.workloads.{workload_name}"], - ) - ) - return self._execute_request(update_state_request) - - def add_workload(self, workload_name, agent, runtime="podman", runtime_config=""): - """Creates a new Ankaios workload - :param workload_name: The name of the workload - :type workload_name : str - :param agent: The agent the workload should run on - :type agent : str - :param runtime: The runtime to used - :type runtime_config : str - :param runtime: The runtime specific configuration - :type runtime_config : str - :returns: server response to SetState request - :rtype: ank.FromServer - - Example - ------- - - :: - ankaios.add_workload( - workload_name="nginx", - agent="agent_A", - runtime_config=yaml.dump( - { - "image": "docker.io/library/nginx:latest", - "commandOptions": ["--net=host"], - } - ), - ) - """ - update_state_request = ank.Request( - updateStateRequest=ank.UpdateStateRequest( - newState=ank.CompleteState( - desiredState=ank.State( - apiVersion="v0.1", - workloads={ - workload_name: ank.Workload( - agent=agent, - runtime=runtime, - runtimeConfig=runtime_config, - ) - }, - ) - ), - updateMask=[f"desiredState.workloads.{workload_name}"], - ) - ) - return self._execute_request(update_state_request) - - def _execute_request(self, request): - request_id = str(uuid4()) - response_queue = Queue() - self._response_queues[request_id] = response_queue - request.requestId = request_id - - to_server = ank.ToServer(request=request) - - self._request_queue.put(to_server) - return response_queue.get() diff --git a/dev/new_main.py b/dev/new_main.py deleted file mode 100644 index c58ade8..0000000 --- a/dev/new_main.py +++ /dev/null @@ -1,12 +0,0 @@ -from ankaios_sdk import Ankaios, AnkaiosLogLevel - - -if __name__ == "__main__": - ank = Ankaios() - ank.set_logger_level(AnkaiosLogLevel.INFO) - - ank.start_read() - ank.write_to_control_interface() - ank.join_read() - - exit(0) diff --git a/src/AnkaiosSDK.egg-info/PKG-INFO b/src/AnkaiosSDK.egg-info/PKG-INFO deleted file mode 100644 index c791182..0000000 --- a/src/AnkaiosSDK.egg-info/PKG-INFO +++ /dev/null @@ -1,32 +0,0 @@ -Metadata-Version: 2.1 -Name: AnkaiosSDK -Version: 0.1.0 -Summary: Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. -Home-page: https://eclipse-ankaios.github.io/ankaios/latest/ -Author: Elektrobit Automotive GmbH and Ankaios contributors -License: Apache-2.0 -Project-URL: Documentation, https://eclipse-ankaios.github.io/ankaios/latest/ -Project-URL: Source, https://github.com/eclipse-ankaios/ank-sdk-python -Project-URL: Bug Tracker, https://github.com/eclipse-ankaios/ank-sdk-python/issues -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3 -Classifier: License :: OSI Approved :: Apache Software License -Classifier: Operating System :: OS Independent -Requires-Python: >=3.6 -Description-Content-Type: text/markdown -License-File: LICENSE - -# Ankaios Python SDK - -Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. - -# To do -- Finish the marked TODO methods from Ankaios.py -- Fix protobuf versioning warning -- Improve the WorkloadState handling -- Add utest for 100% coverage (currently done Workload and WorkloadState) -- Fix Lint (currently done Workload.py) -- Improve requirements, requirements-dev and setup.py:install_requires -- Enable github workflow verifications -- Add to pip wheel - diff --git a/src/AnkaiosSDK.egg-info/SOURCES.txt b/src/AnkaiosSDK.egg-info/SOURCES.txt deleted file mode 100644 index df6abe5..0000000 --- a/src/AnkaiosSDK.egg-info/SOURCES.txt +++ /dev/null @@ -1,24 +0,0 @@ -LICENSE -MANIFEST.in -README.md -requirements.txt -setup.cfg -setup.py -src/AnkaiosSDK/Ankaios.py -src/AnkaiosSDK/__init__.py -src/AnkaiosSDK.egg-info/PKG-INFO -src/AnkaiosSDK.egg-info/SOURCES.txt -src/AnkaiosSDK.egg-info/dependency_links.txt -src/AnkaiosSDK.egg-info/requires.txt -src/AnkaiosSDK.egg-info/top_level.txt -src/AnkaiosSDK/_components/CompleteState.py -src/AnkaiosSDK/_components/Request.py -src/AnkaiosSDK/_components/Response.py -src/AnkaiosSDK/_components/Workload.py -src/AnkaiosSDK/_components/WorkloadState.py -src/AnkaiosSDK/_components/__init__.py -src/AnkaiosSDK/_protos/__init__.py -src/AnkaiosSDK/_protos/ank_base_pb2.py -src/AnkaiosSDK/_protos/ank_base_pb2_grpc.py -src/AnkaiosSDK/_protos/control_api_pb2.py -src/AnkaiosSDK/_protos/control_api_pb2_grpc.py \ No newline at end of file diff --git a/src/AnkaiosSDK.egg-info/dependency_links.txt b/src/AnkaiosSDK.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/AnkaiosSDK.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/AnkaiosSDK.egg-info/requires.txt b/src/AnkaiosSDK.egg-info/requires.txt deleted file mode 100644 index 141c816..0000000 --- a/src/AnkaiosSDK.egg-info/requires.txt +++ /dev/null @@ -1,3 +0,0 @@ -grpcio-tools -protobuf -setuptools diff --git a/src/AnkaiosSDK.egg-info/top_level.txt b/src/AnkaiosSDK.egg-info/top_level.txt deleted file mode 100644 index 467f9bb..0000000 --- a/src/AnkaiosSDK.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -AnkaiosSDK From fbac2f93e278f24cdc2fa174194102adf7a714f3 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 18 Sep 2024 13:58:13 +0300 Subject: [PATCH 03/72] Move AnkaiosSDK out of src folder --- .gitignore | 2 +- {src/AnkaiosSDK => AnkaiosSDK}/Ankaios.py | 0 {src/AnkaiosSDK => AnkaiosSDK}/__init__.py | 0 .../_components/CompleteState.py | 0 .../AnkaiosSDK => AnkaiosSDK}/_components/Request.py | 0 .../_components/Response.py | 0 .../_components/Workload.py | 0 .../_components/WorkloadState.py | 0 .../_components/__init__.py | 0 {src/AnkaiosSDK => AnkaiosSDK}/_protos/.gitignore | 0 {src/AnkaiosSDK => AnkaiosSDK}/_protos/__init__.py | 6 ++++++ .../AnkaiosSDK => AnkaiosSDK}/_protos/ank_base.proto | 0 .../_protos/control_api.proto | 0 MANIFEST.in | 4 ++-- ci_dev.yml | 4 ++-- run_tests.py | 12 +++++++----- setup.cfg | 2 +- setup.py | 6 +++--- tests/WorkloadState/test_workload_execution_state.py | 4 ++-- tests/WorkloadState/test_workload_instance_name.py | 2 +- tests/WorkloadState/test_workload_state.py | 4 ++-- .../WorkloadState/test_workload_state_collection.py | 4 ++-- tests/WorkloadState/test_workload_state_enum.py | 2 +- tests/WorkloadState/test_workload_substate_enum.py | 4 ++-- tests/test_workload.py | 4 ++-- 25 files changed, 34 insertions(+), 26 deletions(-) rename {src/AnkaiosSDK => AnkaiosSDK}/Ankaios.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/__init__.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_components/CompleteState.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_components/Request.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_components/Response.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_components/Workload.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_components/WorkloadState.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_components/__init__.py (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_protos/.gitignore (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_protos/__init__.py (75%) rename {src/AnkaiosSDK => AnkaiosSDK}/_protos/ank_base.proto (100%) rename {src/AnkaiosSDK => AnkaiosSDK}/_protos/control_api.proto (100%) diff --git a/.gitignore b/.gitignore index 145128e..7d6e3a4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ reports/ .pytest_cache/ # Build directory -src/AnkaiosSDK.egg-info \ No newline at end of file +AnkaiosSDK.egg-info \ No newline at end of file diff --git a/src/AnkaiosSDK/Ankaios.py b/AnkaiosSDK/Ankaios.py similarity index 100% rename from src/AnkaiosSDK/Ankaios.py rename to AnkaiosSDK/Ankaios.py diff --git a/src/AnkaiosSDK/__init__.py b/AnkaiosSDK/__init__.py similarity index 100% rename from src/AnkaiosSDK/__init__.py rename to AnkaiosSDK/__init__.py diff --git a/src/AnkaiosSDK/_components/CompleteState.py b/AnkaiosSDK/_components/CompleteState.py similarity index 100% rename from src/AnkaiosSDK/_components/CompleteState.py rename to AnkaiosSDK/_components/CompleteState.py diff --git a/src/AnkaiosSDK/_components/Request.py b/AnkaiosSDK/_components/Request.py similarity index 100% rename from src/AnkaiosSDK/_components/Request.py rename to AnkaiosSDK/_components/Request.py diff --git a/src/AnkaiosSDK/_components/Response.py b/AnkaiosSDK/_components/Response.py similarity index 100% rename from src/AnkaiosSDK/_components/Response.py rename to AnkaiosSDK/_components/Response.py diff --git a/src/AnkaiosSDK/_components/Workload.py b/AnkaiosSDK/_components/Workload.py similarity index 100% rename from src/AnkaiosSDK/_components/Workload.py rename to AnkaiosSDK/_components/Workload.py diff --git a/src/AnkaiosSDK/_components/WorkloadState.py b/AnkaiosSDK/_components/WorkloadState.py similarity index 100% rename from src/AnkaiosSDK/_components/WorkloadState.py rename to AnkaiosSDK/_components/WorkloadState.py diff --git a/src/AnkaiosSDK/_components/__init__.py b/AnkaiosSDK/_components/__init__.py similarity index 100% rename from src/AnkaiosSDK/_components/__init__.py rename to AnkaiosSDK/_components/__init__.py diff --git a/src/AnkaiosSDK/_protos/.gitignore b/AnkaiosSDK/_protos/.gitignore similarity index 100% rename from src/AnkaiosSDK/_protos/.gitignore rename to AnkaiosSDK/_protos/.gitignore diff --git a/src/AnkaiosSDK/_protos/__init__.py b/AnkaiosSDK/_protos/__init__.py similarity index 75% rename from src/AnkaiosSDK/_protos/__init__.py rename to AnkaiosSDK/_protos/__init__.py index 7410f15..4938f8c 100644 --- a/src/AnkaiosSDK/_protos/__init__.py +++ b/AnkaiosSDK/_protos/__init__.py @@ -12,6 +12,12 @@ # # SPDX-License-Identifier: Apache-2.0 +# TODO remove this line after the issue is fixed +# https://github.com/grpc/grpc/issues/37609 +# https://github.com/protocolbuffers/protobuf/issues/18096 +import warnings +warnings.filterwarnings("ignore", ".*obsolete", UserWarning, "google.protobuf.runtime_version") + try: import AnkaiosSDK._protos.ank_base_pb2 as _ank_base import AnkaiosSDK._protos.control_api_pb2 as _control_api diff --git a/src/AnkaiosSDK/_protos/ank_base.proto b/AnkaiosSDK/_protos/ank_base.proto similarity index 100% rename from src/AnkaiosSDK/_protos/ank_base.proto rename to AnkaiosSDK/_protos/ank_base.proto diff --git a/src/AnkaiosSDK/_protos/control_api.proto b/AnkaiosSDK/_protos/control_api.proto similarity index 100% rename from src/AnkaiosSDK/_protos/control_api.proto rename to AnkaiosSDK/_protos/control_api.proto diff --git a/MANIFEST.in b/MANIFEST.in index 69c68a5..71b73fd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ -# Include all .proto files in the src/protos directory -recursive-include src/protos *.proto +# Include all .proto files in the _protos directory +recursive-include AnkaiosSDK/_protos *.proto # Include the README file include README.md diff --git a/ci_dev.yml b/ci_dev.yml index 4271d55..6bef726 100644 --- a/ci_dev.yml +++ b/ci_dev.yml @@ -27,7 +27,7 @@ jobs: - name: Run pylint run: | - pylint src tests > pylint_report.txt + pylint AnkaiosSDK tests > pylint_report.txt - name: Upload pylint report uses: actions/upload-artifact@v3 @@ -54,7 +54,7 @@ jobs: - name: Run tests with coverage run: | - coverage run -m unittest discover -s src + coverage run -m unittest discover -s AnkaiosSDK coverage report coverage html diff --git a/run_tests.py b/run_tests.py index 2347df7..b055b07 100644 --- a/run_tests.py +++ b/run_tests.py @@ -18,6 +18,7 @@ import subprocess +PROJECT_NAME = "AnkaiosSDK" REPORT_DIR = "reports" COVERAGE_DIR = os.path.join(REPORT_DIR, "coverage") UTEST_DIR = os.path.join(REPORT_DIR, "utest") @@ -29,6 +30,7 @@ def run_pytest_utest(): pytest.main([ '--junitxml={}'.format(os.path.join(UTEST_DIR, 'utest_report.xml')), 'tests', + # '-p', 'no:warnings', '-vv' ]) @@ -36,12 +38,12 @@ def run_pytest_utest(): def run_pytest_cov(): os.makedirs(COVERAGE_DIR, exist_ok=True) pytest.main([ - '--cov=src', - # '--cov-config=.coveragerc', + '--cov={}'.format(PROJECT_NAME), '--cov-report=html:{}'.format(os.path.join(COVERAGE_DIR, 'html')), '--cov-report=xml:{}'.format(os.path.join(COVERAGE_DIR, 'cov_report.xml')), '--cov-report=term', 'tests', + '-p', 'no:warnings', '-vv' ]) @@ -49,7 +51,7 @@ def run_pytest_cov(): def run_pylint(): os.makedirs(PYLINT_DIR, exist_ok=True) result = subprocess.run([ - 'pylint', 'src', 'tests', '--rcfile=.pylintrc', '--output-format=parseable' + 'pylint', PROJECT_NAME, 'tests', '--rcfile=.pylintrc', '--output-format=parseable' ], capture_output=True, text=True) pylint_output = result.stdout @@ -69,8 +71,7 @@ def run_pylint(): if __name__ == "__main__": - os.makedirs(REPORT_DIR, exist_ok=True) - parser = argparse.ArgumentParser(description='Run tests for AnkaiosSDK Python package') + parser = argparse.ArgumentParser(description=f'Run tests for {PROJECT_NAME} Python package') parser.add_argument('-c', '--cov', action='store_true', help='Run coverage') parser.add_argument('-u', '--utest', action='store_true', help='Run unit tests') parser.add_argument('-l', '--lint', action='store_true', help='Run pylint') @@ -80,6 +81,7 @@ def run_pylint(): if not any([args.cov, args.utest, args.lint, args.all]): parser.print_help() exit(0) + os.makedirs(REPORT_DIR, exist_ok=True) if args.cov or args.all: run_pytest_cov() diff --git a/setup.cfg b/setup.cfg index c736525..09a517f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ testpaths = tests [coverage:run] -source = src +source = . omit = */__init__.py */_protos/*_pb2.py diff --git a/setup.py b/setup.py index 4d9e0f7..08bf547 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def generate_protos(): """Generate python protobuf files from the proto files.""" - protos_dir = f"src/{PROJECT_NAME}/_protos" + protos_dir = f"{PROJECT_NAME}/_protos" proto_files = ["ank_base.proto", "control_api.proto"] for proto_file in proto_files: @@ -67,8 +67,8 @@ def generate_protos(): long_description_content_type="text/markdown", url="https://eclipse-ankaios.github.io/ankaios/latest/", python_requires='>=3.6', - package_dir={'': 'src'}, - packages=find_packages(where="src"), + package_dir={'': '.'}, + packages=find_packages(where="."), include_package_data=True, classifiers=[ "Programming Language :: Python :: 3", diff --git a/tests/WorkloadState/test_workload_execution_state.py b/tests/WorkloadState/test_workload_execution_state.py index 4dc6979..139a680 100644 --- a/tests/WorkloadState/test_workload_execution_state.py +++ b/tests/WorkloadState/test_workload_execution_state.py @@ -13,8 +13,8 @@ # SPDX-License-Identifier: Apache-2.0 import pytest -from src.AnkaiosSDK import WorkloadExecutionState, WorkloadStateEnum, WorkloadSubStateEnum -from src.AnkaiosSDK._protos import _ank_base +from AnkaiosSDK import WorkloadExecutionState, WorkloadStateEnum, WorkloadSubStateEnum +from AnkaiosSDK._protos import _ank_base def test_interpret_state(): diff --git a/tests/WorkloadState/test_workload_instance_name.py b/tests/WorkloadState/test_workload_instance_name.py index e64374d..1c5f845 100644 --- a/tests/WorkloadState/test_workload_instance_name.py +++ b/tests/WorkloadState/test_workload_instance_name.py @@ -12,7 +12,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from src.AnkaiosSDK import WorkloadInstanceName +from AnkaiosSDK import WorkloadInstanceName def test_creation(): diff --git a/tests/WorkloadState/test_workload_state.py b/tests/WorkloadState/test_workload_state.py index 484ae7d..5e0a10d 100644 --- a/tests/WorkloadState/test_workload_state.py +++ b/tests/WorkloadState/test_workload_state.py @@ -12,8 +12,8 @@ # # SPDX-License-Identifier: Apache-2.0 -from src.AnkaiosSDK import WorkloadState -from src.AnkaiosSDK._protos import _ank_base +from AnkaiosSDK import WorkloadState +from AnkaiosSDK._protos import _ank_base def test_creation(): diff --git a/tests/WorkloadState/test_workload_state_collection.py b/tests/WorkloadState/test_workload_state_collection.py index f47089a..d8c7624 100644 --- a/tests/WorkloadState/test_workload_state_collection.py +++ b/tests/WorkloadState/test_workload_state_collection.py @@ -13,8 +13,8 @@ # SPDX-License-Identifier: Apache-2.0 import pytest -from src.AnkaiosSDK import WorkloadStateCollection, WorkloadState, WorkloadExecutionState -from src.AnkaiosSDK._protos import _ank_base +from AnkaiosSDK import WorkloadStateCollection, WorkloadState, WorkloadExecutionState +from AnkaiosSDK._protos import _ank_base def test_get(): diff --git a/tests/WorkloadState/test_workload_state_enum.py b/tests/WorkloadState/test_workload_state_enum.py index 83502da..b1ee29b 100644 --- a/tests/WorkloadState/test_workload_state_enum.py +++ b/tests/WorkloadState/test_workload_state_enum.py @@ -12,7 +12,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from src.AnkaiosSDK import WorkloadStateEnum +from AnkaiosSDK import WorkloadStateEnum def test_get(): diff --git a/tests/WorkloadState/test_workload_substate_enum.py b/tests/WorkloadState/test_workload_substate_enum.py index 7bf258d..e23669a 100644 --- a/tests/WorkloadState/test_workload_substate_enum.py +++ b/tests/WorkloadState/test_workload_substate_enum.py @@ -13,8 +13,8 @@ # SPDX-License-Identifier: Apache-2.0 import pytest -from src.AnkaiosSDK import WorkloadStateEnum, WorkloadSubStateEnum -from src.AnkaiosSDK._protos import _ank_base +from AnkaiosSDK import WorkloadStateEnum, WorkloadSubStateEnum +from AnkaiosSDK._protos import _ank_base @pytest.mark.parametrize("state, field, expected", [ diff --git a/tests/test_workload.py b/tests/test_workload.py index e468e87..a00d75b 100644 --- a/tests/test_workload.py +++ b/tests/test_workload.py @@ -14,8 +14,8 @@ import pytest from unittest.mock import patch, mock_open -from src.AnkaiosSDK import Workload, WorkloadBuilder -from src.AnkaiosSDK._protos import _ank_base +from AnkaiosSDK import Workload, WorkloadBuilder +from AnkaiosSDK._protos import _ank_base @pytest.fixture def workload(): From 379295f03eb4f3c3dfdb25ccbcb62bbc7142b1c0 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 20 Sep 2024 11:20:53 +0300 Subject: [PATCH 04/72] Add Manifest and other fixes --- AnkaiosSDK/Ankaios.py | 78 +++-- AnkaiosSDK/_components/CompleteState.py | 44 ++- AnkaiosSDK/_components/Manifest.py | 65 ++++ AnkaiosSDK/_components/Response.py | 9 +- AnkaiosSDK/_components/Workload.py | 128 +++++++- AnkaiosSDK/_components/WorkloadState.py | 290 +++++++++++++++--- AnkaiosSDK/_components/__init__.py | 13 + tests/{ => Workload}/test_workload.py | 106 +++---- tests/Workload/test_workload_builder.py | 86 ++++++ tests/WorkloadState/__init__.py | 13 - .../test_workload_state_collection.py | 1 + .../test_workload_substate_enum.py | 41 +-- 12 files changed, 696 insertions(+), 178 deletions(-) create mode 100644 AnkaiosSDK/_components/Manifest.py rename tests/{ => Workload}/test_workload.py (59%) create mode 100644 tests/Workload/test_workload_builder.py delete mode 100644 tests/WorkloadState/__init__.py diff --git a/AnkaiosSDK/Ankaios.py b/AnkaiosSDK/Ankaios.py index ed31447..7fbb483 100644 --- a/AnkaiosSDK/Ankaios.py +++ b/AnkaiosSDK/Ankaios.py @@ -18,7 +18,8 @@ from google.protobuf.internal.decoder import _DecodeVarint from ._protos import _control_api -from ._components import Workload, CompleteState, Request, Response, ResponseEvent, WorkloadStateCollection +from ._components import Workload, CompleteState, Request, Response, \ + ResponseEvent, WorkloadStateCollection, Manifest __all__ = ["Ankaios", "AnkaiosLogLevel"] @@ -49,7 +50,7 @@ def __init__(self) -> None: self.path = self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH self._read_thread = None - self._read = False + self._connected = False self._responses_lock = Lock() self._responses: dict[str, ResponseEvent] = {} @@ -83,7 +84,7 @@ def _read_from_control_interface(self) -> None: with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: - while self._read: + while self._connected: # Buffer for reading in the byte size of the proto msg varint_buffer = b'' while True: @@ -123,7 +124,7 @@ def _read_from_control_interface(self) -> None: def _get_response_by_id(self, request_id: str, timeout: int = 10) -> Response: """Returns the response by the request id.""" - if not self._read: + if not self._connected: raise ValueError("Reading from the control interface is not started.") with self._responses_lock: @@ -145,7 +146,7 @@ def _write_to_pipe(self, request: Request) -> None: def _send_request(self, request: Request, timeout: int = 10) -> Response: """Send a request and wait for the response.""" - if not self._read: + if not self._connected: raise ValueError("Cannot request if not connected.") self._write_to_pipe(request) @@ -161,36 +162,73 @@ def set_logger_level(self, level: AnkaiosLogLevel) -> None: def connect(self) -> None: """Connect to the control interface by starting to read from the input fifo.""" - if self._read: - raise ValueError("Reading from the control interface is already started.") + if self._connected: + raise ValueError("Already connected.") self._read_thread = Thread(target=self._read_from_control_interface) self._read_thread.start() - self._read = True + self._connected = True def disconnect(self) -> None: """Disconnect from the control interface by stopping to read from the input fifo.""" - if not self._read: - raise ValueError("Reading from the control interface is not started.") - self._read = False + if not self._connected: + raise ValueError("Already disconnected.") + self._connected = False self._read_thread.join() - def apply_manifest(self, manifest: dict) -> None: - # TODO apply_manifest - ank apply - pass + def apply_manifest(self, manifest: Manifest) -> None: + """Send a request to apply a manifest.""" + request = Request(request_type="update_state") + request.set_complete_state(manifest.generate_complete_state()) + for mask in manifest.calculate_masks(): + request.add_mask(mask) - def delete_manifest(self, manifest: dict) -> None: - # TODO delete_manifest - pass + # Send request + try: + response = self._send_request(request) + except TimeoutError as e: + self.logger.error(f"{e}") + return + + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error(f"Error while trying to apply manifest: {content}") + elif content_type == "update_state_success": + self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". + format(content["added_workloads"], content["deleted_workloads"])) + + def delete_manifest(self, manifest: Manifest) -> None: + """Send a request to delete a manifest.""" + request = Request(request_type="update_state") + request.set_complete_state(CompleteState()) + for mask in manifest.calculate_masks(): + request.add_mask(mask) + + # Send request + try: + response = self._send_request(request) + except TimeoutError as e: + self.logger.error(f"{e}") + return - def run_workload(self, workload_name: str, workload: Workload) -> None: + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error(f"Error while trying to delete manifest: {content}") + elif content_type == "update_state_success": + self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". + format(content["added_workloads"], content["deleted_workloads"])) + + def run_workload(self, workload: Workload) -> None: """Send a request to run a workload.""" complete_state = CompleteState() - complete_state.set_workload(workload_name, workload) + complete_state.set_workload(workload) # Create the request request = Request(request_type="update_state") request.set_complete_state(complete_state) - request.add_mask(f"desiredState.workloads.{workload_name}") + for mask in workload._get_masks(): + request.add_mask(mask) # Send request try: diff --git a/AnkaiosSDK/_components/CompleteState.py b/AnkaiosSDK/_components/CompleteState.py index f632e47..9246f24 100644 --- a/AnkaiosSDK/_components/CompleteState.py +++ b/AnkaiosSDK/_components/CompleteState.py @@ -28,7 +28,7 @@ class CompleteState: def __init__(self, api_version: str = DEFAULT_API_VERSION) -> None: self._complete_state = _ank_base.CompleteState() self._set_api_version(api_version) - self._workloads: dict[str, Workload] = {} + self._workloads: list[Workload] = [] self._workload_state_collection = WorkloadStateCollection() def __str__(self) -> str: @@ -38,16 +38,19 @@ def _set_api_version(self, version: str) -> None: """Set the API version for the complete state.""" self._complete_state.desiredState.apiVersion = version - def set_workload(self, name: str, workload: Workload) -> None: + def set_workload(self, workload: Workload) -> None: """Add a workload to the complete state.""" - self._workloads[name] = workload + self._workloads.append(workload) - def get_workload(self, name: str) -> Workload: + def get_workload(self, workload_name: str) -> Workload: """Get a workload from the complete state by it's name.""" - return self._workloads.get(name) + for wl in self._workloads: + if wl.name == workload_name: + return wl + return None - def get_workloads(self) -> dict[str, Workload]: - """Get a workloads from the complete state.""" + def get_workloads(self) -> list[Workload]: + """Get a workloads dict from the complete state.""" return self._workloads def get_workload_states(self) -> WorkloadStateCollection: @@ -58,21 +61,30 @@ def get_agents(self) -> list[str]: """Get the connected agents.""" # Return keys because the value "AgentAttributes" is not yet implemented return self._complete_state.agents.keys() + + def _from_dict(self, dict_state: dict) -> None: + """Convert a dictionary to a CompleteState object.""" + self._complete_state = _ank_base.CompleteState() + self._set_api_version(dict_state.get("apiVersion", DEFAULT_API_VERSION)) + self._workloads = [] + for workload_name, workload_dict in dict_state.get("workloads").items(): + self._workloads.append(Workload._from_dict(workload_name, workload_dict)) def _to_proto(self) -> _ank_base.CompleteState: """Convert the CompleteState object to a proto message.""" # Clear previous workloads - for name, workload in self._workloads.items(): - self._complete_state.desiredState.workloads.workloads[name].CopyFrom(workload._to_proto()) + for workload in self._workloads: + self._complete_state.desiredState.workloads.workloads[workload.name].CopyFrom(workload._to_proto()) return self._complete_state def _from_proto(self, proto: _ank_base.CompleteState) -> None: """Convert the proto message to a CompleteState object.""" self._complete_state = proto self._workloads = {} - for name, workload in self._complete_state.desiredState.workloads.workloads.items(): - self._workloads[name] = Workload() - self._workloads[name]._from_proto(workload) + for workload_name, proto_workload in self._complete_state.desiredState.workloads.workloads.items(): + workload = Workload(workload_name) + workload._from_proto(proto_workload) + self._workloads.append(workload) self._workload_state_collection._from_proto(self._complete_state.workloadStates) @@ -80,12 +92,12 @@ def _from_proto(self, proto: _ank_base.CompleteState) -> None: complete_state = CompleteState() # Create workload - workload = Workload() - workload.update_agent_name("agent_A") + workload = Workload.builder().workload_name("nginx").build() + workload2 = Workload.builder().workload_name("dyn_nginx").build() # Add workload to complete state - complete_state.set_workload("dynamic_nginx", workload) - complete_state.set_workload("dynamic_nginx2", workload) + complete_state.set_workload(workload) + complete_state.set_workload(workload2) print(complete_state) diff --git a/AnkaiosSDK/_components/Manifest.py b/AnkaiosSDK/_components/Manifest.py new file mode 100644 index 0000000..92da278 --- /dev/null +++ b/AnkaiosSDK/_components/Manifest.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import yaml +from .CompleteState import CompleteState + + +class Manifest(): + def __init__(self, manifest: dict) -> None: + self._manifest: dict = manifest + + if not self.check(): + raise ValueError("Invalid manifest") + + @staticmethod + def from_file(file_path: str) -> 'Manifest': + try: + with open(file_path, 'r') as file: + return Manifest.from_string(file.read()) + except Exception as e: + raise ValueError(f"Error reading manifest file: {e}") + + @staticmethod + def from_string(manifest: str) -> 'Manifest': + try: + return Manifest.from_dict(yaml.safe_load(manifest)) + except Exception as e: + raise ValueError(f"Error parsing manifest: {e}") + + @staticmethod + def from_dict(manifest: dict) -> 'Manifest': + return Manifest(manifest) + + def check(self) -> bool: + if "apiVersion" not in self._manifest.keys(): + return False + if "workloads" not in self._manifest.keys(): + return False + wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", + "dependencies", "tags", "controlInterfaceAccess"] + for wl_name in self._manifest["workloads"]: + for key in self._manifest["workloads"][wl_name].keys(): + if key not in wl_allowed_keys: + return False + return True + + def calculate_masks(self) -> list[str]: + return [f"desiredState.workloads.{key}" + for key in self._manifest["workloads"].keys()] + + def generate_complete_state(self) -> CompleteState: + complete_state = CompleteState() + complete_state._from_dict(self._manifest) + return complete_state diff --git a/AnkaiosSDK/_components/Response.py b/AnkaiosSDK/_components/Response.py index d9a53eb..905586b 100644 --- a/AnkaiosSDK/_components/Response.py +++ b/AnkaiosSDK/_components/Response.py @@ -101,13 +101,12 @@ def wait_for_response(self, timeout: int) -> Response: complete_state = CompleteState() # Create workload - workload = Workload( - agent_name="agent_A" - ) + workload = Workload.builder().workload_name("nginx").build() + workload2 = Workload.builder().workload_name("dyn_nginx").build() # Add workload to complete state - complete_state.set_workload("dynamic_nginx", workload) - complete_state.set_workload("dynamic_nginx2", workload) + complete_state.set_workload(workload) + complete_state.set_workload(workload2) from_ankaios = _control_api.FromAnkaios( diff --git a/AnkaiosSDK/_components/Workload.py b/AnkaiosSDK/_components/Workload.py index a616d8f..449bece 100644 --- a/AnkaiosSDK/_components/Workload.py +++ b/AnkaiosSDK/_components/Workload.py @@ -22,6 +22,7 @@ Usage: - Create a workload using the WorkloadBuilder: workload = Workload.builder() \ + .workload_name("nginx") \ .agent_name("agent_A") \ .runtime("podman") \ .restart_policy("NEVER") \ @@ -59,12 +60,23 @@ class Workload: """ A class to represent a workload. + + Attributes: + name (str): The workload name. """ - def __init__(self) -> None: + def __init__(self, name: str) -> None: """ Initialize a Workload object. + The Workload object should be created using the Workload.builder() method. + + Args: + name (str): The workload name. """ self._workload = _ank_base.Workload() + self._main_mask = f"desiredState.workloads.{name}" + self._masks = [] + self.__from_builder = False + self.name = name def __str__(self) -> str: """ @@ -85,6 +97,23 @@ def builder() -> "WorkloadBuilder": """ return WorkloadBuilder() + def _set_from_builder(self) -> None: + """ + Set the __from_builder attribute to True. + """ + self.__from_builder = True + + def update_workload_name(self, name: str) -> None: + """ + Set the workload name. + + Args: + name (str): The workload name to update. + """ + self.name = name + if not self.__from_builder: + self._add_mask(self._main_mask) + def update_agent_name(self, agent_name: str) -> None: """ Set the agent name for the workload. @@ -93,6 +122,8 @@ def update_agent_name(self, agent_name: str) -> None: agent_name (str): The agent name to update. """ self._workload.agent = agent_name + if not self.__from_builder: + self._add_mask(f"{self._main_mask}.agent") def update_runtime(self, runtime: str) -> None: """ @@ -102,6 +133,8 @@ def update_runtime(self, runtime: str) -> None: runtime (str): The runtime to update. """ self._workload.runtime = runtime + if not self.__from_builder: + self._add_mask(f"{self._main_mask}.runtime") def update_runtime_config(self, config: str) -> None: """ @@ -111,6 +144,8 @@ def update_runtime_config(self, config: str) -> None: config (str): The runtime configuration to update. """ self._workload.runtimeConfig = config + if not self.__from_builder: + self._add_mask(f"{self._main_mask}.runtimeConfig") def update_runtime_config_from_file(self, config_file: str) -> None: """ @@ -120,7 +155,7 @@ def update_runtime_config_from_file(self, config_file: str) -> None: config_file (str): The path to the configuration file. """ with open(config_file, "r", encoding="utf-8") as file: - self._workload.runtimeConfig = file.read() + self.update_runtime_config(file.read()) def update_restart_policy(self, policy: str) -> None: """ @@ -143,6 +178,8 @@ def update_restart_policy(self, policy: str) -> None: raise ValueError("Invalid restart policy. Supported values " + "'NEVER', 'ON_FAILURE', 'ALWAYS'.") self._workload.restartPolicy = policy_map[policy] + if not self.__from_builder: + self._add_mask(f"{self._main_mask}.restartPolicy") def add_dependency(self, workload_name: str, condition: str) -> None: """ @@ -166,6 +203,8 @@ def add_dependency(self, workload_name: str, condition: str) -> None: raise ValueError("Invalid condition. Supported values: " + "'RUNNING', 'SUCCEEDED', 'FAILED'.") self._workload.dependencies.dependencies[workload_name] = condition_map[condition] + if not self.__from_builder: + self._add_mask(f"{self._main_mask}.dependencies") def get_dependencies(self) -> dict: """ @@ -205,6 +244,8 @@ def add_tag(self, key: str, value: str) -> None: """ tag = _ank_base.Tag(key=key, value=value) self._workload.tags.tags.append(tag) + if not self.__from_builder: + self._add_mask(f"{self._main_mask}.tags") def get_tags(self) -> list[tuple[str, str]]: """ @@ -230,6 +271,56 @@ def update_tags(self, tags: list) -> None: for key, value in tags: self.add_tag(key, value) + def _add_mask(self, mask: str) -> None: + """ + Add a mask to the list of masks. + + Args: + mask (str): The mask to add. + """ + if mask not in self._masks: + self._masks.append(mask) + + def _get_masks(self) -> list[str]: + """ + Return the list of masks. + + Returns: + list: A list of masks. + """ + if self._main_mask in self._masks: + return [self._main_mask] + return self._masks + + @staticmethod + def _from_dict(workload_name: str, dict_workload: dict) -> "Workload": + """ + Convert a dictionary to a Workload object. + + Args: + workload_name (str): The name of the workload. + dict_workload (dict): The dictionary to convert. + + Returns: + Workload: The Workload object created from the dictionary. + """ + workload = Workload.builder().workload_name(workload_name) + if "agent" in dict_workload: + workload = workload.agent_name(dict_workload["agent"]) + if "runtime" in dict_workload: + workload = workload.runtime(dict_workload["runtime"]) + if "runtimeConfig" in dict_workload: + workload = workload.runtime_config(dict_workload["runtimeConfig"]) + if "restartPolicy" in dict_workload: + workload = workload.restart_policy(dict_workload["restartPolicy"]) + if "dependencies" in dict_workload: + for dep_key, dep_value in dict_workload["dependencies"].items(): + workload = workload.add_dependency(dep_key, dep_value) + if "tags" in dict_workload: + for tag_key, tag_value in dict_workload["tags"].items(): + workload = workload.add_tag(tag_key, tag_value) + return workload.build() + def _to_proto(self) -> _ank_base.Workload: """ Convert the Workload object to a proto message. @@ -252,11 +343,21 @@ def _from_proto(self, proto: _ank_base.Workload) -> None: class WorkloadBuilder: """ A builder class to create a Workload object. + + Attributes: + wl_name (str): The workload name. + wl_agent_name (str): The agent name. + wl_runtime (str): The runtime. + wl_runtime_config (str): The runtime configuration. + wl_restart_policy (str): The restart policy. + dependencies (dict): The dependencies. + tags (list): The tags. """ def __init__(self) -> None: """ Initialize a WorkloadBuilder object. """ + self.wl_name = None self.wl_agent_name = None self.wl_runtime = None self.wl_runtime_config = None @@ -264,6 +365,19 @@ def __init__(self) -> None: self.dependencies = {} self.tags = [] + def workload_name(self, workload_name: str) -> "WorkloadBuilder": + """ + Set the workload name. + + Args: + workload_name (str): The workload name to set. + + Returns: + WorkloadBuilder: The builder object. + """ + self.wl_name = workload_name + return self + def agent_name(self, agent_name: str) -> "WorkloadBuilder": """ Set the agent name. @@ -361,7 +475,7 @@ def add_tag(self, key: str, value: str) -> "WorkloadBuilder": def build(self) -> Workload: """ Build the Workload object. - Required fields: agent name, runtime and runtime configuration. + Required fields: workload name, agent name, runtime and runtime configuration. Returns: Workload: The built Workload object. @@ -369,7 +483,11 @@ def build(self) -> Workload: Raises: ValueError: If required fields are not set. """ - workload = Workload() + if self.wl_name is None: + raise ValueError("Workload can not be built without a name.") + + workload = Workload(self.wl_name) + workload._set_from_builder() # pylint: disable=protected-access if self.wl_agent_name is None: raise ValueError("Workload can not be built without an agent name.") @@ -388,4 +506,6 @@ def build(self) -> Workload: workload.update_dependencies(self.dependencies) if len(self.tags) > 0: workload.update_tags(self.tags) + + workload._add_mask(f"desiredState.workloads.{workload.name}") # pylint: disable=protected-access return workload diff --git a/AnkaiosSDK/_components/WorkloadState.py b/AnkaiosSDK/_components/WorkloadState.py index 918ff6a..00f26c5 100644 --- a/AnkaiosSDK/_components/WorkloadState.py +++ b/AnkaiosSDK/_components/WorkloadState.py @@ -12,16 +12,59 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module defines various classes and enumerations related to the state of workloads. +It provides functionality to interpret and manage the states and sub-states of workloads, +including converting between different representations and handling collections of workload states. + +Classes: + WorkloadExecutionState: Represents the execution state and sub-state of a workload. + WorkloadInstanceName: Represents the name of a workload instance. + WorkloadState: Represents the state of a workload (execution state and name). + WorkloadStateCollection: A collection of workload states. + +Enums: + WorkloadStateEnum: Enumeration for different states of a workload. + WorkloadSubStateEnum: Enumeration for different sub-states of a workload. + +Usage: + - Get all workload states: + workload_state_collection = WorkloadStateCollection() + list_of_workload_states = workload_state_collection.get_as_list() + dict_of_workload_states = workload_state_collection.get_as_dict() + + - Unpack a workload state: + workload_state = WorkloadState() + agent_name = workload_state.workload_instance_name.agent_name + workload_name = workload_state.workload_instance_name.workload_name + state = workload_state.execution_state.state + substate = workload_state.execution_state.substate + info = workload_state.execution_state.info +""" + from typing import TypeAlias from enum import Enum from .._protos import _ank_base -__all__ = ["WorkloadStateCollection", "WorkloadState", "WorkloadInstanceName", +__all__ = ["WorkloadStateCollection", "WorkloadState", "WorkloadInstanceName", "WorkloadExecutionState", "WorkloadStateEnum", "WorkloadSubStateEnum"] class WorkloadStateEnum(Enum): + """ + Enumeration for different states of a workload. + + Attributes: + AgentDisconnected (int): The agent is disconnected. + Pending (int): The workload is pending. + Running (int): The workload is running. + Stopping (int): The workload is stopping. + Succeeded (int): The workload has succeeded. + Failed (int): The workload has failed. + NotScheduled (int): The workload is not scheduled. + Removed (int): The workload has been removed. + """ AgentDisconnected: int = 0 Pending: int = 1 Running: int = 2 @@ -32,15 +75,54 @@ class WorkloadStateEnum(Enum): Removed: int = 7 def __str__(self) -> str: + """ + Return the name of the enumeration member. + + Returns: + str: The name of the enumeration member. + """ return self.name - + @staticmethod def _get(field: str) -> "WorkloadStateEnum": + """ + Get the enumeration member corresponding to the given field name. + + Args: + field (str): The field name to look up. + + Returns: + WorkloadStateEnum: The enumeration member corresponding to the field name. + + Raises: + KeyError: If the field name does not correspond to any enumeration member. + """ field = field[0].upper() + field[1:] # Capitalize the first letter return WorkloadStateEnum[field] class WorkloadSubStateEnum(Enum): + """ + Enumeration for different sub-states of a workload. + + Attributes: + AGENT_DISCONNECTED (int): The agent is disconnected. + PENDING_INITIAL (int): The workload is in the initial pending state. + PENDING_WAITING_TO_START (int): The workload is waiting to start. + PENDING_STARTING (int): The workload is starting. + PENDING_STARTING_FAILED (int): The workload failed to start. + RUNNING_OK (int): The workload is running successfully. + STOPPING (int): The workload is stopping. + STOPPING_WAITING_TO_STOP (int): The workload is waiting to stop. + STOPPING_REQUESTED_AT_RUNTIME (int): The workload stop was requested at runtime. + STOPPING_DELETE_FAILED (int): The workload stop failed to delete. + SUCCEEDED_OK (int): The workload succeeded successfully. + FAILED_EXEC_FAILED (int): The workload failed due to execution failure. + FAILED_UNKNOWN (int): The workload failed due to an unknown reason. + FAILED_LOST (int): The workload failed because it was lost. + NOT_SCHEDULED (int): The workload is not scheduled. + REMOVED (int): The workload has been removed. + """ AGENT_DISCONNECTED: int = 0 PENDING_INITIAL: int = 1 PENDING_WAITING_TO_START: int = 2 @@ -59,21 +141,45 @@ class WorkloadSubStateEnum(Enum): REMOVED: int = 15 def __str__(self) -> str: + """ + Return the name of the enumeration member. + + Returns: + str: The name of the enumeration member. + """ return self.name @staticmethod def _get(state: WorkloadStateEnum, field: _ank_base) -> "WorkloadSubStateEnum": + """ + Get the enumeration member corresponding to the given state and field. + + Args: + state (WorkloadStateEnum): The state of the workload. + field (_ank_base): The field to look up. + + Returns: + WorkloadSubStateEnum: The enumeration member corresponding to the state and field. + + Raises: + ValueError: If the field does not correspond to any enumeration member. + """ proto_mapper = {} if state == WorkloadStateEnum.AgentDisconnected: proto_mapper = { - _ank_base.AGENT_DISCONNECTED: WorkloadSubStateEnum.AGENT_DISCONNECTED + _ank_base.AGENT_DISCONNECTED: + WorkloadSubStateEnum.AGENT_DISCONNECTED } elif state == WorkloadStateEnum.Pending: proto_mapper = { - _ank_base.PENDING_INITIAL: WorkloadSubStateEnum.PENDING_INITIAL, - _ank_base.PENDING_WAITING_TO_START: WorkloadSubStateEnum.PENDING_WAITING_TO_START, - _ank_base.PENDING_STARTING: WorkloadSubStateEnum.PENDING_STARTING, - _ank_base.PENDING_STARTING_FAILED: WorkloadSubStateEnum.PENDING_STARTING_FAILED + _ank_base.PENDING_INITIAL: + WorkloadSubStateEnum.PENDING_INITIAL, + _ank_base.PENDING_WAITING_TO_START: + WorkloadSubStateEnum.PENDING_WAITING_TO_START, + _ank_base.PENDING_STARTING: + WorkloadSubStateEnum.PENDING_STARTING, + _ank_base.PENDING_STARTING_FAILED: + WorkloadSubStateEnum.PENDING_STARTING_FAILED } elif state == WorkloadStateEnum.Running: proto_mapper = { @@ -82,86 +188,187 @@ def _get(state: WorkloadStateEnum, field: _ank_base) -> "WorkloadSubStateEnum": elif state == WorkloadStateEnum.Stopping: proto_mapper = { _ank_base.STOPPING: WorkloadSubStateEnum.STOPPING, - _ank_base.STOPPING_WAITING_TO_STOP: WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP, - _ank_base.STOPPING_REQUESTED_AT_RUNTIME: WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME, - _ank_base.STOPPING_DELETE_FAILED: WorkloadSubStateEnum.STOPPING_DELETE_FAILED + _ank_base.STOPPING_WAITING_TO_STOP: + WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP, + _ank_base.STOPPING_REQUESTED_AT_RUNTIME: + WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME, + _ank_base.STOPPING_DELETE_FAILED: + WorkloadSubStateEnum.STOPPING_DELETE_FAILED } elif state == WorkloadStateEnum.Succeeded: proto_mapper = { - _ank_base.SUCCEEDED_OK: WorkloadSubStateEnum.SUCCEEDED_OK + _ank_base.SUCCEEDED_OK: + WorkloadSubStateEnum.SUCCEEDED_OK } elif state == WorkloadStateEnum.Failed: proto_mapper = { - _ank_base.FAILED_EXEC_FAILED: WorkloadSubStateEnum.FAILED_EXEC_FAILED, - _ank_base.FAILED_UNKNOWN: WorkloadSubStateEnum.FAILED_UNKNOWN, - _ank_base.FAILED_LOST: WorkloadSubStateEnum.FAILED_LOST + _ank_base.FAILED_EXEC_FAILED: + WorkloadSubStateEnum.FAILED_EXEC_FAILED, + _ank_base.FAILED_UNKNOWN: + WorkloadSubStateEnum.FAILED_UNKNOWN, + _ank_base.FAILED_LOST: + WorkloadSubStateEnum.FAILED_LOST } elif state == WorkloadStateEnum.NotScheduled: proto_mapper = { - _ank_base.NOT_SCHEDULED: WorkloadSubStateEnum.NOT_SCHEDULED + _ank_base.NOT_SCHEDULED: + WorkloadSubStateEnum.NOT_SCHEDULED } elif state == WorkloadStateEnum.Removed: proto_mapper = { - _ank_base.REMOVED: WorkloadSubStateEnum.REMOVED + _ank_base.REMOVED: + WorkloadSubStateEnum.REMOVED } if field not in proto_mapper: raise ValueError(f"No corresponding WorkloadSubStateEnum value for enum: {field}") return proto_mapper[field] - + def _sub_state2ank_base(self) -> _ank_base: + """ + Convert the WorkloadSubStateEnum member to the corresponding _ank_base value. + + Returns: + _ank_base: The corresponding _ank_base value. + + Raises: + ValueError: If there is no corresponding _ank_base value for the enumeration member. + """ try: return getattr(_ank_base, self.name) - except AttributeError: # pragma: no cover - raise ValueError(f"No corresponding ank_base value for enum: {self.name}") + except AttributeError as e: # pragma: no cover + raise ValueError(f"No corresponding ank_base value for enum: {self.name}") from e +# pylint: disable=too-few-public-methods class WorkloadExecutionState: + """ + Represents the execution state of a workload. + + Attributes: + state (WorkloadStateEnum): The state of the workload. + substate (WorkloadSubStateEnum): The sub-state of the workload. + info (str): Additional information about the workload state. + """ def __init__(self, state: _ank_base.ExecutionState) -> None: + """ + Initializes a WorkloadExecutionState instance. + + Args: + state (_ank_base.ExecutionState): The execution state to interpret. + """ self.state: WorkloadStateEnum = None self.substate: WorkloadSubStateEnum = None self.info: str = None self._interpret_state(state) - + def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: + """ + Interprets the execution state and sets the state, substate, and info attributes. + + Args: + exec_state (_ank_base.ExecutionState): The execution state to interpret. + + Raises: + ValueError: If the execution state is invalid. + """ self.info = str(exec_state.additionalInfo) field = exec_state.WhichOneof("ExecutionStateEnum") if field is None: raise ValueError("Invalid state for workload.") - self.state = WorkloadStateEnum._get(field) - self.substate = WorkloadSubStateEnum._get(self.state, exec_state.__getattribute__(field)) + self.state = WorkloadStateEnum._get(field) # pylint: disable=protected-access + self.substate = WorkloadSubStateEnum._get(self.state, getattr(exec_state, field)) # pylint: disable=protected-access +# pylint: disable=too-few-public-methods class WorkloadInstanceName: + """ + Represents the name of a workload instance. + + Attributes: + agent_name (str): The name of the agent. + workload_name (str): The name of the workload. + workload_id (str): The ID of the workload. + """ def __init__(self, agent_name: str, workload_name: str, workload_id: str) -> None: + """ + Initializes a WorkloadInstanceName instance. + + Args: + agent_name (str): The name of the agent. + workload_name (str): The name of the workload. + workload_id (str): The ID of the workload. + """ self.agent_name = agent_name self.workload_name = workload_name self.workload_id = workload_id def __str__(self) -> str: + """ + Returns the string representation of the workload instance name. + + Returns: + str: The string representation of the workload instance name. + """ return f"{self.agent_name}.{self.workload_name}.{self.workload_id}" +# pylint: disable=too-few-public-methods class WorkloadState: - def __init__(self, agent_name: str, workload_name: str, workload_id: str, state: _ank_base.ExecutionState) -> None: + """ + Represents the state of a workload. + + Attributes: + execution_state (WorkloadExecutionState): The execution state of the workload. + workload_instance_name (WorkloadInstanceName): The name of the workload instance. + """ + def __init__(self, agent_name: str, workload_name: str, + workload_id: str, state: _ank_base.ExecutionState) -> None: + """ + Initializes a WorkloadState instance. + + Args: + agent_name (str): The name of the agent. + workload_name (str): The name of the workload. + workload_id (str): The ID of the workload. + state (_ank_base.ExecutionState): The execution state to interpret. + """ self.execution_state = WorkloadExecutionState(state) self.workload_instance_name = WorkloadInstanceName(agent_name, workload_name, workload_id) class WorkloadStateCollection: + """ + A class that represents a collection of workload states and provides methods to manipulate them. + """ ExecutionsStatesForId: TypeAlias = dict[str, WorkloadExecutionState] ExecutionsStatesOfWorkload: TypeAlias = dict[str, ExecutionsStatesForId] WorkloadStatesMap: TypeAlias = dict[str, ExecutionsStatesOfWorkload] def __init__(self) -> None: + """ + Initializes a WorkloadStateCollection instance. + """ self._workload_states: list[WorkloadState] = [] def add_workload_state(self, state: WorkloadState) -> None: + """ + Adds a workload state to the collection. + + Args: + state (WorkloadState): The workload state to add. + """ self._workload_states.append(state) def get_as_dict(self) -> WorkloadStatesMap: + """ + Returns the workload states as a list. + + Returns: + list[WorkloadState]: A list of workload states. + """ return_dict = self.WorkloadStatesMap() for state in self._workload_states: @@ -178,29 +385,30 @@ def get_as_dict(self) -> WorkloadStatesMap: return return_dict def get_as_list(self) -> list[WorkloadState]: + """ + Returns the workload states as a list. + + Returns: + list[WorkloadState]: A list of workload states. + """ return self._workload_states - + def _from_proto(self, state: _ank_base.WorkloadStatesMap) -> None: + """ + Populates the collection from a proto message. + + Args: + state (_ank_base.WorkloadStatesMap): The proto message to interpret. + """ for agent_name in state.agentStateMap: - for workload_name in state.agentStateMap[agent_name].wlNameStateMap: - for workload_id in state.agentStateMap[agent_name].wlNameStateMap[workload_name].idStateMap: + for workload_name in state.agentStateMap[agent_name].\ + wlNameStateMap: + for workload_id in state.agentStateMap[agent_name].\ + wlNameStateMap[workload_name].idStateMap: self.add_workload_state(WorkloadState( agent_name, workload_name, workload_id, - state.agentStateMap[agent_name].wlNameStateMap[workload_name].idStateMap[workload_id] + state.agentStateMap[agent_name].wlNameStateMap[workload_name].\ + idStateMap[workload_id] )) - - -# Example usage -if __name__ == "__main__": - # Create a WorkloadState object - workload_state = WorkloadExecutionState(_ank_base.ExecutionState( - additionalInfo="Info about pending", - pending=_ank_base.PENDING_STARTING - )) - - # Print the state, substate, and info - print(workload_state.state) # Will get a WorkloadStateEnum object - print(workload_state.substate) # Will get a WorkloadSubStateEnum object - print(workload_state.info) diff --git a/AnkaiosSDK/_components/__init__.py b/AnkaiosSDK/_components/__init__.py index cb49a62..a5f7b37 100644 --- a/AnkaiosSDK/_components/__init__.py +++ b/AnkaiosSDK/_components/__init__.py @@ -12,10 +12,23 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module initializes the AnkaiosSDK package by importing all necessary components. + +Imports: + Workload component: responsible for defining the workload of the system. + WorkloadState component: responsible for accessing the state of the workload. + CompleteState component: responsible for accessing the complete state of the system. + Request component: responsible for defining a request to be sent to the system. + Response component: responsible for defining a response from the system. + Manifest component: responsible for defining a manifest object. +""" + from .Workload import * from .WorkloadState import * from .CompleteState import * from .Request import * from .Response import * +from .Manifest import * __all__ = [name for name in globals() if not name.startswith('_')] diff --git a/tests/test_workload.py b/tests/Workload/test_workload.py similarity index 59% rename from tests/test_workload.py rename to tests/Workload/test_workload.py index a00d75b..abf5cac 100644 --- a/tests/test_workload.py +++ b/tests/Workload/test_workload.py @@ -17,24 +17,33 @@ from AnkaiosSDK import Workload, WorkloadBuilder from AnkaiosSDK._protos import _ank_base + @pytest.fixture def workload(): return Workload.builder() \ + .workload_name("workload_test") \ .agent_name("agent_Test") \ .runtime("runtime_test") \ .restart_policy("NEVER") \ .runtime_config("config_test") \ - .add_dependency("workload_test", "RUNNING") \ + .add_dependency("workload_test_other", "RUNNING") \ .add_tag("key1", "value1") \ .add_tag("key2", "value2") \ .build() + def test_builder(workload): builder = workload.builder() assert builder is not None assert isinstance(builder, WorkloadBuilder) + def test_update_fields(workload): + assert workload._get_masks() == ["desiredState.workloads.workload_test"] + + workload.update_workload_name("new_workload_test") + assert workload.name == "new_workload_test" + workload.update_agent_name("new_agent_Test") assert workload._workload.agent == "new_agent_Test" @@ -53,6 +62,7 @@ def test_update_fields(workload): workload.update_restart_policy("ON_FAILURE") assert workload._workload.restartPolicy == _ank_base.ON_FAILURE + def test_dependencies(workload): assert len(workload.get_dependencies()) == 1 @@ -71,6 +81,7 @@ def test_dependencies(workload): workload.update_dependencies(deps) assert len(workload.get_dependencies()) == 2 + def test_tags(workload): assert len(workload.get_tags()) == 2 @@ -84,6 +95,7 @@ def test_tags(workload): assert len(workload.get_tags()) == 2 + def test_proto(workload): proto = workload._to_proto() assert proto is not None @@ -91,74 +103,50 @@ def test_proto(workload): assert proto.runtime == "runtime_test" assert proto.restartPolicy == _ank_base.NEVER assert proto.runtimeConfig == "config_test" - assert proto.dependencies.dependencies == {"workload_test": _ank_base.ADD_COND_RUNNING} + assert proto.dependencies.dependencies == {"workload_test_other": _ank_base.ADD_COND_RUNNING} assert proto.tags == _ank_base.Tags(tags=[ _ank_base.Tag(key="key1", value="value1"), _ank_base.Tag(key="key2", value="value2") ]) - new_workload = Workload() + new_workload = Workload("workload_test") new_workload._from_proto(proto) assert new_workload is not None assert str(workload) == str(new_workload) -@pytest.fixture -def builder(): - return WorkloadBuilder() - -def test_workload_fields(builder): - assert builder.agent_name("agent_Test") == builder - assert builder.wl_agent_name == "agent_Test" - - assert builder.runtime("runtime_test") == builder - assert builder.wl_runtime == "runtime_test" - - assert builder.runtime_config("config_test") == builder - assert builder.wl_runtime_config == "config_test" - with patch("builtins.open", mock_open(read_data="config_test_from_file")): - assert builder.runtime_config_from_file("config_test_from_file") == builder - assert builder.wl_runtime_config == "config_test_from_file" +def test_from_dict(workload): + workload_dict = { + "name": "workload_test", + "agent": "agent_Test", + "runtime": "runtime_test", + "restartPolicy": "NEVER", + "runtimeConfig": "config_test", + "dependencies": {"workload_test_other": "RUNNING"}, + "tags": {"key1": "value1", "key2": "value2"} + } - assert builder.restart_policy("NEVER") == builder - assert builder.wl_restart_policy == "NEVER" - -def test_add_dependency(builder): - assert len(builder.dependencies) == 0 - - assert builder.add_dependency("workload_test", "RUNNING") == builder - assert builder.dependencies == {"workload_test": "RUNNING"} - - assert builder.add_dependency("workload_test_other", "RUNNING") == builder - assert builder.dependencies == {"workload_test": "RUNNING", "workload_test_other": "RUNNING"} - -def test_add_tag(builder): - assert len(builder.tags) == 0 - - assert builder.add_tag("key_test", "abc") == builder - assert builder.tags == [("key_test", "abc")] - - assert builder.add_tag("key_test", "bcd") == builder - assert builder.tags == [("key_test", "abc"), ("key_test", "bcd")] - -def test_build(builder): - with pytest.raises(ValueError, match="Workload can not be built without an agent name."): - builder.build() - builder.agent_name("agent_Test") - - with pytest.raises(ValueError, match="Workload can not be built without a runtime."): - builder.build() - builder.runtime("runtime_test") - - with pytest.raises(ValueError, match="Workload can not be built without a runtime configuration."): - builder.build() - builder.runtime_config("config_test") + new_workload = Workload._from_dict("workload_test", workload_dict) + assert new_workload is not None + assert str(workload) == str(new_workload) - builder.restart_policy("NEVER") - builder.add_dependency("workload_test", "RUNNING") - builder.add_tag("key_test", "abc") - - workload = builder.build() - assert workload is not None - assert isinstance(workload, Workload) \ No newline at end of file +@pytest.mark.parametrize("function_name, data, mask", [ + ("update_workload_name", {"name": "workload_test"}, "desiredState.workloads.workload_test"), + ("update_agent_name", {"agent_name": "agent_Test"}, "desiredState.workloads.workload_test.agent"), + ("update_runtime", {"runtime": "runtime_test"}, "desiredState.workloads.workload_test.runtime"), + ("update_restart_policy", {"policy": "NEVER"}, "desiredState.workloads.workload_test.restartPolicy"), + ("update_runtime_config", {"config": "config_test"}, "desiredState.workloads.workload_test.runtimeConfig"), + ("add_dependency", {"workload_name": "workload_test_other", "condition": "RUNNING"}, "desiredState.workloads.workload_test.dependencies"), + ("add_tag", {"key": "key1", "value": "value1"}, "desiredState.workloads.workload_test.tags"), +]) +def test_mask_generation(function_name, data, mask): + workload = Workload("workload_test") + + # Call function and assert the mask has been added + getattr(workload, function_name)(**data) + assert workload._get_masks() == [mask] + + # Updating the mask again should not add a new mask + getattr(workload, function_name)(**data) + assert len(workload._get_masks()) == 1 diff --git a/tests/Workload/test_workload_builder.py b/tests/Workload/test_workload_builder.py new file mode 100644 index 0000000..c41a0d8 --- /dev/null +++ b/tests/Workload/test_workload_builder.py @@ -0,0 +1,86 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from unittest.mock import patch, mock_open +from AnkaiosSDK import Workload, WorkloadBuilder + + +@pytest.fixture +def builder(): + return WorkloadBuilder() + + +def test_workload_fields(builder): + assert builder.agent_name("agent_Test") == builder + assert builder.wl_agent_name == "agent_Test" + + assert builder.runtime("runtime_test") == builder + assert builder.wl_runtime == "runtime_test" + + assert builder.runtime_config("config_test") == builder + assert builder.wl_runtime_config == "config_test" + + with patch("builtins.open", mock_open(read_data="config_test_from_file")): + assert builder.runtime_config_from_file("config_test_from_file") == builder + assert builder.wl_runtime_config == "config_test_from_file" + + assert builder.restart_policy("NEVER") == builder + assert builder.wl_restart_policy == "NEVER" + + +def test_add_dependency(builder): + assert len(builder.dependencies) == 0 + + assert builder.add_dependency("workload_test", "RUNNING") == builder + assert builder.dependencies == {"workload_test": "RUNNING"} + + assert builder.add_dependency("workload_test_other", "RUNNING") == builder + assert builder.dependencies == {"workload_test": "RUNNING", "workload_test_other": "RUNNING"} + + +def test_add_tag(builder): + assert len(builder.tags) == 0 + + assert builder.add_tag("key_test", "abc") == builder + assert builder.tags == [("key_test", "abc")] + + assert builder.add_tag("key_test", "bcd") == builder + assert builder.tags == [("key_test", "abc"), ("key_test", "bcd")] + + +def test_build(builder): + with pytest.raises(ValueError, match="Workload can not be built without a name."): + builder.build() + builder = builder.workload_name("workload_test") + + with pytest.raises(ValueError, match="Workload can not be built without an agent name."): + builder.build() + builder = builder.agent_name("agent_Test") + + with pytest.raises(ValueError, match="Workload can not be built without a runtime."): + builder.build() + builder = builder.runtime("runtime_test") + + with pytest.raises(ValueError, match="Workload can not be built without a runtime configuration."): + builder.build() + + workload = builder.runtime_config("config_test") \ + .restart_policy("NEVER") \ + .add_dependency("workload_test_other", "RUNNING") \ + .add_tag("key_test", "abc") \ + .build() + + assert workload is not None + assert isinstance(workload, Workload) diff --git a/tests/WorkloadState/__init__.py b/tests/WorkloadState/__init__.py deleted file mode 100644 index 094cebe..0000000 --- a/tests/WorkloadState/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2024 Elektrobit Automotive GmbH -# -# This program and the accompanying materials are made available under the -# terms of the Apache License, Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0. -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/tests/WorkloadState/test_workload_state_collection.py b/tests/WorkloadState/test_workload_state_collection.py index d8c7624..47eec55 100644 --- a/tests/WorkloadState/test_workload_state_collection.py +++ b/tests/WorkloadState/test_workload_state_collection.py @@ -47,6 +47,7 @@ def test_get(): assert "1234" in workload_states_dict["agent_Test"]["workload_Test"].keys() assert type(workload_states_dict["agent_Test"]["workload_Test"]["1234"]) == WorkloadExecutionState + def test_from_proto(): ank_workload_state = _ank_base.WorkloadStatesMap( agentStateMap={"agent_Test": _ank_base.ExecutionsStatesOfWorkload( diff --git a/tests/WorkloadState/test_workload_substate_enum.py b/tests/WorkloadState/test_workload_substate_enum.py index e23669a..4ca8d26 100644 --- a/tests/WorkloadState/test_workload_substate_enum.py +++ b/tests/WorkloadState/test_workload_substate_enum.py @@ -17,26 +17,27 @@ from AnkaiosSDK._protos import _ank_base -@pytest.mark.parametrize("state, field, expected", [ - (WorkloadStateEnum.AgentDisconnected, _ank_base.AGENT_DISCONNECTED, WorkloadSubStateEnum.AGENT_DISCONNECTED), - (WorkloadStateEnum.Pending, _ank_base.PENDING_INITIAL, WorkloadSubStateEnum.PENDING_INITIAL), - (WorkloadStateEnum.Pending, _ank_base.PENDING_WAITING_TO_START, WorkloadSubStateEnum.PENDING_WAITING_TO_START), - (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING, WorkloadSubStateEnum.PENDING_STARTING), - (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING_FAILED, WorkloadSubStateEnum.PENDING_STARTING_FAILED), - (WorkloadStateEnum.Running, _ank_base.RUNNING_OK, WorkloadSubStateEnum.RUNNING_OK), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING, WorkloadSubStateEnum.STOPPING), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_WAITING_TO_STOP, WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_REQUESTED_AT_RUNTIME, WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_DELETE_FAILED, WorkloadSubStateEnum.STOPPING_DELETE_FAILED), - (WorkloadStateEnum.Succeeded, _ank_base.SUCCEEDED_OK, WorkloadSubStateEnum.SUCCEEDED_OK), - (WorkloadStateEnum.Failed, _ank_base.FAILED_EXEC_FAILED, WorkloadSubStateEnum.FAILED_EXEC_FAILED), - (WorkloadStateEnum.Failed, _ank_base.FAILED_UNKNOWN, WorkloadSubStateEnum.FAILED_UNKNOWN), - (WorkloadStateEnum.Failed, _ank_base.FAILED_LOST, WorkloadSubStateEnum.FAILED_LOST), - (WorkloadStateEnum.NotScheduled, _ank_base.NOT_SCHEDULED, WorkloadSubStateEnum.NOT_SCHEDULED), - (WorkloadStateEnum.Removed, _ank_base.REMOVED, WorkloadSubStateEnum.REMOVED) -]) -def test_get(state: WorkloadStateEnum, field: _ank_base, expected: WorkloadSubStateEnum): - assert WorkloadSubStateEnum._get(state, field) == expected +def test_get(): + data = [ + (WorkloadStateEnum.AgentDisconnected, _ank_base.AGENT_DISCONNECTED, WorkloadSubStateEnum.AGENT_DISCONNECTED), + (WorkloadStateEnum.Pending, _ank_base.PENDING_INITIAL, WorkloadSubStateEnum.PENDING_INITIAL), + (WorkloadStateEnum.Pending, _ank_base.PENDING_WAITING_TO_START, WorkloadSubStateEnum.PENDING_WAITING_TO_START), + (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING, WorkloadSubStateEnum.PENDING_STARTING), + (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING_FAILED, WorkloadSubStateEnum.PENDING_STARTING_FAILED), + (WorkloadStateEnum.Running, _ank_base.RUNNING_OK, WorkloadSubStateEnum.RUNNING_OK), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING, WorkloadSubStateEnum.STOPPING), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_WAITING_TO_STOP, WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_REQUESTED_AT_RUNTIME, WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_DELETE_FAILED, WorkloadSubStateEnum.STOPPING_DELETE_FAILED), + (WorkloadStateEnum.Succeeded, _ank_base.SUCCEEDED_OK, WorkloadSubStateEnum.SUCCEEDED_OK), + (WorkloadStateEnum.Failed, _ank_base.FAILED_EXEC_FAILED, WorkloadSubStateEnum.FAILED_EXEC_FAILED), + (WorkloadStateEnum.Failed, _ank_base.FAILED_UNKNOWN, WorkloadSubStateEnum.FAILED_UNKNOWN), + (WorkloadStateEnum.Failed, _ank_base.FAILED_LOST, WorkloadSubStateEnum.FAILED_LOST), + (WorkloadStateEnum.NotScheduled, _ank_base.NOT_SCHEDULED, WorkloadSubStateEnum.NOT_SCHEDULED), + (WorkloadStateEnum.Removed, _ank_base.REMOVED, WorkloadSubStateEnum.REMOVED) + ] + for state, field, expected in data: + assert WorkloadSubStateEnum._get(state, field) == expected def test_get_error(): From 2b0abe167a14c9f62768475a400f857d0f5d8ad8 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 24 Sep 2024 14:36:01 +0300 Subject: [PATCH 05/72] Finalize testing: 100% coverage and 10/10 lint --- .pylintrc | 2 + AnkaiosSDK/Ankaios.py | 496 ++++++++++++++---- AnkaiosSDK/__init__.py | 9 + AnkaiosSDK/_components/CompleteState.py | 195 ++++--- AnkaiosSDK/_components/Manifest.py | 107 +++- AnkaiosSDK/_components/Request.py | 87 ++- AnkaiosSDK/_components/Response.py | 141 +++-- AnkaiosSDK/_components/Workload.py | 8 +- AnkaiosSDK/_components/WorkloadState.py | 16 +- AnkaiosSDK/_protos/__init__.py | 11 +- run_tests.py | 33 +- tests/Response/__init__.py | 13 + tests/Response/test_response.py | 115 ++++ tests/Response/test_response_event.py | 41 ++ tests/Workload/__init__.py | 13 + tests/Workload/test_workload.py | 136 ++++- tests/Workload/test_workload_builder.py | 61 ++- tests/WorkloadState/__init__.py | 13 + .../test_workload_execution_state.py | 11 + .../test_workload_instance_name.py | 8 + tests/WorkloadState/test_workload_state.py | 7 + .../test_workload_state_collection.py | 18 +- .../WorkloadState/test_workload_state_enum.py | 8 + .../test_workload_substate_enum.py | 68 ++- tests/test_ankaios.py | 471 +++++++++++++++++ tests/test_complete_state.py | 141 +++++ tests/test_manifest.py | 133 +++++ tests/test_request.py | 85 +++ 28 files changed, 2106 insertions(+), 341 deletions(-) create mode 100644 tests/Response/__init__.py create mode 100644 tests/Response/test_response.py create mode 100644 tests/Response/test_response_event.py create mode 100644 tests/Workload/__init__.py create mode 100644 tests/WorkloadState/__init__.py create mode 100644 tests/test_ankaios.py create mode 100644 tests/test_complete_state.py create mode 100644 tests/test_manifest.py create mode 100644 tests/test_request.py diff --git a/.pylintrc b/.pylintrc index 07cc419..912ded2 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,3 +9,5 @@ disable= E1101, # no-member # module names have the same name with the classes within. They cannot be snake_case C0103, # invalid-name + # Protected members are accessed throughout the project to be able to hide that functionality from the user. + W0212, # protected-access diff --git a/AnkaiosSDK/Ankaios.py b/AnkaiosSDK/Ankaios.py index 7fbb483..aac4d4a 100644 --- a/AnkaiosSDK/Ankaios.py +++ b/AnkaiosSDK/Ankaios.py @@ -12,8 +12,51 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This script defines the Ankaios class for interacting with the Ankaios control interface. + +Classes: + - Ankaios: Handles the interaction with the Ankaios control interface. + +Usage: + - Create an Ankaios object and connect to the control interface: + with Ankaios() as ankaios: + pass + + - Apply a manifest: + ankaios.apply_manifest(manifest) + + - Delete a manifest: + ankaios.delete_manifest(manifest) + + - Run a workload: + ankaios.run_workload(workload) + + - Delete a workload: + ankaios.delete_workload(workload_name) + + - Get a workload: + workload = ankaios.get_workload(workload_name) + + - Get the state: + state = ankaios.get_state() + + - Get the agents: + agents = ankaios.get_agents() + + - Get the workload states: + workload_states = ankaios.get_workload_states() + + - Get the workload states on an agent: + workload_states = ankaios.get_workload_states_on_agent(agent_name) + + - Get the workload states on a workload name: + workload_states = ankaios.get_workload_states_on_workload_name(workload_name) +""" + import logging -from threading import Thread, Lock +from enum import Enum +import threading from google.protobuf.internal.encoder import _VarintBytes from google.protobuf.internal.decoder import _DecodeVarint @@ -25,8 +68,17 @@ __all__ = ["Ankaios", "AnkaiosLogLevel"] -"""Ankaios log levels.""" -class AnkaiosLogLevel: +class AnkaiosLogLevel(Enum): + """ + Ankaios log levels. + + Attributes: + FATAL (int): Fatal log level. + ERROR (int): Error log level. + WARN (int): Warning log level. + INFO (int): Info log level. + DEBUG (int): Debug log level. + """ FATAL = logging.FATAL ERROR = logging.ERROR WARN = logging.WARN @@ -34,15 +86,19 @@ class AnkaiosLogLevel: DEBUG = logging.DEBUG -""" -Ankaios SDK for Python to interact with the Ankaios control interface. - -This SDK provides the functionality to interact with the Ankaios control interface -by sending requests to add a new workload dynamically and to request the workload states. -""" class Ankaios: + """ + A class to interact with the Ankaios control interface. It provides the functionality to + interact with the Ankaios control interface by sending requests. + + Attributes: + ANKAIOS_CONTROL_INTERFACE_BASE_PATH (str): The base path for the Ankaios control interface. + DEFAULT_TIMEOUT (int): The default timeout, if not manually provided. + logger (logging.Logger): The logger for the Ankaios class. + path (str): The path to the control interface. + """ ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" - WAITING_TIME_IN_SEC = 5 + DEFAULT_TIMEOUT = 5 def __init__(self) -> None: """Initialize the Ankaios object.""" @@ -51,24 +107,33 @@ def __init__(self) -> None: self._read_thread = None self._connected = False - self._responses_lock = Lock() + self._responses_lock = threading.Lock() self._responses: dict[str, ResponseEvent] = {} self._create_logger() def __enter__(self) -> "Ankaios": - """Connect to the control interface.""" + """ + Connect to the control interface. + + Returns: + Ankaios: The Ankaios object. + """ self.connect() return self def __exit__(self, exc_type, exc_value, traceback) -> None: - """Disconnect from the control interface.""" + """ + Disconnect from the control interface. + + Args: + exc_type (type): The exception type. + exc_value (Exception): The exception instance. + traceback (traceback): The traceback object. + """ + if exc_type is not None: # pragma: no cover + self.logger.error("An exception occurred: %s, %s, %s", exc_type, exc_value, traceback) self.disconnect() - pass - - def __del__(self) -> None: - """Destroy the Ankaios object.""" - self.logger.debug("Destroyed object of %s", str(self.__class__.__name__)) def _create_logger(self) -> None: """Create a logger with custom format and default log level.""" @@ -79,8 +144,13 @@ def _create_logger(self) -> None: self.logger.addHandler(handler) self.set_logger_level(AnkaiosLogLevel.INFO) - def _read_from_control_interface(self) -> None: - """Reads from the control interface input fifo and saves the response.""" + def _read_from_control_interface_old(self) -> None: # pragma: no cover + """ + Reads from the control interface input fifo and saves the response. + This is meant to be run in a separate thread. + It reads the response from the control interface and saves it in the responses dictionary, + by triggering the corresponding ResponseEvent. + """ with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: @@ -89,10 +159,12 @@ def _read_from_control_interface(self) -> None: varint_buffer = b'' while True: # Consume byte for byte - next_byte = f.read(1) + next_byte: bytes = f.read(1) + print(f"Read next_byte: {next_byte}, type: {type(next_byte)}") if not next_byte: break varint_buffer += next_byte + print(f"Updated varint_buffer: {varint_buffer}, type: {type(varint_buffer)}") # Stop if the most significant bit is 0 (indicating the last byte of the varint) if next_byte[0] & 0b10000000 == 0: break @@ -122,8 +194,70 @@ def _read_from_control_interface(self) -> None: self._responses[request_id] = ResponseEvent(response) self._responses[request_id].set() - def _get_response_by_id(self, request_id: str, timeout: int = 10) -> Response: - """Returns the response by the request id.""" + def _read_from_control_interface(self) -> None: + """ + Reads from the control interface input fifo and saves the response. + This is meant to be run in a separate thread. + It reads the response from the control interface and saves it in the responses dictionary, + by triggering the corresponding ResponseEvent. + """ + # pylint: disable=consider-using-with + f = open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") + + try: + while self._connected: + # Buffer for reading in the byte size of the proto msg + varint_buffer = bytearray() + while True: + # Consume byte for byte + next_byte = f.read(1) + if not next_byte: # pragma: no cover + break + varint_buffer += next_byte + # Stop if the most significant bit is 0 (indicating the last byte of the varint) + if next_byte[0] & 0b10000000 == 0: + break + # Decode the varint and receive the proto msg length + msg_len, _ = _DecodeVarint(varint_buffer, 0) + + # Buffer for the proto msg itself + msg_buf = bytearray() + for _ in range(msg_len): + # Read exact amount of byte according to the calculated proto msg length + next_byte = f.read(1) + if not next_byte: # pragma: no cover + break + msg_buf += next_byte + + try: + response = Response(msg_buf) + except ValueError as e: # pragma: no cover + self.logger.error("Error while reading: %s", e) + continue + + request_id = response.get_request_id() + with self._responses_lock: + if request_id in self._responses: + self._responses[request_id].set_response(response) + else: + self._responses[request_id] = ResponseEvent(response) + self._responses[request_id].set() + except Exception as e: # pylint: disable=broad-exception-caught + self.logger.error("Error while reading fifo file: %s", e) + finally: + f.close() + + def _get_response_by_id(self, request_id: str, timeout: int = DEFAULT_TIMEOUT) -> Response: + """ + Returns the response by the request id. + + Args: + request_id (str): The ID of the request. + timeout (int): The maximum time to wait for the response, in seconds. + + Returns: + Response: The response object. + """ if not self._connected: raise ValueError("Reading from the control interface is not started.") @@ -135,17 +269,31 @@ def _get_response_by_id(self, request_id: str, timeout: int = 10) -> Response: return self._responses[request_id].wait_for_response(timeout) def _write_to_pipe(self, request: Request) -> None: - """Writes the request into the control interface output fifo""" + """ + Writes the request into the control interface output fifo. + + Args: + request (Request): The request object to be written. + """ with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: - request_to_ankaios = _control_api.ToAnkaios(request=request._get()) + request_to_ankaios = _control_api.ToAnkaios(request=request._to_proto()) # Send the byte length of the proto msg f.write(_VarintBytes(request_to_ankaios.ByteSize())) # Send the proto msg itself f.write(request_to_ankaios.SerializeToString()) f.flush() - def _send_request(self, request: Request, timeout: int = 10) -> Response: - """Send a request and wait for the response.""" + def _send_request(self, request: Request, timeout: int = DEFAULT_TIMEOUT) -> Response: + """ + Send a request and wait for the response. + + Args: + request (Request): The request object to be sent. + timeout (int): The maximum time to wait for the response, in seconds. + + Returns: + Response: The response object. + """ if not self._connected: raise ValueError("Cannot request if not connected.") self._write_to_pipe(request) @@ -157,70 +305,100 @@ def _send_request(self, request: Request, timeout: int = 10) -> Response: return response def set_logger_level(self, level: AnkaiosLogLevel) -> None: - """Set the log level of the logger.""" - self.logger.setLevel(level) + """ + Set the log level of the logger. + + Args: + level (AnkaiosLogLevel): The log level to be set. + """ + self.logger.setLevel(level.value) def connect(self) -> None: - """Connect to the control interface by starting to read from the input fifo.""" + """ + Connect to the control interface by starting to read from the input fifo. + + Raises: + ValueError: If already connected. + """ if self._connected: raise ValueError("Already connected.") - self._read_thread = Thread(target=self._read_from_control_interface) - self._read_thread.start() self._connected = True + self._read_thread = threading.Thread(target=self._read_from_control_interface) + self._read_thread.start() def disconnect(self) -> None: - """Disconnect from the control interface by stopping to read from the input fifo.""" + """ + Disconnect from the control interface by stopping to read from the input fifo. + + Raises: + ValueError: If already disconnected. + """ if not self._connected: raise ValueError("Already disconnected.") self._connected = False self._read_thread.join() def apply_manifest(self, manifest: Manifest) -> None: - """Send a request to apply a manifest.""" + """ + Send a request to apply a manifest. + + Args: + manifest (Manifest): The manifest object to be applied. + """ request = Request(request_type="update_state") request.set_complete_state(manifest.generate_complete_state()) - for mask in manifest.calculate_masks(): + for mask in manifest._calculate_masks(): request.add_mask(mask) # Send request try: response = self._send_request(request) except TimeoutError as e: - self.logger.error(f"{e}") + self.logger.error("%s", e) return # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error(f"Error while trying to apply manifest: {content}") + self.logger.error("Error while trying to apply manifest: %s", content) elif content_type == "update_state_success": - self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". - format(content["added_workloads"], content["deleted_workloads"])) + self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", + content["added_workloads"], content["deleted_workloads"]) def delete_manifest(self, manifest: Manifest) -> None: - """Send a request to delete a manifest.""" + """ + Send a request to delete a manifest. + + Args: + manifest (Manifest): The manifest object to be deleted. + """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) - for mask in manifest.calculate_masks(): + for mask in manifest._calculate_masks(): request.add_mask(mask) # Send request try: response = self._send_request(request) except TimeoutError as e: - self.logger.error(f"{e}") + self.logger.error("%s", e) return # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error(f"Error while trying to delete manifest: {content}") + self.logger.error("Error while trying to delete manifest: %s", content) elif content_type == "update_state_success": - self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". - format(content["added_workloads"], content["deleted_workloads"])) + self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", + content["added_workloads"], content["deleted_workloads"]) def run_workload(self, workload: Workload) -> None: - """Send a request to run a workload.""" + """ + Send a request to run a workload. + + Args: + workload (Workload): The workload object to be run. + """ complete_state = CompleteState() complete_state.set_workload(workload) @@ -234,19 +412,24 @@ def run_workload(self, workload: Workload) -> None: try: response = self._send_request(request) except TimeoutError as e: - self.logger.error(f"{e}") + self.logger.error("%s", e) return # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error(f"Error while trying to run workload: {content}") + self.logger.error("Error while trying to run workload: %s", content) elif content_type == "update_state_success": - self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". - format(content["added_workloads"], content["deleted_workloads"])) + self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", + content["added_workloads"], content["deleted_workloads"]) def delete_workload(self, workload_name: str) -> None: - """Send a request to delete a workload.""" + """ + Send a request to delete a workload. + + Args: + workload_name (str): The name of the workload to be deleted. + """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) request.add_mask(f"desiredState.workloads.{workload_name}") @@ -254,99 +437,180 @@ def delete_workload(self, workload_name: str) -> None: try: response = self._send_request(request) except TimeoutError as e: - self.logger.error(f"{e}") + self.logger.error("%s", e) return # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error(f"Error while trying to delete workload: {content}") + self.logger.error("Error while trying to delete workload: %s", content) elif content_type == "update_state_success": - self.logger.info("Update successfull: {} added workloads, {} deleted workloads.". - format(content["added_workloads"], content["deleted_workloads"])) - - def get_workload(self, workload_name: str, timeout: int = 10) -> Workload: - """Get the workload from the requested complete state.""" - state = self.get_state(timeout, [f"desiredState.workloads.{workload_name}"]) - if state is not None: - return state.get_workload(workload_name) + self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", + content["added_workloads"], content["deleted_workloads"]) + + def get_workload(self, workload_name: str, + state: CompleteState = None, + timeout: int = DEFAULT_TIMEOUT) -> Workload: + """ + Get the workload from the requested complete state. + + Args: + workload_name (str): The name of the workload. + state (CompleteState): The complete state to get the workload from. + timeout (int): The maximum time to wait for the response, in seconds. + + Returns: + Workload: The workload object. + """ + if state is None: + state = self.get_state(timeout, [f"desiredState.workloads.{workload_name}"]) + return state.get_workload(workload_name) if state is not None else None def set_config_from_file(self, name: str, config_path: str) -> None: - """Set the config from a file.""" - with open(config_path, "r") as f: + """ + Set the config from a file. + + Args: + name (str): The name of the config. + config_path (str): The path to the config file. + """ + with open(config_path, "r", encoding="utf-8") as f: config = f.read() self.set_config(name, config) + # TODO Ankaios.set_config # pylint: disable=fixme def set_config(self, name: str, config: dict) -> None: - # TODO set_config - not yet implemented + """ + Set the config. + + Args: + name (str): The name of the config. + config (dict): The config dictionary. + """ raise NotImplementedError("set_config is not implemented yet.") + # TODO Ankaios.get_config this # pylint: disable=fixme def get_config(self, name: str) -> dict: - # TODO get_config - not yet implemented + """ + Get the config. + + Args: + name (str): The name of the config. + + Returns: + dict: The config dictionary. + """ raise NotImplementedError("get_config is not implemented yet.") + # TODO Ankaios.delete_config this # pylint: disable=fixme def delete_config(self, name: str) -> None: - # TODO delete_config - not yet implemented + """ + Delete the config. + + Args: + name (str): The name of the config. + """ raise NotImplementedError("delete_config is not implemented yet.") - def get_state(self, timeout: int = 10, field_mask: list[str] = list()) -> CompleteState: - """Send a request to get the complete state""" + def get_state(self, timeout: int = DEFAULT_TIMEOUT, + field_mask: list[str] = None) -> CompleteState: + """ + Send a request to get the complete state. + + Args: + timeout (int): The maximum time to wait for the response, in seconds. + field_mask (list[str]): The list of field masks to filter the state. + + Returns: + CompleteState: The complete state object. + """ request = Request(request_type="get_state") - for mask in field_mask: - request.add_mask(mask) + if field_mask is not None: + for mask in field_mask: + request.add_mask(mask) try: response = self._send_request(request, timeout) except TimeoutError as e: - self.logger.error(f"{e}") + self.logger.error("%s", e) return None # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error(f"Error while trying to get the state: {content}") + self.logger.error("Error while trying to get the state: %s", content) return None - complete_state = CompleteState(content) - return complete_state - - def get_agents(self, timeout: int = 10) -> list[str]: - """Get the agents from the requested complete state.""" - state = self.get_state(timeout) - if state is not None: - return state.get_agents() - - def get_workload_states(self, timeout: int = 10) -> WorkloadStateCollection: - state = self.get_state(timeout) - if state is not None: - return state.get_workload_states() - - def get_workload_states_on_agent(self, agent_name: str, timeout: int = 10) -> WorkloadStateCollection: - state = self.get_state(timeout, ["workloadStates." + agent_name]) - if state is not None: - return state.get_workload_states() - - def get_workload_states_on_workload_name(self, workload_name: str, timeout: int = 10) -> WorkloadStateCollection: - state = self.get_state(timeout, ["workloadStates." + workload_name]) - if state is not None: - return state.get_workload_states() - - -if __name__ == "__main__": - with Ankaios() as ankaios: - # Create workload - workload = Workload( - agent_name="agent_A", - runtime="podman", - restart_policy="NEVER", - runtime_config="image: docker.io/library/nginx\ncommandOptions: [\"-p\", \"8080:80\"]" - ) - - # Run workload - ankaios.run_workload("dynamic_nginx", workload) - - # Get state - complete_state = ankaios.get_state(field_mask=["workloadStates.agent_A.dynamic_nginx"]) - print(complete_state) - - # Delete workload - ankaios.delete_workload("dynamic_nginx") + return content + + def get_agents(self, state: CompleteState = None, timeout: int = DEFAULT_TIMEOUT) -> list[str]: + """ + Get the agents from the requested complete state. + + Args: + state (CompleteState): The complete state to get the agents from. + timeout (int): The maximum time to wait for the response, in seconds. + + Returns: + list[str]: The list of agent names. + """ + if state is None: + state = self.get_state(timeout) + return state.get_agents() if state is not None else None + + def get_workload_states(self, + state: CompleteState= None, + timeout: int = DEFAULT_TIMEOUT) -> WorkloadStateCollection: + """ + Get the workload states from the requested complete state. + If a state is not provided, it will be requested. + + Args: + state (CompleteState): The complete state to get the workload states from. + timeout (int): The maximum time to wait for the response, in seconds. + + Returns: + WorkloadStateCollection: The collection of workload states. + """ + if state is None: + state = self.get_state(timeout) + return state.get_workload_states() if state is not None else None + + def get_workload_states_on_agent(self, agent_name: str, + state: CompleteState = None, + timeout: int = DEFAULT_TIMEOUT) -> WorkloadStateCollection: + """ + Get the workload states on a specific agent from the requested complete state. + If a state is not provided, it will be requested. + + Args: + agent_name (str): The name of the agent. + state (CompleteState): The complete state to get the workload states from. + timeout (int): The maximum time to wait for the response, in seconds. + + Returns: + WorkloadStateCollection: The collection of workload states on the specified agent. + """ + if state is None: + state = self.get_state(timeout, ["workloadStates." + agent_name]) + return state.get_workload_states() if state is not None else None + + def get_workload_states_on_workload_name(self, workload_name: str, + state: CompleteState = None, + timeout: int = DEFAULT_TIMEOUT + ) -> WorkloadStateCollection: + """ + Get the workload states on a specific workload name from the requested complete state. + If a state is not provided, it will be requested. + + Args: + workload_name (str): The name of the workload. + state (CompleteState): The complete state to get the workload states from. + timeout (int): The maximum time to wait for the response, in seconds. + + Returns: + WorkloadStateCollection: The collection of workload states on the specified + workload name. + """ + if state is None: + state = self.get_state(timeout, ["workloadStates." + workload_name]) + return state.get_workload_states() if state is not None else None diff --git a/AnkaiosSDK/__init__.py b/AnkaiosSDK/__init__.py index 177beb0..841b8dc 100644 --- a/AnkaiosSDK/__init__.py +++ b/AnkaiosSDK/__init__.py @@ -12,6 +12,15 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module contains the AnkaiosSDK package. +It exposes to the user all the classes available in the SDK. + +Imports: + Ankaios: The main SDK class. + All the other classes, available in the _components folder. +""" + from .Ankaios import * from ._components import * diff --git a/AnkaiosSDK/_components/CompleteState.py b/AnkaiosSDK/_components/CompleteState.py index 9246f24..0f8fccb 100644 --- a/AnkaiosSDK/_components/CompleteState.py +++ b/AnkaiosSDK/_components/CompleteState.py @@ -12,6 +12,35 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This script defines the CompleteState class for managing the state of the system. + +Classes: + - CompleteState: Represents the complete state of the system. + +Usage: + - Create a CompleteState instance: + complete_state = CompleteState() + + - Get the API version of the complete state: + api_version = complete_state.get_api_version() + + - Add a workload to the complete state: + complete_state.set_workload(workload) + + - Get a workload from the complete state: + workload = complete_state.get_workload("nginx") + + - Get a list of workloads from the complete state: + workloads = complete_state.get_workloads() + + - Get the connected agents: + agents = complete_state.get_agents() + + - Get the workload states: + workload_states = complete_state.get_workload_states() +""" + from .._protos import _ank_base from .Workload import Workload from .WorkloadState import WorkloadStateCollection @@ -23,125 +52,139 @@ class CompleteState: """ - A class to represent the complete state + A class to represent the complete state. """ def __init__(self, api_version: str = DEFAULT_API_VERSION) -> None: + """ + Initializes a CompleteState instance with the given API version. + + Args: + api_version (str): The API version to set for the complete state. + """ self._complete_state = _ank_base.CompleteState() self._set_api_version(api_version) self._workloads: list[Workload] = [] self._workload_state_collection = WorkloadStateCollection() def __str__(self) -> str: + """ + Returns the string representation of the complete state. + + Returns: + str: The string representation of the complete state. + """ return str(self._to_proto()) def _set_api_version(self, version: str) -> None: - """Set the API version for the complete state.""" + """ + Sets the API version for the complete state. + + Args: + version (str): The API version to set. + """ self._complete_state.desiredState.apiVersion = version + def get_api_version(self) -> str: + """ + Gets the API version of the complete state. + + Returns: + str: The API version of the complete state. + """ + return str(self._complete_state.desiredState.apiVersion) + def set_workload(self, workload: Workload) -> None: - """Add a workload to the complete state.""" + """ + Adds a workload to the complete state. + + Args: + workload (Workload): The workload to add. + """ self._workloads.append(workload) def get_workload(self, workload_name: str) -> Workload: - """Get a workload from the complete state by it's name.""" + """ + Gets a workload from the complete state by its name. + + Args: + workload_name (str): The name of the workload to retrieve. + + Returns: + Workload: The workload with the specified name, or None if not found. + """ for wl in self._workloads: if wl.name == workload_name: return wl return None def get_workloads(self) -> list[Workload]: - """Get a workloads dict from the complete state.""" + """ + Gets a list of workloads from the complete state. + + Returns: + list[Workload]: A list of workloads in the complete state. + """ return self._workloads - + def get_workload_states(self) -> WorkloadStateCollection: - """Get the workload states.""" + """ + Gets the workload states. + + Returns: + WorkloadStateCollection: The collection of workload states. + """ return self._workload_state_collection def get_agents(self) -> list[str]: - """Get the connected agents.""" + """ + Gets the connected agents. + + Returns: + list[str]: A list of connected agents. + """ # Return keys because the value "AgentAttributes" is not yet implemented - return self._complete_state.agents.keys() - + return list(self._complete_state.agents.agents.keys()) + def _from_dict(self, dict_state: dict) -> None: - """Convert a dictionary to a CompleteState object.""" + """ + Converts a dictionary to a CompleteState object. + + Args: + dict_state (dict): The dictionary representing the complete state. + """ self._complete_state = _ank_base.CompleteState() - self._set_api_version(dict_state.get("apiVersion", DEFAULT_API_VERSION)) + self._set_api_version(dict_state.get("apiVersion", self.get_api_version())) self._workloads = [] + if dict_state.get("workloads") is None: + return for workload_name, workload_dict in dict_state.get("workloads").items(): self._workloads.append(Workload._from_dict(workload_name, workload_dict)) def _to_proto(self) -> _ank_base.CompleteState: - """Convert the CompleteState object to a proto message.""" + """ + Converts the CompleteState object to a proto message. + + Returns: + _ank_base.CompleteState: The protobuf message representing the complete state. + """ # Clear previous workloads for workload in self._workloads: - self._complete_state.desiredState.workloads.workloads[workload.name].CopyFrom(workload._to_proto()) + self._complete_state.desiredState.workloads.workloads[workload.name]\ + .CopyFrom(workload._to_proto()) return self._complete_state def _from_proto(self, proto: _ank_base.CompleteState) -> None: - """Convert the proto message to a CompleteState object.""" + """ + Converts the proto message to a CompleteState object. + + Args: + proto (_ank_base.CompleteState): The protobuf message representing the complete state. + """ self._complete_state = proto - self._workloads = {} - for workload_name, proto_workload in self._complete_state.desiredState.workloads.workloads.items(): + self._workloads = [] + for workload_name, proto_workload in self._complete_state.desiredState\ + .workloads.workloads.items(): workload = Workload(workload_name) workload._from_proto(proto_workload) self._workloads.append(workload) self._workload_state_collection._from_proto(self._complete_state.workloadStates) - - -if __name__ == "__main__": - complete_state = CompleteState() - - # Create workload - workload = Workload.builder().workload_name("nginx").build() - workload2 = Workload.builder().workload_name("dyn_nginx").build() - - # Add workload to complete state - complete_state.set_workload(workload) - complete_state.set_workload(workload2) - - print(complete_state) - - new_complete_state = CompleteState() - new_complete_state._from_proto(complete_state._to_proto()) - print(new_complete_state) - - complete_state_workload_states = CompleteState() - complete_state_workload_states._from_proto(_ank_base.CompleteState( - workloadStates=_ank_base.WorkloadStatesMap(agentStateMap={ - "agent_A": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ - "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ - "1234": _ank_base.ExecutionState( - additionalInfo="Random info", - succeeded=_ank_base.SUCCEEDED_OK, - ) - }) - }), - "agent_B": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ - "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ - "5678": _ank_base.ExecutionState( - additionalInfo="Random info", - pending=_ank_base.PENDING_WAITING_TO_START, - ) - }), - "dyn_nginx": _ank_base.ExecutionsStatesForId(idStateMap={ - "9012": _ank_base.ExecutionState( - additionalInfo="Random info", - stopping=_ank_base.STOPPING_WAITING_TO_STOP, - ) - }) - }) - }) - ) - ) - - print(complete_state_workload_states._complete_state.workloadStates) - - print("\nFor agent_B:") - workload_states_by_agent = complete_state_workload_states.get_workload_states_on_agent("agent_B") - for key in workload_states_by_agent: - print(f"Workload ID: {key}, workload name: {workload_states_by_agent[key][0]}, state: {workload_states_by_agent[key][1]}") - - print("\nFor nginx workloads:") - workload_states_by_name = complete_state_workload_states.get_workload_states_on_workload_name("nginx") - for key in workload_states_by_name: - print(f"Workload ID: {key}, agent name: {workload_states_by_name[key][0]}, state: {workload_states_by_name[key][1]}") diff --git a/AnkaiosSDK/_components/Manifest.py b/AnkaiosSDK/_components/Manifest.py index 92da278..810e6c3 100644 --- a/AnkaiosSDK/_components/Manifest.py +++ b/AnkaiosSDK/_components/Manifest.py @@ -12,12 +12,45 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module defines the Manifest class for handling ankaios manifests. + +Classes: + - Manifest: Represents a workload manifest and provides methods to validate and load it. + +Usage: + - Load a manifest from a file: + manifest = Manifest.from_file("path/to/manifest.yaml") + + - Load a manifest from a string: + manifest = Manifest.from_string("apiVersion: 1.0\nworkloads: {}") + + - Load a manifest from a dictionary: + manifest = Manifest.from_dict({"apiVersion": "1.0", "workloads": {}}) + + - Generate a CompleteState instance from the manifest: + complete_state = manifest.generate_complete_state() +""" + import yaml from .CompleteState import CompleteState class Manifest(): + """ + Represents a workload manifest. + The manifest can be loaded from a yaml file, string or dictionary. + """ def __init__(self, manifest: dict) -> None: + """ + Initializes a Manifest instance with the given manifest data. + + Args: + manifest (dict): The manifest data. + + Raises: + ValueError: If the manifest data is invalid. + """ self._manifest: dict = manifest if not self.check(): @@ -25,41 +58,93 @@ def __init__(self, manifest: dict) -> None: @staticmethod def from_file(file_path: str) -> 'Manifest': + """ + Loads a manifest from a file. + + Args: + file_path (str): The path to the manifest file. + + Returns: + Manifest: An instance of the Manifest class with the loaded data. + + Raises: + FileNotFoundError: If the file does not exist. + yaml.YAMLError: If there is an error parsing the YAML file. + """ try: - with open(file_path, 'r') as file: + with open(file_path, 'r', encoding="utf-8") as file: return Manifest.from_string(file.read()) except Exception as e: - raise ValueError(f"Error reading manifest file: {e}") - + raise ValueError(f"Error reading manifest file: {e}") from e + @staticmethod def from_string(manifest: str) -> 'Manifest': + """ + Creates a Manifest instance from a YAML string. + + Args: + manifest (str): The YAML string representing the manifest. + + Returns: + Manifest: An instance of the Manifest class with the parsed data. + + Raises: + ValueError: If there is an error parsing the YAML string. + """ try: return Manifest.from_dict(yaml.safe_load(manifest)) except Exception as e: - raise ValueError(f"Error parsing manifest: {e}") - + raise ValueError(f"Error parsing manifest: {e}") from e + @staticmethod def from_dict(manifest: dict) -> 'Manifest': + """ + Creates a Manifest instance from a dictionary. + + Args: + manifest (dict): The dictionary representing the manifest. + + Returns: + Manifest: An instance of the Manifest class with the given data. + """ return Manifest(manifest) - + def check(self) -> bool: + """ + Validates the manifest data. + + Returns: + bool: True if the manifest data is valid, False otherwise. + """ if "apiVersion" not in self._manifest.keys(): return False if "workloads" not in self._manifest.keys(): return False - wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", + wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", "dependencies", "tags", "controlInterfaceAccess"] for wl_name in self._manifest["workloads"]: for key in self._manifest["workloads"][wl_name].keys(): if key not in wl_allowed_keys: return False return True - - def calculate_masks(self) -> list[str]: - return [f"desiredState.workloads.{key}" + + def _calculate_masks(self) -> list[str]: + """ + Calculates the masks for the workloads in the manifest. + + Returns: + list[str]: A list of masks for the workloads. + """ + return [f"desiredState.workloads.{key}" for key in self._manifest["workloads"].keys()] - + def generate_complete_state(self) -> CompleteState: + """ + Generates a CompleteState instance from the manifest. + + Returns: + CompleteState: An instance of the CompleteState class populated with the manifest data. + """ complete_state = CompleteState() complete_state._from_dict(self._manifest) return complete_state diff --git a/AnkaiosSDK/_components/Request.py b/AnkaiosSDK/_components/Request.py index 73d3609..b3b0f19 100644 --- a/AnkaiosSDK/_components/Request.py +++ b/AnkaiosSDK/_components/Request.py @@ -12,6 +12,28 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module defines the Request class for creating and handling requests to the Ankaios system. + +Classes: + Request: Represents a request to the Ankaios system and provides methods to get and set + the state of the system. + +Usage: + - Create a Request for updating the state: + request = Request(request_type="update_state") + request.set_complete_state(complete_state) + + - Create a Request for getting the state: + request = Request(request_type="get_state") + + - Get the request ID: + request_id = request.get_id() + + - Add a mask to the request: + request.add_mask("desiredState.workloads") +""" + import uuid from .._protos import _ank_base from .CompleteState import CompleteState @@ -21,7 +43,19 @@ class Request: + """ + Represents a request to the Ankaios system. + """ def __init__(self, request_type: str) -> None: + """ + Initializes a Request instance with the given request type. + + Args: + request_type (str): The type of the request, either "update_state" or "get_state". + + Raises: + ValueError: If the request type is invalid. + """ self._request = _ank_base.Request() self._request.requestId = str(uuid.uuid4()) self._request_type = request_type @@ -30,40 +64,55 @@ def __init__(self, request_type: str) -> None: raise ValueError("Invalid request type. Supported values: 'update_state', 'get_state'.") def __str__(self) -> str: + """ + Returns the string representation of the request. + + Returns: + str: The string representation of the request. + """ return str(self._to_proto()) - def _get_id(self) -> str: - """Get the request ID.""" + def get_id(self) -> str: + """ + Gets the request ID. + + Returns: + str: The request ID. + """ return self._request.requestId def set_complete_state(self, complete_state: CompleteState) -> None: - """Set the complete state for the request.""" + """ + Sets the complete state for the request. + + Args: + complete_state (CompleteState): The complete state to set for the request. + + Raises: + ValueError: If the request type is not "update_state". + """ if self._request_type != "update_state": raise ValueError("Complete state can only be set for an update state request.") self._request.updateStateRequest.newState.CopyFrom(complete_state._to_proto()) def add_mask(self, mask: str) -> None: - """Set the update mask for the request.""" + """ + Sets the update mask for the request. + + Args: + mask (str): The mask to set for the request. + """ if self._request_type == "update_state": self._request.updateStateRequest.updateMask.append(mask) elif self._request_type == "get_state": self._request.completeStateRequest.fieldMask.append(mask) - else: - raise ValueError("Invalid request type.") def _to_proto(self) -> _ank_base.Request: - """Convert the Request object to a proto message.""" - return self._request + """ + Converts the Request object to a proto message. - -if __name__ == "__main__": - request_update = Request(request_type="update_state") - - # Create the CompleteState object - complete_state = CompleteState() - request_update.set_complete_state(complete_state) - print(request_update) - - request_get = Request(request_type="get_state") - print(request_get) + Returns: + _ank_base.Request: The protobuf message representing the request. + """ + return self._request diff --git a/AnkaiosSDK/_components/Response.py b/AnkaiosSDK/_components/Response.py index 905586b..53c2777 100644 --- a/AnkaiosSDK/_components/Response.py +++ b/AnkaiosSDK/_components/Response.py @@ -12,19 +12,51 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This script defines the Response and ResponseEvent classes, +used for receiving messages from the control interface. + +Classes: + - Response: Represents a response from the control interface. + - ResponseEvent: Represents an event used to wait for a response. + +Usage: + - Get response content: + response = Response() + (content_type, content) = response.get_content() + + - Check if the request_id matches: + response = Response() + if response.check_request_id("1234"): + print("Request ID matches") +""" + from typing import Union from threading import Event -from .._protos import _ank_base, _control_api +from .._protos import _control_api from .CompleteState import CompleteState -from .Workload import Workload __all__ = ["Response", "ResponseEvent"] class Response: + """ + Represents a response received from the Ankaios system. + + Attributes: + buffer (bytes): The received message buffer. + content_type (str): The type of the response content + (e.g., "error", "complete_state", "update_state_success"). + content: The content of the response, which can be a string, CompleteState, or dictionary. + """ def __init__(self, message_buffer: bytes) -> None: - """Initialize the Response object with the received message buffer.""" + """ + Initializes the Response object with the received message buffer. + + Args: + message_buffer (bytes): The received message buffer. + """ self.buffer = message_buffer self._response = None self.content_type = None @@ -34,18 +66,27 @@ def __init__(self, message_buffer: bytes) -> None: self._from_proto() def _parse_response(self) -> None: + """ + Parses the received message buffer into a protobuf response message. + + Raises: + ValueError: If there is an error parsing the message buffer. + """ from_ankaios = _control_api.FromAnkaios() try: # Deserialize the received proto msg from_ankaios.ParseFromString(self.buffer) except Exception as e: - raise ValueError(f"Invalid response, parsing error: '{e}'") + raise ValueError(f"Invalid response, parsing error: '{e}'") from e self._response = from_ankaios.response def _from_proto(self) -> None: """ - Convert the proto message to a Response object. + Converts the parsed protobuf message to a Response object. This can be either an error, a complete state, or an update state success. + + Raises: + ValueError: If the response type is invalid. """ if self._response.HasField("error"): self.content_type = "error" @@ -64,63 +105,83 @@ def _from_proto(self) -> None: raise ValueError("Invalid response type.") def get_request_id(self) -> str: - """Get the request_id of the response.""" + """ + Gets the request id of the response. + + Returns: + str: The request id of the response. + """ return self._response.requestId def check_request_id(self, request_id: str) -> bool: - """Check if the request_id of the response matches the given request_id.""" + """ + Checks if the request id of the response matches the given request id. + + Args: + request_id (str): The request id to check against. + + Returns: + bool: True if the request_id matches, False otherwise. + """ return self._response.requestId == request_id def get_content(self) -> tuple[str, Union[str, CompleteState, dict]]: - """Get the content of the response.""" + """ + Gets the content of the response. + + Returns: + (tuple[str, Union[str, CompleteState, dict]]): A tuple containing the content type + and the content of the response. + """ return (self.content_type, self.content) class ResponseEvent(Event): + """ + Represents an event that holds a Response object. + """ def __init__(self, response: Response = None) -> None: + """ + Initializes the ResponseEvent with an optional Response object. + + Args: + response Optional(Response): The response to associate with the event. Defaults to None. + """ super().__init__() self._response = response def set_response(self, response: Response) -> None: - """Set the response.""" + """ + Sets the response and triggers the event. + + Args: + response (Response): The response to set. + """ self._response = response self.set() def get_response(self) -> Response: - """Get the response.""" + """ + Gets the response associated with the event. + + Returns: + Response: The response associated with the event. + """ return self._response def wait_for_response(self, timeout: int) -> Response: - """Wait for the response.""" - if not self.wait(timeout): - raise TimeoutError("Timeout while waiting for the response.") - return self.get_response() - - -if __name__ == "__main__": - complete_state = CompleteState() - - # Create workload - workload = Workload.builder().workload_name("nginx").build() - workload2 = Workload.builder().workload_name("dyn_nginx").build() - - # Add workload to complete state - complete_state.set_workload(workload) - complete_state.set_workload(workload2) - + """ + Waits for the response to be set, with a specified timeout. - from_ankaios = _control_api.FromAnkaios( - response=_ank_base.Response( - requestId="1234", - completeState=complete_state._to_proto() - ) - ) + Args: + timeout (int): The maximum time to wait for the response, in seconds. - response = Response(from_ankaios.SerializeToString()) - print(response.get_request_id()) - (content_type, content) = response.get_content() - print(content_type) - print(content) + Returns: + Response: The response associated with the event. - if response.check_request_id("1234"): - print("Request ID matches") \ No newline at end of file + Raises: + TimeoutError: If the response is not set within the specified timeout. + """ + if not self.wait(timeout): + raise TimeoutError("Timeout while waiting for the response.") + return self.get_response() diff --git a/AnkaiosSDK/_components/Workload.py b/AnkaiosSDK/_components/Workload.py index 449bece..0732199 100644 --- a/AnkaiosSDK/_components/Workload.py +++ b/AnkaiosSDK/_components/Workload.py @@ -16,8 +16,8 @@ This script defines the Workload and WorkloadBuilder classes for creating and managing workloads. Classes: - Workload: Represents a workload with various attributes and methods to update them. - WorkloadBuilder: A builder class to create a Workload object with a fluent interface. + - Workload: Represents a workload with various attributes and methods to update them. + - WorkloadBuilder: A builder class to create a Workload object with a fluent interface. Usage: - Create a workload using the WorkloadBuilder: @@ -487,7 +487,7 @@ def build(self) -> Workload: raise ValueError("Workload can not be built without a name.") workload = Workload(self.wl_name) - workload._set_from_builder() # pylint: disable=protected-access + workload._set_from_builder() if self.wl_agent_name is None: raise ValueError("Workload can not be built without an agent name.") @@ -507,5 +507,5 @@ def build(self) -> Workload: if len(self.tags) > 0: workload.update_tags(self.tags) - workload._add_mask(f"desiredState.workloads.{workload.name}") # pylint: disable=protected-access + workload._add_mask(f"desiredState.workloads.{workload.name}") return workload diff --git a/AnkaiosSDK/_components/WorkloadState.py b/AnkaiosSDK/_components/WorkloadState.py index 00f26c5..261a226 100644 --- a/AnkaiosSDK/_components/WorkloadState.py +++ b/AnkaiosSDK/_components/WorkloadState.py @@ -18,14 +18,14 @@ including converting between different representations and handling collections of workload states. Classes: - WorkloadExecutionState: Represents the execution state and sub-state of a workload. - WorkloadInstanceName: Represents the name of a workload instance. - WorkloadState: Represents the state of a workload (execution state and name). - WorkloadStateCollection: A collection of workload states. + - WorkloadExecutionState: Represents the execution state and sub-state of a workload. + - WorkloadInstanceName: Represents the name of a workload instance. + - WorkloadState: Represents the state of a workload (execution state and name). + - WorkloadStateCollection: A collection of workload states. Enums: - WorkloadStateEnum: Enumeration for different states of a workload. - WorkloadSubStateEnum: Enumeration for different sub-states of a workload. + - WorkloadStateEnum: Enumeration for different states of a workload. + - WorkloadSubStateEnum: Enumeration for different sub-states of a workload. Usage: - Get all workload states: @@ -278,8 +278,8 @@ def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: if field is None: raise ValueError("Invalid state for workload.") - self.state = WorkloadStateEnum._get(field) # pylint: disable=protected-access - self.substate = WorkloadSubStateEnum._get(self.state, getattr(exec_state, field)) # pylint: disable=protected-access + self.state = WorkloadStateEnum._get(field) + self.substate = WorkloadSubStateEnum._get(self.state, getattr(exec_state, field)) # pylint: disable=too-few-public-methods diff --git a/AnkaiosSDK/_protos/__init__.py b/AnkaiosSDK/_protos/__init__.py index 4938f8c..3e8a700 100644 --- a/AnkaiosSDK/_protos/__init__.py +++ b/AnkaiosSDK/_protos/__init__.py @@ -12,7 +12,16 @@ # # SPDX-License-Identifier: Apache-2.0 -# TODO remove this line after the issue is fixed +""" +This module contains the AnkaiosSDK protobuf components. +It contains the proto files and the generated protobuf classes. + +Imports: + ank_base_pb2: Used for general grpc messages. + control_api_pb2: Used for exchanging messages with the control interface. +""" + +# TODO remove this line after the issue is fixed # pylint: disable=fixme # https://github.com/grpc/grpc/issues/37609 # https://github.com/protocolbuffers/protobuf/issues/18096 import warnings diff --git a/run_tests.py b/run_tests.py index b055b07..3da7207 100644 --- a/run_tests.py +++ b/run_tests.py @@ -12,6 +12,16 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +Run tests for AnkaiosSDK Python package. +This script runs unit tests, coverage and pylint and +saves the results in the reports directory. + +Example usage: + # This will run the unit tests with the --full-trace option + python3 run_tests.py -u --full-trace +""" + import os import pytest import argparse @@ -25,17 +35,17 @@ PYLINT_DIR = os.path.join(REPORT_DIR, "pylint") -def run_pytest_utest(): +def run_pytest_utest(args): os.makedirs(UTEST_DIR, exist_ok=True) pytest.main([ '--junitxml={}'.format(os.path.join(UTEST_DIR, 'utest_report.xml')), 'tests', # '-p', 'no:warnings', '-vv' - ]) + ] + args) -def run_pytest_cov(): +def run_pytest_cov(args): os.makedirs(COVERAGE_DIR, exist_ok=True) pytest.main([ '--cov={}'.format(PROJECT_NAME), @@ -45,14 +55,14 @@ def run_pytest_cov(): 'tests', '-p', 'no:warnings', '-vv' - ]) + ] + args) -def run_pylint(): +def run_pylint(args): os.makedirs(PYLINT_DIR, exist_ok=True) result = subprocess.run([ 'pylint', PROJECT_NAME, 'tests', '--rcfile=.pylintrc', '--output-format=parseable' - ], capture_output=True, text=True) + ] + args, capture_output=True, text=True) pylint_output = result.stdout rating_line = None @@ -71,21 +81,22 @@ def run_pylint(): if __name__ == "__main__": - parser = argparse.ArgumentParser(description=f'Run tests for {PROJECT_NAME} Python package') + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('-c', '--cov', action='store_true', help='Run coverage') parser.add_argument('-u', '--utest', action='store_true', help='Run unit tests') parser.add_argument('-l', '--lint', action='store_true', help='Run pylint') parser.add_argument('-a', '--all', action='store_true', help='Run all tests') - args = parser.parse_args() + args, extra_args = parser.parse_known_args() if not any([args.cov, args.utest, args.lint, args.all]): parser.print_help() exit(0) os.makedirs(REPORT_DIR, exist_ok=True) if args.cov or args.all: - run_pytest_cov() + run_pytest_cov(extra_args) if args.utest or args.all: - run_pytest_utest() + run_pytest_utest(extra_args) if args.lint or args.all: - run_pylint() + run_pylint(extra_args) diff --git a/tests/Response/__init__.py b/tests/Response/__init__.py new file mode 100644 index 0000000..561306d --- /dev/null +++ b/tests/Response/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/Response/test_response.py b/tests/Response/test_response.py new file mode 100644 index 0000000..c51591f --- /dev/null +++ b/tests/Response/test_response.py @@ -0,0 +1,115 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module contains unit tests for the Response class in the AnkaiosSDK. +""" + +import pytest +from google.protobuf.internal.encoder import _VarintBytes +from AnkaiosSDK import Response, CompleteState +from AnkaiosSDK._protos import _ank_base, _control_api + + +MESSAGE_BUFFER_ERROR = _control_api.FromAnkaios( + response=_ank_base.Response( + requestId="1234", + error=_ank_base.Error( + message="Test error message", + ) + ) +).SerializeToString() + +MESSAGE_BUFFER_COMPLETE_STATE = _control_api.FromAnkaios( + response=_ank_base.Response( + requestId="1234", + completeState=_ank_base.CompleteState( + desiredState=_ank_base.State( + apiVersion="v0.1", + workloads=_ank_base.WorkloadMap( + workloads={}, + ) + ) + ) + ) +).SerializeToString() + +MESSAGE_UPDATE_SUCCESS = _control_api.FromAnkaios( + response=_ank_base.Response( + requestId="1234", + UpdateStateSuccess=_ank_base.UpdateStateSuccess( + addedWorkloads=["new_nginx"], + deletedWorkloads=["old_nginx"], + ) + ) +) +MESSAGE_BUFFER_UPDATE_SUCCESS = MESSAGE_UPDATE_SUCCESS.SerializeToString() +MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH = _VarintBytes(MESSAGE_UPDATE_SUCCESS.ByteSize()) + +MESSAGE_BUFFER_INVALID_RESPONSE = _control_api.FromAnkaios( + response=_ank_base.Response( + requestId="1234", + ) +).SerializeToString() + + +def test_initialisation(): + """ + Test the initialisation of a Response object. + This step tests the parsing of the response buffer into a proto object + and the conversion to a Response object. + """ + # Test error message + response = Response(MESSAGE_BUFFER_ERROR) + assert response.content_type == "error" + assert response.content == "Test error message" + + # Test CompleteState message + response = Response(MESSAGE_BUFFER_COMPLETE_STATE) + assert response.content_type == "complete_state" + assert isinstance(response.content, CompleteState) + + # Test UpdateStateSuccess message + response = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + assert response.content_type == "update_state_success" + assert response.content == { + "added_workloads": ["new_nginx"], + "deleted_workloads": ["old_nginx"], + } + + # Test invalid buffer + with pytest.raises(ValueError, match="Invalid response, parsing error"): + _ = Response(b"invalid_buffer{") + + # Test invalid response type + with pytest.raises(ValueError, match="Invalid response type"): + response = Response(MESSAGE_BUFFER_INVALID_RESPONSE) + + +def test_getters(): + """ + Test the getter methods of the Response class. + """ + response = Response(MESSAGE_BUFFER_ERROR) + assert response.get_request_id() == "1234" + assert response.get_content() == ("error", "Test error message") + + +def test_check_request_id(): + """ + Test the check_request_id method of the Response class. + """ + response = Response(MESSAGE_BUFFER_ERROR) + assert response.check_request_id("1234") + assert not response.check_request_id("4321") diff --git a/tests/Response/test_response_event.py b/tests/Response/test_response_event.py new file mode 100644 index 0000000..2dde50d --- /dev/null +++ b/tests/Response/test_response_event.py @@ -0,0 +1,41 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module contains unit tests for the ResponseEvent class in the AnkaiosSDK. +""" + +import pytest +from AnkaiosSDK import ResponseEvent, Response +from tests.Response.test_response import MESSAGE_BUFFER_ERROR + + +def test_event(): + """ + Test the set and wait for event. + """ + response_event = ResponseEvent() + assert not response_event.is_set() + + response = Response(MESSAGE_BUFFER_ERROR) + response_event.set_response(response) + assert response_event.is_set() + + assert response_event.wait_for_response(0.01) == response + + response_event.clear() + assert not response_event.is_set() + + with pytest.raises(TimeoutError): + response_event.wait_for_response(0.01) diff --git a/tests/Workload/__init__.py b/tests/Workload/__init__.py new file mode 100644 index 0000000..561306d --- /dev/null +++ b/tests/Workload/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/Workload/test_workload.py b/tests/Workload/test_workload.py index abf5cac..a4dcc96 100644 --- a/tests/Workload/test_workload.py +++ b/tests/Workload/test_workload.py @@ -12,16 +12,31 @@ # # SPDX-License-Identifier: Apache-2.0 -import pytest +""" +This module contains unit tests for the Workload class in the AnkaiosSDK. + +Fixtures: + workload: Returns a Workload instance with some default values. + +Helper Functions: + generate_workload: Helper function to generate a Workload instance with some default values. +""" + from unittest.mock import patch, mock_open +import pytest from AnkaiosSDK import Workload, WorkloadBuilder from AnkaiosSDK._protos import _ank_base -@pytest.fixture -def workload(): +def generate_test_workload(workload_name: str = "workload_test") -> Workload: + """ + Helper function to generate a Workload instance with some default values. + + Returns: + Workload: A Workload instance. + """ return Workload.builder() \ - .workload_name("workload_test") \ + .workload_name(workload_name) \ .agent_name("agent_Test") \ .runtime("runtime_test") \ .restart_policy("NEVER") \ @@ -32,13 +47,36 @@ def workload(): .build() -def test_builder(workload): +@pytest.fixture +def workload() -> Workload: + """ + Fixture that returns a Workload instance with some default values. + + Returns: + Workload: A Workload instance. + """ + return generate_test_workload() + + +def test_builder(workload): # pylint: disable=redefined-outer-name + """ + Test the builder method of the Workload class. + + Args: + workload (Workload): The Workload fixture. + """ builder = workload.builder() assert builder is not None assert isinstance(builder, WorkloadBuilder) -def test_update_fields(workload): +def test_update_fields(workload): # pylint: disable=redefined-outer-name + """ + Test updating various fields of the Workload instance. + + Args: + workload (Workload): The Workload fixture. + """ assert workload._get_masks() == ["desiredState.workloads.workload_test"] workload.update_workload_name("new_workload_test") @@ -56,14 +94,20 @@ def test_update_fields(workload): with patch("builtins.open", mock_open(read_data="new_config_test_from_file")): workload.update_runtime_config_from_file("new_config_test_from_file") assert workload._workload.runtimeConfig == "new_config_test_from_file" - + with pytest.raises(ValueError): workload.update_restart_policy("INVALID_POLICY") workload.update_restart_policy("ON_FAILURE") assert workload._workload.restartPolicy == _ank_base.ON_FAILURE -def test_dependencies(workload): +def test_dependencies(workload): # pylint: disable=redefined-outer-name + """ + Test adding and updating dependencies of the Workload instance. + + Args: + workload (Workload): The Workload fixture. + """ assert len(workload.get_dependencies()) == 1 with pytest.raises(ValueError): @@ -73,7 +117,7 @@ def test_dependencies(workload): assert len(workload.get_dependencies()) == 2 workload.add_dependency("another_workload_test", "FAILED") - + deps = workload.get_dependencies() assert len(deps) == 3 deps.pop("other_workload_test") @@ -82,7 +126,13 @@ def test_dependencies(workload): assert len(workload.get_dependencies()) == 2 -def test_tags(workload): +def test_tags(workload): # pylint: disable=redefined-outer-name + """ + Test adding and updating tags of the Workload instance. + + Args: + workload (Workload): The Workload fixture. + """ assert len(workload.get_tags()) == 2 # Allow duplicate tags @@ -96,7 +146,13 @@ def test_tags(workload): assert len(workload.get_tags()) == 2 -def test_proto(workload): +def test_to_proto(workload): # pylint: disable=redefined-outer-name + """ + Test converting the Workload instance to protobuf message. + + Args: + workload (Workload): The Workload fixture. + """ proto = workload._to_proto() assert proto is not None assert proto.agent == "agent_Test" @@ -105,17 +161,32 @@ def test_proto(workload): assert proto.runtimeConfig == "config_test" assert proto.dependencies.dependencies == {"workload_test_other": _ank_base.ADD_COND_RUNNING} assert proto.tags == _ank_base.Tags(tags=[ - _ank_base.Tag(key="key1", value="value1"), + _ank_base.Tag(key="key1", value="value1"), _ank_base.Tag(key="key2", value="value2") ]) + +def test_from_proto(workload): # pylint: disable=redefined-outer-name + """ + Test converting theprotobuf message to a Workload instance. + + Args: + workload (Workload): The Workload fixture. + """ + proto = workload._to_proto() new_workload = Workload("workload_test") new_workload._from_proto(proto) assert new_workload is not None assert str(workload) == str(new_workload) -def test_from_dict(workload): +def test_from_dict(workload): # pylint: disable=redefined-outer-name + """ + Test creating a Workload instance from a dictionary. + + Args: + workload (Workload): The Workload fixture. + """ workload_dict = { "name": "workload_test", "agent": "agent_Test", @@ -132,21 +203,36 @@ def test_from_dict(workload): @pytest.mark.parametrize("function_name, data, mask", [ - ("update_workload_name", {"name": "workload_test"}, "desiredState.workloads.workload_test"), - ("update_agent_name", {"agent_name": "agent_Test"}, "desiredState.workloads.workload_test.agent"), - ("update_runtime", {"runtime": "runtime_test"}, "desiredState.workloads.workload_test.runtime"), - ("update_restart_policy", {"policy": "NEVER"}, "desiredState.workloads.workload_test.restartPolicy"), - ("update_runtime_config", {"config": "config_test"}, "desiredState.workloads.workload_test.runtimeConfig"), - ("add_dependency", {"workload_name": "workload_test_other", "condition": "RUNNING"}, "desiredState.workloads.workload_test.dependencies"), - ("add_tag", {"key": "key1", "value": "value1"}, "desiredState.workloads.workload_test.tags"), + ("update_workload_name", {"name": "workload_test"}, + "desiredState.workloads.workload_test"), + ("update_agent_name", {"agent_name": "agent_Test"}, + "desiredState.workloads.workload_test.agent"), + ("update_runtime", {"runtime": "runtime_test"}, + "desiredState.workloads.workload_test.runtime"), + ("update_restart_policy", {"policy": "NEVER"}, + "desiredState.workloads.workload_test.restartPolicy"), + ("update_runtime_config", {"config": "config_test"}, + "desiredState.workloads.workload_test.runtimeConfig"), + ("add_dependency", {"workload_name": "workload_test_other", "condition": "RUNNING"}, + "desiredState.workloads.workload_test.dependencies"), + ("add_tag", {"key": "key1", "value": "value1"}, + "desiredState.workloads.workload_test.tags"), ]) def test_mask_generation(function_name, data, mask): - workload = Workload("workload_test") + """ + Test the generation of masks when updating fields of the Workload instance. + + Args: + function_name (str): The name of the function to call on the Workload instance. + data (dict): The data to pass to the function. + mask (str): The expected mask to be generated. + """ + my_workload = Workload("workload_test") # Call function and assert the mask has been added - getattr(workload, function_name)(**data) - assert workload._get_masks() == [mask] + getattr(my_workload, function_name)(**data) + assert my_workload._get_masks() == [mask] # Updating the mask again should not add a new mask - getattr(workload, function_name)(**data) - assert len(workload._get_masks()) == 1 + getattr(my_workload, function_name)(**data) + assert len(my_workload._get_masks()) == 1 diff --git a/tests/Workload/test_workload_builder.py b/tests/Workload/test_workload_builder.py index c41a0d8..db3a0ee 100644 --- a/tests/Workload/test_workload_builder.py +++ b/tests/Workload/test_workload_builder.py @@ -12,20 +12,39 @@ # # SPDX-License-Identifier: Apache-2.0 -import pytest +""" +This module contains unit tests for the WorkloadBuilder class in the AnkaiosSDK. + +Fixtures: + builder: Returns a WorkloadBuilder instance. +""" + from unittest.mock import patch, mock_open +import pytest from AnkaiosSDK import Workload, WorkloadBuilder @pytest.fixture def builder(): + """ + Fixture that returns a WorkloadBuilder instance. + + Returns: + WorkloadBuilder: A WorkloadBuilder instance. + """ return WorkloadBuilder() -def test_workload_fields(builder): +def test_workload_fields(builder): # pylint: disable=redefined-outer-name + """ + Test setting various fields of the WorkloadBuilder instance. + + Args: + builder (WorkloadBuilder): The WorkloadBuilder fixture. + """ assert builder.agent_name("agent_Test") == builder assert builder.wl_agent_name == "agent_Test" - + assert builder.runtime("runtime_test") == builder assert builder.wl_runtime == "runtime_test" @@ -40,7 +59,13 @@ def test_workload_fields(builder): assert builder.wl_restart_policy == "NEVER" -def test_add_dependency(builder): +def test_add_dependency(builder): # pylint: disable=redefined-outer-name + """ + Test adding dependencies to the WorkloadBuilder instance. + + Args: + builder (WorkloadBuilder): The WorkloadBuilder fixture. + """ assert len(builder.dependencies) == 0 assert builder.add_dependency("workload_test", "RUNNING") == builder @@ -50,7 +75,13 @@ def test_add_dependency(builder): assert builder.dependencies == {"workload_test": "RUNNING", "workload_test_other": "RUNNING"} -def test_add_tag(builder): +def test_add_tag(builder): # pylint: disable=redefined-outer-name + """ + Test adding tags to the WorkloadBuilder instance. + + Args: + builder (WorkloadBuilder): The WorkloadBuilder fixture. + """ assert len(builder.tags) == 0 assert builder.add_tag("key_test", "abc") == builder @@ -60,20 +91,30 @@ def test_add_tag(builder): assert builder.tags == [("key_test", "abc"), ("key_test", "bcd")] -def test_build(builder): - with pytest.raises(ValueError, match="Workload can not be built without a name."): +def test_build(builder): # pylint: disable=redefined-outer-name + """ + Test building a Workload instance from the WorkloadBuilder instance. + + Args: + builder (WorkloadBuilder): The WorkloadBuilder fixture. + """ + with pytest.raises(ValueError, match= + "Workload can not be built without a name."): builder.build() builder = builder.workload_name("workload_test") - with pytest.raises(ValueError, match="Workload can not be built without an agent name."): + with pytest.raises(ValueError, match= + "Workload can not be built without an agent name."): builder.build() builder = builder.agent_name("agent_Test") - with pytest.raises(ValueError, match="Workload can not be built without a runtime."): + with pytest.raises(ValueError, match= + "Workload can not be built without a runtime."): builder.build() builder = builder.runtime("runtime_test") - with pytest.raises(ValueError, match="Workload can not be built without a runtime configuration."): + with pytest.raises(ValueError, match= + "Workload can not be built without a runtime configuration."): builder.build() workload = builder.runtime_config("config_test") \ diff --git a/tests/WorkloadState/__init__.py b/tests/WorkloadState/__init__.py new file mode 100644 index 0000000..561306d --- /dev/null +++ b/tests/WorkloadState/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/tests/WorkloadState/test_workload_execution_state.py b/tests/WorkloadState/test_workload_execution_state.py index 139a680..8bb0f39 100644 --- a/tests/WorkloadState/test_workload_execution_state.py +++ b/tests/WorkloadState/test_workload_execution_state.py @@ -12,12 +12,19 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module contains unit tests for the WorkloadExecutionState class in the AnkaiosSDK. +""" + import pytest from AnkaiosSDK import WorkloadExecutionState, WorkloadStateEnum, WorkloadSubStateEnum from AnkaiosSDK._protos import _ank_base def test_interpret_state(): + """ + Test the interpretation of a valid execution state in the WorkloadExecutionState class. + """ workload_state = WorkloadExecutionState( _ank_base.ExecutionState( additionalInfo="Dummy information", @@ -31,6 +38,10 @@ def test_interpret_state(): def test_interpret_state_error(): + """ + Test the handling of an invalid execution state in the WorkloadExecutionState class, + ensuring it raises a ValueError. + """ with pytest.raises(ValueError, match="Invalid state for workload."): WorkloadExecutionState( _ank_base.ExecutionState( diff --git a/tests/WorkloadState/test_workload_instance_name.py b/tests/WorkloadState/test_workload_instance_name.py index 1c5f845..ade867c 100644 --- a/tests/WorkloadState/test_workload_instance_name.py +++ b/tests/WorkloadState/test_workload_instance_name.py @@ -12,10 +12,18 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module contains unit tests for the WorkloadInstanceName class in the AnkaiosSDK. +""" + from AnkaiosSDK import WorkloadInstanceName def test_creation(): + """ + Test the creation of a WorkloadInstanceName instance, + ensuring it is correctly initialized with the provided attributes. + """ workload_instance_name = WorkloadInstanceName( agent_name="agent_Test", workload_name="workload_Test", diff --git a/tests/WorkloadState/test_workload_state.py b/tests/WorkloadState/test_workload_state.py index 5e0a10d..5035df2 100644 --- a/tests/WorkloadState/test_workload_state.py +++ b/tests/WorkloadState/test_workload_state.py @@ -12,11 +12,18 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module contains unit tests for the WorkloadState class in the AnkaiosSDK. +""" + from AnkaiosSDK import WorkloadState from AnkaiosSDK._protos import _ank_base def test_creation(): + """ + Test the creation of a WorkloadState instance. + """ workload_state = WorkloadState( agent_name="agent_Test", workload_name="workload_Test", diff --git a/tests/WorkloadState/test_workload_state_collection.py b/tests/WorkloadState/test_workload_state_collection.py index 47eec55..4a839e5 100644 --- a/tests/WorkloadState/test_workload_state_collection.py +++ b/tests/WorkloadState/test_workload_state_collection.py @@ -12,12 +12,19 @@ # # SPDX-License-Identifier: Apache-2.0 -import pytest +""" +This module contains unit tests for the WorkloadStateCollection class in the AnkaiosSDK. +""" + from AnkaiosSDK import WorkloadStateCollection, WorkloadState, WorkloadExecutionState from AnkaiosSDK._protos import _ank_base def test_get(): + """ + Test the basic functionality of the WorkloadStateCollection class, + including adding a workload state and retrieving it as a list and dictionary. + """ workload_state_collection = WorkloadStateCollection() assert workload_state_collection is not None assert len(workload_state_collection._workload_states) == 0 @@ -34,10 +41,12 @@ def test_get(): state=execution_state ) + # Test get_as_list workload_state_collection.add_workload_state(workload_state) assert len(workload_state_collection._workload_states) == 1 assert workload_state_collection.get_as_list() == [workload_state] + # Test get_as_dict workload_states_dict = workload_state_collection.get_as_dict() assert len(workload_states_dict) == 1 assert "agent_Test" in workload_states_dict.keys() @@ -45,10 +54,15 @@ def test_get(): assert "workload_Test" in workload_states_dict["agent_Test"].keys() assert len(workload_states_dict["agent_Test"]["workload_Test"]) == 1 assert "1234" in workload_states_dict["agent_Test"]["workload_Test"].keys() - assert type(workload_states_dict["agent_Test"]["workload_Test"]["1234"]) == WorkloadExecutionState + assert isinstance(workload_states_dict["agent_Test"]["workload_Test"]["1234"], + WorkloadExecutionState) def test_from_proto(): + """ + Test the _from_proto method of the WorkloadStateCollection class, + ensuring it correctly populates the collection from a proto message. + """ ank_workload_state = _ank_base.WorkloadStatesMap( agentStateMap={"agent_Test": _ank_base.ExecutionsStatesOfWorkload( wlNameStateMap={"workload_Test": _ank_base.ExecutionsStatesForId( diff --git a/tests/WorkloadState/test_workload_state_enum.py b/tests/WorkloadState/test_workload_state_enum.py index b1ee29b..33cf12b 100644 --- a/tests/WorkloadState/test_workload_state_enum.py +++ b/tests/WorkloadState/test_workload_state_enum.py @@ -12,10 +12,18 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module contains unit tests for the WorkloadExecutionState class in the AnkaiosSDK. +""" + from AnkaiosSDK import WorkloadStateEnum def test_get(): + """ + Test the get method of the WorkloadStateEnum class, + ensuring it correctly retrieves the enumeration member and its string representation. + """ field = "agentDisconnected" workload_state = WorkloadStateEnum._get(field) assert workload_state == WorkloadStateEnum.AgentDisconnected diff --git a/tests/WorkloadState/test_workload_substate_enum.py b/tests/WorkloadState/test_workload_substate_enum.py index 4ca8d26..b06e7af 100644 --- a/tests/WorkloadState/test_workload_substate_enum.py +++ b/tests/WorkloadState/test_workload_substate_enum.py @@ -12,40 +12,72 @@ # # SPDX-License-Identifier: Apache-2.0 +""" +This module contains unit tests for the WorkloadSubStateEnum class in the AnkaiosSDK. +""" + import pytest -from AnkaiosSDK import WorkloadStateEnum, WorkloadSubStateEnum +from AnkaiosSDK import WorkloadSubStateEnum, WorkloadStateEnum from AnkaiosSDK._protos import _ank_base def test_get(): + """ + Test the get method of the WorkloadSubStateEnum class, + ensuring it correctly retrieves the enumeration member based on the state and field. + """ data = [ - (WorkloadStateEnum.AgentDisconnected, _ank_base.AGENT_DISCONNECTED, WorkloadSubStateEnum.AGENT_DISCONNECTED), - (WorkloadStateEnum.Pending, _ank_base.PENDING_INITIAL, WorkloadSubStateEnum.PENDING_INITIAL), - (WorkloadStateEnum.Pending, _ank_base.PENDING_WAITING_TO_START, WorkloadSubStateEnum.PENDING_WAITING_TO_START), - (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING, WorkloadSubStateEnum.PENDING_STARTING), - (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING_FAILED, WorkloadSubStateEnum.PENDING_STARTING_FAILED), - (WorkloadStateEnum.Running, _ank_base.RUNNING_OK, WorkloadSubStateEnum.RUNNING_OK), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING, WorkloadSubStateEnum.STOPPING), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_WAITING_TO_STOP, WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_REQUESTED_AT_RUNTIME, WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_DELETE_FAILED, WorkloadSubStateEnum.STOPPING_DELETE_FAILED), - (WorkloadStateEnum.Succeeded, _ank_base.SUCCEEDED_OK, WorkloadSubStateEnum.SUCCEEDED_OK), - (WorkloadStateEnum.Failed, _ank_base.FAILED_EXEC_FAILED, WorkloadSubStateEnum.FAILED_EXEC_FAILED), - (WorkloadStateEnum.Failed, _ank_base.FAILED_UNKNOWN, WorkloadSubStateEnum.FAILED_UNKNOWN), - (WorkloadStateEnum.Failed, _ank_base.FAILED_LOST, WorkloadSubStateEnum.FAILED_LOST), - (WorkloadStateEnum.NotScheduled, _ank_base.NOT_SCHEDULED, WorkloadSubStateEnum.NOT_SCHEDULED), - (WorkloadStateEnum.Removed, _ank_base.REMOVED, WorkloadSubStateEnum.REMOVED) + (WorkloadStateEnum.AgentDisconnected, _ank_base.AGENT_DISCONNECTED, + WorkloadSubStateEnum.AGENT_DISCONNECTED), + (WorkloadStateEnum.Pending, _ank_base.PENDING_INITIAL, + WorkloadSubStateEnum.PENDING_INITIAL), + (WorkloadStateEnum.Pending, _ank_base.PENDING_WAITING_TO_START, + WorkloadSubStateEnum.PENDING_WAITING_TO_START), + (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING, + WorkloadSubStateEnum.PENDING_STARTING), + (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING_FAILED, + WorkloadSubStateEnum.PENDING_STARTING_FAILED), + (WorkloadStateEnum.Running, _ank_base.RUNNING_OK, + WorkloadSubStateEnum.RUNNING_OK), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING, + WorkloadSubStateEnum.STOPPING), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_WAITING_TO_STOP, + WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_REQUESTED_AT_RUNTIME, + WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME), + (WorkloadStateEnum.Stopping, _ank_base.STOPPING_DELETE_FAILED, + WorkloadSubStateEnum.STOPPING_DELETE_FAILED), + (WorkloadStateEnum.Succeeded, _ank_base.SUCCEEDED_OK, + WorkloadSubStateEnum.SUCCEEDED_OK), + (WorkloadStateEnum.Failed, _ank_base.FAILED_EXEC_FAILED, + WorkloadSubStateEnum.FAILED_EXEC_FAILED), + (WorkloadStateEnum.Failed, _ank_base.FAILED_UNKNOWN, + WorkloadSubStateEnum.FAILED_UNKNOWN), + (WorkloadStateEnum.Failed, _ank_base.FAILED_LOST, + WorkloadSubStateEnum.FAILED_LOST), + (WorkloadStateEnum.NotScheduled, _ank_base.NOT_SCHEDULED, + WorkloadSubStateEnum.NOT_SCHEDULED), + (WorkloadStateEnum.Removed, _ank_base.REMOVED, + WorkloadSubStateEnum.REMOVED) ] for state, field, expected in data: assert WorkloadSubStateEnum._get(state, field) == expected def test_get_error(): + """ + Test the get method of the WorkloadSubStateEnum class, + ensuring it raises a ValueError for an invalid state and field combination. + """ with pytest.raises(ValueError): - WorkloadSubStateEnum._get(WorkloadStateEnum.AgentDisconnected, _ank_base.PENDING_WAITING_TO_START) + WorkloadSubStateEnum._get(WorkloadStateEnum.AgentDisconnected, + _ank_base.PENDING_WAITING_TO_START) def test_sub_state2ank_base(): + """ + Test the conversion from WorkloadSubStateEnum to _ank_base. + """ substate = WorkloadSubStateEnum.FAILED_UNKNOWN assert substate._sub_state2ank_base() == _ank_base.FAILED_UNKNOWN assert str(substate) == "FAILED_UNKNOWN" diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py new file mode 100644 index 0000000..1dd0fc7 --- /dev/null +++ b/tests/test_ankaios.py @@ -0,0 +1,471 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module contains unit tests for the Ankaios class in the AnkaiosSDK. +""" + +from io import StringIO +import logging +from unittest.mock import patch, mock_open, MagicMock +import pytest +from AnkaiosSDK import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, Manifest, CompleteState +from tests.Workload.test_workload import generate_test_workload +from tests.test_request import generate_test_request +from tests.Response.test_response import MESSAGE_BUFFER_ERROR, MESSAGE_BUFFER_COMPLETE_STATE, \ + MESSAGE_BUFFER_UPDATE_SUCCESS, MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH +from tests.test_manifest import MANIFEST_DICT + + +def test_logger(): + """ + Test the logger functionality of the Ankaios class. + """ + ankaios = Ankaios() + assert ankaios.logger.level == AnkaiosLogLevel.INFO.value + ankaios.set_logger_level(AnkaiosLogLevel.ERROR) + assert ankaios.logger.level == AnkaiosLogLevel.ERROR.value + + str_stream = StringIO() + handler = logging.StreamHandler(str_stream) + ankaios.logger.addHandler(handler) + + ankaios.logger.debug("Debug message") + assert str_stream.getvalue() == "" + ankaios.logger.error("Error message") + assert "Error message" in str_stream.getvalue() + + +def test_connection(): + """ + Test the connect / disconnect functionality of the Ankaios class. + """ + ankaios = Ankaios() + assert not ankaios._connected + + with patch("threading.Thread") as MockThread: + mock_thread_instance = MagicMock() + MockThread.return_value = mock_thread_instance + + ankaios.connect() + MockThread.assert_called_once_with(target=ankaios._read_from_control_interface) + mock_thread_instance.start.assert_called_once() + assert ankaios._connected + + with pytest.raises(ValueError, match="Already connected."): + ankaios.connect() + + ankaios.disconnect() + mock_thread_instance.join.assert_called_once() + assert not ankaios._connected + + with pytest.raises(ValueError, match="Already disconnected."): + ankaios.disconnect() + + with Ankaios() as ank: + assert ank._connected + + +def test_read_from_control_interface(): + """ + Test the _read_from_control_interface method of the Ankaios class. + """ + input_file_content = MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH + \ + MESSAGE_BUFFER_UPDATE_SUCCESS + + # Test response comes first + with patch("builtins.open", mock_open()) as mock_file: + mock_file_handle = mock_file.return_value.__enter__.return_value + mock_file_handle.read.side_effect = [bytes([b]) for b in input_file_content] + + ankaios = Ankaios() + + # will call _read_from_control_interface + ankaios.connect() + + # will stop the thread after reading the message + ankaios.disconnect() + + mock_file.assert_called_once_with( + f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") + assert "1234" in list(ankaios._responses) + assert ankaios._responses["1234"].is_set() + + # Test request set first + with patch("builtins.open", mock_open()) as mock_file: + mock_file_handle = mock_file.return_value.__enter__.return_value + mock_file_handle.read.side_effect = [bytes([b]) for b in input_file_content] + + ankaios = Ankaios() + ankaios._responses["1234"] = ResponseEvent() + + # will call _read_from_control_interface + ankaios.connect() + + # will stop the thread after reading the message + ankaios.disconnect() + + mock_file.assert_called_once_with( + f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") + assert "1234" in list(ankaios._responses) + assert ankaios._responses["1234"].is_set() + + +def test_get_reponse_by_id(): + """ + Test the get_response_by_id method of the Ankaios class. + """ + ankaios = Ankaios() + with pytest.raises(ValueError, match="Reading from the control interface is not started."): + ankaios._get_response_by_id("1234") + ankaios._connected = True + + assert not ankaios._responses + with patch("AnkaiosSDK.ResponseEvent.wait_for_response") as mock_wait: + ankaios._get_response_by_id("1234") + mock_wait.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) + assert list(ankaios._responses.keys()) == ["1234"] + assert isinstance(ankaios._responses["1234"], ResponseEvent) + + response = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios._responses["1234"] = ResponseEvent(response) + assert ankaios._get_response_by_id("1234") == response + assert not list(ankaios._responses.keys()) + + +def test_write_to_pipe(): + """ + Test the _write_to_pipe method of the Ankaios class. + """ + with patch("builtins.open", mock_open()) as mock_file: + ankaios = Ankaios() + ankaios._write_to_pipe(generate_test_request()) + + mock_file.assert_called_once_with( + f"{ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") + mock_file().write.assert_called() + mock_file().flush.assert_called_once() + + +def test_send_request(): + """ + Test the _send_request method of the Ankaios class. + """ + ankaios = Ankaios() + with pytest.raises(ValueError, match="Cannot request if not connected."): + ankaios._send_request(None) + ankaios._connected = True + + request = generate_test_request() + with patch("AnkaiosSDK.Ankaios._write_to_pipe") as mock_write, \ + patch("AnkaiosSDK.Ankaios._get_response_by_id") as mock_get_response: + ankaios._send_request(request) + mock_write.assert_called_once_with(request) + mock_get_response.assert_called_once_with(request.get_id(), Ankaios.DEFAULT_TIMEOUT) + + with patch("AnkaiosSDK.Ankaios._write_to_pipe") as mock_write, \ + patch("AnkaiosSDK.Ankaios._get_response_by_id") as mock_get_response: + mock_get_response.side_effect = TimeoutError() + with pytest.raises(TimeoutError): + ankaios._send_request(request) + mock_write.assert_called_once_with(request) + + +def test_apply_manifest(): + """ + Test the apply manifest method of the Ankaios class. + """ + ankaios = Ankaios() + ankaios.logger = MagicMock() + manifest = Manifest(MANIFEST_DICT) + + # Test success + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios.apply_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() + + # Test error + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + ankaios.apply_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + ankaios.apply_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + +def test_delete_manifest(): + """ + Test the delete manifest method of the Ankaios class. + """ + ankaios = Ankaios() + ankaios.logger = MagicMock() + manifest = Manifest(MANIFEST_DICT) + + # Test success + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios.delete_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() + + # Test error + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + ankaios.delete_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + ankaios.delete_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + +def test_run_workload(): + """ + Test the run workload method of the Ankaios class. + """ + ankaios = Ankaios() + ankaios.logger = MagicMock() + workload = generate_test_workload() + + # Test success + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios.run_workload(workload) + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() + + # Test error + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + ankaios.run_workload(workload) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + ankaios.run_workload(workload) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + +def test_delete_workload(): + """ + Test the delete workload method of the Ankaios class. + """ + ankaios = Ankaios() + ankaios.logger = MagicMock() + + # Test success + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios.delete_workload("nginx") + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() + + # Test error + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + ankaios.delete_workload("nginx") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + ankaios.delete_workload("nginx") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + +def test_get_workload(): + """ + Test the get workload method of the Ankaios class. + """ + ankaios = Ankaios() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload") as mock_state_get_workload: + mock_get_state.return_value = CompleteState() + ankaios.get_workload("nginx") + mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT, + ["desiredState.workloads.nginx"]) + mock_state_get_workload.assert_called_once_with("nginx") + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload") as mock_state_get_workload: + ankaios.get_workload("nginx", state=CompleteState()) + mock_get_state.assert_not_called() + mock_state_get_workload.assert_called_once_with("nginx") + +def test_set_config(): + """ + Test the set config methods of the Ankaios class. + """ + ankaios = Ankaios() + + with patch("builtins.open", mock_open()) as mock_file, \ + patch("AnkaiosSDK.Ankaios.set_config") as mock_set_config: + mock_file().read.return_value = {'config_test': 'value'} + ankaios.set_config_from_file(name="config_test", config_path=r"path/to/config") + + mock_file.assert_called_with(r"path/to/config", "r", encoding="utf-8") + mock_file().read.assert_called_once() + mock_set_config.assert_called_once_with("config_test", {'config_test': 'value'}) + + with pytest.raises(NotImplementedError, match="not implemented yet"): + ankaios.set_config(name="config_test", config={'config_test': 'value'}) + + +def test_get_config(): + """ + Test the get config method of the Ankaios class. + """ + ankaios = Ankaios() + + with pytest.raises(NotImplementedError, match="not implemented yet"): + ankaios.get_config(name="config_test") + + +def test_delete_config(): + """ + Test the delete config method of the Ankaios class. + """ + ankaios = Ankaios() + + with pytest.raises(NotImplementedError, match="not implemented yet"): + ankaios.delete_config(name="config_test") + + +def test_get_state(): + """ + Test the get state method of the Ankaios class. + """ + ankaios = Ankaios() + ankaios.logger = MagicMock() + + # Test success + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_COMPLETE_STATE) + result = ankaios.get_state() + mock_send_request.assert_called_once() + assert isinstance(result, CompleteState) + + # Test error + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + result = ankaios.get_state(field_mask=["invalid_mask"]) + mock_send_request.assert_called_once() + assert result is None + ankaios.logger.error.assert_called() + + # Test timeout + with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + result = ankaios.get_state() + mock_send_request.assert_called_once() + assert result is None + ankaios.logger.error.assert_called() + + +def test_get_agents(): + """ + Test the get agents method of the Ankaios class. + """ + ankaios = Ankaios() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_agents") as mock_state_get_agents: + mock_get_state.return_value = CompleteState() + ankaios.get_agents() + mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) + mock_state_get_agents.assert_called_once() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_agents") as mock_state_get_agents: + ankaios.get_agents(state=CompleteState()) + mock_get_state.assert_not_called() + mock_state_get_agents.assert_called_once() + + +def test_get_workload_states(): + """ + Test the get workload states method of the Ankaios class. + """ + ankaios = Ankaios() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + mock_get_state.return_value = CompleteState() + ankaios.get_workload_states() + mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) + mock_state_get_workload_states.assert_called_once() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + ankaios.get_workload_states(state=CompleteState()) + mock_get_state.assert_not_called() + mock_state_get_workload_states.assert_called_once() + + +def test_get_workload_states_on_agent(): + """ + Test the get workload states on agent method of the Ankaios class. + """ + ankaios = Ankaios() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + mock_get_state.return_value = CompleteState() + ankaios.get_workload_states_on_agent("agent_A") + mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT, ["workloadStates.agent_A"]) + mock_state_get_workload_states.assert_called_once() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + ankaios.get_workload_states_on_agent("agent_A", state=CompleteState()) + mock_get_state.assert_not_called() + mock_state_get_workload_states.assert_called_once() + + +def test_get_workload_states_on_workload_name(): + """ + Test the get workload states on workload name method of the Ankaios class. + """ + ankaios = Ankaios() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + mock_get_state.return_value = CompleteState() + ankaios.get_workload_states_on_workload_name("nginx") + mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT, ["workloadStates.nginx"]) + mock_state_get_workload_states.assert_called_once() + + with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ + patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + ankaios.get_workload_states_on_workload_name("nginx", state=CompleteState()) + mock_get_state.assert_not_called() + mock_state_get_workload_states.assert_called_once() diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py new file mode 100644 index 0000000..1fbafad --- /dev/null +++ b/tests/test_complete_state.py @@ -0,0 +1,141 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module contains unit tests for the Manifest class in the AnkaiosSDK. +""" + +from AnkaiosSDK import CompleteState, WorkloadStateCollection +from AnkaiosSDK._protos import _ank_base +from tests.Workload.test_workload import generate_test_workload + + +def test_general_functionality(): + """ + Test general functionality of the CompleteState class. + """ + complete_state = CompleteState(api_version="v0.1") + assert complete_state.get_api_version() == "v0.1" + complete_state._set_api_version("v0.2") + assert complete_state.get_api_version() == "v0.2" + assert str(complete_state) == "desiredState {\n apiVersion: \"v0.2\"\n}\n" + + +def test_workload_functionality(): + """ + Test the functionality of CompleteState class regarding setting and getting workloads. + """ + complete_state = CompleteState() + assert len(complete_state.get_workloads()) == 0 + + wl_nginx = generate_test_workload("nginx_test") + complete_state.set_workload(wl_nginx) + assert len(complete_state.get_workloads()) == 1 + assert complete_state.get_workload("nginx_test") == wl_nginx + + assert complete_state.get_workload("invalid") is None + + +def test_workload_states(): + """ + Test the functionality of CompleteState class regarding setting and getting workload states. + """ + complete_state = CompleteState() + complete_state._from_proto(_ank_base.CompleteState( + workloadStates=_ank_base.WorkloadStatesMap(agentStateMap={ + "agent_A": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ + "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ + "1234": _ank_base.ExecutionState( + additionalInfo="Random info", + succeeded=_ank_base.SUCCEEDED_OK, + ) + }) + }), + "agent_B": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ + "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ + "5678": _ank_base.ExecutionState( + additionalInfo="Random info", + pending=_ank_base.PENDING_WAITING_TO_START, + ) + }), + "dyn_nginx": _ank_base.ExecutionsStatesForId(idStateMap={ + "9012": _ank_base.ExecutionState( + additionalInfo="Random info", + stopping=_ank_base.STOPPING_WAITING_TO_STOP, + ) + }) + }) + }) + ) + ) + + workload_states = complete_state.get_workload_states() + assert isinstance(workload_states, WorkloadStateCollection) + assert len(workload_states.get_as_list()) == 3 + + +def test_get_agents(): + """ + Test the get_agents method of the CompleteState class. + """ + complete_state = CompleteState() + complete_state._from_proto(_ank_base.CompleteState( + agents=_ank_base.AgentMap( + agents={"agent_A": _ank_base.AgentAttributes(), + "agent_B": _ank_base.AgentAttributes()} + ) + )) + assert len(complete_state.get_agents()) == 2 + assert "agent_A" in complete_state.get_agents() + assert "agent_B" in complete_state.get_agents() + + +def test_from_dict(): + """ + Test the from_dict method of the CompleteState class. + """ + complete_state = CompleteState() + complete_state._from_dict({ + "apiVersion": "v0.1", + "workloads": { + "nginx": { + "runtime": "podman", + "restartPolicy": "NEVER", + "agent": "agent_A", + "runtimeConfig": "config", + } + } + }) + assert complete_state.get_api_version() == "v0.1" + assert len(complete_state.get_workloads()) == 1 + + complete_state._from_dict({ + "apiVersion": "v0.2", + }) + assert complete_state.get_api_version() == "v0.2" + assert len(complete_state.get_workloads()) == 0 + + +def test_proto(): + """ + Test converting the CompleteState instance to and from a protobuf message. + """ + complete_state = CompleteState(api_version="v0.1") + wl_nginx = generate_test_workload("nginx_test") + complete_state.set_workload(wl_nginx) + + new_complete_state = CompleteState() + new_complete_state._from_proto(complete_state._to_proto()) + + assert str(complete_state) == str(new_complete_state) diff --git a/tests/test_manifest.py b/tests/test_manifest.py new file mode 100644 index 0000000..aa9b808 --- /dev/null +++ b/tests/test_manifest.py @@ -0,0 +1,133 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module contains unit tests for the Manifest class in the AnkaiosSDK. +""" + +from unittest.mock import patch, mock_open +import pytest +from AnkaiosSDK import Manifest, CompleteState + + +MANIFEST_CONTENT = """apiVersion: v0.1 +workloads: + nginx_test: + runtime: podman + restartPolicy: NEVER + agent: agent_A + runtimeConfig: | + image: ghcr.io/eclipse-ankaios/tests/nginx:alpine-slim + commandOptions: ["-p", "8081:80"]""" + +MANIFEST_DICT = { + 'apiVersion': 'v0.1', + 'workloads': { + 'nginx_test': { + 'runtime': 'podman', + 'restartPolicy': 'NEVER', + 'agent': 'agent_A', + 'runtimeConfig': 'image: ghcr.io/eclipse-ankaios/tests/nginx:alpine-slim\ncommandOptions: ["-p", "8081:80"]' # pylint: disable=line-too-long + } + } +} + + +def test_from_file(): + """ + Test the from_file method of the Manifest class, + ensuring it correctly loads a manifest from a file and handles errors. + """ + with patch("builtins.open", mock_open(read_data=MANIFEST_CONTENT)), \ + patch("AnkaiosSDK.Manifest.from_string") as mock_from_string: + _ = Manifest.from_file("manifest.yaml") + mock_from_string.assert_called_once_with(MANIFEST_CONTENT) + + with pytest.raises(ValueError, match="Error reading manifest file"): + _ = Manifest.from_file("invalid_path") + + +def test_from_string(): + """ + Test the from_string method of the Manifest class, + ensuring it correctly parses a manifest from a YAML string and handles errors. + """ + with patch("AnkaiosSDK.Manifest.from_dict") as mock_from_dict: + _ = Manifest.from_string(MANIFEST_CONTENT) + mock_from_dict.assert_called_once_with(MANIFEST_DICT) + + with pytest.raises(ValueError, match="Error parsing manifest"): + _ = Manifest.from_string("invalid_manifest") + + +def test_from_dict(): + """ + Test the from_dict method of the Manifest class, + ensuring it correctly creates a Manifest instance from a dictionary and handles errors. + """ + manifest = Manifest.from_dict(MANIFEST_DICT) + assert manifest._manifest == MANIFEST_DICT + + with pytest.raises(ValueError, match="Invalid manifest"): + _ = Manifest.from_dict({}) + + +def test_check(): + """ + Test the check method of the Manifest class, + ensuring it correctly validates the manifest data and handles errors. + """ + manifest = Manifest(MANIFEST_DICT) + assert manifest.check() + + with pytest.raises(ValueError, match="Invalid manifest"): + manifest = Manifest({}) + + with pytest.raises(ValueError, match="Invalid manifest"): + manifest = Manifest({'apiVersion': 'v0.1'}) + + with pytest.raises(ValueError, match="Invalid manifest"): + manifest = Manifest({'apiVersion': 'v0.1', 'workloads': + {'nginx_test': {'invalid_key': ''}}}) + + +def test_calculate_masks(): + """ + Test the calculated masks for the manifest data, + ensuring they are correctly generated based on the workload names. + """ + manifest_dict = MANIFEST_DICT.copy() + manifest_dict["workloads"]["nginx_test_other"] = { + 'runtime': 'podman', + 'restartPolicy': 'NEVER', + 'agent': 'agent_B', + 'runtimeConfig': 'image: ghcr.io/eclipse-ankaios/tests/nginx:alpine-slim\ncommandOptions: ["-p", "8082:80"]' # pylint: disable=line-too-long + } + manifest = Manifest(manifest_dict) + assert len(manifest._calculate_masks()) == 2 + assert manifest._calculate_masks() == [ + "desiredState.workloads.nginx_test", + "desiredState.workloads.nginx_test_other" + ] + + +def test_generate_complete_state(): + """ + Test the CompleteState instance generation from a Manifest instance. + """ + with patch("AnkaiosSDK.CompleteState._from_dict") as mock_complete_state: + manifest = Manifest(MANIFEST_DICT) + complete_state = manifest.generate_complete_state() + mock_complete_state.assert_called_once_with(manifest._manifest) + assert isinstance(complete_state, CompleteState) diff --git a/tests/test_request.py b/tests/test_request.py new file mode 100644 index 0000000..aa4ebf2 --- /dev/null +++ b/tests/test_request.py @@ -0,0 +1,85 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module contains unit tests for the Request class in the AnkaiosSDK. +""" + +import pytest +from AnkaiosSDK import Request, CompleteState +from tests.Workload.test_workload import generate_test_workload + + +def generate_test_request(request_type: str = "update_state") -> Request: + """ + Helper function to generate a Request instance with some default values. + + Returns: + Request: A Request instance. + """ + if request_type == "update_state": + request = Request("update_state") + complete_state = CompleteState() + complete_state.set_workload(generate_test_workload()) + request.set_complete_state(complete_state) + return request + return Request("get_state") + + +def test_general_functionality(): + """ + Test general functionality of the Request class. + """ + with pytest.raises(ValueError, match="Invalid request type."): + Request("invalid") + + request = Request("update_state") + assert request.get_id() is not None + assert str(request) == f"requestId: \"{request.get_id()}\"\n" + + +def test_update_state(): + """ + Test the update state request type. + """ + request = Request("update_state") + complete_state = CompleteState() + request.set_complete_state(complete_state) + assert request._request.updateStateRequest.newState == complete_state._to_proto() + + request.add_mask("test_mask") + assert request._request.updateStateRequest.updateMask == ["test_mask"] + + with pytest.raises(ValueError, + match="Complete state can only be set for an update state request."): + Request("get_state").set_complete_state(CompleteState()) + +def test_get_state(): + """ + Test the get state request type. + """ + request = Request("get_state") + request.add_mask("test_mask") + assert request._request.completeStateRequest.fieldMask == ["test_mask"] + + +def test_proto(): + """ + Test the conversion to proto message. + """ + request = Request("update_state") + assert request._to_proto().requestId == request.get_id() + + request = Request("get_state") + assert request._to_proto().requestId == request.get_id() From 6ff6ce30ef432e5e460572fe116e5c54a123372c Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 24 Sep 2024 14:37:23 +0300 Subject: [PATCH 06/72] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8c6f523..626aa0f 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,8 @@ Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. # To do -- Finish the marked TODO methods from Ankaios.py +- Finish the marked TODO methods from Ankaios.py, realted to configuration handling. - Fix protobuf versioning warning -- Add utest for 100% coverage (currently done Workload and WorkloadState) -- Fix Lint (currently done Workload.py) - Improve requirements, requirements-dev and setup.py:install_requires - Enable github workflow verifications -- Add to pip wheel \ No newline at end of file +- Add to pip wheel From e17f99f700adb744004147f14b1294c6744deb18 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 25 Sep 2024 08:46:43 +0300 Subject: [PATCH 07/72] Delete old read function --- AnkaiosSDK/Ankaios.py | 50 ------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/AnkaiosSDK/Ankaios.py b/AnkaiosSDK/Ankaios.py index aac4d4a..2207654 100644 --- a/AnkaiosSDK/Ankaios.py +++ b/AnkaiosSDK/Ankaios.py @@ -144,56 +144,6 @@ def _create_logger(self) -> None: self.logger.addHandler(handler) self.set_logger_level(AnkaiosLogLevel.INFO) - def _read_from_control_interface_old(self) -> None: # pragma: no cover - """ - Reads from the control interface input fifo and saves the response. - This is meant to be run in a separate thread. - It reads the response from the control interface and saves it in the responses dictionary, - by triggering the corresponding ResponseEvent. - """ - - with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") as f: - - while self._connected: - # Buffer for reading in the byte size of the proto msg - varint_buffer = b'' - while True: - # Consume byte for byte - next_byte: bytes = f.read(1) - print(f"Read next_byte: {next_byte}, type: {type(next_byte)}") - if not next_byte: - break - varint_buffer += next_byte - print(f"Updated varint_buffer: {varint_buffer}, type: {type(varint_buffer)}") - # Stop if the most significant bit is 0 (indicating the last byte of the varint) - if next_byte[0] & 0b10000000 == 0: - break - # Decode the varint and receive the proto msg length - msg_len, _ = _DecodeVarint(varint_buffer, 0) - - # Buffer for the proto msg itself - msg_buf = b'' - for _ in range(msg_len): - # Read exact amount of byte according to the calculated proto msg length - next_byte = f.read(1) - if not next_byte: - break - msg_buf += next_byte - - try: - response = Response(msg_buf) - except ValueError as e: - print(f"{e}") - continue - - request_id = response.get_request_id() - with self._responses_lock: - if request_id in self._responses: - self._responses[request_id].set_response(response) - else: - self._responses[request_id] = ResponseEvent(response) - self._responses[request_id].set() - def _read_from_control_interface(self) -> None: """ Reads from the control interface input fifo and saves the response. From 626ce19cc0ebb33c295ec2f3d6eb8841425f313c Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 25 Sep 2024 11:26:17 +0300 Subject: [PATCH 08/72] Fix sdk setup --- setup.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 08bf547..b923ed9 100644 --- a/setup.py +++ b/setup.py @@ -14,14 +14,14 @@ import os from setuptools import setup, find_packages -from grpc_tools import protoc - PROJECT_NAME = "AnkaiosSDK" def generate_protos(): """Generate python protobuf files from the proto files.""" + from grpc_tools import protoc + protos_dir = f"{PROJECT_NAME}/_protos" proto_files = ["ank_base.proto", "control_api.proto"] @@ -40,20 +40,17 @@ def generate_protos(): ] if protoc.main(command) != 0: raise Exception(f"Error: {proto_file} compilation failed") - + # Fix the import path in the generated control_api_pb2 # https://github.com/protocolbuffers/protobuf/issues/1491#issuecomment-261914766 if "control_api" in proto_file: with open(output_file, 'r') as file: filedata = file.read() newdata = filedata.replace( - "import ank_base_pb2 as ank__base__pb2", + "import ank_base_pb2 as ank__base__pb2", "from . import ank_base_pb2 as ank__base__pb2") with open(output_file, 'w') as file: file.write(newdata) - - -generate_protos() setup( @@ -81,8 +78,14 @@ def generate_protos(): "Bug Tracker": "https://github.com/eclipse-ankaios/ank-sdk-python/issues", }, install_requires=[ - "setuptools", - "protobuf", - "grpcio-tools", + "protobuf>=3.20.2", + "PyYAML", ], + setup_requires=[ + "protobuf>=3.20.2", + "grpcio-tools>=1.66.1", + ] ) + + +generate_protos() From 9341862078df357337f45562b1b0ba239338848b Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 25 Sep 2024 12:30:07 +0300 Subject: [PATCH 09/72] Fix protobuf versioning warning --- AnkaiosSDK/Ankaios.py | 2 +- AnkaiosSDK/_protos/__init__.py | 6 ------ README.md | 2 -- setup.py | 6 +++--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/AnkaiosSDK/Ankaios.py b/AnkaiosSDK/Ankaios.py index 2207654..7ea3a9d 100644 --- a/AnkaiosSDK/Ankaios.py +++ b/AnkaiosSDK/Ankaios.py @@ -180,7 +180,7 @@ def _read_from_control_interface(self) -> None: msg_buf += next_byte try: - response = Response(msg_buf) + response = Response(bytes(msg_buf)) except ValueError as e: # pragma: no cover self.logger.error("Error while reading: %s", e) continue diff --git a/AnkaiosSDK/_protos/__init__.py b/AnkaiosSDK/_protos/__init__.py index 3e8a700..efbba05 100644 --- a/AnkaiosSDK/_protos/__init__.py +++ b/AnkaiosSDK/_protos/__init__.py @@ -21,12 +21,6 @@ control_api_pb2: Used for exchanging messages with the control interface. """ -# TODO remove this line after the issue is fixed # pylint: disable=fixme -# https://github.com/grpc/grpc/issues/37609 -# https://github.com/protocolbuffers/protobuf/issues/18096 -import warnings -warnings.filterwarnings("ignore", ".*obsolete", UserWarning, "google.protobuf.runtime_version") - try: import AnkaiosSDK._protos.ank_base_pb2 as _ank_base import AnkaiosSDK._protos.control_api_pb2 as _control_api diff --git a/README.md b/README.md index 626aa0f..15740dd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,5 @@ Eclipse Ankaios Python SDK provides a convenient python interface for interactin # To do - Finish the marked TODO methods from Ankaios.py, realted to configuration handling. -- Fix protobuf versioning warning -- Improve requirements, requirements-dev and setup.py:install_requires - Enable github workflow verifications - Add to pip wheel diff --git a/setup.py b/setup.py index b923ed9..74ff821 100644 --- a/setup.py +++ b/setup.py @@ -78,12 +78,12 @@ def generate_protos(): "Bug Tracker": "https://github.com/eclipse-ankaios/ank-sdk-python/issues", }, install_requires=[ - "protobuf>=3.20.2", + "protobuf==5.27.2", "PyYAML", ], setup_requires=[ - "protobuf>=3.20.2", - "grpcio-tools>=1.66.1", + "protobuf==5.27.2", + "grpcio-tools>=1.56.2", ] ) From 339e887169da69ed216a33467150080dec5e5a5b Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 25 Sep 2024 12:52:15 +0300 Subject: [PATCH 10/72] Add CI verification --- .github/workflows/.gitkeep | 0 .github/workflows/ci.yml | 54 +++++++++++++++++++++++++++++++ README.md | 1 - ci_dev.yml | 65 -------------------------------------- requirements.txt | 3 -- requirements_dev.txt | 6 ---- setup.py | 10 +++++- 7 files changed, 63 insertions(+), 76 deletions(-) delete mode 100644 .github/workflows/.gitkeep create mode 100644 .github/workflows/ci.yml delete mode 100644 ci_dev.yml delete mode 100644 requirements.txt delete mode 100644 requirements_dev.txt diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..67a7df1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + + - name: Run unit tests + run: python3 run_tests.py --utest + + - name: Run coverage + run: python3 run_tests.py --cov + + - name: Run lint + run: python3 run_tests.py --lint + + - name: Upload unit test report + uses: actions/upload-artifact@v2 + with: + name: unit-test-report + path: reports/utest + + - name: Upload coverage report + uses: actions/upload-artifact@v2 + with: + name: coverage-report + path: reports/coverage + + - name: Upload lint report + uses: actions/upload-artifact@v2 + with: + name: pylint-report + path: reports/pylint diff --git a/README.md b/README.md index 15740dd..4f86818 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,4 @@ Eclipse Ankaios Python SDK provides a convenient python interface for interactin # To do - Finish the marked TODO methods from Ankaios.py, realted to configuration handling. -- Enable github workflow verifications - Add to pip wheel diff --git a/ci_dev.yml b/ci_dev.yml deleted file mode 100644 index 6bef726..0000000 --- a/ci_dev.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - lint: - name: Lint Code - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - pip install pylint - - - name: Run pylint - run: | - pylint AnkaiosSDK tests > pylint_report.txt - - - name: Upload pylint report - uses: actions/upload-artifact@v3 - with: - name: pylint-report - path: pylint_report.txt - - test: - name: Run Tests and Coverage - runs-on: ubuntu-latest - needs: lint - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - pip install pytest coverage - - - name: Run tests with coverage - run: | - coverage run -m unittest discover -s AnkaiosSDK - coverage report - coverage html - - - name: Upload coverage report - uses: actions/upload-artifact@v3 - with: - name: coverage-report - path: coverage.html diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 666ae4d..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -setuptools # For setting up the package -protobuf # Core protobuf library -grpcio-tools # For generating .pb2 files \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index fe4967d..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,6 +0,0 @@ -setuptools # For setting up the package -protobuf # Core protobuf library -grpcio-tools # For generating .pb2 files -pytest # For testing -pytest-cov # For coverage -pylint # For linting diff --git a/setup.py b/setup.py index 74ff821..341ab5b 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,15 @@ def generate_protos(): setup_requires=[ "protobuf==5.27.2", "grpcio-tools>=1.56.2", - ] + ], + extras_require={ + # Development dependencies + 'dev': [ + 'pytest', + 'pytest-cov', + 'pylint', + ], + }, ) From 95c5d349b95b5715681fbb71f0a743788f641258 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 25 Sep 2024 12:55:13 +0300 Subject: [PATCH 11/72] Update versions for CI --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67a7df1..c3106f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,19 +36,19 @@ jobs: run: python3 run_tests.py --lint - name: Upload unit test report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: unit-test-report path: reports/utest - name: Upload coverage report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: coverage-report path: reports/coverage - name: Upload lint report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: pylint-report path: reports/pylint From fbae064fa7e316924b82254be21a3602e7cad883 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 25 Sep 2024 12:59:04 +0300 Subject: [PATCH 12/72] Update versions for CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3106f7..6c3cdab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.10' From c7b5b53113d256c912e6e527828c750de1781861 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 25 Sep 2024 14:35:25 +0300 Subject: [PATCH 13/72] Update MANIFEST.in --- MANIFEST.in | 3 --- 1 file changed, 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 71b73fd..ab99ff7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,3 @@ include README.md # Include the license file include LICENSE - -# Include the requirements.txt file -include requirements.txt \ No newline at end of file From 8d97e1500e85d250498cf5ecfe4a27880e4b7889 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 26 Sep 2024 13:27:07 +0300 Subject: [PATCH 14/72] Refactor to follow PEP8 and update tests --- .github/workflows/ci.yml | 77 ++++++- .gitignore | 4 +- AnkaiosSDK/_components/__init__.py | 34 --- MANIFEST.in | 2 +- {AnkaiosSDK => ankaios_sdk}/__init__.py | 4 +- ankaios_sdk/_components/__init__.py | 41 ++++ .../_components/complete_state.py | 40 ++-- .../_components/manifest.py | 19 +- .../_components/request.py | 30 ++- .../_components/response.py | 36 +-- .../_components/workload.py | 63 ++++-- .../_components/workload_state.py | 91 +++++--- .../_protos/.gitignore | 0 .../_protos/__init__.py | 6 +- .../_protos/ank_base.proto | 0 .../_protos/control_api.proto | 0 .../Ankaios.py => ankaios_sdk/ankaios.py | 207 +++++++++++------- run_tests.py | 58 ++++- setup.py | 3 +- tests/Response/test_response.py | 10 +- tests/Response/test_response_event.py | 4 +- tests/Workload/test_workload.py | 22 +- tests/Workload/test_workload_builder.py | 36 ++- .../test_workload_execution_state.py | 18 +- .../test_workload_instance_name.py | 5 +- tests/WorkloadState/test_workload_state.py | 7 +- .../test_workload_state_collection.py | 19 +- .../WorkloadState/test_workload_state_enum.py | 8 +- .../test_workload_substate_enum.py | 10 +- tests/test_ankaios.py | 166 ++++++++------ tests/test_complete_state.py | 12 +- tests/test_manifest.py | 39 ++-- tests/test_request.py | 14 +- 33 files changed, 699 insertions(+), 386 deletions(-) delete mode 100644 AnkaiosSDK/_components/__init__.py rename {AnkaiosSDK => ankaios_sdk}/__init__.py (92%) create mode 100644 ankaios_sdk/_components/__init__.py rename AnkaiosSDK/_components/CompleteState.py => ankaios_sdk/_components/complete_state.py (84%) rename AnkaiosSDK/_components/Manifest.py => ankaios_sdk/_components/manifest.py (93%) rename AnkaiosSDK/_components/Request.py => ankaios_sdk/_components/request.py (83%) rename AnkaiosSDK/_components/Response.py => ankaios_sdk/_components/response.py (88%) rename AnkaiosSDK/_components/Workload.py => ankaios_sdk/_components/workload.py (93%) rename AnkaiosSDK/_components/WorkloadState.py => ankaios_sdk/_components/workload_state.py (86%) rename {AnkaiosSDK => ankaios_sdk}/_protos/.gitignore (100%) rename {AnkaiosSDK => ankaios_sdk}/_protos/__init__.py (83%) rename {AnkaiosSDK => ankaios_sdk}/_protos/ank_base.proto (100%) rename {AnkaiosSDK => ankaios_sdk}/_protos/control_api.proto (100%) rename AnkaiosSDK/Ankaios.py => ankaios_sdk/ankaios.py (77%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c3cdab..5867b1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,8 @@ on: - main jobs: - test: + setup: runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 @@ -25,30 +24,90 @@ jobs: run: | python3 -m pip install --upgrade pip pip install .[dev] + - name: Save cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + unit_test: + runs-on: ubuntu-latest + needs: setup + steps: + - uses: actions/checkout@v4 + - name: Restore cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- - name: Run unit tests run: python3 run_tests.py --utest - - - name: Run coverage - run: python3 run_tests.py --cov - - - name: Run lint - run: python3 run_tests.py --lint - - name: Upload unit test report uses: actions/upload-artifact@v4 with: name: unit-test-report path: reports/utest + coverage: + runs-on: ubuntu-latest + needs: setup + steps: + - uses: actions/checkout@v4 + - name: Restore cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Run coverage + run: python3 run_tests.py --cov - name: Upload coverage report uses: actions/upload-artifact@v4 with: name: coverage-report path: reports/coverage + lint: + runs-on: ubuntu-latest + needs: setup + steps: + - uses: actions/checkout@v4 + - name: Restore cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Run lint + run: python3 run_tests.py --lint - name: Upload lint report uses: actions/upload-artifact@v4 with: name: pylint-report path: reports/pylint + + codestyle: + runs-on: ubuntu-latest + needs: setup + steps: + - uses: actions/checkout@v4 + - name: Restore cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Run pep8 codestyle check + run: python3 run_tests.py --pep8 + - name: Upload codestyle report + uses: actions/upload-artifact@v4 + with: + name: codestyle-report + path: reports/codestyle \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7d6e3a4..f34e6e9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ reports/ .pytest_cache/ # Build directory -AnkaiosSDK.egg-info \ No newline at end of file +ankaios_sdk.egg-info +build/ +dist/ \ No newline at end of file diff --git a/AnkaiosSDK/_components/__init__.py b/AnkaiosSDK/_components/__init__.py deleted file mode 100644 index a5f7b37..0000000 --- a/AnkaiosSDK/_components/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2024 Elektrobit Automotive GmbH -# -# This program and the accompanying materials are made available under the -# terms of the Apache License, Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0. -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# SPDX-License-Identifier: Apache-2.0 - -""" -This module initializes the AnkaiosSDK package by importing all necessary components. - -Imports: - Workload component: responsible for defining the workload of the system. - WorkloadState component: responsible for accessing the state of the workload. - CompleteState component: responsible for accessing the complete state of the system. - Request component: responsible for defining a request to be sent to the system. - Response component: responsible for defining a response from the system. - Manifest component: responsible for defining a manifest object. -""" - -from .Workload import * -from .WorkloadState import * -from .CompleteState import * -from .Request import * -from .Response import * -from .Manifest import * - -__all__ = [name for name in globals() if not name.startswith('_')] diff --git a/MANIFEST.in b/MANIFEST.in index ab99ff7..28efb90 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ # Include all .proto files in the _protos directory -recursive-include AnkaiosSDK/_protos *.proto +recursive-include ankaios_sdk/_protos *.proto # Include the README file include README.md diff --git a/AnkaiosSDK/__init__.py b/ankaios_sdk/__init__.py similarity index 92% rename from AnkaiosSDK/__init__.py rename to ankaios_sdk/__init__.py index 841b8dc..48e7166 100644 --- a/AnkaiosSDK/__init__.py +++ b/ankaios_sdk/__init__.py @@ -13,7 +13,7 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains the AnkaiosSDK package. +This module contains the ankaios_sdk package. It exposes to the user all the classes available in the SDK. Imports: @@ -21,7 +21,7 @@ All the other classes, available in the _components folder. """ -from .Ankaios import * +from .ankaios import * from ._components import * __all__ = [name for name in globals() if not name.startswith('_')] diff --git a/ankaios_sdk/_components/__init__.py b/ankaios_sdk/_components/__init__.py new file mode 100644 index 0000000..704dbee --- /dev/null +++ b/ankaios_sdk/_components/__init__.py @@ -0,0 +1,41 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module initializes the ankaios_sdk package by importing all +necessary components. + +Imports: + Workload component: responsible for defining the + workload of the system. + WorkloadState component: responsible for accessing + the state of the workload. + CompleteState component: responsible for accessing + the complete state of the system. + Request component: responsible for defining a request + to be sent to the system. + Response component: responsible for defining a response + from the system. + Manifest component: responsible for defining a manifest + object. +""" + +from .workload import * +from .workload_state import * +from .complete_state import * +from .request import * +from .response import * +from .manifest import * + +__all__ = [name for name in globals() if not name.startswith('_')] diff --git a/AnkaiosSDK/_components/CompleteState.py b/ankaios_sdk/_components/complete_state.py similarity index 84% rename from AnkaiosSDK/_components/CompleteState.py rename to ankaios_sdk/_components/complete_state.py index 0f8fccb..a16a263 100644 --- a/AnkaiosSDK/_components/CompleteState.py +++ b/ankaios_sdk/_components/complete_state.py @@ -13,7 +13,8 @@ # SPDX-License-Identifier: Apache-2.0 """ -This script defines the CompleteState class for managing the state of the system. +This script defines the CompleteState class for managing +the state of the system. Classes: - CompleteState: Represents the complete state of the system. @@ -41,12 +42,13 @@ workload_states = complete_state.get_workload_states() """ +__all__ = ["CompleteState"] + from .._protos import _ank_base -from .Workload import Workload -from .WorkloadState import WorkloadStateCollection +from .workload import Workload +from .workload_state import WorkloadStateCollection -__all__ = ["CompleteState"] DEFAULT_API_VERSION = "v0.1" @@ -110,7 +112,8 @@ def get_workload(self, workload_name: str) -> Workload: workload_name (str): The name of the workload to retrieve. Returns: - Workload: The workload with the specified name, or None if not found. + Workload: The workload with the specified name, + or None if not found. """ for wl in self._workloads: if wl.name == workload_name: @@ -142,7 +145,7 @@ def get_agents(self) -> list[str]: Returns: list[str]: A list of connected agents. """ - # Return keys because the value "AgentAttributes" is not yet implemented + # "AgentAttributes" does not contain anything at the moment return list(self._complete_state.agents.agents.keys()) def _from_dict(self, dict_state: dict) -> None: @@ -153,24 +156,30 @@ def _from_dict(self, dict_state: dict) -> None: dict_state (dict): The dictionary representing the complete state. """ self._complete_state = _ank_base.CompleteState() - self._set_api_version(dict_state.get("apiVersion", self.get_api_version())) + self._set_api_version( + dict_state.get("apiVersion", self.get_api_version()) + ) self._workloads = [] if dict_state.get("workloads") is None: return - for workload_name, workload_dict in dict_state.get("workloads").items(): - self._workloads.append(Workload._from_dict(workload_name, workload_dict)) + for workload_name, workload_dict in \ + dict_state.get("workloads").items(): + self._workloads.append( + Workload._from_dict(workload_name, workload_dict) + ) def _to_proto(self) -> _ank_base.CompleteState: """ Converts the CompleteState object to a proto message. Returns: - _ank_base.CompleteState: The protobuf message representing the complete state. + _ank_base.CompleteState: The protobuf message representing + the complete state. """ # Clear previous workloads for workload in self._workloads: - self._complete_state.desiredState.workloads.workloads[workload.name]\ - .CopyFrom(workload._to_proto()) + self._complete_state.desiredState.workloads.\ + workloads[workload.name].CopyFrom(workload._to_proto()) return self._complete_state def _from_proto(self, proto: _ank_base.CompleteState) -> None: @@ -178,7 +187,8 @@ def _from_proto(self, proto: _ank_base.CompleteState) -> None: Converts the proto message to a CompleteState object. Args: - proto (_ank_base.CompleteState): The protobuf message representing the complete state. + proto (_ank_base.CompleteState): The protobuf message representing + the complete state. """ self._complete_state = proto self._workloads = [] @@ -187,4 +197,6 @@ def _from_proto(self, proto: _ank_base.CompleteState) -> None: workload = Workload(workload_name) workload._from_proto(proto_workload) self._workloads.append(workload) - self._workload_state_collection._from_proto(self._complete_state.workloadStates) + self._workload_state_collection._from_proto( + self._complete_state.workloadStates + ) diff --git a/AnkaiosSDK/_components/Manifest.py b/ankaios_sdk/_components/manifest.py similarity index 93% rename from AnkaiosSDK/_components/Manifest.py rename to ankaios_sdk/_components/manifest.py index 810e6c3..67db6bc 100644 --- a/AnkaiosSDK/_components/Manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -16,24 +16,25 @@ This module defines the Manifest class for handling ankaios manifests. Classes: - - Manifest: Represents a workload manifest and provides methods to validate and load it. + - Manifest: Represents a workload manifest and provides methods to + validate and load it. Usage: - Load a manifest from a file: manifest = Manifest.from_file("path/to/manifest.yaml") - + - Load a manifest from a string: manifest = Manifest.from_string("apiVersion: 1.0\nworkloads: {}") - + - Load a manifest from a dictionary: manifest = Manifest.from_dict({"apiVersion": "1.0", "workloads": {}}) - + - Generate a CompleteState instance from the manifest: complete_state = manifest.generate_complete_state() """ import yaml -from .CompleteState import CompleteState +from .complete_state import CompleteState class Manifest(): @@ -120,8 +121,9 @@ def check(self) -> bool: return False if "workloads" not in self._manifest.keys(): return False - wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", - "dependencies", "tags", "controlInterfaceAccess"] + wl_allowed_keys = ["runtime", "agent", "restartPolicy", + "runtimeConfig", "dependencies", "tags", + "controlInterfaceAccess"] for wl_name in self._manifest["workloads"]: for key in self._manifest["workloads"][wl_name].keys(): if key not in wl_allowed_keys: @@ -143,7 +145,8 @@ def generate_complete_state(self) -> CompleteState: Generates a CompleteState instance from the manifest. Returns: - CompleteState: An instance of the CompleteState class populated with the manifest data. + CompleteState: An instance of the CompleteState class + populated with the manifest data. """ complete_state = CompleteState() complete_state._from_dict(self._manifest) diff --git a/AnkaiosSDK/_components/Request.py b/ankaios_sdk/_components/request.py similarity index 83% rename from AnkaiosSDK/_components/Request.py rename to ankaios_sdk/_components/request.py index b3b0f19..0aacc95 100644 --- a/AnkaiosSDK/_components/Request.py +++ b/ankaios_sdk/_components/request.py @@ -13,11 +13,12 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module defines the Request class for creating and handling requests to the Ankaios system. +This module defines the Request class for creating and handling +requests to the Ankaios system. Classes: - Request: Represents a request to the Ankaios system and provides methods to get and set - the state of the system. + Request: Represents a request to the Ankaios system and provides + methods to get and set the state of the system. Usage: - Create a Request for updating the state: @@ -34,12 +35,11 @@ request.add_mask("desiredState.workloads") """ +__all__ = ["Request"] + import uuid from .._protos import _ank_base -from .CompleteState import CompleteState - - -__all__ = ["Request"] +from .complete_state import CompleteState class Request: @@ -51,7 +51,8 @@ def __init__(self, request_type: str) -> None: Initializes a Request instance with the given request type. Args: - request_type (str): The type of the request, either "update_state" or "get_state". + request_type (str): The type of the request, + either "update_state" or "get_state". Raises: ValueError: If the request type is invalid. @@ -61,7 +62,8 @@ def __init__(self, request_type: str) -> None: self._request_type = request_type if request_type not in ["update_state", "get_state"]: - raise ValueError("Invalid request type. Supported values: 'update_state', 'get_state'.") + raise ValueError("Invalid request type. Supported values: " + + "'update_state', 'get_state'.") def __str__(self) -> str: """ @@ -86,15 +88,19 @@ def set_complete_state(self, complete_state: CompleteState) -> None: Sets the complete state for the request. Args: - complete_state (CompleteState): The complete state to set for the request. + complete_state (CompleteState): The complete state to + set for the request. Raises: ValueError: If the request type is not "update_state". """ if self._request_type != "update_state": - raise ValueError("Complete state can only be set for an update state request.") + raise ValueError("Complete state can only be set " + + "for an update state request.") - self._request.updateStateRequest.newState.CopyFrom(complete_state._to_proto()) + self._request.updateStateRequest.newState.CopyFrom( + complete_state._to_proto() + ) def add_mask(self, mask: str) -> None: """ diff --git a/AnkaiosSDK/_components/Response.py b/ankaios_sdk/_components/response.py similarity index 88% rename from AnkaiosSDK/_components/Response.py rename to ankaios_sdk/_components/response.py index 53c2777..f3241d7 100644 --- a/AnkaiosSDK/_components/Response.py +++ b/ankaios_sdk/_components/response.py @@ -24,20 +24,19 @@ - Get response content: response = Response() (content_type, content) = response.get_content() - + - Check if the request_id matches: response = Response() if response.check_request_id("1234"): print("Request ID matches") """ +__all__ = ["Response", "ResponseEvent"] + from typing import Union from threading import Event from .._protos import _control_api -from .CompleteState import CompleteState - - -__all__ = ["Response", "ResponseEvent"] +from .complete_state import CompleteState class Response: @@ -47,8 +46,9 @@ class Response: Attributes: buffer (bytes): The received message buffer. content_type (str): The type of the response content - (e.g., "error", "complete_state", "update_state_success"). - content: The content of the response, which can be a string, CompleteState, or dictionary. + (e.g., "error", "complete_state", "update_state_success"). + content: The content of the response, which can be a string, + CompleteState, or dictionary. """ def __init__(self, message_buffer: bytes) -> None: """ @@ -83,7 +83,8 @@ def _parse_response(self) -> None: def _from_proto(self) -> None: """ Converts the parsed protobuf message to a Response object. - This can be either an error, a complete state, or an update state success. + This can be either an error, a complete state, + or an update state success. Raises: ValueError: If the response type is invalid. @@ -98,8 +99,10 @@ def _from_proto(self) -> None: elif self._response.HasField("UpdateStateSuccess"): self.content_type = "update_state_success" self.content = { - "added_workloads": self._response.UpdateStateSuccess.addedWorkloads, - "deleted_workloads": self._response.UpdateStateSuccess.deletedWorkloads, + "added_workloads": + self._response.UpdateStateSuccess.addedWorkloads, + "deleted_workloads": + self._response.UpdateStateSuccess.deletedWorkloads, } else: raise ValueError("Invalid response type.") @@ -130,8 +133,8 @@ def get_content(self) -> tuple[str, Union[str, CompleteState, dict]]: Gets the content of the response. Returns: - (tuple[str, Union[str, CompleteState, dict]]): A tuple containing the content type - and the content of the response. + (tuple[str, Union[str, CompleteState, dict]]): A tuple containing + the content type and the content of the response. """ return (self.content_type, self.content) @@ -145,7 +148,8 @@ def __init__(self, response: Response = None) -> None: Initializes the ResponseEvent with an optional Response object. Args: - response Optional(Response): The response to associate with the event. Defaults to None. + response Optional(Response): The response to associate with + the event. Defaults to None. """ super().__init__() self._response = response @@ -174,13 +178,15 @@ def wait_for_response(self, timeout: int) -> Response: Waits for the response to be set, with a specified timeout. Args: - timeout (int): The maximum time to wait for the response, in seconds. + timeout (int): The maximum time to wait for the response, + in seconds. Returns: Response: The response associated with the event. Raises: - TimeoutError: If the response is not set within the specified timeout. + TimeoutError: If the response is not set within the + specified timeout. """ if not self.wait(timeout): raise TimeoutError("Timeout while waiting for the response.") diff --git a/AnkaiosSDK/_components/Workload.py b/ankaios_sdk/_components/workload.py similarity index 93% rename from AnkaiosSDK/_components/Workload.py rename to ankaios_sdk/_components/workload.py index 0732199..0481770 100644 --- a/AnkaiosSDK/_components/Workload.py +++ b/ankaios_sdk/_components/workload.py @@ -13,11 +13,14 @@ # SPDX-License-Identifier: Apache-2.0 """ -This script defines the Workload and WorkloadBuilder classes for creating and managing workloads. +This script defines the Workload and WorkloadBuilder classes for +creating and managing workloads. Classes: - - Workload: Represents a workload with various attributes and methods to update them. - - WorkloadBuilder: A builder class to create a Workload object with a fluent interface. + - Workload: Represents a workload with various attributes and + methods to update them. + - WorkloadBuilder: A builder class to create a Workload object + with a fluent interface. Usage: - Create a workload using the WorkloadBuilder: @@ -26,8 +29,8 @@ .agent_name("agent_A") \ .runtime("podman") \ .restart_policy("NEVER") \ - .runtime_config("image: docker.io/library/nginx\n" + - "commandOptions: [\"-p\", \"8080:80\"]") \ + .runtime_config("image: docker.io/library/nginx\n" + + "commandOptions: [\"-p\", \"8080:80\"]") \ .add_dependency("other_workload", "RUNNING") \ .add_tag("key1", "value1") \ .add_tag("key2", "value2") \ @@ -50,11 +53,10 @@ print(workload) """ - -from .._protos import _ank_base +__all__ = ["Workload", "WorkloadBuilder"] -__all__ = ["Workload", "WorkloadBuilder"] +from .._protos import _ank_base class Workload: @@ -67,7 +69,8 @@ class Workload: def __init__(self, name: str) -> None: """ Initialize a Workload object. - The Workload object should be created using the Workload.builder() method. + The Workload object should be created using the + Workload.builder() method. Args: name (str): The workload name. @@ -175,8 +178,8 @@ def update_restart_policy(self, policy: str) -> None: } if policy not in policy_map: - raise ValueError("Invalid restart policy. Supported values " + - "'NEVER', 'ON_FAILURE', 'ALWAYS'.") + raise ValueError("Invalid restart policy. Supported values " + + "'NEVER', 'ON_FAILURE', 'ALWAYS'.") self._workload.restartPolicy = policy_map[policy] if not self.__from_builder: self._add_mask(f"{self._main_mask}.restartPolicy") @@ -200,9 +203,10 @@ def add_dependency(self, workload_name: str, condition: str) -> None: } if condition not in condition_map: - raise ValueError("Invalid condition. Supported values: " + - "'RUNNING', 'SUCCEEDED', 'FAILED'.") - self._workload.dependencies.dependencies[workload_name] = condition_map[condition] + raise ValueError("Invalid condition. Supported values: " + + "'RUNNING', 'SUCCEEDED', 'FAILED'.") + self._workload.dependencies.dependencies[workload_name] = \ + condition_map[condition] if not self.__from_builder: self._add_mask(f"{self._main_mask}.dependencies") @@ -211,7 +215,8 @@ def get_dependencies(self) -> dict: Return the dependencies of the workload. Returns: - dict: A dictionary of dependencies with workload names as keys and conditions as values. + dict: A dictionary of dependencies with workload names + as keys and conditions as values. """ deps = dict(self._workload.dependencies.dependencies) for dep in deps: @@ -228,7 +233,8 @@ def update_dependencies(self, dependencies: dict) -> None: Update the dependencies of the workload. Args: - dependencies (dict): A dictionary of dependencies with workload names and values. + dependencies (dict): A dictionary of dependencies with + workload names and values. """ self._workload.dependencies.dependencies.clear() for workload_name, condition in dependencies.items(): @@ -300,7 +306,7 @@ def _from_dict(workload_name: str, dict_workload: dict) -> "Workload": Args: workload_name (str): The name of the workload. dict_workload (dict): The dictionary to convert. - + Returns: Workload: The Workload object created from the dictionary. """ @@ -326,7 +332,8 @@ def _to_proto(self) -> _ank_base.Workload: Convert the Workload object to a proto message. Returns: - _ank_base.Workload: The proto message representation of the Workload object. + _ank_base.Workload: The proto message representation + of the Workload object. """ return self._workload @@ -417,7 +424,9 @@ def runtime_config(self, runtime_config: str) -> "WorkloadBuilder": self.wl_runtime_config = runtime_config return self - def runtime_config_from_file(self, runtime_config_path: str) -> "WorkloadBuilder": + def runtime_config_from_file( + self, runtime_config_path: str + ) -> "WorkloadBuilder": """ Set the runtime configuration using a file. @@ -444,7 +453,9 @@ def restart_policy(self, restart_policy: str) -> "WorkloadBuilder": self.wl_restart_policy = restart_policy return self - def add_dependency(self, workload_name: str, condition: str) -> "WorkloadBuilder": + def add_dependency( + self, workload_name: str, condition: str + ) -> "WorkloadBuilder": """ Add a dependency. @@ -475,7 +486,8 @@ def add_tag(self, key: str, value: str) -> "WorkloadBuilder": def build(self) -> Workload: """ Build the Workload object. - Required fields: workload name, agent name, runtime and runtime configuration. + Required fields: workload name, agent name, runtime and + runtime configuration. Returns: Workload: The built Workload object. @@ -490,11 +502,14 @@ def build(self) -> Workload: workload._set_from_builder() if self.wl_agent_name is None: - raise ValueError("Workload can not be built without an agent name.") + raise ValueError("Workload can not be built without an " + + "agent name.") if self.wl_runtime is None: - raise ValueError("Workload can not be built without a runtime.") + raise ValueError("Workload can not be built without a " + + "runtime.") if self.wl_runtime_config is None: - raise ValueError("Workload can not be built without a runtime configuration.") + raise ValueError("Workload can not be built without a " + + "runtime configuration.") workload.update_agent_name(self.wl_agent_name) workload.update_runtime(self.wl_runtime) diff --git a/AnkaiosSDK/_components/WorkloadState.py b/ankaios_sdk/_components/workload_state.py similarity index 86% rename from AnkaiosSDK/_components/WorkloadState.py rename to ankaios_sdk/_components/workload_state.py index 261a226..33ae4ef 100644 --- a/AnkaiosSDK/_components/WorkloadState.py +++ b/ankaios_sdk/_components/workload_state.py @@ -13,14 +13,17 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module defines various classes and enumerations related to the state of workloads. -It provides functionality to interpret and manage the states and sub-states of workloads, -including converting between different representations and handling collections of workload states. +This module defines various classes and enumerations related to the state +of workloads. It provides functionality to interpret and manage the states +and sub-states of workloads, including converting between different +representations and handling collections of workload states. Classes: - - WorkloadExecutionState: Represents the execution state and sub-state of a workload. + - WorkloadExecutionState: Represents the execution state and + sub-state of a workload. - WorkloadInstanceName: Represents the name of a workload instance. - - WorkloadState: Represents the state of a workload (execution state and name). + - WorkloadState: Represents the state of a workload + (execution state and name). - WorkloadStateCollection: A collection of workload states. Enums: @@ -42,15 +45,15 @@ info = workload_state.execution_state.info """ +__all__ = ["WorkloadStateCollection", "WorkloadState", + "WorkloadInstanceName", "WorkloadExecutionState", + "WorkloadStateEnum", "WorkloadSubStateEnum"] + from typing import TypeAlias from enum import Enum from .._protos import _ank_base -__all__ = ["WorkloadStateCollection", "WorkloadState", "WorkloadInstanceName", - "WorkloadExecutionState", "WorkloadStateEnum", "WorkloadSubStateEnum"] - - class WorkloadStateEnum(Enum): """ Enumeration for different states of a workload. @@ -92,10 +95,12 @@ def _get(field: str) -> "WorkloadStateEnum": field (str): The field name to look up. Returns: - WorkloadStateEnum: The enumeration member corresponding to the field name. + WorkloadStateEnum: The enumeration member corresponding + to the field name. Raises: - KeyError: If the field name does not correspond to any enumeration member. + KeyError: If the field name does not correspond to + any enumeration member. """ field = field[0].upper() + field[1:] # Capitalize the first letter return WorkloadStateEnum[field] @@ -114,7 +119,8 @@ class WorkloadSubStateEnum(Enum): RUNNING_OK (int): The workload is running successfully. STOPPING (int): The workload is stopping. STOPPING_WAITING_TO_STOP (int): The workload is waiting to stop. - STOPPING_REQUESTED_AT_RUNTIME (int): The workload stop was requested at runtime. + STOPPING_REQUESTED_AT_RUNTIME (int): The workload stop was + requested at runtime. STOPPING_DELETE_FAILED (int): The workload stop failed to delete. SUCCEEDED_OK (int): The workload succeeded successfully. FAILED_EXEC_FAILED (int): The workload failed due to execution failure. @@ -150,7 +156,8 @@ def __str__(self) -> str: return self.name @staticmethod - def _get(state: WorkloadStateEnum, field: _ank_base) -> "WorkloadSubStateEnum": + def _get(state: WorkloadStateEnum, + field: _ank_base) -> "WorkloadSubStateEnum": """ Get the enumeration member corresponding to the given state and field. @@ -159,10 +166,12 @@ def _get(state: WorkloadStateEnum, field: _ank_base) -> "WorkloadSubStateEnum": field (_ank_base): The field to look up. Returns: - WorkloadSubStateEnum: The enumeration member corresponding to the state and field. + WorkloadSubStateEnum: The enumeration member corresponding + to the state and field. Raises: - ValueError: If the field does not correspond to any enumeration member. + ValueError: If the field does not correspond to + any enumeration member. """ proto_mapper = {} if state == WorkloadStateEnum.AgentDisconnected: @@ -220,23 +229,27 @@ def _get(state: WorkloadStateEnum, field: _ank_base) -> "WorkloadSubStateEnum": WorkloadSubStateEnum.REMOVED } if field not in proto_mapper: - raise ValueError(f"No corresponding WorkloadSubStateEnum value for enum: {field}") + raise ValueError("No corresponding WorkloadSubStateEnum " + + f"value for enum: {field}") return proto_mapper[field] def _sub_state2ank_base(self) -> _ank_base: """ - Convert the WorkloadSubStateEnum member to the corresponding _ank_base value. + Convert the WorkloadSubStateEnum member to the corresponding + _ank_base value. Returns: _ank_base: The corresponding _ank_base value. Raises: - ValueError: If there is no corresponding _ank_base value for the enumeration member. + ValueError: If there is no corresponding _ank_base + value for the enumeration member. """ try: return getattr(_ank_base, self.name) except AttributeError as e: # pragma: no cover - raise ValueError(f"No corresponding ank_base value for enum: {self.name}") from e + raise ValueError("No corresponding ank_base value " + + f"for enum: {self.name}") from e # pylint: disable=too-few-public-methods @@ -264,10 +277,12 @@ def __init__(self, state: _ank_base.ExecutionState) -> None: def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: """ - Interprets the execution state and sets the state, substate, and info attributes. + Interprets the execution state and sets the state, substate, + and info attributes. Args: - exec_state (_ank_base.ExecutionState): The execution state to interpret. + exec_state (_ank_base.ExecutionState): The execution + state to interpret. Raises: ValueError: If the execution state is invalid. @@ -279,7 +294,9 @@ def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: raise ValueError("Invalid state for workload.") self.state = WorkloadStateEnum._get(field) - self.substate = WorkloadSubStateEnum._get(self.state, getattr(exec_state, field)) + self.substate = WorkloadSubStateEnum._get( + self.state, getattr(exec_state, field) + ) # pylint: disable=too-few-public-methods @@ -292,7 +309,8 @@ class WorkloadInstanceName: workload_name (str): The name of the workload. workload_id (str): The ID of the workload. """ - def __init__(self, agent_name: str, workload_name: str, workload_id: str) -> None: + def __init__(self, agent_name: str, + workload_name: str, workload_id: str) -> None: """ Initializes a WorkloadInstanceName instance. @@ -321,8 +339,10 @@ class WorkloadState: Represents the state of a workload. Attributes: - execution_state (WorkloadExecutionState): The execution state of the workload. - workload_instance_name (WorkloadInstanceName): The name of the workload instance. + execution_state (WorkloadExecutionState): The execution state + of the workload. + workload_instance_name (WorkloadInstanceName): The name of the + workload instance. """ def __init__(self, agent_name: str, workload_name: str, workload_id: str, state: _ank_base.ExecutionState) -> None: @@ -336,12 +356,15 @@ def __init__(self, agent_name: str, workload_name: str, state (_ank_base.ExecutionState): The execution state to interpret. """ self.execution_state = WorkloadExecutionState(state) - self.workload_instance_name = WorkloadInstanceName(agent_name, workload_name, workload_id) + self.workload_instance_name = WorkloadInstanceName( + agent_name, workload_name, workload_id + ) class WorkloadStateCollection: """ - A class that represents a collection of workload states and provides methods to manipulate them. + A class that represents a collection of workload states and provides + methods to manipulate them. """ ExecutionsStatesForId: TypeAlias = dict[str, WorkloadExecutionState] ExecutionsStatesOfWorkload: TypeAlias = dict[str, ExecutionsStatesForId] @@ -378,10 +401,12 @@ def get_as_dict(self) -> WorkloadStatesMap: workload_name = state.workload_instance_name.workload_name if workload_name not in return_dict[agent_name]: - return_dict[agent_name][workload_name] = self.ExecutionsStatesForId() + return_dict[agent_name][workload_name] = \ + self.ExecutionsStatesForId() workload_id = state.workload_instance_name.workload_id - return_dict[agent_name][workload_name][workload_id] = state.execution_state + return_dict[agent_name][workload_name][workload_id] = \ + state.execution_state return return_dict def get_as_list(self) -> list[WorkloadState]: @@ -398,7 +423,8 @@ def _from_proto(self, state: _ank_base.WorkloadStatesMap) -> None: Populates the collection from a proto message. Args: - state (_ank_base.WorkloadStatesMap): The proto message to interpret. + state (_ank_base.WorkloadStatesMap): The proto message + to interpret. """ for agent_name in state.agentStateMap: for workload_name in state.agentStateMap[agent_name].\ @@ -409,6 +435,7 @@ def _from_proto(self, state: _ank_base.WorkloadStatesMap) -> None: agent_name, workload_name, workload_id, - state.agentStateMap[agent_name].wlNameStateMap[workload_name].\ - idStateMap[workload_id] + state.agentStateMap[agent_name] + .wlNameStateMap[workload_name] + .idStateMap[workload_id] )) diff --git a/AnkaiosSDK/_protos/.gitignore b/ankaios_sdk/_protos/.gitignore similarity index 100% rename from AnkaiosSDK/_protos/.gitignore rename to ankaios_sdk/_protos/.gitignore diff --git a/AnkaiosSDK/_protos/__init__.py b/ankaios_sdk/_protos/__init__.py similarity index 83% rename from AnkaiosSDK/_protos/__init__.py rename to ankaios_sdk/_protos/__init__.py index efbba05..191fec6 100644 --- a/AnkaiosSDK/_protos/__init__.py +++ b/ankaios_sdk/_protos/__init__.py @@ -13,7 +13,7 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains the AnkaiosSDK protobuf components. +This module contains the ankaios_sdk protobuf components. It contains the proto files and the generated protobuf classes. Imports: @@ -22,8 +22,8 @@ """ try: - import AnkaiosSDK._protos.ank_base_pb2 as _ank_base - import AnkaiosSDK._protos.control_api_pb2 as _control_api + import ankaios_sdk._protos.ank_base_pb2 as _ank_base + import ankaios_sdk._protos.control_api_pb2 as _control_api except ImportError as r: raise r diff --git a/AnkaiosSDK/_protos/ank_base.proto b/ankaios_sdk/_protos/ank_base.proto similarity index 100% rename from AnkaiosSDK/_protos/ank_base.proto rename to ankaios_sdk/_protos/ank_base.proto diff --git a/AnkaiosSDK/_protos/control_api.proto b/ankaios_sdk/_protos/control_api.proto similarity index 100% rename from AnkaiosSDK/_protos/control_api.proto rename to ankaios_sdk/_protos/control_api.proto diff --git a/AnkaiosSDK/Ankaios.py b/ankaios_sdk/ankaios.py similarity index 77% rename from AnkaiosSDK/Ankaios.py rename to ankaios_sdk/ankaios.py index 7ea3a9d..1ca277a 100644 --- a/AnkaiosSDK/Ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -13,7 +13,8 @@ # SPDX-License-Identifier: Apache-2.0 """ -This script defines the Ankaios class for interacting with the Ankaios control interface. +This script defines the Ankaios class for interacting with the +Ankaios control interface. Classes: - Ankaios: Handles the interaction with the Ankaios control interface. @@ -22,38 +23,41 @@ - Create an Ankaios object and connect to the control interface: with Ankaios() as ankaios: pass - + - Apply a manifest: ankaios.apply_manifest(manifest) - + - Delete a manifest: ankaios.delete_manifest(manifest) - + - Run a workload: ankaios.run_workload(workload) - + - Delete a workload: ankaios.delete_workload(workload_name) - + - Get a workload: workload = ankaios.get_workload(workload_name) - + - Get the state: state = ankaios.get_state() - + - Get the agents: agents = ankaios.get_agents() - + - Get the workload states: workload_states = ankaios.get_workload_states() - Get the workload states on an agent: workload_states = ankaios.get_workload_states_on_agent(agent_name) - + - Get the workload states on a workload name: - workload_states = ankaios.get_workload_states_on_workload_name(workload_name) + workload_states = \ + ankaios.get_workload_states_on_workload_name(workload_name) """ +__all__ = ["Ankaios", "AnkaiosLogLevel"] + import logging from enum import Enum import threading @@ -65,13 +69,10 @@ ResponseEvent, WorkloadStateCollection, Manifest -__all__ = ["Ankaios", "AnkaiosLogLevel"] - - class AnkaiosLogLevel(Enum): """ Ankaios log levels. - + Attributes: FATAL (int): Fatal log level. ERROR (int): Error log level. @@ -88,17 +89,19 @@ class AnkaiosLogLevel(Enum): class Ankaios: """ - A class to interact with the Ankaios control interface. It provides the functionality to - interact with the Ankaios control interface by sending requests. + A class to interact with the Ankaios control interface. It provides + the functionality to interact with the Ankaios control interface + by sending requests. Attributes: - ANKAIOS_CONTROL_INTERFACE_BASE_PATH (str): The base path for the Ankaios control interface. + ANKAIOS_CONTROL_INTERFACE_BASE_PATH (str): The base path for the + Ankaios control interface. DEFAULT_TIMEOUT (int): The default timeout, if not manually provided. logger (logging.Logger): The logger for the Ankaios class. path (str): The path to the control interface. """ ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" - DEFAULT_TIMEOUT = 5 + DEFAULT_TIMEOUT = 5.0 def __init__(self) -> None: """Initialize the Ankaios object.""" @@ -115,7 +118,7 @@ def __init__(self) -> None: def __enter__(self) -> "Ankaios": """ Connect to the control interface. - + Returns: Ankaios: The Ankaios object. """ @@ -132,12 +135,14 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: traceback (traceback): The traceback object. """ if exc_type is not None: # pragma: no cover - self.logger.error("An exception occurred: %s, %s, %s", exc_type, exc_value, traceback) + self.logger.error("An exception occurred: %s, %s, %s", + exc_type, exc_value, traceback) self.disconnect() def _create_logger(self) -> None: """Create a logger with custom format and default log level.""" - formatter = logging.Formatter('%(asctime)s %(message)s', datefmt="%FT%TZ") + formatter = logging.Formatter('%(asctime)s %(message)s', + datefmt="%FT%TZ") self.logger = logging.getLogger("Ankaios logger") handler = logging.StreamHandler() handler.setFormatter(formatter) @@ -147,9 +152,9 @@ def _create_logger(self) -> None: def _read_from_control_interface(self) -> None: """ Reads from the control interface input fifo and saves the response. - This is meant to be run in a separate thread. - It reads the response from the control interface and saves it in the responses dictionary, - by triggering the corresponding ResponseEvent. + This is meant to be run in a separate thread. + It reads the response from the control interface and saves it in the + responses dictionary, by triggering the corresponding ResponseEvent. """ # pylint: disable=consider-using-with f = open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") @@ -164,7 +169,8 @@ def _read_from_control_interface(self) -> None: if not next_byte: # pragma: no cover break varint_buffer += next_byte - # Stop if the most significant bit is 0 (indicating the last byte of the varint) + # Stop if the most significant bit is 0 + # (indicating the last byte of the varint) if next_byte[0] & 0b10000000 == 0: break # Decode the varint and receive the proto msg length @@ -173,7 +179,7 @@ def _read_from_control_interface(self) -> None: # Buffer for the proto msg itself msg_buf = bytearray() for _ in range(msg_len): - # Read exact amount of byte according to the calculated proto msg length + # Read the message according to the length next_byte = f.read(1) if not next_byte: # pragma: no cover break @@ -197,19 +203,22 @@ def _read_from_control_interface(self) -> None: finally: f.close() - def _get_response_by_id(self, request_id: str, timeout: int = DEFAULT_TIMEOUT) -> Response: + def _get_response_by_id(self, request_id: str, + timeout: float = DEFAULT_TIMEOUT) -> Response: """ Returns the response by the request id. Args: request_id (str): The ID of the request. - timeout (int): The maximum time to wait for the response, in seconds. + timeout (float): The maximum time to wait for the response, + in seconds. Returns: Response: The response object. """ if not self._connected: - raise ValueError("Reading from the control interface is not started.") + raise ValueError("Reading from the control interface " + + "is not started.") with self._responses_lock: if request_id in self._responses: @@ -225,21 +234,26 @@ def _write_to_pipe(self, request: Request) -> None: Args: request (Request): The request object to be written. """ - with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") as f: - request_to_ankaios = _control_api.ToAnkaios(request=request._to_proto()) + with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", + "ab") as f: + request_to_ankaios = _control_api.ToAnkaios( + request=request._to_proto() + ) # Send the byte length of the proto msg f.write(_VarintBytes(request_to_ankaios.ByteSize())) # Send the proto msg itself f.write(request_to_ankaios.SerializeToString()) f.flush() - def _send_request(self, request: Request, timeout: int = DEFAULT_TIMEOUT) -> Response: + def _send_request(self, request: Request, + timeout: float = DEFAULT_TIMEOUT) -> Response: """ Send a request and wait for the response. Args: request (Request): The request object to be sent. - timeout (int): The maximum time to wait for the response, in seconds. + timeout (float): The maximum time to wait for the response, + in seconds. Returns: Response: The response object. @@ -265,21 +279,25 @@ def set_logger_level(self, level: AnkaiosLogLevel) -> None: def connect(self) -> None: """ - Connect to the control interface by starting to read from the input fifo. - + Connect to the control interface by starting to read + from the input fifo. + Raises: ValueError: If already connected. """ if self._connected: raise ValueError("Already connected.") self._connected = True - self._read_thread = threading.Thread(target=self._read_from_control_interface) + self._read_thread = threading.Thread( + target=self._read_from_control_interface + ) self._read_thread.start() def disconnect(self) -> None: """ - Disconnect from the control interface by stopping to read from the input fifo. - + Disconnect from the control interface by stopping to read + from the input fifo. + Raises: ValueError: If already disconnected. """ @@ -310,10 +328,14 @@ def apply_manifest(self, manifest: Manifest) -> None: # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error("Error while trying to apply manifest: %s", content) + self.logger.error("Error while trying to apply manifest: %s", + content) elif content_type == "update_state_success": - self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", - content["added_workloads"], content["deleted_workloads"]) + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + content["added_workloads"], content["deleted_workloads"] + ) def delete_manifest(self, manifest: Manifest) -> None: """ @@ -337,10 +359,14 @@ def delete_manifest(self, manifest: Manifest) -> None: # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error("Error while trying to delete manifest: %s", content) + self.logger.error("Error while trying to delete manifest: %s", + content) elif content_type == "update_state_success": - self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", - content["added_workloads"], content["deleted_workloads"]) + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + content["added_workloads"], content["deleted_workloads"] + ) def run_workload(self, workload: Workload) -> None: """ @@ -368,10 +394,14 @@ def run_workload(self, workload: Workload) -> None: # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error("Error while trying to run workload: %s", content) + self.logger.error("Error while trying to run workload: %s", + content) elif content_type == "update_state_success": - self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", - content["added_workloads"], content["deleted_workloads"]) + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + content["added_workloads"], content["deleted_workloads"] + ) def delete_workload(self, workload_name: str) -> None: """ @@ -393,27 +423,34 @@ def delete_workload(self, workload_name: str) -> None: # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error("Error while trying to delete workload: %s", content) + self.logger.error("Error while trying to delete workload: %s", + content) elif content_type == "update_state_success": - self.logger.info("Update successfull: %s added workloads, %s deleted workloads.", - content["added_workloads"], content["deleted_workloads"]) + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + content["added_workloads"], content["deleted_workloads"] + ) def get_workload(self, workload_name: str, state: CompleteState = None, - timeout: int = DEFAULT_TIMEOUT) -> Workload: + timeout: float = DEFAULT_TIMEOUT) -> Workload: """ Get the workload from the requested complete state. Args: workload_name (str): The name of the workload. state (CompleteState): The complete state to get the workload from. - timeout (int): The maximum time to wait for the response, in seconds. + timeout (float): The maximum time to wait for the response, + in seconds. Returns: Workload: The workload object. """ if state is None: - state = self.get_state(timeout, [f"desiredState.workloads.{workload_name}"]) + state = self.get_state( + timeout, [f"desiredState.workloads.{workload_name}"] + ) return state.get_workload(workload_name) if state is not None else None def set_config_from_file(self, name: str, config_path: str) -> None: @@ -462,14 +499,16 @@ def delete_config(self, name: str) -> None: """ raise NotImplementedError("delete_config is not implemented yet.") - def get_state(self, timeout: int = DEFAULT_TIMEOUT, + def get_state(self, timeout: float = DEFAULT_TIMEOUT, field_mask: list[str] = None) -> CompleteState: """ Send a request to get the complete state. Args: - timeout (int): The maximum time to wait for the response, in seconds. - field_mask (list[str]): The list of field masks to filter the state. + timeout (float): The maximum time to wait for the response, + in seconds. + field_mask (list[str]): The list of field masks to filter + the state. Returns: CompleteState: The complete state object. @@ -487,18 +526,21 @@ def get_state(self, timeout: int = DEFAULT_TIMEOUT, # Interpret response (content_type, content) = response.get_content() if content_type == "error": - self.logger.error("Error while trying to get the state: %s", content) + self.logger.error("Error while trying to get the state: %s", + content) return None return content - def get_agents(self, state: CompleteState = None, timeout: int = DEFAULT_TIMEOUT) -> list[str]: + def get_agents(self, state: CompleteState = None, + timeout: float = DEFAULT_TIMEOUT) -> list[str]: """ Get the agents from the requested complete state. Args: state (CompleteState): The complete state to get the agents from. - timeout (int): The maximum time to wait for the response, in seconds. + timeout (float): The maximum time to wait for the response, + in seconds. Returns: list[str]: The list of agent names. @@ -508,15 +550,18 @@ def get_agents(self, state: CompleteState = None, timeout: int = DEFAULT_TIMEOUT return state.get_agents() if state is not None else None def get_workload_states(self, - state: CompleteState= None, - timeout: int = DEFAULT_TIMEOUT) -> WorkloadStateCollection: + state: CompleteState = None, + timeout: float = DEFAULT_TIMEOUT + ) -> WorkloadStateCollection: """ Get the workload states from the requested complete state. If a state is not provided, it will be requested. Args: - state (CompleteState): The complete state to get the workload states from. - timeout (int): The maximum time to wait for the response, in seconds. + state (CompleteState): The complete state to get + the workload states from. + timeout (float): The maximum time to wait for the response, + in seconds. Returns: WorkloadStateCollection: The collection of workload states. @@ -527,18 +572,23 @@ def get_workload_states(self, def get_workload_states_on_agent(self, agent_name: str, state: CompleteState = None, - timeout: int = DEFAULT_TIMEOUT) -> WorkloadStateCollection: + timeout: float = DEFAULT_TIMEOUT + ) -> WorkloadStateCollection: """ - Get the workload states on a specific agent from the requested complete state. + Get the workload states on a specific agent from the requested + complete state. If a state is not provided, it will be requested. Args: agent_name (str): The name of the agent. - state (CompleteState): The complete state to get the workload states from. - timeout (int): The maximum time to wait for the response, in seconds. + state (CompleteState): The complete state to get + the workload states from. + timeout (float): The maximum time to wait for the response, + in seconds. Returns: - WorkloadStateCollection: The collection of workload states on the specified agent. + WorkloadStateCollection: The collection of workload states on the + specified agent. """ if state is None: state = self.get_state(timeout, ["workloadStates." + agent_name]) @@ -546,21 +596,26 @@ def get_workload_states_on_agent(self, agent_name: str, def get_workload_states_on_workload_name(self, workload_name: str, state: CompleteState = None, - timeout: int = DEFAULT_TIMEOUT + timeout: float = DEFAULT_TIMEOUT ) -> WorkloadStateCollection: """ - Get the workload states on a specific workload name from the requested complete state. + Get the workload states on a specific workload name from the requested + complete state. If a state is not provided, it will be requested. Args: workload_name (str): The name of the workload. - state (CompleteState): The complete state to get the workload states from. - timeout (int): The maximum time to wait for the response, in seconds. + state (CompleteState): The complete state to get + the workload states from. + timeout (float): The maximum time to wait for the response, + in seconds. Returns: - WorkloadStateCollection: The collection of workload states on the specified - workload name. + WorkloadStateCollection: The collection of workload states on the + specified workload name. """ if state is None: - state = self.get_state(timeout, ["workloadStates." + workload_name]) + state = self.get_state( + timeout, ["workloadStates." + workload_name] + ) return state.get_workload_states() if state is not None else None diff --git a/run_tests.py b/run_tests.py index 3da7207..635c3e0 100644 --- a/run_tests.py +++ b/run_tests.py @@ -13,7 +13,7 @@ # SPDX-License-Identifier: Apache-2.0 """ -Run tests for AnkaiosSDK Python package. +Run tests for ankaios_sdk Python package. This script runs unit tests, coverage and pylint and saves the results in the reports directory. @@ -23,45 +23,51 @@ """ import os +import re import pytest import argparse import subprocess -PROJECT_NAME = "AnkaiosSDK" +PROJECT_NAME = "ankaios_sdk" REPORT_DIR = "reports" COVERAGE_DIR = os.path.join(REPORT_DIR, "coverage") UTEST_DIR = os.path.join(REPORT_DIR, "utest") PYLINT_DIR = os.path.join(REPORT_DIR, "pylint") +CODESTYLE_DIR = os.path.join(REPORT_DIR, "codestyle") def run_pytest_utest(args): os.makedirs(UTEST_DIR, exist_ok=True) - pytest.main([ + result = pytest.main([ '--junitxml={}'.format(os.path.join(UTEST_DIR, 'utest_report.xml')), 'tests', # '-p', 'no:warnings', '-vv' ] + args) + exit(result) def run_pytest_cov(args): os.makedirs(COVERAGE_DIR, exist_ok=True) - pytest.main([ + result = pytest.main([ '--cov={}'.format(PROJECT_NAME), '--cov-report=html:{}'.format(os.path.join(COVERAGE_DIR, 'html')), '--cov-report=xml:{}'.format(os.path.join(COVERAGE_DIR, 'cov_report.xml')), '--cov-report=term', + '--cov-fail-under=100', 'tests', '-p', 'no:warnings', '-vv' ] + args) + exit(result) def run_pylint(args): os.makedirs(PYLINT_DIR, exist_ok=True) result = subprocess.run([ - 'pylint', PROJECT_NAME, 'tests', '--rcfile=.pylintrc', '--output-format=parseable' + 'pylint', PROJECT_NAME, 'tests', '--rcfile=.pylintrc', + '--output-format=parseable' ] + args, capture_output=True, text=True) pylint_output = result.stdout @@ -73,11 +79,36 @@ def run_pylint(args): rating_line = line break + rating = 0.0 if rating_line: print(rating_line) + + rating_re = re.search(r'rated at (\d+\.\d+)/10', rating_line) + if rating_re: + rating = float(rating_re.group(1)) with open(os.path.join(PYLINT_DIR, 'pylint_report.txt'), 'w') as f: f.write('\n'.join(output_lines)) + if rating < 10.0: + exit(1) + + +def run_pycodestyle(args): + os.makedirs(CODESTYLE_DIR, exist_ok=True) + result = subprocess.run([ + 'pycodestyle', PROJECT_NAME, 'tests', + '--exclude=*_pb2.py,*_pb2_grpc.py,' # Exclude generated files + ] + args, capture_output=True, text=True) + + output_lines = result.stdout.split('\n') + if output_lines[-1] == '': + output_lines.pop() + print(f"PEP8 report: {len(output_lines)} violations found.") + + with open(os.path.join(CODESTYLE_DIR, 'codestyle_report.txt'), 'w') as f: + f.write('\n'.join(output_lines)) + if len(output_lines) > 0: + exit(1) if __name__ == "__main__": @@ -86,17 +117,24 @@ def run_pylint(args): parser.add_argument('-c', '--cov', action='store_true', help='Run coverage') parser.add_argument('-u', '--utest', action='store_true', help='Run unit tests') parser.add_argument('-l', '--lint', action='store_true', help='Run pylint') - parser.add_argument('-a', '--all', action='store_true', help='Run all tests') + parser.add_argument('-p', '--pep8', action='store_true', help='Run pep8 codestyle check') args, extra_args = parser.parse_known_args() - if not any([args.cov, args.utest, args.lint, args.all]): + if not any([args.cov, args.utest, args.lint, args.pep8]): parser.print_help() exit(0) + if sum([args.cov, args.utest, args.lint, args.pep8]) > 1: + print("Please select only one test type.") + parser.print_help() + exit(0) + os.makedirs(REPORT_DIR, exist_ok=True) - if args.cov or args.all: + if args.cov: run_pytest_cov(extra_args) - if args.utest or args.all: + elif args.utest: run_pytest_utest(extra_args) - if args.lint or args.all: + elif args.lint: run_pylint(extra_args) + elif args.pep8: + run_pycodestyle(extra_args) diff --git a/setup.py b/setup.py index 341ab5b..0b9ec56 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ import os from setuptools import setup, find_packages -PROJECT_NAME = "AnkaiosSDK" +PROJECT_NAME = "ankaios_sdk" def generate_protos(): @@ -91,6 +91,7 @@ def generate_protos(): 'pytest', 'pytest-cov', 'pylint', + 'pycodestyle', ], }, ) diff --git a/tests/Response/test_response.py b/tests/Response/test_response.py index c51591f..16e664e 100644 --- a/tests/Response/test_response.py +++ b/tests/Response/test_response.py @@ -13,13 +13,13 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the Response class in the AnkaiosSDK. +This module contains unit tests for the Response class in the ankaios_sdk. """ import pytest from google.protobuf.internal.encoder import _VarintBytes -from AnkaiosSDK import Response, CompleteState -from AnkaiosSDK._protos import _ank_base, _control_api +from ankaios_sdk import Response, CompleteState +from ankaios_sdk._protos import _ank_base, _control_api MESSAGE_BUFFER_ERROR = _control_api.FromAnkaios( @@ -55,7 +55,9 @@ ) ) MESSAGE_BUFFER_UPDATE_SUCCESS = MESSAGE_UPDATE_SUCCESS.SerializeToString() -MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH = _VarintBytes(MESSAGE_UPDATE_SUCCESS.ByteSize()) +MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH = _VarintBytes( + MESSAGE_UPDATE_SUCCESS.ByteSize() +) MESSAGE_BUFFER_INVALID_RESPONSE = _control_api.FromAnkaios( response=_ank_base.Response( diff --git a/tests/Response/test_response_event.py b/tests/Response/test_response_event.py index 2dde50d..fef9099 100644 --- a/tests/Response/test_response_event.py +++ b/tests/Response/test_response_event.py @@ -13,11 +13,11 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the ResponseEvent class in the AnkaiosSDK. +This module contains unit tests for the ResponseEvent class in the ankaios_sdk. """ import pytest -from AnkaiosSDK import ResponseEvent, Response +from ankaios_sdk import ResponseEvent, Response from tests.Response.test_response import MESSAGE_BUFFER_ERROR diff --git a/tests/Workload/test_workload.py b/tests/Workload/test_workload.py index a4dcc96..fd6d5cd 100644 --- a/tests/Workload/test_workload.py +++ b/tests/Workload/test_workload.py @@ -13,19 +13,20 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the Workload class in the AnkaiosSDK. +This module contains unit tests for the Workload class in the ankaios_sdk. Fixtures: workload: Returns a Workload instance with some default values. Helper Functions: - generate_workload: Helper function to generate a Workload instance with some default values. + generate_workload: Helper function to generate a Workload instance + with some default values. """ from unittest.mock import patch, mock_open import pytest -from AnkaiosSDK import Workload, WorkloadBuilder -from AnkaiosSDK._protos import _ank_base +from ankaios_sdk import Workload, WorkloadBuilder +from ankaios_sdk._protos import _ank_base def generate_test_workload(workload_name: str = "workload_test") -> Workload: @@ -91,7 +92,9 @@ def test_update_fields(workload): # pylint: disable=redefined-outer-name workload.update_runtime_config("new_config_test") assert workload._workload.runtimeConfig == "new_config_test" - with patch("builtins.open", mock_open(read_data="new_config_test_from_file")): + with patch("builtins.open", mock_open( + read_data="new_config_test_from_file" + )): workload.update_runtime_config_from_file("new_config_test_from_file") assert workload._workload.runtimeConfig == "new_config_test_from_file" @@ -159,7 +162,8 @@ def test_to_proto(workload): # pylint: disable=redefined-outer-name assert proto.runtime == "runtime_test" assert proto.restartPolicy == _ank_base.NEVER assert proto.runtimeConfig == "config_test" - assert proto.dependencies.dependencies == {"workload_test_other": _ank_base.ADD_COND_RUNNING} + assert proto.dependencies.dependencies == {"workload_test_other": + _ank_base.ADD_COND_RUNNING} assert proto.tags == _ank_base.Tags(tags=[ _ank_base.Tag(key="key1", value="value1"), _ank_base.Tag(key="key2", value="value2") @@ -213,7 +217,8 @@ def test_from_dict(workload): # pylint: disable=redefined-outer-name "desiredState.workloads.workload_test.restartPolicy"), ("update_runtime_config", {"config": "config_test"}, "desiredState.workloads.workload_test.runtimeConfig"), - ("add_dependency", {"workload_name": "workload_test_other", "condition": "RUNNING"}, + ("add_dependency", {"workload_name": "workload_test_other", + "condition": "RUNNING"}, "desiredState.workloads.workload_test.dependencies"), ("add_tag", {"key": "key1", "value": "value1"}, "desiredState.workloads.workload_test.tags"), @@ -223,7 +228,8 @@ def test_mask_generation(function_name, data, mask): Test the generation of masks when updating fields of the Workload instance. Args: - function_name (str): The name of the function to call on the Workload instance. + function_name (str): The name of the function to call on + the Workload instance. data (dict): The data to pass to the function. mask (str): The expected mask to be generated. """ diff --git a/tests/Workload/test_workload_builder.py b/tests/Workload/test_workload_builder.py index db3a0ee..aef6731 100644 --- a/tests/Workload/test_workload_builder.py +++ b/tests/Workload/test_workload_builder.py @@ -13,7 +13,8 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the WorkloadBuilder class in the AnkaiosSDK. +This module contains unit tests for the WorkloadBuilder +class in the ankaios_sdk. Fixtures: builder: Returns a WorkloadBuilder instance. @@ -21,7 +22,7 @@ from unittest.mock import patch, mock_open import pytest -from AnkaiosSDK import Workload, WorkloadBuilder +from ankaios_sdk import Workload, WorkloadBuilder @pytest.fixture @@ -52,7 +53,9 @@ def test_workload_fields(builder): # pylint: disable=redefined-outer-name assert builder.wl_runtime_config == "config_test" with patch("builtins.open", mock_open(read_data="config_test_from_file")): - assert builder.runtime_config_from_file("config_test_from_file") == builder + assert builder.runtime_config_from_file( + "config_test_from_file" + ) == builder assert builder.wl_runtime_config == "config_test_from_file" assert builder.restart_policy("NEVER") == builder @@ -72,7 +75,8 @@ def test_add_dependency(builder): # pylint: disable=redefined-outer-name assert builder.dependencies == {"workload_test": "RUNNING"} assert builder.add_dependency("workload_test_other", "RUNNING") == builder - assert builder.dependencies == {"workload_test": "RUNNING", "workload_test_other": "RUNNING"} + assert builder.dependencies == {"workload_test": "RUNNING", + "workload_test_other": "RUNNING"} def test_add_tag(builder): # pylint: disable=redefined-outer-name @@ -98,23 +102,31 @@ def test_build(builder): # pylint: disable=redefined-outer-name Args: builder (WorkloadBuilder): The WorkloadBuilder fixture. """ - with pytest.raises(ValueError, match= - "Workload can not be built without a name."): + with pytest.raises( + ValueError, + match="Workload can not be built without a name." + ): builder.build() builder = builder.workload_name("workload_test") - with pytest.raises(ValueError, match= - "Workload can not be built without an agent name."): + with pytest.raises( + ValueError, + match="Workload can not be built without an agent name." + ): builder.build() builder = builder.agent_name("agent_Test") - with pytest.raises(ValueError, match= - "Workload can not be built without a runtime."): + with pytest.raises( + ValueError, + match="Workload can not be built without a runtime." + ): builder.build() builder = builder.runtime("runtime_test") - with pytest.raises(ValueError, match= - "Workload can not be built without a runtime configuration."): + with pytest.raises( + ValueError, + match="Workload can not be built without a runtime configuration." + ): builder.build() workload = builder.runtime_config("config_test") \ diff --git a/tests/WorkloadState/test_workload_execution_state.py b/tests/WorkloadState/test_workload_execution_state.py index 8bb0f39..0470d51 100644 --- a/tests/WorkloadState/test_workload_execution_state.py +++ b/tests/WorkloadState/test_workload_execution_state.py @@ -13,17 +13,20 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the WorkloadExecutionState class in the AnkaiosSDK. +This module contains unit tests for the WorkloadExecutionState +class in the ankaios_sdk. """ import pytest -from AnkaiosSDK import WorkloadExecutionState, WorkloadStateEnum, WorkloadSubStateEnum -from AnkaiosSDK._protos import _ank_base +from ankaios_sdk import WorkloadExecutionState, WorkloadStateEnum, \ + WorkloadSubStateEnum +from ankaios_sdk._protos import _ank_base def test_interpret_state(): """ - Test the interpretation of a valid execution state in the WorkloadExecutionState class. + Test the interpretation of a valid execution state in the + WorkloadExecutionState class. """ workload_state = WorkloadExecutionState( _ank_base.ExecutionState( @@ -33,14 +36,15 @@ def test_interpret_state(): ) assert workload_state.state == WorkloadStateEnum.Pending - assert workload_state.substate == WorkloadSubStateEnum.PENDING_WAITING_TO_START + assert workload_state.substate == \ + WorkloadSubStateEnum.PENDING_WAITING_TO_START assert workload_state.info == "Dummy information" def test_interpret_state_error(): """ - Test the handling of an invalid execution state in the WorkloadExecutionState class, - ensuring it raises a ValueError. + Test the handling of an invalid execution state in the + WorkloadExecutionState class, ensuring it raises a ValueError. """ with pytest.raises(ValueError, match="Invalid state for workload."): WorkloadExecutionState( diff --git a/tests/WorkloadState/test_workload_instance_name.py b/tests/WorkloadState/test_workload_instance_name.py index ade867c..3b540d1 100644 --- a/tests/WorkloadState/test_workload_instance_name.py +++ b/tests/WorkloadState/test_workload_instance_name.py @@ -13,10 +13,11 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the WorkloadInstanceName class in the AnkaiosSDK. +This module contains unit tests for the WorkloadInstanceName +class in the ankaios_sdk. """ -from AnkaiosSDK import WorkloadInstanceName +from ankaios_sdk import WorkloadInstanceName def test_creation(): diff --git a/tests/WorkloadState/test_workload_state.py b/tests/WorkloadState/test_workload_state.py index 5035df2..3760af9 100644 --- a/tests/WorkloadState/test_workload_state.py +++ b/tests/WorkloadState/test_workload_state.py @@ -13,11 +13,12 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the WorkloadState class in the AnkaiosSDK. +This module contains unit tests for the WorkloadState +class in the ankaios_sdk. """ -from AnkaiosSDK import WorkloadState -from AnkaiosSDK._protos import _ank_base +from ankaios_sdk import WorkloadState +from ankaios_sdk._protos import _ank_base def test_creation(): diff --git a/tests/WorkloadState/test_workload_state_collection.py b/tests/WorkloadState/test_workload_state_collection.py index 4a839e5..0230a42 100644 --- a/tests/WorkloadState/test_workload_state_collection.py +++ b/tests/WorkloadState/test_workload_state_collection.py @@ -13,17 +13,20 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the WorkloadStateCollection class in the AnkaiosSDK. +This module contains unit tests for the WorkloadStateCollection +class in the ankaios_sdk. """ -from AnkaiosSDK import WorkloadStateCollection, WorkloadState, WorkloadExecutionState -from AnkaiosSDK._protos import _ank_base +from ankaios_sdk import WorkloadStateCollection, WorkloadState, \ + WorkloadExecutionState +from ankaios_sdk._protos import _ank_base def test_get(): """ - Test the basic functionality of the WorkloadStateCollection class, - including adding a workload state and retrieving it as a list and dictionary. + Test the basic functionality of the WorkloadStateCollection + class, including adding a workload state and retrieving it + as a list and dictionary. """ workload_state_collection = WorkloadStateCollection() assert workload_state_collection is not None @@ -54,8 +57,10 @@ def test_get(): assert "workload_Test" in workload_states_dict["agent_Test"].keys() assert len(workload_states_dict["agent_Test"]["workload_Test"]) == 1 assert "1234" in workload_states_dict["agent_Test"]["workload_Test"].keys() - assert isinstance(workload_states_dict["agent_Test"]["workload_Test"]["1234"], - WorkloadExecutionState) + assert isinstance( + workload_states_dict["agent_Test"]["workload_Test"]["1234"], + WorkloadExecutionState + ) def test_from_proto(): diff --git a/tests/WorkloadState/test_workload_state_enum.py b/tests/WorkloadState/test_workload_state_enum.py index 33cf12b..1ffcfa2 100644 --- a/tests/WorkloadState/test_workload_state_enum.py +++ b/tests/WorkloadState/test_workload_state_enum.py @@ -13,16 +13,18 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the WorkloadExecutionState class in the AnkaiosSDK. +This module contains unit tests for the WorkloadExecutionState +class in the ankaios_sdk. """ -from AnkaiosSDK import WorkloadStateEnum +from ankaios_sdk import WorkloadStateEnum def test_get(): """ Test the get method of the WorkloadStateEnum class, - ensuring it correctly retrieves the enumeration member and its string representation. + ensuring it correctly retrieves the enumeration member + and its string representation. """ field = "agentDisconnected" workload_state = WorkloadStateEnum._get(field) diff --git a/tests/WorkloadState/test_workload_substate_enum.py b/tests/WorkloadState/test_workload_substate_enum.py index b06e7af..6cce481 100644 --- a/tests/WorkloadState/test_workload_substate_enum.py +++ b/tests/WorkloadState/test_workload_substate_enum.py @@ -13,18 +13,20 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the WorkloadSubStateEnum class in the AnkaiosSDK. +This module contains unit tests for the WorkloadSubStateEnum +class in the ankaios_sdk. """ import pytest -from AnkaiosSDK import WorkloadSubStateEnum, WorkloadStateEnum -from AnkaiosSDK._protos import _ank_base +from ankaios_sdk import WorkloadSubStateEnum, WorkloadStateEnum +from ankaios_sdk._protos import _ank_base def test_get(): """ Test the get method of the WorkloadSubStateEnum class, - ensuring it correctly retrieves the enumeration member based on the state and field. + ensuring it correctly retrieves the enumeration member + based on the state and field. """ data = [ (WorkloadStateEnum.AgentDisconnected, _ank_base.AGENT_DISCONNECTED, diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 1dd0fc7..d8d1c10 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -13,18 +13,20 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the Ankaios class in the AnkaiosSDK. +This module contains unit tests for the Ankaios class in the ankaios_sdk. """ from io import StringIO import logging from unittest.mock import patch, mock_open, MagicMock import pytest -from AnkaiosSDK import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, Manifest, CompleteState +from ankaios_sdk import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, \ + Manifest, CompleteState from tests.Workload.test_workload import generate_test_workload from tests.test_request import generate_test_request -from tests.Response.test_response import MESSAGE_BUFFER_ERROR, MESSAGE_BUFFER_COMPLETE_STATE, \ - MESSAGE_BUFFER_UPDATE_SUCCESS, MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH +from tests.Response.test_response import MESSAGE_BUFFER_ERROR, \ + MESSAGE_BUFFER_COMPLETE_STATE, MESSAGE_BUFFER_UPDATE_SUCCESS, \ + MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH from tests.test_manifest import MANIFEST_DICT @@ -59,7 +61,9 @@ def test_connection(): MockThread.return_value = mock_thread_instance ankaios.connect() - MockThread.assert_called_once_with(target=ankaios._read_from_control_interface) + MockThread.assert_called_once_with( + target=ankaios._read_from_control_interface + ) mock_thread_instance.start.assert_called_once() assert ankaios._connected @@ -82,12 +86,13 @@ def test_read_from_control_interface(): Test the _read_from_control_interface method of the Ankaios class. """ input_file_content = MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH + \ - MESSAGE_BUFFER_UPDATE_SUCCESS + MESSAGE_BUFFER_UPDATE_SUCCESS # Test response comes first with patch("builtins.open", mock_open()) as mock_file: mock_file_handle = mock_file.return_value.__enter__.return_value - mock_file_handle.read.side_effect = [bytes([b]) for b in input_file_content] + mock_file_handle.read.side_effect = \ + [bytes([b]) for b in input_file_content] ankaios = Ankaios() @@ -105,7 +110,8 @@ def test_read_from_control_interface(): # Test request set first with patch("builtins.open", mock_open()) as mock_file: mock_file_handle = mock_file.return_value.__enter__.return_value - mock_file_handle.read.side_effect = [bytes([b]) for b in input_file_content] + mock_file_handle.read.side_effect = \ + [bytes([b]) for b in input_file_content] ankaios = Ankaios() ankaios._responses["1234"] = ResponseEvent() @@ -127,12 +133,15 @@ def test_get_reponse_by_id(): Test the get_response_by_id method of the Ankaios class. """ ankaios = Ankaios() - with pytest.raises(ValueError, match="Reading from the control interface is not started."): + with pytest.raises( + ValueError, + match="Reading from the control interface is not started." + ): ankaios._get_response_by_id("1234") ankaios._connected = True assert not ankaios._responses - with patch("AnkaiosSDK.ResponseEvent.wait_for_response") as mock_wait: + with patch("ankaios_sdk.ResponseEvent.wait_for_response") as mock_wait: ankaios._get_response_by_id("1234") mock_wait.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) assert list(ankaios._responses.keys()) == ["1234"] @@ -168,14 +177,18 @@ def test_send_request(): ankaios._connected = True request = generate_test_request() - with patch("AnkaiosSDK.Ankaios._write_to_pipe") as mock_write, \ - patch("AnkaiosSDK.Ankaios._get_response_by_id") as mock_get_response: + with patch("ankaios_sdk.Ankaios._write_to_pipe") as mock_write, \ + patch("ankaios_sdk.Ankaios._get_response_by_id") \ + as mock_get_response: ankaios._send_request(request) mock_write.assert_called_once_with(request) - mock_get_response.assert_called_once_with(request.get_id(), Ankaios.DEFAULT_TIMEOUT) + mock_get_response.assert_called_once_with( + request.get_id(), Ankaios.DEFAULT_TIMEOUT + ) - with patch("AnkaiosSDK.Ankaios._write_to_pipe") as mock_write, \ - patch("AnkaiosSDK.Ankaios._get_response_by_id") as mock_get_response: + with patch("ankaios_sdk.Ankaios._write_to_pipe") as mock_write, \ + patch("ankaios_sdk.Ankaios._get_response_by_id") \ + as mock_get_response: mock_get_response.side_effect = TimeoutError() with pytest.raises(TimeoutError): ankaios._send_request(request) @@ -191,21 +204,22 @@ def test_apply_manifest(): manifest = Manifest(MANIFEST_DICT) # Test success - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: - mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() @@ -221,21 +235,22 @@ def test_delete_manifest(): manifest = Manifest(MANIFEST_DICT) # Test success - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: - mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() @@ -251,21 +266,22 @@ def test_run_workload(): workload = generate_test_workload() # Test success - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: - mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) ankaios.run_workload(workload) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) ankaios.run_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() ankaios.run_workload(workload) mock_send_request.assert_called_once() @@ -280,21 +296,22 @@ def test_delete_workload(): ankaios.logger = MagicMock() # Test success - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: - mock_send_request.return_value = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) ankaios.delete_workload("nginx") mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) ankaios.delete_workload("nginx") mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() ankaios.delete_workload("nginx") mock_send_request.assert_called_once() @@ -307,20 +324,25 @@ def test_get_workload(): """ ankaios = Ankaios() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload") as mock_state_get_workload: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload") \ + as mock_state_get_workload: mock_get_state.return_value = CompleteState() ankaios.get_workload("nginx") - mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT, - ["desiredState.workloads.nginx"]) + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, + ["desiredState.workloads.nginx"] + ) mock_state_get_workload.assert_called_once_with("nginx") - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload") as mock_state_get_workload: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload") \ + as mock_state_get_workload: ankaios.get_workload("nginx", state=CompleteState()) mock_get_state.assert_not_called() mock_state_get_workload.assert_called_once_with("nginx") + def test_set_config(): """ Test the set config methods of the Ankaios class. @@ -328,13 +350,16 @@ def test_set_config(): ankaios = Ankaios() with patch("builtins.open", mock_open()) as mock_file, \ - patch("AnkaiosSDK.Ankaios.set_config") as mock_set_config: + patch("ankaios_sdk.Ankaios.set_config") as mock_set_config: mock_file().read.return_value = {'config_test': 'value'} - ankaios.set_config_from_file(name="config_test", config_path=r"path/to/config") + ankaios.set_config_from_file(name="config_test", + config_path=r"path/to/config") mock_file.assert_called_with(r"path/to/config", "r", encoding="utf-8") mock_file().read.assert_called_once() - mock_set_config.assert_called_once_with("config_test", {'config_test': 'value'}) + mock_set_config.assert_called_once_with( + "config_test", {'config_test': 'value'} + ) with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.set_config(name="config_test", config={'config_test': 'value'}) @@ -368,14 +393,15 @@ def test_get_state(): ankaios.logger = MagicMock() # Test success - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: - mock_send_request.return_value = Response(MESSAGE_BUFFER_COMPLETE_STATE) + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) result = ankaios.get_state() mock_send_request.assert_called_once() assert isinstance(result, CompleteState) # Test error - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) result = ankaios.get_state(field_mask=["invalid_mask"]) mock_send_request.assert_called_once() @@ -383,7 +409,7 @@ def test_get_state(): ankaios.logger.error.assert_called() # Test timeout - with patch("AnkaiosSDK.Ankaios._send_request") as mock_send_request: + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() result = ankaios.get_state() mock_send_request.assert_called_once() @@ -397,15 +423,17 @@ def test_get_agents(): """ ankaios = Ankaios() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_agents") as mock_state_get_agents: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_agents") \ + as mock_state_get_agents: mock_get_state.return_value = CompleteState() ankaios.get_agents() mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) mock_state_get_agents.assert_called_once() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_agents") as mock_state_get_agents: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_agents") \ + as mock_state_get_agents: ankaios.get_agents(state=CompleteState()) mock_get_state.assert_not_called() mock_state_get_agents.assert_called_once() @@ -417,15 +445,17 @@ def test_get_workload_states(): """ ankaios = Ankaios() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: mock_get_state.return_value = CompleteState() ankaios.get_workload_states() mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) mock_state_get_workload_states.assert_called_once() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: ankaios.get_workload_states(state=CompleteState()) mock_get_state.assert_not_called() mock_state_get_workload_states.assert_called_once() @@ -437,15 +467,19 @@ def test_get_workload_states_on_agent(): """ ankaios = Ankaios() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: mock_get_state.return_value = CompleteState() ankaios.get_workload_states_on_agent("agent_A") - mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT, ["workloadStates.agent_A"]) + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, ["workloadStates.agent_A"] + ) mock_state_get_workload_states.assert_called_once() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: ankaios.get_workload_states_on_agent("agent_A", state=CompleteState()) mock_get_state.assert_not_called() mock_state_get_workload_states.assert_called_once() @@ -457,15 +491,21 @@ def test_get_workload_states_on_workload_name(): """ ankaios = Ankaios() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: mock_get_state.return_value = CompleteState() ankaios.get_workload_states_on_workload_name("nginx") - mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT, ["workloadStates.nginx"]) + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, ["workloadStates.nginx"] + ) mock_state_get_workload_states.assert_called_once() - with patch("AnkaiosSDK.Ankaios.get_state") as mock_get_state, \ - patch("AnkaiosSDK.CompleteState.get_workload_states") as mock_state_get_workload_states: - ankaios.get_workload_states_on_workload_name("nginx", state=CompleteState()) + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: + ankaios.get_workload_states_on_workload_name( + "nginx", state=CompleteState() + ) mock_get_state.assert_not_called() mock_state_get_workload_states.assert_called_once() diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index 1fbafad..db86bc3 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -13,11 +13,11 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the Manifest class in the AnkaiosSDK. +This module contains unit tests for the Manifest class in the ankaios_sdk. """ -from AnkaiosSDK import CompleteState, WorkloadStateCollection -from AnkaiosSDK._protos import _ank_base +from ankaios_sdk import CompleteState, WorkloadStateCollection +from ankaios_sdk._protos import _ank_base from tests.Workload.test_workload import generate_test_workload @@ -34,7 +34,8 @@ def test_general_functionality(): def test_workload_functionality(): """ - Test the functionality of CompleteState class regarding setting and getting workloads. + Test the functionality of CompleteState class + regarding setting and getting workloads. """ complete_state = CompleteState() assert len(complete_state.get_workloads()) == 0 @@ -49,7 +50,8 @@ def test_workload_functionality(): def test_workload_states(): """ - Test the functionality of CompleteState class regarding setting and getting workload states. + Test the functionality of CompleteState class regarding + setting and getting workload states. """ complete_state = CompleteState() complete_state._from_proto(_ank_base.CompleteState( diff --git a/tests/test_manifest.py b/tests/test_manifest.py index aa9b808..9234d2f 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -13,12 +13,12 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the Manifest class in the AnkaiosSDK. +This module contains unit tests for the Manifest class in the ankaios_sdk. """ from unittest.mock import patch, mock_open import pytest -from AnkaiosSDK import Manifest, CompleteState +from ankaios_sdk import Manifest, CompleteState MANIFEST_CONTENT = """apiVersion: v0.1 @@ -28,17 +28,16 @@ restartPolicy: NEVER agent: agent_A runtimeConfig: | - image: ghcr.io/eclipse-ankaios/tests/nginx:alpine-slim - commandOptions: ["-p", "8081:80"]""" + image: image/test""" MANIFEST_DICT = { - 'apiVersion': 'v0.1', + 'apiVersion': 'v0.1', 'workloads': { 'nginx_test': { - 'runtime': 'podman', - 'restartPolicy': 'NEVER', - 'agent': 'agent_A', - 'runtimeConfig': 'image: ghcr.io/eclipse-ankaios/tests/nginx:alpine-slim\ncommandOptions: ["-p", "8081:80"]' # pylint: disable=line-too-long + 'runtime': 'podman', + 'restartPolicy': 'NEVER', + 'agent': 'agent_A', + 'runtimeConfig': 'image: image/test' } } } @@ -50,7 +49,7 @@ def test_from_file(): ensuring it correctly loads a manifest from a file and handles errors. """ with patch("builtins.open", mock_open(read_data=MANIFEST_CONTENT)), \ - patch("AnkaiosSDK.Manifest.from_string") as mock_from_string: + patch("ankaios_sdk.Manifest.from_string") as mock_from_string: _ = Manifest.from_file("manifest.yaml") mock_from_string.assert_called_once_with(MANIFEST_CONTENT) @@ -61,9 +60,10 @@ def test_from_file(): def test_from_string(): """ Test the from_string method of the Manifest class, - ensuring it correctly parses a manifest from a YAML string and handles errors. + ensuring it correctly parses a manifest from a YAML + string and handles errors. """ - with patch("AnkaiosSDK.Manifest.from_dict") as mock_from_dict: + with patch("ankaios_sdk.Manifest.from_dict") as mock_from_dict: _ = Manifest.from_string(MANIFEST_CONTENT) mock_from_dict.assert_called_once_with(MANIFEST_DICT) @@ -74,7 +74,8 @@ def test_from_string(): def test_from_dict(): """ Test the from_dict method of the Manifest class, - ensuring it correctly creates a Manifest instance from a dictionary and handles errors. + ensuring it correctly creates a Manifest instance + from a dictionary and handles errors. """ manifest = Manifest.from_dict(MANIFEST_DICT) assert manifest._manifest == MANIFEST_DICT @@ -109,15 +110,15 @@ def test_calculate_masks(): """ manifest_dict = MANIFEST_DICT.copy() manifest_dict["workloads"]["nginx_test_other"] = { - 'runtime': 'podman', - 'restartPolicy': 'NEVER', - 'agent': 'agent_B', - 'runtimeConfig': 'image: ghcr.io/eclipse-ankaios/tests/nginx:alpine-slim\ncommandOptions: ["-p", "8082:80"]' # pylint: disable=line-too-long + 'runtime': 'podman', + 'restartPolicy': 'NEVER', + 'agent': 'agent_B', + 'runtimeConfig': 'image: image/test' } manifest = Manifest(manifest_dict) assert len(manifest._calculate_masks()) == 2 assert manifest._calculate_masks() == [ - "desiredState.workloads.nginx_test", + "desiredState.workloads.nginx_test", "desiredState.workloads.nginx_test_other" ] @@ -126,7 +127,7 @@ def test_generate_complete_state(): """ Test the CompleteState instance generation from a Manifest instance. """ - with patch("AnkaiosSDK.CompleteState._from_dict") as mock_complete_state: + with patch("ankaios_sdk.CompleteState._from_dict") as mock_complete_state: manifest = Manifest(MANIFEST_DICT) complete_state = manifest.generate_complete_state() mock_complete_state.assert_called_once_with(manifest._manifest) diff --git a/tests/test_request.py b/tests/test_request.py index aa4ebf2..4d60d97 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -13,11 +13,11 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains unit tests for the Request class in the AnkaiosSDK. +This module contains unit tests for the Request class in the ankaios_sdk. """ import pytest -from AnkaiosSDK import Request, CompleteState +from ankaios_sdk import Request, CompleteState from tests.Workload.test_workload import generate_test_workload @@ -56,15 +56,19 @@ def test_update_state(): request = Request("update_state") complete_state = CompleteState() request.set_complete_state(complete_state) - assert request._request.updateStateRequest.newState == complete_state._to_proto() + assert request._request.updateStateRequest.newState == \ + complete_state._to_proto() request.add_mask("test_mask") assert request._request.updateStateRequest.updateMask == ["test_mask"] - with pytest.raises(ValueError, - match="Complete state can only be set for an update state request."): + with pytest.raises( + ValueError, + match="Complete state can only be set for an update state request." + ): Request("get_state").set_complete_state(CompleteState()) + def test_get_state(): """ Test the get state request type. From 303fa6840d37a620d25a6c13f0ebc9ddacef515f Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 26 Sep 2024 13:37:55 +0300 Subject: [PATCH 15/72] Fix CI verification --- .github/workflows/ci.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5867b1b..f2c0843 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,19 +11,17 @@ on: jobs: setup: runs-on: ubuntu-latest + services: + python: + image: python:3.10 steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install dependencies run: | - python3 -m pip install --upgrade pip pip install .[dev] + - name: Save cache uses: actions/cache@v3 with: @@ -110,4 +108,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: codestyle-report - path: reports/codestyle \ No newline at end of file + path: reports/codestyle From 9e50953185aebbb7e305d83c65eee812b1ec4e68 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 26 Sep 2024 13:51:48 +0300 Subject: [PATCH 16/72] Fix CI verification --- .github/workflows/ci.yml | 120 +++++++++++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2c0843..ed8d952 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,39 +11,60 @@ on: jobs: setup: runs-on: ubuntu-latest - services: - python: - image: python:3.10 + outputs: + cache-key: ${{ steps.cache-deps.outputs.cache-hit }} steps: - name: Checkout code uses: actions/checkout@v4 - - name: Install dependencies - run: | - pip install .[dev] + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' - - name: Save cache + - name: Cache dependencies + id: cache-deps uses: actions/cache@v3 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + path: | + ~/.cache/pip + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} restore-keys: | - ${{ runner.os }}-pip- + python-deps-${{ runner.os }}- + + - name: Install dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: | + python3 -m pip install --upgrade pip + pip install .[dev] unit_test: - runs-on: ubuntu-latest needs: setup + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Restore cache uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + - name: Run unit tests run: python3 run_tests.py --utest + continue-on-error: true + - name: Upload unit test report uses: actions/upload-artifact@v4 with: @@ -51,19 +72,32 @@ jobs: path: reports/utest coverage: - runs-on: ubuntu-latest needs: setup + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Restore cache uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + - name: Run coverage run: python3 run_tests.py --cov + continue-on-error: true + - name: Upload coverage report uses: actions/upload-artifact@v4 with: @@ -71,19 +105,32 @@ jobs: path: reports/coverage lint: - runs-on: ubuntu-latest needs: setup + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Restore cache uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + - name: Run lint run: python3 run_tests.py --lint + continue-on-error: true + - name: Upload lint report uses: actions/upload-artifact@v4 with: @@ -91,19 +138,32 @@ jobs: path: reports/pylint codestyle: - runs-on: ubuntu-latest needs: setup + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Restore cache uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + - name: Run pep8 codestyle check run: python3 run_tests.py --pep8 + continue-on-error: true + - name: Upload codestyle report uses: actions/upload-artifact@v4 with: From 79288ec91729b408a8e055cf2a1857a93a9aa8e7 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 26 Sep 2024 14:00:28 +0300 Subject: [PATCH 17/72] Fix CI verification cache version --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed8d952..7319b43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Cache dependencies id: cache-deps - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.cache/pip @@ -51,7 +51,7 @@ jobs: python-version: '3.10' - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} @@ -84,7 +84,7 @@ jobs: python-version: '3.10' - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} @@ -117,7 +117,7 @@ jobs: python-version: '3.10' - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} @@ -150,7 +150,7 @@ jobs: python-version: '3.10' - name: Restore cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} From 4603e86f39f47578a7f58398635c9992e95fc63e Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 26 Sep 2024 14:25:56 +0300 Subject: [PATCH 18/72] Fix test names and pylint --- .pylintrc | 2 - ankaios_sdk/_components/workload_state.py | 39 ++++++++++--------- tests/{Response => response}/__init__.py | 0 tests/{Response => response}/test_response.py | 0 .../test_response_event.py | 2 +- tests/test_ankaios.py | 10 ++--- tests/test_complete_state.py | 2 +- tests/test_request.py | 2 +- tests/{Workload => workload}/__init__.py | 0 tests/{Workload => workload}/test_workload.py | 0 .../test_workload_builder.py | 0 .../__init__.py | 0 .../test_workload_execution_state.py | 2 +- .../test_workload_instance_name.py | 0 .../test_workload_state.py | 0 .../test_workload_state_collection.py | 0 .../test_workload_state_enum.py | 11 ++++-- .../test_workload_substate_enum.py | 34 ++++++++-------- 18 files changed, 54 insertions(+), 50 deletions(-) rename tests/{Response => response}/__init__.py (100%) rename tests/{Response => response}/test_response.py (100%) rename tests/{Response => response}/test_response_event.py (95%) rename tests/{Workload => workload}/__init__.py (100%) rename tests/{Workload => workload}/test_workload.py (100%) rename tests/{Workload => workload}/test_workload_builder.py (100%) rename tests/{WorkloadState => workload_state}/__init__.py (100%) rename tests/{WorkloadState => workload_state}/test_workload_execution_state.py (96%) rename tests/{WorkloadState => workload_state}/test_workload_instance_name.py (100%) rename tests/{WorkloadState => workload_state}/test_workload_state.py (100%) rename tests/{WorkloadState => workload_state}/test_workload_state_collection.py (100%) rename tests/{WorkloadState => workload_state}/test_workload_state_enum.py (69%) rename tests/{WorkloadState => workload_state}/test_workload_substate_enum.py (70%) diff --git a/.pylintrc b/.pylintrc index 912ded2..6673730 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,5 @@ ignore-patterns= disable= # classes from protos are not recognized by pylint E1101, # no-member - # module names have the same name with the classes within. They cannot be snake_case - C0103, # invalid-name # Protected members are accessed throughout the project to be able to hide that functionality from the user. W0212, # protected-access diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index 33ae4ef..466a2e7 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -68,14 +68,14 @@ class WorkloadStateEnum(Enum): NotScheduled (int): The workload is not scheduled. Removed (int): The workload has been removed. """ - AgentDisconnected: int = 0 - Pending: int = 1 - Running: int = 2 - Stopping: int = 3 - Succeeded: int = 4 - Failed: int = 5 - NotScheduled: int = 6 - Removed: int = 7 + AGENT_DISCONNECTED: int = 0 + PENDING: int = 1 + RUNNING: int = 2 + STOPPING: int = 3 + SUCCEEDED: int = 4 + FAILED: int = 5 + NOT_SCHEDULED: int = 6 + REMOVED: int = 7 def __str__(self) -> str: """ @@ -102,8 +102,11 @@ def _get(field: str) -> "WorkloadStateEnum": KeyError: If the field name does not correspond to any enumeration member. """ - field = field[0].upper() + field[1:] # Capitalize the first letter - return WorkloadStateEnum[field] + if field == "agentDisconnected": + return WorkloadStateEnum.AGENT_DISCONNECTED + if field == "notScheduled": + return WorkloadStateEnum.NOT_SCHEDULED + return WorkloadStateEnum[field.upper()] class WorkloadSubStateEnum(Enum): @@ -174,12 +177,12 @@ def _get(state: WorkloadStateEnum, any enumeration member. """ proto_mapper = {} - if state == WorkloadStateEnum.AgentDisconnected: + if state == WorkloadStateEnum.AGENT_DISCONNECTED: proto_mapper = { _ank_base.AGENT_DISCONNECTED: WorkloadSubStateEnum.AGENT_DISCONNECTED } - elif state == WorkloadStateEnum.Pending: + elif state == WorkloadStateEnum.PENDING: proto_mapper = { _ank_base.PENDING_INITIAL: WorkloadSubStateEnum.PENDING_INITIAL, @@ -190,11 +193,11 @@ def _get(state: WorkloadStateEnum, _ank_base.PENDING_STARTING_FAILED: WorkloadSubStateEnum.PENDING_STARTING_FAILED } - elif state == WorkloadStateEnum.Running: + elif state == WorkloadStateEnum.RUNNING: proto_mapper = { _ank_base.RUNNING_OK: WorkloadSubStateEnum.RUNNING_OK } - elif state == WorkloadStateEnum.Stopping: + elif state == WorkloadStateEnum.STOPPING: proto_mapper = { _ank_base.STOPPING: WorkloadSubStateEnum.STOPPING, _ank_base.STOPPING_WAITING_TO_STOP: @@ -204,12 +207,12 @@ def _get(state: WorkloadStateEnum, _ank_base.STOPPING_DELETE_FAILED: WorkloadSubStateEnum.STOPPING_DELETE_FAILED } - elif state == WorkloadStateEnum.Succeeded: + elif state == WorkloadStateEnum.SUCCEEDED: proto_mapper = { _ank_base.SUCCEEDED_OK: WorkloadSubStateEnum.SUCCEEDED_OK } - elif state == WorkloadStateEnum.Failed: + elif state == WorkloadStateEnum.FAILED: proto_mapper = { _ank_base.FAILED_EXEC_FAILED: WorkloadSubStateEnum.FAILED_EXEC_FAILED, @@ -218,12 +221,12 @@ def _get(state: WorkloadStateEnum, _ank_base.FAILED_LOST: WorkloadSubStateEnum.FAILED_LOST } - elif state == WorkloadStateEnum.NotScheduled: + elif state == WorkloadStateEnum.NOT_SCHEDULED: proto_mapper = { _ank_base.NOT_SCHEDULED: WorkloadSubStateEnum.NOT_SCHEDULED } - elif state == WorkloadStateEnum.Removed: + elif state == WorkloadStateEnum.REMOVED: proto_mapper = { _ank_base.REMOVED: WorkloadSubStateEnum.REMOVED diff --git a/tests/Response/__init__.py b/tests/response/__init__.py similarity index 100% rename from tests/Response/__init__.py rename to tests/response/__init__.py diff --git a/tests/Response/test_response.py b/tests/response/test_response.py similarity index 100% rename from tests/Response/test_response.py rename to tests/response/test_response.py diff --git a/tests/Response/test_response_event.py b/tests/response/test_response_event.py similarity index 95% rename from tests/Response/test_response_event.py rename to tests/response/test_response_event.py index fef9099..ce40138 100644 --- a/tests/Response/test_response_event.py +++ b/tests/response/test_response_event.py @@ -18,7 +18,7 @@ import pytest from ankaios_sdk import ResponseEvent, Response -from tests.Response.test_response import MESSAGE_BUFFER_ERROR +from tests.response.test_response import MESSAGE_BUFFER_ERROR def test_event(): diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index d8d1c10..025eae6 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -22,9 +22,9 @@ import pytest from ankaios_sdk import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, \ Manifest, CompleteState -from tests.Workload.test_workload import generate_test_workload +from tests.workload.test_workload import generate_test_workload from tests.test_request import generate_test_request -from tests.Response.test_response import MESSAGE_BUFFER_ERROR, \ +from tests.response.test_response import MESSAGE_BUFFER_ERROR, \ MESSAGE_BUFFER_COMPLETE_STATE, MESSAGE_BUFFER_UPDATE_SUCCESS, \ MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH from tests.test_manifest import MANIFEST_DICT @@ -56,12 +56,12 @@ def test_connection(): ankaios = Ankaios() assert not ankaios._connected - with patch("threading.Thread") as MockThread: + with patch("threading.Thread") as mock_thread: mock_thread_instance = MagicMock() - MockThread.return_value = mock_thread_instance + mock_thread.return_value = mock_thread_instance ankaios.connect() - MockThread.assert_called_once_with( + mock_thread.assert_called_once_with( target=ankaios._read_from_control_interface ) mock_thread_instance.start.assert_called_once() diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index db86bc3..4199f3c 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -18,7 +18,7 @@ from ankaios_sdk import CompleteState, WorkloadStateCollection from ankaios_sdk._protos import _ank_base -from tests.Workload.test_workload import generate_test_workload +from tests.workload.test_workload import generate_test_workload def test_general_functionality(): diff --git a/tests/test_request.py b/tests/test_request.py index 4d60d97..3d5220f 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -18,7 +18,7 @@ import pytest from ankaios_sdk import Request, CompleteState -from tests.Workload.test_workload import generate_test_workload +from tests.workload.test_workload import generate_test_workload def generate_test_request(request_type: str = "update_state") -> Request: diff --git a/tests/Workload/__init__.py b/tests/workload/__init__.py similarity index 100% rename from tests/Workload/__init__.py rename to tests/workload/__init__.py diff --git a/tests/Workload/test_workload.py b/tests/workload/test_workload.py similarity index 100% rename from tests/Workload/test_workload.py rename to tests/workload/test_workload.py diff --git a/tests/Workload/test_workload_builder.py b/tests/workload/test_workload_builder.py similarity index 100% rename from tests/Workload/test_workload_builder.py rename to tests/workload/test_workload_builder.py diff --git a/tests/WorkloadState/__init__.py b/tests/workload_state/__init__.py similarity index 100% rename from tests/WorkloadState/__init__.py rename to tests/workload_state/__init__.py diff --git a/tests/WorkloadState/test_workload_execution_state.py b/tests/workload_state/test_workload_execution_state.py similarity index 96% rename from tests/WorkloadState/test_workload_execution_state.py rename to tests/workload_state/test_workload_execution_state.py index 0470d51..5e5a12b 100644 --- a/tests/WorkloadState/test_workload_execution_state.py +++ b/tests/workload_state/test_workload_execution_state.py @@ -35,7 +35,7 @@ def test_interpret_state(): ) ) - assert workload_state.state == WorkloadStateEnum.Pending + assert workload_state.state == WorkloadStateEnum.PENDING assert workload_state.substate == \ WorkloadSubStateEnum.PENDING_WAITING_TO_START assert workload_state.info == "Dummy information" diff --git a/tests/WorkloadState/test_workload_instance_name.py b/tests/workload_state/test_workload_instance_name.py similarity index 100% rename from tests/WorkloadState/test_workload_instance_name.py rename to tests/workload_state/test_workload_instance_name.py diff --git a/tests/WorkloadState/test_workload_state.py b/tests/workload_state/test_workload_state.py similarity index 100% rename from tests/WorkloadState/test_workload_state.py rename to tests/workload_state/test_workload_state.py diff --git a/tests/WorkloadState/test_workload_state_collection.py b/tests/workload_state/test_workload_state_collection.py similarity index 100% rename from tests/WorkloadState/test_workload_state_collection.py rename to tests/workload_state/test_workload_state_collection.py diff --git a/tests/WorkloadState/test_workload_state_enum.py b/tests/workload_state/test_workload_state_enum.py similarity index 69% rename from tests/WorkloadState/test_workload_state_enum.py rename to tests/workload_state/test_workload_state_enum.py index 1ffcfa2..3c591c7 100644 --- a/tests/WorkloadState/test_workload_state_enum.py +++ b/tests/workload_state/test_workload_state_enum.py @@ -26,7 +26,10 @@ def test_get(): ensuring it correctly retrieves the enumeration member and its string representation. """ - field = "agentDisconnected" - workload_state = WorkloadStateEnum._get(field) - assert workload_state == WorkloadStateEnum.AgentDisconnected - assert str(workload_state) == "AgentDisconnected" + workload_state = WorkloadStateEnum._get("agentDisconnected") + assert workload_state == WorkloadStateEnum.AGENT_DISCONNECTED + workload_state = WorkloadStateEnum._get("pending") + assert workload_state == WorkloadStateEnum.PENDING + workload_state = WorkloadStateEnum._get("notScheduled") + assert workload_state == WorkloadStateEnum.NOT_SCHEDULED + assert str(workload_state) == "NOT_SCHEDULED" diff --git a/tests/WorkloadState/test_workload_substate_enum.py b/tests/workload_state/test_workload_substate_enum.py similarity index 70% rename from tests/WorkloadState/test_workload_substate_enum.py rename to tests/workload_state/test_workload_substate_enum.py index 6cce481..01badac 100644 --- a/tests/WorkloadState/test_workload_substate_enum.py +++ b/tests/workload_state/test_workload_substate_enum.py @@ -29,37 +29,37 @@ def test_get(): based on the state and field. """ data = [ - (WorkloadStateEnum.AgentDisconnected, _ank_base.AGENT_DISCONNECTED, + (WorkloadStateEnum.AGENT_DISCONNECTED, _ank_base.AGENT_DISCONNECTED, WorkloadSubStateEnum.AGENT_DISCONNECTED), - (WorkloadStateEnum.Pending, _ank_base.PENDING_INITIAL, + (WorkloadStateEnum.PENDING, _ank_base.PENDING_INITIAL, WorkloadSubStateEnum.PENDING_INITIAL), - (WorkloadStateEnum.Pending, _ank_base.PENDING_WAITING_TO_START, + (WorkloadStateEnum.PENDING, _ank_base.PENDING_WAITING_TO_START, WorkloadSubStateEnum.PENDING_WAITING_TO_START), - (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING, + (WorkloadStateEnum.PENDING, _ank_base.PENDING_STARTING, WorkloadSubStateEnum.PENDING_STARTING), - (WorkloadStateEnum.Pending, _ank_base.PENDING_STARTING_FAILED, + (WorkloadStateEnum.PENDING, _ank_base.PENDING_STARTING_FAILED, WorkloadSubStateEnum.PENDING_STARTING_FAILED), - (WorkloadStateEnum.Running, _ank_base.RUNNING_OK, + (WorkloadStateEnum.RUNNING, _ank_base.RUNNING_OK, WorkloadSubStateEnum.RUNNING_OK), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING, + (WorkloadStateEnum.STOPPING, _ank_base.STOPPING, WorkloadSubStateEnum.STOPPING), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_WAITING_TO_STOP, + (WorkloadStateEnum.STOPPING, _ank_base.STOPPING_WAITING_TO_STOP, WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_REQUESTED_AT_RUNTIME, + (WorkloadStateEnum.STOPPING, _ank_base.STOPPING_REQUESTED_AT_RUNTIME, WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME), - (WorkloadStateEnum.Stopping, _ank_base.STOPPING_DELETE_FAILED, + (WorkloadStateEnum.STOPPING, _ank_base.STOPPING_DELETE_FAILED, WorkloadSubStateEnum.STOPPING_DELETE_FAILED), - (WorkloadStateEnum.Succeeded, _ank_base.SUCCEEDED_OK, + (WorkloadStateEnum.SUCCEEDED, _ank_base.SUCCEEDED_OK, WorkloadSubStateEnum.SUCCEEDED_OK), - (WorkloadStateEnum.Failed, _ank_base.FAILED_EXEC_FAILED, + (WorkloadStateEnum.FAILED, _ank_base.FAILED_EXEC_FAILED, WorkloadSubStateEnum.FAILED_EXEC_FAILED), - (WorkloadStateEnum.Failed, _ank_base.FAILED_UNKNOWN, + (WorkloadStateEnum.FAILED, _ank_base.FAILED_UNKNOWN, WorkloadSubStateEnum.FAILED_UNKNOWN), - (WorkloadStateEnum.Failed, _ank_base.FAILED_LOST, + (WorkloadStateEnum.FAILED, _ank_base.FAILED_LOST, WorkloadSubStateEnum.FAILED_LOST), - (WorkloadStateEnum.NotScheduled, _ank_base.NOT_SCHEDULED, + (WorkloadStateEnum.NOT_SCHEDULED, _ank_base.NOT_SCHEDULED, WorkloadSubStateEnum.NOT_SCHEDULED), - (WorkloadStateEnum.Removed, _ank_base.REMOVED, + (WorkloadStateEnum.REMOVED, _ank_base.REMOVED, WorkloadSubStateEnum.REMOVED) ] for state, field, expected in data: @@ -72,7 +72,7 @@ def test_get_error(): ensuring it raises a ValueError for an invalid state and field combination. """ with pytest.raises(ValueError): - WorkloadSubStateEnum._get(WorkloadStateEnum.AgentDisconnected, + WorkloadSubStateEnum._get(WorkloadStateEnum.AGENT_DISCONNECTED, _ank_base.PENDING_WAITING_TO_START) From f39736124000561e509bd00fe8f842b2ea665567 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 27 Sep 2024 11:50:02 +0300 Subject: [PATCH 19/72] Prepare publish and release jobs --- .github/workflows/{ci.yml => build.yml} | 4 +- .github/workflows/publish.yml | 44 +++++++++++++ .github/workflows/publish_to_test.yml | 48 ++++++++++++++ .github/workflows/release.yml | 85 +++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 1 deletion(-) rename .github/workflows/{ci.yml => build.yml} (98%) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/publish_to_test.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/build.yml similarity index 98% rename from .github/workflows/ci.yml rename to .github/workflows/build.yml index 7319b43..75a9d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: CI +name: Install and test on: push: @@ -7,6 +7,8 @@ on: pull_request: branches: - main + workflow_dispatch: + workflow_call: jobs: setup: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6018be3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,44 @@ +name: Publish to Pypi + +on: + workflow_dispatch: + workflow_call: + +jobs: + build: + uses: ./.github/workflows/build.yml + + release: + needs: build + permissions: write-all + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + + - name: Restore cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + pip install twine + + - name: Build package + run: | + python3 setup.py sdist bdist_wheel + + - name: Publish package to Pypi + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/publish_to_test.yml b/.github/workflows/publish_to_test.yml new file mode 100644 index 0000000..287feb6 --- /dev/null +++ b/.github/workflows/publish_to_test.yml @@ -0,0 +1,48 @@ +name: Publish to Test Pypi + +on: + workflow_dispatch: + +jobs: + build: + uses: ./.github/workflows/build.yml + + release: + needs: build + permissions: write-all + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + + - name: Restore cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + pip install twine + + - name: Add _test to the package name + run: | + sed -i 's/PROJECT_NAME = "ankaios_sdk"/PROJECT_NAME = "ankaios_sdk_test"/g' setup.py + + - name: Build package + run: | + python3 setup.py sdist bdist_wheel + + - name: Publish package to test Pypi + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} + run: | + python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ebff575 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +name: Release + +on: + push: + tags: + - v* + workflow_dispatch: + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +concurrency: + group: "release-${{ github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + build: + uses: ./.github/workflows/build.yml + + release: + needs: build + permissions: write-all + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + + - name: Restore cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[dev] + pip install twine + + - name: Download artifacts for unit tests + uses: actions/download-artifact@v4.1.7 + with: + name: unit-test-report + path: dist/unit-test-report + + - name: Download artifacts for coverage + uses: actions/download-artifact@v4.1.7 + with: + name: coverage-report + path: dist/coverage-report + + - name: Download artifacts for lint + uses: actions/download-artifact@v4.1.7 + with: + name: pylint-report + path: dist/pylint-report + + - name: Download artifacts for codestyle + uses: actions/download-artifact@v4.1.7 + with: + name: codestyle-report + path: dist/codestyle-report + + - name: Build package + run: | + python3 setup.py sdist bdist_wheel + + - name: Package release + id: package + run: | + cd dist + gh release upload ${{ github.ref }} \ + ankaios_sdk-*.tar.gz ankaios_sdk-*.whl \ + unit-test-report.zip \ + coverage-report.zip \ + pylint-report.zip \ + codestyle-report.zip + + publish: + uses: ./.github/workflows/publish.yml From 25febbe47b04eda6938b1027bdf872e45439e82c Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 27 Sep 2024 11:51:33 +0300 Subject: [PATCH 20/72] Change workflow name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75a9d45..af9be7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Install and test +name: Build and run tests on: push: From ab8a518924eec27f1fa0b836b506109f96c9431e Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 27 Sep 2024 12:02:20 +0300 Subject: [PATCH 21/72] Fix publish workflow --- .github/workflows/publish.yml | 2 +- .github/workflows/publish_to_test.yml | 11 ++++++----- pyproject.toml | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6018be3..a2d1f32 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,7 +8,7 @@ jobs: build: uses: ./.github/workflows/build.yml - release: + publish: needs: build permissions: write-all runs-on: ubuntu-latest diff --git a/.github/workflows/publish_to_test.yml b/.github/workflows/publish_to_test.yml index 287feb6..901ae9f 100644 --- a/.github/workflows/publish_to_test.yml +++ b/.github/workflows/publish_to_test.yml @@ -7,7 +7,7 @@ jobs: build: uses: ./.github/workflows/build.yml - release: + publish: needs: build permissions: write-all runs-on: ubuntu-latest @@ -26,16 +26,17 @@ jobs: path: ~/.cache/pip key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + # Needed because this is the name of the package on the Test Pypi index + - name: Add _test to the package name + run: | + sed -i 's/PROJECT_NAME = "ankaios_sdk"/PROJECT_NAME = "ankaios_sdk_test"/g' setup.py + - name: Install dependencies run: | python3 -m pip install --upgrade pip pip install .[dev] pip install twine - - name: Add _test to the package name - run: | - sed -i 's/PROJECT_NAME = "ankaios_sdk"/PROJECT_NAME = "ankaios_sdk_test"/g' setup.py - - name: Build package run: | python3 setup.py sdist bdist_wheel diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" From 901b6c99158c60f7597ab0576d24588206e1096a Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 1 Oct 2024 09:02:05 +0300 Subject: [PATCH 22/72] Add contributor files --- .github/ISSUE_TEMPLATE/bug_report.md | 41 +++++++++++ .github/ISSUE_TEMPLATE/enchancement.md | 34 ++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 9 +++ CODE_OF_CONDUCT.md | 94 ++++++++++++++++++++++++++ CONTRIBUTING.md | 25 +++++++ 5 files changed, 203 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/enchancement.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8feccc9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: bug +assignees: '' + +--- + + + +## Current Behavior + + +## Expected Behavior + + +## Steps to Reproduce + + +1. +2. +3. +4. + +## Context (Environment) + + + +## Logs + + + + +## Additional Information + + +## Final result + + +**To be filled by the one closing the issue.** diff --git a/.github/ISSUE_TEMPLATE/enchancement.md b/.github/ISSUE_TEMPLATE/enchancement.md new file mode 100644 index 0000000..b8a2218 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enchancement.md @@ -0,0 +1,34 @@ +--- +name: Enhancement +about: Enhancement with goals and optional tasks +title: "" +labels: enhancement +assignees: '' + +--- + +## Description + + + +## Goals + + + +## Final result + + +### Summary + +**To be filled when the final solution is sketched.** + +### Tasks + + + +- [ ] Task 1 +- [ ] Task 2 +- [ ] ... diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a2302d3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +Issues: # + + + +# Definition of Done + +The PR shall be merged only if all items mentioned in [CONTRIBUTING.md](https://github.com/eclipse-ankaios/ankaios/blob/main/CONTRIBUTING.md#how-to-contribute) have been followed. In case an item is not applicable as described, please provide a short explanation in the description. + +- [ ] All steps in [CONTRIBUTING.md](https://github.com/eclipse-ankaios/ankaios/blob/main/CONTRIBUTING.md#how-to-contribute) have been handled diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..45e68dd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,94 @@ +# Community Code of Conduct + +**Version 2.0 +January 1, 2023** + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as community members, contributors, Committers[^1], and Project Leads (collectively "Contributors") pledge to make participation in our projects and our community a harassment-free and inclusive experience for everyone. + +This Community Code of Conduct ("Code") outlines our behavior expectations as members of our community in all Eclipse Foundation activities, both offline and online. It is not intended to govern scenarios or behaviors outside of the scope of Eclipse Foundation activities. Nor is it intended to replace or supersede the protections offered to all our community members under the law. Please follow both the spirit and letter of this Code and encourage other Contributors to follow these principles into our work. Failure to read or acknowledge this Code does not excuse a Contributor from compliance with the Code. + +## Our Standards + +Examples of behavior that contribute to creating a positive and professional environment include: + +- Using welcoming and inclusive language; +- Actively encouraging all voices; +- Helping others bring their perspectives and listening actively. If you find yourself dominating a discussion, it is especially important to encourage other voices to join in; +- Being respectful of differing viewpoints and experiences; +- Gracefully accepting constructive criticism; +- Focusing on what is best for the community; +- Showing empathy towards other community members; +- Being direct but professional; and +- Leading by example by holding yourself and others accountable + +Examples of unacceptable behavior by Contributors include: + +- The use of sexualized language or imagery; +- Unwelcome sexual attention or advances; +- Trolling, insulting/derogatory comments, and personal or political attacks; +- Public or private harassment, repeated harassment; +- Publishing others' private information, such as a physical or electronic address, without explicit permission; +- Violent threats or language directed against another person; +- Sexist, racist, or otherwise discriminatory jokes and language; +- Posting sexually explicit or violent material; +- Sharing private content, such as emails sent privately or non-publicly, or unlogged forums such as IRC channel history; +- Personal insults, especially those using racist or sexist terms; +- Excessive or unnecessary profanity; +- Advocating for, or encouraging, any of the above behavior; and +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +With the support of the Eclipse Foundation employees, consultants, officers, and directors (collectively, the "Staff"), Committers, and Project Leads, the Eclipse Foundation Conduct Committee (the "Conduct Committee") is responsible for clarifying the standards of acceptable behavior. The Conduct Committee takes appropriate and fair corrective action in response to any instances of unacceptable behavior. + +## Scope + +This Code applies within all Project, Working Group, and Interest Group spaces and communication channels of the Eclipse Foundation (collectively, "Eclipse spaces"), within any Eclipse-organized event or meeting, and in public spaces when an individual is representing an Eclipse Foundation Project, Working Group, Interest Group, or their communities. Examples of representing a Project or community include posting via an official social media account, personal accounts, or acting as an appointed representative at an online or offline event. Representation of Projects, Working Groups, and Interest Groups may be further defined and clarified by Committers, Project Leads, or the Staff. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Conduct Committee via .org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Without the explicit consent of the reporter, the Conduct Committee is obligated to maintain confidentiality with regard to the reporter of an incident. The Conduct Committee is further obligated to ensure that the respondent is provided with sufficient information about the complaint to reply. If such details cannot be provided while maintaining confidentiality, the Conduct Committee will take the respondent‘s inability to provide a defense into account in its deliberations and decisions. Further details of enforcement guidelines may be posted separately. + +Staff, Committers and Project Leads have the right to report, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code, or to block temporarily or permanently any Contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Any such actions will be reported to the Conduct Committee for transparency and record keeping. + +Any Staff (including officers and directors of the Eclipse Foundation), Committers, Project Leads, or Conduct Committee members who are the subject of a complaint to the Conduct Committee will be recused from the process of resolving any such complaint. + +## Responsibility + +The responsibility for administering this Code rests with the Conduct Committee, with oversight by the Executive Director and the Board of Directors. For additional information on the Conduct Committee and its process, please write to . + +## Investigation of Potential Code Violations + +All conflict is not bad as a healthy debate may sometimes be necessary to push us to do our best. It is, however, unacceptable to be disrespectful or offensive, or violate this Code. If you see someone engaging in objectionable behavior violating this Code, we encourage you to address the behavior directly with those involved. If for some reason, you are unable to resolve the matter or feel uncomfortable doing so, or if the behavior is threatening or harassing, please report it following the procedure laid out below. + +Reports should be directed to . It is the Conduct Committee’s role to receive and address reported violations of this Code and to ensure a fair and speedy resolution. + +The Eclipse Foundation takes all reports of potential Code violations seriously and is committed to confidentiality and a full investigation of all allegations. The identity of the reporter will be omitted from the details of the report supplied to the accused. Contributors who are being investigated for a potential Code violation will have an opportunity to be heard prior to any final determination. Those found to have violated the Code can seek reconsideration of the violation and disciplinary action decisions. Every effort will be made to have all matters disposed of within 60 days of the receipt of the complaint. + +## Actions + +Contributors who do not follow this Code in good faith may face temporary or permanent repercussions as determined by the Conduct Committee. + +This Code does not address all conduct. It works in conjunction with our [Communication Channel Guidelines](https://www.eclipse.org/org/documents/communication-channel-guidelines/), [Social Media Guidelines](https://www.eclipse.org/org/documents/social_media_guidelines.php), [Bylaws](https://www.eclipse.org/org/documents/eclipse-foundation-be-bylaws-en.pdf), and [Internal Rules](https://www.eclipse.org/org/documents/ef-be-internal-rules.pdf) which set out additional protections for, and obligations of, all contributors. The Foundation has additional policies that provide further guidance on other matters. + +It’s impossible to spell out every possible scenario that might be deemed a violation of this Code. Instead, we rely on one another’s good judgment to uphold a high standard of integrity within all Eclipse Spaces. Sometimes, identifying the right thing to do isn’t an easy call. In such a scenario, raise the issue as early as possible. + +## No Retaliation + +The Eclipse community relies upon and values the help of Contributors who identify potential problems that may need to be addressed within an Eclipse Space. Any retaliation against a Contributor who raises an issue honestly is a violation of this Code. That a Contributor has raised a concern honestly or participated in an investigation, cannot be the basis for any adverse action, including threats, harassment, or discrimination. If you work with someone who has raised a concern or provided information in an investigation, you should continue to treat the person with courtesy and respect. If you believe someone has retaliated against you, report the matter as described by this Code. Honest reporting does not mean that you have to be right when you raise a concern; you just have to believe that the information you are providing is accurate. + +False reporting, especially when intended to retaliate or exclude, is itself a violation of this Code and will not be accepted or tolerated. + +Everyone is encouraged to ask questions about this Code. Your feedback is welcome, and you will get a response within three business days. Write to . + +## Amendments + +The Eclipse Foundation Board of Directors may amend this Code from time to time and may vary the procedures it sets out where appropriate in a particular case. + +### Attribution + +This Code was inspired by the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.4, available [here](https://www.contributor-covenant.org/version/1/4/code-of-conduct/). + +[^1]: Capitalized terms used herein without definition shall have the meanings assigned to them in the Bylaws. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..54b0c34 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing + +Welcome to the Ankaios community. Start here for info on how to contribute and help improve our project. +Please observe our [Community Code of Conduct](./CODE_OF_CONDUCT.md). + +## How to Contribute + +This project welcomes contributions and suggestions. +You'll also need to create an [Eclipse Foundation account](https://accounts.eclipse.org/) and agree to the [Eclipse Contributor Agreement](https://www.eclipse.org/legal/ECA.php). See more info at . + +If you have a bug to report or a feature to suggest, please use the New Issue button on the Issues page to access templates for these items. + +Code contributions are to be submitted via pull requests. +For this fork this repository, apply the suggested changes and create a +pull request to integrate them. +Before creating the request, please ensure the following which we will check +besides a technical review: + +- **No breaks**: All builds and tests pass (GitHub actions). +- **Python coding guidelines**: Make sure to follow the coding schema of the project. + +## Communication + +Please join our [developer mailing list](https://accounts.eclipse.org/mailing-list/ankaios-dev) for up to date information or use the Ankaios [discussion forum](https://github.com/eclipse-ankaios/ankaios/discussions). +If you are looking for the main project, you can find it [here](https://github.com/eclipse-ankaios/ankaios/tree/main). From 7f866a6af415c5a46c69bf4579f0ba296ca6ef6e Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 3 Oct 2024 08:45:45 +0300 Subject: [PATCH 23/72] Update description of sdk --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0b9ec56..939deac 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ def generate_protos(): license="Apache-2.0", author="Elektrobit Automotive GmbH and Ankaios contributors", # author_email="", - description="Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform.", + description="Eclipse Ankaios Python SDK - provides a convenient Python interface for interacting with the Ankaios platform.", long_description=open('README.md').read(), long_description_content_type="text/markdown", url="https://eclipse-ankaios.github.io/ankaios/latest/", From 8b89b3fd345126860aaa58802e25d2762840e93e Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 3 Oct 2024 15:03:08 +0300 Subject: [PATCH 24/72] Fix workload.update_workload_name --- ankaios_sdk/_components/workload.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index 0481770..3c9b875 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -76,11 +76,13 @@ def __init__(self, name: str) -> None: name (str): The workload name. """ self._workload = _ank_base.Workload() - self._main_mask = f"desiredState.workloads.{name}" + self._main_mask = "" self._masks = [] self.__from_builder = False self.name = name + self._update_masks() + def __str__(self) -> str: """ Return a string representation of the Workload object. @@ -106,6 +108,20 @@ def _set_from_builder(self) -> None: """ self.__from_builder = True + def _update_masks(self) -> None: + """ + Update the masks of the Workload object. + """ + old_main_mask = self._main_mask + self._main_mask = f"desiredState.workloads.{self.name}" + if old_main_mask == "": + return + # pylint: disable=consider-using-enumerate + for i in range(len(self._masks)): + self._masks[i] = self._masks[i].replace( + old_main_mask, self._main_mask + ) + def update_workload_name(self, name: str) -> None: """ Set the workload name. @@ -114,6 +130,7 @@ def update_workload_name(self, name: str) -> None: name (str): The workload name to update. """ self.name = name + self._update_masks() if not self.__from_builder: self._add_mask(self._main_mask) From 11d72eb919418e6aecd657f402a58258abe224de Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 4 Oct 2024 12:12:00 +0300 Subject: [PATCH 25/72] Improve workload logic --- ankaios_sdk/_components/workload.py | 60 ++++++----------------------- ankaios_sdk/ankaios.py | 2 +- tests/workload/test_workload.py | 7 ++-- 3 files changed, 17 insertions(+), 52 deletions(-) diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index 3c9b875..e353671 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -76,12 +76,9 @@ def __init__(self, name: str) -> None: name (str): The workload name. """ self._workload = _ank_base.Workload() - self._main_mask = "" - self._masks = [] - self.__from_builder = False self.name = name - - self._update_masks() + self._main_mask = f"desiredState.workloads.{self.name}" + self.masks = [self._main_mask] def __str__(self) -> str: """ @@ -102,26 +99,6 @@ def builder() -> "WorkloadBuilder": """ return WorkloadBuilder() - def _set_from_builder(self) -> None: - """ - Set the __from_builder attribute to True. - """ - self.__from_builder = True - - def _update_masks(self) -> None: - """ - Update the masks of the Workload object. - """ - old_main_mask = self._main_mask - self._main_mask = f"desiredState.workloads.{self.name}" - if old_main_mask == "": - return - # pylint: disable=consider-using-enumerate - for i in range(len(self._masks)): - self._masks[i] = self._masks[i].replace( - old_main_mask, self._main_mask - ) - def update_workload_name(self, name: str) -> None: """ Set the workload name. @@ -130,8 +107,7 @@ def update_workload_name(self, name: str) -> None: name (str): The workload name to update. """ self.name = name - self._update_masks() - if not self.__from_builder: + if self._main_mask not in self.masks: self._add_mask(self._main_mask) def update_agent_name(self, agent_name: str) -> None: @@ -142,7 +118,7 @@ def update_agent_name(self, agent_name: str) -> None: agent_name (str): The agent name to update. """ self._workload.agent = agent_name - if not self.__from_builder: + if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.agent") def update_runtime(self, runtime: str) -> None: @@ -153,7 +129,7 @@ def update_runtime(self, runtime: str) -> None: runtime (str): The runtime to update. """ self._workload.runtime = runtime - if not self.__from_builder: + if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.runtime") def update_runtime_config(self, config: str) -> None: @@ -164,7 +140,7 @@ def update_runtime_config(self, config: str) -> None: config (str): The runtime configuration to update. """ self._workload.runtimeConfig = config - if not self.__from_builder: + if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.runtimeConfig") def update_runtime_config_from_file(self, config_file: str) -> None: @@ -198,7 +174,7 @@ def update_restart_policy(self, policy: str) -> None: raise ValueError("Invalid restart policy. Supported values " + "'NEVER', 'ON_FAILURE', 'ALWAYS'.") self._workload.restartPolicy = policy_map[policy] - if not self.__from_builder: + if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.restartPolicy") def add_dependency(self, workload_name: str, condition: str) -> None: @@ -224,7 +200,7 @@ def add_dependency(self, workload_name: str, condition: str) -> None: + "'RUNNING', 'SUCCEEDED', 'FAILED'.") self._workload.dependencies.dependencies[workload_name] = \ condition_map[condition] - if not self.__from_builder: + if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.dependencies") def get_dependencies(self) -> dict: @@ -267,7 +243,7 @@ def add_tag(self, key: str, value: str) -> None: """ tag = _ank_base.Tag(key=key, value=value) self._workload.tags.tags.append(tag) - if not self.__from_builder: + if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.tags") def get_tags(self) -> list[tuple[str, str]]: @@ -301,19 +277,8 @@ def _add_mask(self, mask: str) -> None: Args: mask (str): The mask to add. """ - if mask not in self._masks: - self._masks.append(mask) - - def _get_masks(self) -> list[str]: - """ - Return the list of masks. - - Returns: - list: A list of masks. - """ - if self._main_mask in self._masks: - return [self._main_mask] - return self._masks + if mask not in self.masks: + self.masks.append(mask) @staticmethod def _from_dict(workload_name: str, dict_workload: dict) -> "Workload": @@ -362,6 +327,7 @@ def _from_proto(self, proto: _ank_base.Workload) -> None: proto (_ank_base.Workload): The proto message to convert. """ self._workload = proto + self.masks = [] class WorkloadBuilder: @@ -516,7 +482,6 @@ def build(self) -> Workload: raise ValueError("Workload can not be built without a name.") workload = Workload(self.wl_name) - workload._set_from_builder() if self.wl_agent_name is None: raise ValueError("Workload can not be built without an " @@ -539,5 +504,4 @@ def build(self) -> Workload: if len(self.tags) > 0: workload.update_tags(self.tags) - workload._add_mask(f"desiredState.workloads.{workload.name}") return workload diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 1ca277a..559a098 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -381,7 +381,7 @@ def run_workload(self, workload: Workload) -> None: # Create the request request = Request(request_type="update_state") request.set_complete_state(complete_state) - for mask in workload._get_masks(): + for mask in workload.masks: request.add_mask(mask) # Send request diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index fd6d5cd..8266e08 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -78,7 +78,7 @@ def test_update_fields(workload): # pylint: disable=redefined-outer-name Args: workload (Workload): The Workload fixture. """ - assert workload._get_masks() == ["desiredState.workloads.workload_test"] + assert workload.masks == ["desiredState.workloads.workload_test"] workload.update_workload_name("new_workload_test") assert workload.name == "new_workload_test" @@ -234,11 +234,12 @@ def test_mask_generation(function_name, data, mask): mask (str): The expected mask to be generated. """ my_workload = Workload("workload_test") + my_workload.masks = [] # Call function and assert the mask has been added getattr(my_workload, function_name)(**data) - assert my_workload._get_masks() == [mask] + assert my_workload.masks == [mask] # Updating the mask again should not add a new mask getattr(my_workload, function_name)(**data) - assert len(my_workload._get_masks()) == 1 + assert len(my_workload.masks) == 1 From 32d6fb680a6f49de592de868855e7a3053e06f12 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 7 Oct 2024 18:28:11 +0300 Subject: [PATCH 26/72] Prepare the configs implementation --- .github/workflows/publish_to_test.yml | 49 -------------- README.md | 4 -- ankaios_sdk/_components/workload.py | 79 +++++++++++++++-------- ankaios_sdk/_components/workload_state.py | 62 ++---------------- ankaios_sdk/ankaios.py | 67 +++++++++++++++---- setup.py | 2 +- tests/test_ankaios.py | 51 ++++++++------- tests/workload/test_workload.py | 49 ++++++++++---- tests/workload/test_workload_builder.py | 45 ++++++++++--- 9 files changed, 210 insertions(+), 198 deletions(-) delete mode 100644 .github/workflows/publish_to_test.yml diff --git a/.github/workflows/publish_to_test.yml b/.github/workflows/publish_to_test.yml deleted file mode 100644 index 901ae9f..0000000 --- a/.github/workflows/publish_to_test.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Publish to Test Pypi - -on: - workflow_dispatch: - -jobs: - build: - uses: ./.github/workflows/build.yml - - publish: - needs: build - permissions: write-all - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.10' - - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - # Needed because this is the name of the package on the Test Pypi index - - name: Add _test to the package name - run: | - sed -i 's/PROJECT_NAME = "ankaios_sdk"/PROJECT_NAME = "ankaios_sdk_test"/g' setup.py - - - name: Install dependencies - run: | - python3 -m pip install --upgrade pip - pip install .[dev] - pip install twine - - - name: Build package - run: | - python3 setup.py sdist bdist_wheel - - - name: Publish package to test Pypi - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} - run: | - python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* diff --git a/README.md b/README.md index 4f86818..3208e23 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ # Ankaios Python SDK Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. - -# To do -- Finish the marked TODO methods from Ankaios.py, realted to configuration handling. -- Add to pip wheel diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index e353671..c859d92 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -31,7 +31,7 @@ .restart_policy("NEVER") \ .runtime_config("image: docker.io/library/nginx\n" + "commandOptions: [\"-p\", \"8080:80\"]") \ - .add_dependency("other_workload", "RUNNING") \ + .add_dependency("other_workload", "ADD_COND_RUNNING") \ .add_tag("key1", "value1") \ .add_tag("key2", "value2") \ .build() @@ -41,7 +41,7 @@ - Update dependencies: deps = workload.get_dependencies() - deps["other_workload"] = "SUCCEEDED" + deps["other_workload"] = "ADD_COND_SUCCEEDED" workload.update_dependencies(deps) - Update tags: @@ -164,23 +164,18 @@ def update_restart_policy(self, policy: str) -> None: Raises: ValueError: If an invalid restart policy is provided. """ - policy_map = { - "NEVER": _ank_base.NEVER, - "ON_FAILURE": _ank_base.ON_FAILURE, - "ALWAYS": _ank_base.ALWAYS - } - - if policy not in policy_map: - raise ValueError("Invalid restart policy. Supported values " - + "'NEVER', 'ON_FAILURE', 'ALWAYS'.") - self._workload.restartPolicy = policy_map[policy] + if policy not in _ank_base.RestartPolicy.keys(): + raise ValueError("Invalid restart policy. Supported values: " + + ", ".join(_ank_base.RestartPolicy.keys()) + ".") + self._workload.restartPolicy = _ank_base.RestartPolicy.Value(policy) if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.restartPolicy") def add_dependency(self, workload_name: str, condition: str) -> None: """ Add a dependency to the workload. - Supported values: 'RUNNING', 'SUCCEEDED', 'FAILED'. + Supported values: 'ADD_COND_RUNNING', 'ADD_COND_SUCCEEDED', + 'ADD_COND_FAILED'. Args: workload_name (str): The name of the dependent workload. @@ -189,17 +184,11 @@ def add_dependency(self, workload_name: str, condition: str) -> None: Raises: ValueError: If an invalid condition is provided. """ - condition_map = { - "RUNNING": _ank_base.ADD_COND_RUNNING, - "SUCCEEDED": _ank_base.ADD_COND_SUCCEEDED, - "FAILED": _ank_base.ADD_COND_FAILED - } - - if condition not in condition_map: + if condition not in _ank_base.AddCondition.keys(): raise ValueError("Invalid condition. Supported values: " - + "'RUNNING', 'SUCCEEDED', 'FAILED'.") + + ", ".join(_ank_base.AddCondition.keys()) + ".") self._workload.dependencies.dependencies[workload_name] = \ - condition_map[condition] + _ank_base.AddCondition.Value(condition) if self._main_mask not in self.masks: self._add_mask(f"{self._main_mask}.dependencies") @@ -213,12 +202,7 @@ def get_dependencies(self) -> dict: """ deps = dict(self._workload.dependencies.dependencies) for dep in deps: - if deps[dep] == _ank_base.ADD_COND_RUNNING: - deps[dep] = "RUNNING" - elif deps[dep] == _ank_base.ADD_COND_SUCCEEDED: - deps[dep] = "SUCCEEDED" - elif deps[dep] == _ank_base.ADD_COND_FAILED: - deps[dep] = "FAILED" + deps[dep] = _ank_base.AddCondition.Name(deps[dep]) return deps def update_dependencies(self, dependencies: dict) -> None: @@ -270,6 +254,35 @@ def update_tags(self, tags: list) -> None: for key, value in tags: self.add_tag(key, value) + def add_config(self, alias: str, name: str) -> None: + """ + Link a configuration to the workload. + + Args: + alias (str): The alias of the configuration. + name (str): The name of the configuration. + """ + raise NotImplementedError("add_config is not implemented yet.") + + def get_configs(self) -> tuple[tuple[str, str]]: + """ + Return the configurations linked to the workload. + + Returns: + tuple: A tuple containing the alias and name of the configurations. + """ + raise NotImplementedError("get_configs is not implemented yet.") + + def update_configs(self, configs: tuple[tuple[str, str]]) -> None: + """ + Update the configurations linked to the workload. + + Args: + configs (tuple): A tuple containing the alias and + name of the configurations. + """ + raise NotImplementedError("update_configs is not implemented yet.") + def _add_mask(self, mask: str) -> None: """ Add a mask to the list of masks. @@ -466,6 +479,16 @@ def add_tag(self, key: str, value: str) -> "WorkloadBuilder": self.tags.append((key, value)) return self + def add_config(self, alias: str, name: str) -> None: + """ + Link a configuration to the workload. + + Args: + alias (str): The alias of the configuration. + name (str): The name of the configuration. + """ + raise NotImplementedError("add_config is not implemented yet.") + def build(self) -> Workload: """ Build the Workload object. diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index 466a2e7..efc83e7 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -102,6 +102,7 @@ def _get(field: str) -> "WorkloadStateEnum": KeyError: If the field name does not correspond to any enumeration member. """ + # camelCase to SNAKE_CASE if field == "agentDisconnected": return WorkloadStateEnum.AGENT_DISCONNECTED if field == "notScheduled": @@ -176,65 +177,12 @@ def _get(state: WorkloadStateEnum, ValueError: If the field does not correspond to any enumeration member. """ - proto_mapper = {} - if state == WorkloadStateEnum.AGENT_DISCONNECTED: - proto_mapper = { - _ank_base.AGENT_DISCONNECTED: - WorkloadSubStateEnum.AGENT_DISCONNECTED - } - elif state == WorkloadStateEnum.PENDING: - proto_mapper = { - _ank_base.PENDING_INITIAL: - WorkloadSubStateEnum.PENDING_INITIAL, - _ank_base.PENDING_WAITING_TO_START: - WorkloadSubStateEnum.PENDING_WAITING_TO_START, - _ank_base.PENDING_STARTING: - WorkloadSubStateEnum.PENDING_STARTING, - _ank_base.PENDING_STARTING_FAILED: - WorkloadSubStateEnum.PENDING_STARTING_FAILED - } - elif state == WorkloadStateEnum.RUNNING: - proto_mapper = { - _ank_base.RUNNING_OK: WorkloadSubStateEnum.RUNNING_OK - } - elif state == WorkloadStateEnum.STOPPING: - proto_mapper = { - _ank_base.STOPPING: WorkloadSubStateEnum.STOPPING, - _ank_base.STOPPING_WAITING_TO_STOP: - WorkloadSubStateEnum.STOPPING_WAITING_TO_STOP, - _ank_base.STOPPING_REQUESTED_AT_RUNTIME: - WorkloadSubStateEnum.STOPPING_REQUESTED_AT_RUNTIME, - _ank_base.STOPPING_DELETE_FAILED: - WorkloadSubStateEnum.STOPPING_DELETE_FAILED - } - elif state == WorkloadStateEnum.SUCCEEDED: - proto_mapper = { - _ank_base.SUCCEEDED_OK: - WorkloadSubStateEnum.SUCCEEDED_OK - } - elif state == WorkloadStateEnum.FAILED: - proto_mapper = { - _ank_base.FAILED_EXEC_FAILED: - WorkloadSubStateEnum.FAILED_EXEC_FAILED, - _ank_base.FAILED_UNKNOWN: - WorkloadSubStateEnum.FAILED_UNKNOWN, - _ank_base.FAILED_LOST: - WorkloadSubStateEnum.FAILED_LOST - } - elif state == WorkloadStateEnum.NOT_SCHEDULED: - proto_mapper = { - _ank_base.NOT_SCHEDULED: - WorkloadSubStateEnum.NOT_SCHEDULED - } - elif state == WorkloadStateEnum.REMOVED: - proto_mapper = { - _ank_base.REMOVED: - WorkloadSubStateEnum.REMOVED - } - if field not in proto_mapper: + # SNAKE_CASE to CamelCase + state_name = "".join([elem.title() for elem in state.name.split("_")]) + if field not in getattr(_ank_base, state_name).values(): raise ValueError("No corresponding WorkloadSubStateEnum " + f"value for enum: {field}") - return proto_mapper[field] + return WorkloadSubStateEnum[getattr(_ank_base, state_name).Name(field)] def _sub_state2ank_base(self) -> _ank_base: """ diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 559a098..dea5530 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -59,6 +59,7 @@ __all__ = ["Ankaios", "AnkaiosLogLevel"] import logging +from typing import Union from enum import Enum import threading from google.protobuf.internal.encoder import _VarintBytes @@ -87,6 +88,7 @@ class AnkaiosLogLevel(Enum): DEBUG = logging.DEBUG +# pylint: disable=too-many-public-methods class Ankaios: """ A class to interact with the Ankaios control interface. It provides @@ -453,43 +455,82 @@ def get_workload(self, workload_name: str, ) return state.get_workload(workload_name) if state is not None else None + def set_configs_from_file(self, configs_path: str) -> None: + """ + Set the configs from a file. + The configs file should have a dictionary as the top level object. + The names will be the keys of the dictionary. + + Args: + config_path (str): The path to the configs file. + """ + # with open(configs_path, "r", encoding="utf-8") as f: + # configs = f.read() + # self.set_configs(configs) + raise NotImplementedError( + "set_configs_from_file is not implemented yet." + ) + + def set_configs(self, configs: dict) -> None: + """ + Set the configs. The names will be the keys of the dictionary. + + Args: + configs (dict): The configs dictionary. + """ + raise NotImplementedError("set_configs is not implemented yet.") + def set_config_from_file(self, name: str, config_path: str) -> None: """ - Set the config from a file. + Set the config from a file, with the provided name. + If the config exists, it will be replaced. Args: name (str): The name of the config. config_path (str): The path to the config file. """ - with open(config_path, "r", encoding="utf-8") as f: - config = f.read() - self.set_config(name, config) + raise NotImplementedError( + "set_config_from_file is not implemented yet." + ) - # TODO Ankaios.set_config # pylint: disable=fixme - def set_config(self, name: str, config: dict) -> None: + def set_config(self, name: str, config: Union[dict, list, str]) -> None: """ - Set the config. + Set the config with the provided name. + If the config exists, it will eb replaced. Args: name (str): The name of the config. - config (dict): The config dictionary. + config (Union[dict, list, str]): The config dictionary. """ raise NotImplementedError("set_config is not implemented yet.") - # TODO Ankaios.get_config this # pylint: disable=fixme - def get_config(self, name: str) -> dict: + def get_configs(self) -> dict: + """ + Get the configs. The keys will be the names. + + Returns: + dict: The configs dictionary. + """ + raise NotImplementedError("get_configs is not implemented yet.") + + def get_config(self, name: str) -> Union[dict, list, str]: """ - Get the config. + Get the config with the provided name. Args: name (str): The name of the config. Returns: - dict: The config dictionary. + Union[dict, list, str]: The config. """ raise NotImplementedError("get_config is not implemented yet.") - # TODO Ankaios.delete_config this # pylint: disable=fixme + def delete_configs(self) -> None: + """ + Delete all the configs. + """ + raise NotImplementedError("delete_configs is not implemented yet.") + def delete_config(self, name: str) -> None: """ Delete the config. diff --git a/setup.py b/setup.py index 939deac..b9879ec 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def generate_protos(): setup( - name=PROJECT_NAME, + name=PROJECT_NAME.replace("_", "-"), version="0.1.0", license="Apache-2.0", author="Elektrobit Automotive GmbH and Ankaios contributors", diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 025eae6..14f75bf 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -343,43 +343,48 @@ def test_get_workload(): mock_state_get_workload.assert_called_once_with("nginx") -def test_set_config(): +def test_configs(): """ - Test the set config methods of the Ankaios class. + Test the configs methods of the Ankaios class. """ ankaios = Ankaios() - with patch("builtins.open", mock_open()) as mock_file, \ - patch("ankaios_sdk.Ankaios.set_config") as mock_set_config: - mock_file().read.return_value = {'config_test': 'value'} + # Note for the set from file tests: + # with patch("builtins.open", mock_open()) as mock_file, \ + # patch("ankaios_sdk.Ankaios.set_config") as mock_set_config: + # mock_file().read.return_value = {'config_test': 'value'} + # ankaios.set_config_from_file(name="config_test", + # config_path=r"path/to/config") + + # mock_file.assert_called_with( + # r"path/to/config", "r", encoding="utf-8" + # ) + # mock_file().read.assert_called_once() + # mock_set_config.assert_called_once_with( + # "config_test", {'config_test': 'value'} + # ) + + with pytest.raises(NotImplementedError, match="not implemented yet"): + ankaios.set_configs_from_file(configs_path=r"path/to/configs") + + with pytest.raises(NotImplementedError, match="not implemented yet"): + ankaios.set_configs(configs={'name': 'config'}) + + with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.set_config_from_file(name="config_test", config_path=r"path/to/config") - mock_file.assert_called_with(r"path/to/config", "r", encoding="utf-8") - mock_file().read.assert_called_once() - mock_set_config.assert_called_once_with( - "config_test", {'config_test': 'value'} - ) - with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.set_config(name="config_test", config={'config_test': 'value'}) - -def test_get_config(): - """ - Test the get config method of the Ankaios class. - """ - ankaios = Ankaios() + with pytest.raises(NotImplementedError, match="not implemented yet"): + ankaios.get_configs() with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.get_config(name="config_test") - -def test_delete_config(): - """ - Test the delete config method of the Ankaios class. - """ - ankaios = Ankaios() + with pytest.raises(NotImplementedError, match="not implemented yet"): + ankaios.delete_configs() with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.delete_config(name="config_test") diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index 8266e08..3a17c19 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -42,7 +42,7 @@ def generate_test_workload(workload_name: str = "workload_test") -> Workload: .runtime("runtime_test") \ .restart_policy("NEVER") \ .runtime_config("config_test") \ - .add_dependency("workload_test_other", "RUNNING") \ + .add_dependency("workload_test_other", "ADD_COND_RUNNING") \ .add_tag("key1", "value1") \ .add_tag("key2", "value2") \ .build() @@ -71,7 +71,9 @@ def test_builder(workload): # pylint: disable=redefined-outer-name assert isinstance(builder, WorkloadBuilder) -def test_update_fields(workload): # pylint: disable=redefined-outer-name +def test_update_fields( + workload: Workload + ): # pylint: disable=redefined-outer-name """ Test updating various fields of the Workload instance. @@ -104,7 +106,9 @@ def test_update_fields(workload): # pylint: disable=redefined-outer-name assert workload._workload.restartPolicy == _ank_base.ON_FAILURE -def test_dependencies(workload): # pylint: disable=redefined-outer-name +def test_dependencies( + workload: Workload + ): # pylint: disable=redefined-outer-name """ Test adding and updating dependencies of the Workload instance. @@ -114,12 +118,12 @@ def test_dependencies(workload): # pylint: disable=redefined-outer-name assert len(workload.get_dependencies()) == 1 with pytest.raises(ValueError): - workload.add_dependency("other_workload_test", "DANCING") + workload.add_dependency("other_workload_test", "ADD_COND_DANCING") - workload.add_dependency("other_workload_test", "SUCCEEDED") + workload.add_dependency("other_workload_test", "ADD_COND_SUCCEEDED") assert len(workload.get_dependencies()) == 2 - workload.add_dependency("another_workload_test", "FAILED") + workload.add_dependency("another_workload_test", "ADD_COND_FAILED") deps = workload.get_dependencies() assert len(deps) == 3 @@ -129,7 +133,7 @@ def test_dependencies(workload): # pylint: disable=redefined-outer-name assert len(workload.get_dependencies()) == 2 -def test_tags(workload): # pylint: disable=redefined-outer-name +def test_tags(workload: Workload): # pylint: disable=redefined-outer-name """ Test adding and updating tags of the Workload instance. @@ -149,7 +153,24 @@ def test_tags(workload): # pylint: disable=redefined-outer-name assert len(workload.get_tags()) == 2 -def test_to_proto(workload): # pylint: disable=redefined-outer-name +def test_configs(workload: Workload): # pylint: disable=redefined-outer-name + """ + Test adding and updating configurations of the Workload instance. + + Args: + workload (Workload): The Workload fixture. + """ + with pytest.raises(NotImplementedError, match="not implemented yet"): + workload.add_config(alias="alias_test", name="config_test") + + with pytest.raises(NotImplementedError, match="not implemented yet"): + workload.get_configs() + + with pytest.raises(NotImplementedError, match="not implemented yet"): + workload.update_configs(configs=[["alias_test", "config_test"]]) + + +def test_to_proto(workload: Workload): # pylint: disable=redefined-outer-name """ Test converting the Workload instance to protobuf message. @@ -170,7 +191,9 @@ def test_to_proto(workload): # pylint: disable=redefined-outer-name ]) -def test_from_proto(workload): # pylint: disable=redefined-outer-name +def test_from_proto( + workload: Workload + ): # pylint: disable=redefined-outer-name """ Test converting theprotobuf message to a Workload instance. @@ -184,7 +207,7 @@ def test_from_proto(workload): # pylint: disable=redefined-outer-name assert str(workload) == str(new_workload) -def test_from_dict(workload): # pylint: disable=redefined-outer-name +def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name """ Test creating a Workload instance from a dictionary. @@ -197,7 +220,7 @@ def test_from_dict(workload): # pylint: disable=redefined-outer-name "runtime": "runtime_test", "restartPolicy": "NEVER", "runtimeConfig": "config_test", - "dependencies": {"workload_test_other": "RUNNING"}, + "dependencies": {"workload_test_other": "ADD_COND_RUNNING"}, "tags": {"key1": "value1", "key2": "value2"} } @@ -218,12 +241,12 @@ def test_from_dict(workload): # pylint: disable=redefined-outer-name ("update_runtime_config", {"config": "config_test"}, "desiredState.workloads.workload_test.runtimeConfig"), ("add_dependency", {"workload_name": "workload_test_other", - "condition": "RUNNING"}, + "condition": "ADD_COND_RUNNING"}, "desiredState.workloads.workload_test.dependencies"), ("add_tag", {"key": "key1", "value": "value1"}, "desiredState.workloads.workload_test.tags"), ]) -def test_mask_generation(function_name, data, mask): +def test_mask_generation(function_name: str, data: dict, mask: str): """ Test the generation of masks when updating fields of the Workload instance. diff --git a/tests/workload/test_workload_builder.py b/tests/workload/test_workload_builder.py index aef6731..ba7ab32 100644 --- a/tests/workload/test_workload_builder.py +++ b/tests/workload/test_workload_builder.py @@ -36,7 +36,9 @@ def builder(): return WorkloadBuilder() -def test_workload_fields(builder): # pylint: disable=redefined-outer-name +def test_workload_fields( + builder: WorkloadBuilder + ): # pylint: disable=redefined-outer-name """ Test setting various fields of the WorkloadBuilder instance. @@ -62,7 +64,9 @@ def test_workload_fields(builder): # pylint: disable=redefined-outer-name assert builder.wl_restart_policy == "NEVER" -def test_add_dependency(builder): # pylint: disable=redefined-outer-name +def test_add_dependency( + builder: WorkloadBuilder + ): # pylint: disable=redefined-outer-name """ Test adding dependencies to the WorkloadBuilder instance. @@ -71,15 +75,21 @@ def test_add_dependency(builder): # pylint: disable=redefined-outer-name """ assert len(builder.dependencies) == 0 - assert builder.add_dependency("workload_test", "RUNNING") == builder - assert builder.dependencies == {"workload_test": "RUNNING"} + assert builder.add_dependency( + "workload_test", "ADD_COND_RUNNING" + ) == builder + assert builder.dependencies == {"workload_test": "ADD_COND_RUNNING"} - assert builder.add_dependency("workload_test_other", "RUNNING") == builder - assert builder.dependencies == {"workload_test": "RUNNING", - "workload_test_other": "RUNNING"} + assert builder.add_dependency( + "workload_test_other", "ADD_COND_RUNNING" + ) == builder + assert builder.dependencies == {"workload_test": "ADD_COND_RUNNING", + "workload_test_other": "ADD_COND_RUNNING"} -def test_add_tag(builder): # pylint: disable=redefined-outer-name +def test_add_tag( + builder: WorkloadBuilder + ): # pylint: disable=redefined-outer-name """ Test adding tags to the WorkloadBuilder instance. @@ -95,7 +105,22 @@ def test_add_tag(builder): # pylint: disable=redefined-outer-name assert builder.tags == [("key_test", "abc"), ("key_test", "bcd")] -def test_build(builder): # pylint: disable=redefined-outer-name +def test_add_config( + builder: WorkloadBuilder + ): # pylint: disable=redefined-outer-name + """ + Test adding configurations to the WorkloadBuilder instance. + + Args: + builder (WorkloadBuilder): The WorkloadBuilder fixture. + """ + with pytest.raises(NotImplementedError, match="not implemented yet"): + builder.add_config(alias="alias_test", name="config_test") + + +def test_build( + builder: WorkloadBuilder + ): # pylint: disable=redefined-outer-name """ Test building a Workload instance from the WorkloadBuilder instance. @@ -131,7 +156,7 @@ def test_build(builder): # pylint: disable=redefined-outer-name workload = builder.runtime_config("config_test") \ .restart_policy("NEVER") \ - .add_dependency("workload_test_other", "RUNNING") \ + .add_dependency("workload_test_other", "ADD_COND_RUNNING") \ .add_tag("key_test", "abc") \ .build() From 4e51a8106192478a311b488a2268b2c5b650c754 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 8 Oct 2024 10:34:22 +0300 Subject: [PATCH 27/72] Add allow and deny rules to the workload --- ankaios_sdk/_components/workload.py | 226 ++++++++++++++++++++++-- tests/workload/test_workload.py | 62 ++++++- tests/workload/test_workload_builder.py | 20 +++ 3 files changed, 290 insertions(+), 18 deletions(-) diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index c859d92..fd9a82d 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -59,6 +59,7 @@ from .._protos import _ank_base +# pylint: disable=too-many-public-methods class Workload: """ A class to represent a workload. @@ -107,8 +108,7 @@ def update_workload_name(self, name: str) -> None: name (str): The workload name to update. """ self.name = name - if self._main_mask not in self.masks: - self._add_mask(self._main_mask) + self._add_mask(self._main_mask) def update_agent_name(self, agent_name: str) -> None: """ @@ -118,8 +118,7 @@ def update_agent_name(self, agent_name: str) -> None: agent_name (str): The agent name to update. """ self._workload.agent = agent_name - if self._main_mask not in self.masks: - self._add_mask(f"{self._main_mask}.agent") + self._add_mask(f"{self._main_mask}.agent") def update_runtime(self, runtime: str) -> None: """ @@ -129,8 +128,7 @@ def update_runtime(self, runtime: str) -> None: runtime (str): The runtime to update. """ self._workload.runtime = runtime - if self._main_mask not in self.masks: - self._add_mask(f"{self._main_mask}.runtime") + self._add_mask(f"{self._main_mask}.runtime") def update_runtime_config(self, config: str) -> None: """ @@ -140,8 +138,7 @@ def update_runtime_config(self, config: str) -> None: config (str): The runtime configuration to update. """ self._workload.runtimeConfig = config - if self._main_mask not in self.masks: - self._add_mask(f"{self._main_mask}.runtimeConfig") + self._add_mask(f"{self._main_mask}.runtimeConfig") def update_runtime_config_from_file(self, config_file: str) -> None: """ @@ -168,8 +165,7 @@ def update_restart_policy(self, policy: str) -> None: raise ValueError("Invalid restart policy. Supported values: " + ", ".join(_ank_base.RestartPolicy.keys()) + ".") self._workload.restartPolicy = _ank_base.RestartPolicy.Value(policy) - if self._main_mask not in self.masks: - self._add_mask(f"{self._main_mask}.restartPolicy") + self._add_mask(f"{self._main_mask}.restartPolicy") def add_dependency(self, workload_name: str, condition: str) -> None: """ @@ -189,8 +185,7 @@ def add_dependency(self, workload_name: str, condition: str) -> None: + ", ".join(_ank_base.AddCondition.keys()) + ".") self._workload.dependencies.dependencies[workload_name] = \ _ank_base.AddCondition.Value(condition) - if self._main_mask not in self.masks: - self._add_mask(f"{self._main_mask}.dependencies") + self._add_mask(f"{self._main_mask}.dependencies") def get_dependencies(self) -> dict: """ @@ -227,8 +222,7 @@ def add_tag(self, key: str, value: str) -> None: """ tag = _ank_base.Tag(key=key, value=value) self._workload.tags.tags.append(tag) - if self._main_mask not in self.masks: - self._add_mask(f"{self._main_mask}.tags") + self._add_mask(f"{self._main_mask}.tags") def get_tags(self) -> list[tuple[str, str]]: """ @@ -254,6 +248,146 @@ def update_tags(self, tags: list) -> None: for key, value in tags: self.add_tag(key, value) + def add_allow_rule( + self, operation: str, filter_masks: list[str] + ) -> None: + """ + Add an allow rule to the workload. + + Args: + operation (str): The operation the rule allows. + Allowed values: 'Nothing', 'Write', 'Read', 'ReadWrite'. + filter_masks (list): The list of filter masks. + + Raises: + ValueError: If an invalid operation is provided + """ + enum_mapper = { + "Nothing": _ank_base.ReadWriteEnum.RW_NOTHING, + "Write": _ank_base.ReadWriteEnum.RW_WRITE, + "Read": _ank_base.ReadWriteEnum.RW_READ, + "ReadWrite": _ank_base.ReadWriteEnum.RW_READ_WRITE, + } + if operation not in enum_mapper: + raise ValueError( + f"Invalid operation {operation}. " + + "Supported values: " + + ", ".join(enum_mapper.keys()) + "." + ) + self._workload.controlInterfaceAccess.allowRules.append( + _ank_base.AccessRightsRule( + stateRule=_ank_base.StateRule( + operation=enum_mapper[operation], + filterMasks=filter_masks + ) + ) + ) + self._add_mask(f"{self._main_mask}.controlInterfaceAccess") + + def get_allow_rules(self) -> list[tuple[str, list[str]]]: + """ + Return the allow rules of the workload. + + Returns: + list: A list of tuples containing operation and filter masks. + """ + enum_mapper = { + _ank_base.ReadWriteEnum.RW_NOTHING: "Nothing", + _ank_base.ReadWriteEnum.RW_WRITE: "Write", + _ank_base.ReadWriteEnum.RW_READ: "Read", + _ank_base.ReadWriteEnum.RW_READ_WRITE: "ReadWrite", + } + rules = [] + for rule in self._workload.controlInterfaceAccess.allowRules: + rules.append(( + enum_mapper[rule.stateRule.operation], + rule.stateRule.filterMasks + )) + return rules + + def update_allow_rules(self, rules: list[tuple[str, list[str]]]) -> None: + """ + Update the allow rules of the workload. + + Args: + rules (list): A list of tuples containing + operation and filter masks. + """ + while len(self._workload.controlInterfaceAccess.allowRules) > 0: + self._workload.controlInterfaceAccess.allowRules.pop() + for operation, filter_masks in rules: + self.add_allow_rule(operation, filter_masks) + + def add_deny_rule( + self, operation: str, filter_masks: list[str] + ) -> None: + """ + Add a deny rule to the workload. + + Args: + operation (str): The operation the rule denies. + Allowed values: 'Nothing', 'Write', 'Read', 'ReadWrite'. + filter_masks (list): The list of filter masks. + + Raises: + ValueError: If an invalid operation is provided + """ + enum_mapper = { + "Nothing": _ank_base.ReadWriteEnum.RW_NOTHING, + "Write": _ank_base.ReadWriteEnum.RW_WRITE, + "Read": _ank_base.ReadWriteEnum.RW_READ, + "ReadWrite": _ank_base.ReadWriteEnum.RW_READ_WRITE, + } + if operation not in enum_mapper: + raise ValueError( + f"Invalid operation {operation}. " + + "Supported values: " + + ", ".join(enum_mapper.keys()) + "." + ) + self._workload.controlInterfaceAccess.denyRules.append( + _ank_base.AccessRightsRule( + stateRule=_ank_base.StateRule( + operation=enum_mapper[operation], + filterMasks=filter_masks + ) + ) + ) + self._add_mask(f"{self._main_mask}.controlInterfaceAccess") + + def get_deny_rules(self) -> list[tuple[str, list[str]]]: + """ + Return the deny rules of the workload. + + Returns: + list: A list of tuples containing operation and filter masks. + """ + enum_mapper = { + _ank_base.ReadWriteEnum.RW_NOTHING: "Nothing", + _ank_base.ReadWriteEnum.RW_WRITE: "Write", + _ank_base.ReadWriteEnum.RW_READ: "Read", + _ank_base.ReadWriteEnum.RW_READ_WRITE: "ReadWrite", + } + rules = [] + for rule in self._workload.controlInterfaceAccess.denyRules: + rules.append(( + enum_mapper[rule.stateRule.operation], + rule.stateRule.filterMasks + )) + return rules + + def update_deny_rules(self, rules: list[tuple[str, list[str]]]) -> None: + """ + Update the deny rules of the workload. + + Args: + rules (list): A list of tuples containing + operation and filter masks. + """ + while len(self._workload.controlInterfaceAccess.denyRules) > 0: + self._workload.controlInterfaceAccess.denyRules.pop() + for operation, filter_masks in rules: + self.add_deny_rule(operation, filter_masks) + def add_config(self, alias: str, name: str) -> None: """ Link a configuration to the workload. @@ -290,9 +424,10 @@ def _add_mask(self, mask: str) -> None: Args: mask (str): The mask to add. """ - if mask not in self.masks: + if self._main_mask not in self.masks and mask not in self.masks: self.masks.append(mask) + # pylint: disable=too-many-branches @staticmethod def _from_dict(workload_name: str, dict_workload: dict) -> "Workload": """ @@ -318,8 +453,26 @@ def _from_dict(workload_name: str, dict_workload: dict) -> "Workload": for dep_key, dep_value in dict_workload["dependencies"].items(): workload = workload.add_dependency(dep_key, dep_value) if "tags" in dict_workload: - for tag_key, tag_value in dict_workload["tags"].items(): - workload = workload.add_tag(tag_key, tag_value) + for tag in dict_workload["tags"]: + workload = workload.add_tag(tag["key"], tag["value"]) + if "controlInterfaceAccess" in dict_workload: + if "allowRules" in dict_workload["controlInterfaceAccess"]: + for rule in dict_workload[ + "controlInterfaceAccess"][ + "allowRules" + ]: + workload = workload.add_allow_rule( + rule["operation"], rule["filterMask"] + ) + if "denyRules" in dict_workload["controlInterfaceAccess"]: + for rule in dict_workload[ + "controlInterfaceAccess"][ + "denyRules" + ]: + workload = workload.add_deny_rule( + rule["operation"], rule["filterMask"] + ) + return workload.build() def _to_proto(self) -> _ank_base.Workload: @@ -343,6 +496,7 @@ def _from_proto(self, proto: _ank_base.Workload) -> None: self.masks = [] +# pylint: disable=too-many-instance-attributes class WorkloadBuilder: """ A builder class to create a Workload object. @@ -367,6 +521,8 @@ def __init__(self) -> None: self.wl_restart_policy = None self.dependencies = {} self.tags = [] + self.allow_rules = [] + self.deny_rules = [] def workload_name(self, workload_name: str) -> "WorkloadBuilder": """ @@ -479,6 +635,38 @@ def add_tag(self, key: str, value: str) -> "WorkloadBuilder": self.tags.append((key, value)) return self + def add_allow_rule( + self, operation: str, filter_masks: list[str] + ) -> "WorkloadBuilder": + """ + Add an allow rule to the workload. + + Args: + operation (str): The operation the rule allows. + filter_masks (list): The list of filter masks. + + Returns: + WorkloadBuilder: The builder object. + """ + self.allow_rules.append((operation, filter_masks)) + return self + + def add_deny_rule( + self, operation: str, filter_masks: list[str] + ) -> "WorkloadBuilder": + """ + Add a deny rule to the workload. + + Args: + operation (str): The operation the rule denies. + filter_masks (list): The list of filter masks. + + Returns: + WorkloadBuilder: The builder object. + """ + self.deny_rules.append((operation, filter_masks)) + return self + def add_config(self, alias: str, name: str) -> None: """ Link a configuration to the workload. @@ -526,5 +714,9 @@ def build(self) -> Workload: workload.update_dependencies(self.dependencies) if len(self.tags) > 0: workload.update_tags(self.tags) + if len(self.allow_rules) > 0: + workload.update_allow_rules(self.allow_rules) + if len(self.deny_rules) > 0: + workload.update_deny_rules(self.deny_rules) return workload diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index 3a17c19..2e8a4c1 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -45,6 +45,10 @@ def generate_test_workload(workload_name: str = "workload_test") -> Workload: .add_dependency("workload_test_other", "ADD_COND_RUNNING") \ .add_tag("key1", "value1") \ .add_tag("key2", "value2") \ + .add_allow_rule("Write", + ["desiredState.workloads.another_workload"]) \ + .add_deny_rule("Read", + ["workloadStates.agent_Test.another_workload"]) \ .build() @@ -153,6 +157,43 @@ def test_tags(workload: Workload): # pylint: disable=redefined-outer-name assert len(workload.get_tags()) == 2 +def test_rules(workload: Workload): # pylint: disable=redefined-outer-name + """ + Test adding and updating allow and deny rules of the Workload instance. + + Args: + workload (Workload): The Workload fixture. + """ + assert len(workload.get_allow_rules()) == 1 + assert len(workload.get_deny_rules()) == 1 + + with pytest.raises(ValueError): + workload.add_allow_rule("Invalid", ["mask"]) + + with pytest.raises(ValueError): + workload.add_deny_rule("Invalid", ["mask"]) + + workload.add_allow_rule( + "Write", ["desiredState.workloads.another_workload"] + ) + assert len(workload.get_allow_rules()) == 2 + + workload.add_deny_rule( + "Read", ["workloadStates.agent_Test.another_workload"] + ) + assert len(workload.get_deny_rules()) == 2 + + rules = workload.get_allow_rules() + rules = rules[1:] + workload.update_allow_rules(rules) + assert len(workload.get_allow_rules()) == 1 + + rules = workload.get_deny_rules() + rules = rules[1:] + workload.update_deny_rules(rules) + assert len(workload.get_deny_rules()) == 1 + + def test_configs(workload: Workload): # pylint: disable=redefined-outer-name """ Test adding and updating configurations of the Workload instance. @@ -221,7 +262,22 @@ def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name "restartPolicy": "NEVER", "runtimeConfig": "config_test", "dependencies": {"workload_test_other": "ADD_COND_RUNNING"}, - "tags": {"key1": "value1", "key2": "value2"} + "tags": [ + {"key": "key1", "value": "value1"}, + {"key": "key2", "value": "value2"}, + ], + "controlInterfaceAccess": { + "allowRules": [{ + "type": "StateRule", + "operation": "Write", + "filterMask": ["desiredState.workloads.another_workload"] + }], + "denyRules": [{ + "type": "StateRule", + "operation": "Read", + "filterMask": ["workloadStates.agent_Test.another_workload"] + }] + } } new_workload = Workload._from_dict("workload_test", workload_dict) @@ -245,6 +301,10 @@ def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name "desiredState.workloads.workload_test.dependencies"), ("add_tag", {"key": "key1", "value": "value1"}, "desiredState.workloads.workload_test.tags"), + ("add_allow_rule", {"operation": "Write", "filter_masks": ["mask"]}, + "desiredState.workloads.workload_test.controlInterfaceAccess"), + ("add_deny_rule", {"operation": "Write", "filter_masks": ["mask"]}, + "desiredState.workloads.workload_test.controlInterfaceAccess"), ]) def test_mask_generation(function_name: str, data: dict, mask: str): """ diff --git a/tests/workload/test_workload_builder.py b/tests/workload/test_workload_builder.py index ba7ab32..e0e95d0 100644 --- a/tests/workload/test_workload_builder.py +++ b/tests/workload/test_workload_builder.py @@ -105,6 +105,26 @@ def test_add_tag( assert builder.tags == [("key_test", "abc"), ("key_test", "bcd")] +def test_add_rule( + builder: WorkloadBuilder + ): # pylint: disable=redefined-outer-name + """ + Test adding rules to the WorkloadBuilder instance. + + Args: + builder (WorkloadBuilder): The WorkloadBuilder fixture. + """ + assert len(builder.allow_rules) == 0 + + assert builder.add_allow_rule("Write", ["mask"]) == builder + assert builder.allow_rules == [("Write", ["mask"])] + + assert len(builder.deny_rules) == 0 + + assert builder.add_deny_rule("Read", ["mask"]) == builder + assert builder.deny_rules == [("Read", ["mask"])] + + def test_add_config( builder: WorkloadBuilder ): # pylint: disable=redefined-outer-name From 139cbb4f662362573b1b9f756f021351d453753a Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 8 Oct 2024 11:30:34 +0300 Subject: [PATCH 28/72] Update README.md --- README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3208e23..157961d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,92 @@ -# Ankaios Python SDK + + + + Shows Ankaios logo + -Eclipse Ankaios Python SDK provides a convenient python interface for interacting with the Ankaios platform. +# Ankaios Python SDK for Eclipse Ankaios + +Eclipse Ankaios provides workload and container orchestration for automotive +High Performance Computing Software (HPCs). While it can be used for various +fields of applications, it is developed from scratch for automotive use cases +and provides a slim yet powerful solution to manage containerized applications. + +The Python SDK provides easy access from the container (workload) point-of-view +to manage the Ankaios system. A workload can use the Python SDK to run other workloads +and get the state of the Ankaios system. + +## Installation + +### Install via pip + +```sh +pip install ankaios-sdk +``` + +### Clone and Local Build + +```sh +# Clone repository +git clone https://github.com/eclipse-ankaios/ank-sdk-python.git +cd ank-sdk-python + +# Install in editable mode +pip install -e . + +# If you plan on contributing or running tests locally +pip install -e .[dev] +``` + +## Usage + +After installation, you can use the Ankaios SDK to configure and run workloads and request +the state of the Ankaios system and the connected agents. For more information, you can check +the documentation (TBD). + +Example: +```python +from ankaios_sdk import Workload, Ankaios, WorkloadStateEnum, WorkloadSubStateEnum + +# Connect to control interface +with Ankaios() as ankaios: + # Create a new workload + workload = Workload.builder() \ + .workload_name("dynamic_nginx") \ + .agent_name("agent_A") \ + .runtime("podman") \ + .restart_policy("NEVER") \ + .runtime_config("image: docker.io/library/nginx\ncommandOptions: [\"-p\", \"8080:80\"]") \ + .build() + + # Run the workload + ankaios.run_workload(workload) + + # Request the state of the system, filtered with the current workload + complete_state = ankaios.get_state( + timeout=5, + field_mask=["workloadStates.agent_A.dynamic_nginx"]) + + # Get the workload states present in the complete_state + workload_states_dict = complete_state.get_workload_states().get_as_dict() + + # Get the state of the desired workload + dynamic_nginx_state = workload_states_dict["agent_A"]["dynamic_nginx"].values()[0] + + # Check state + if dynamic_nginx_state.state == WorkloadStateEnum.RUNNING and + dynamic_nginx_state.substate == WorkloadSubStateEnum.RUNNING_OK: + print("Workload started running succesfully") + elif dynamic_nginx_state.state == WorkloadStateEnum.FAILED: + print("Workload failed with the following substate: {}".format( + dynamic_nginx_state.substate.name + )) +``` + +## Contributing + +This project welcomes contributions and suggestions. Before contributing, make sure to read the +[contribution guideline](CONTRIBUTING.md). + +## License + +Ankaios Python SDK is licensed using the Apache License Version 2.0. From cf5bf94d5ef67366de8304e45c328c4cff1172f9 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 9 Oct 2024 19:42:40 +0300 Subject: [PATCH 29/72] Add documentation --- .gitignore | 5 +- ankaios_sdk/__init__.py | 23 +++-- ankaios_sdk/_components/__init__.py | 28 +++--- ankaios_sdk/_components/complete_state.py | 39 +++++--- ankaios_sdk/_components/manifest.py | 31 +++++-- ankaios_sdk/_components/request.py | 29 ++++-- ankaios_sdk/_components/response.py | 22 +++-- ankaios_sdk/_components/workload.py | 72 +++++++++------ ankaios_sdk/_components/workload_state.py | 105 +++++++++++----------- ankaios_sdk/_protos/__init__.py | 10 ++- ankaios_sdk/ankaios.py | 85 ++++++++++++------ docs/Makefile | 23 +++++ docs/make.bat | 35 ++++++++ docs/source/_static/.gitkeep | 0 docs/source/_templates/.gitkeep | 0 docs/source/ankaios.rst | 21 +++++ docs/source/complete_state.rst | 13 +++ docs/source/conf.py | 57 ++++++++++++ docs/source/index.rst | 34 +++++++ docs/source/manifest.rst | 13 +++ docs/source/request.rst | 13 +++ docs/source/response.rst | 22 +++++ docs/source/workload.rst | 24 +++++ docs/source/workload_state.rst | 58 ++++++++++++ generate_docs.sh | 18 ++++ setup.cfg | 7 ++ setup.py | 17 ++-- 27 files changed, 636 insertions(+), 168 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/_static/.gitkeep create mode 100644 docs/source/_templates/.gitkeep create mode 100644 docs/source/ankaios.rst create mode 100644 docs/source/complete_state.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/manifest.rst create mode 100644 docs/source/request.rst create mode 100644 docs/source/response.rst create mode 100644 docs/source/workload.rst create mode 100644 docs/source/workload_state.rst create mode 100755 generate_docs.sh diff --git a/.gitignore b/.gitignore index f34e6e9..dfbf9c4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ reports/ # Build directory ankaios_sdk.egg-info build/ -dist/ \ No newline at end of file +dist/ + +# Docs build dir +docs/build/ \ No newline at end of file diff --git a/ankaios_sdk/__init__.py b/ankaios_sdk/__init__.py index 48e7166..e31b39a 100644 --- a/ankaios_sdk/__init__.py +++ b/ankaios_sdk/__init__.py @@ -13,12 +13,25 @@ # SPDX-License-Identifier: Apache-2.0 """ -This module contains the ankaios_sdk package. -It exposes to the user all the classes available in the SDK. +Ankaios Python SDK -Imports: - Ankaios: The main SDK class. - All the other classes, available in the _components folder. +Classes +------- + +- Ankaios: + The main SDK class. +- Workload: + Represents a workload. +- WorkloadBuilder: + A builder class for workloads. +- WorkloadState: + Represents the state of a workload. +- WorkloadStateCollection: + A collection of workload states. +- Manifest: + Represents a workload manifest. +- CompleteState: + Represents the complete state of the system. """ from .ankaios import * diff --git a/ankaios_sdk/_components/__init__.py b/ankaios_sdk/_components/__init__.py index 704dbee..b676e43 100644 --- a/ankaios_sdk/_components/__init__.py +++ b/ankaios_sdk/_components/__init__.py @@ -16,19 +16,21 @@ This module initializes the ankaios_sdk package by importing all necessary components. -Imports: - Workload component: responsible for defining the - workload of the system. - WorkloadState component: responsible for accessing - the state of the workload. - CompleteState component: responsible for accessing - the complete state of the system. - Request component: responsible for defining a request - to be sent to the system. - Response component: responsible for defining a response - from the system. - Manifest component: responsible for defining a manifest - object. +Imports +------- + +- Workload component: + responsible for defining the workload of the system. +- WorkloadState component: + responsible for accessing the state of the workload. +- CompleteState component: + responsible for accessing the complete state of the system. +- Request component: + responsible for defining a request to be sent to the system. +- Response component: + responsible for defining a response from the system. +- Manifest component: + responsible for defining a manifest object. """ from .workload import * diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index a16a263..5be64ef 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -16,29 +16,48 @@ This script defines the CompleteState class for managing the state of the system. -Classes: - - CompleteState: Represents the complete state of the system. +Classes +------- + +- CompleteState: + Represents the complete state of the system. + +Usage +----- + +- Create a CompleteState instance: + .. code-block:: python -Usage: - - Create a CompleteState instance: complete_state = CompleteState() - - Get the API version of the complete state: +- Get the API version of the complete state: + .. code-block:: python + api_version = complete_state.get_api_version() - - Add a workload to the complete state: +- Add a workload to the complete state: + .. code-block:: python + complete_state.set_workload(workload) - - Get a workload from the complete state: +- Get a workload from the complete state: + .. code-block:: python + workload = complete_state.get_workload("nginx") - - Get a list of workloads from the complete state: +- Get a list of workloads from the complete state: + .. code-block:: python + workloads = complete_state.get_workloads() - - Get the connected agents: +- Get the connected agents: + .. code-block:: python + agents = complete_state.get_agents() - - Get the workload states: +- Get the workload states: + .. code-block:: python + workload_states = complete_state.get_workload_states() """ diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index 67db6bc..c93b136 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -15,21 +15,34 @@ """ This module defines the Manifest class for handling ankaios manifests. -Classes: - - Manifest: Represents a workload manifest and provides methods to - validate and load it. +Classes +------- + +- Manifest: + Represents a workload manifest and provides methods \ + to validate and load it. + +Usage +----- + +- Load a manifest from a file: + .. code-block:: python -Usage: - - Load a manifest from a file: manifest = Manifest.from_file("path/to/manifest.yaml") - - Load a manifest from a string: - manifest = Manifest.from_string("apiVersion: 1.0\nworkloads: {}") +- Load a manifest from a string: + .. code-block:: python + + manifest = Manifest.from_string("apiVersion: 1.0\\nworkloads: {}") + +- Load a manifest from a dictionary: + .. code-block:: python - - Load a manifest from a dictionary: manifest = Manifest.from_dict({"apiVersion": "1.0", "workloads": {}}) - - Generate a CompleteState instance from the manifest: +- Generate a CompleteState instance from the manifest: + .. code-block:: python + complete_state = manifest.generate_complete_state() """ diff --git a/ankaios_sdk/_components/request.py b/ankaios_sdk/_components/request.py index 0aacc95..0c87696 100644 --- a/ankaios_sdk/_components/request.py +++ b/ankaios_sdk/_components/request.py @@ -16,22 +16,35 @@ This module defines the Request class for creating and handling requests to the Ankaios system. -Classes: - Request: Represents a request to the Ankaios system and provides - methods to get and set the state of the system. +Classes +------- + +- Request: + Represents a request to the Ankaios system and provides \ + methods to get and set the state of the system. + +Usage +----- + +- Create a Request for updating the state: + .. code-block:: python -Usage: - - Create a Request for updating the state: request = Request(request_type="update_state") request.set_complete_state(complete_state) - - Create a Request for getting the state: +- Create a Request for getting the state: + .. code-block:: python + request = Request(request_type="get_state") - - Get the request ID: +- Get the request ID: + .. code-block:: python + request_id = request.get_id() - - Add a mask to the request: +- Add a mask to the request: + .. code-block:: python + request.add_mask("desiredState.workloads") """ diff --git a/ankaios_sdk/_components/response.py b/ankaios_sdk/_components/response.py index f3241d7..4fe0399 100644 --- a/ankaios_sdk/_components/response.py +++ b/ankaios_sdk/_components/response.py @@ -16,16 +16,26 @@ This script defines the Response and ResponseEvent classes, used for receiving messages from the control interface. -Classes: - - Response: Represents a response from the control interface. - - ResponseEvent: Represents an event used to wait for a response. +Classes +-------- + +- Response: + Represents a response from the control interface. +- ResponseEvent: + Represents an event used to wait for a response. + +Usage +------ + +- Get response content: + .. code-block:: python -Usage: - - Get response content: response = Response() (content_type, content) = response.get_content() - - Check if the request_id matches: +- Check if the request_id matches: + .. code-block:: python + response = Response() if response.check_request_id("1234"): print("Request ID matches") diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index fd9a82d..ea6ecc6 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -16,43 +16,58 @@ This script defines the Workload and WorkloadBuilder classes for creating and managing workloads. -Classes: - - Workload: Represents a workload with various attributes and - methods to update them. - - WorkloadBuilder: A builder class to create a Workload object - with a fluent interface. - -Usage: - - Create a workload using the WorkloadBuilder: - workload = Workload.builder() \ - .workload_name("nginx") \ - .agent_name("agent_A") \ - .runtime("podman") \ - .restart_policy("NEVER") \ - .runtime_config("image: docker.io/library/nginx\n" - + "commandOptions: [\"-p\", \"8080:80\"]") \ - .add_dependency("other_workload", "ADD_COND_RUNNING") \ - .add_tag("key1", "value1") \ - .add_tag("key2", "value2") \ +Classes +-------- + +- Workload: + Represents a workload with various attributes and methods to update them. +- WorkloadBuilder: + A builder class to create a Workload object with a fluent interface. + +Usage +------ + +- Create a workload using the WorkloadBuilder: + .. code-block:: python + + workload = Workload.builder() + .workload_name("nginx") + .agent_name("agent_A") + .runtime("podman") + .restart_policy("NEVER") + .runtime_config("image: docker.io/library/nginx\\n" + + "commandOptions: [\"-p\", \"8080:80\"]") + .add_dependency("other_workload", "ADD_COND_RUNNING") + .add_tag("key1", "value1") + .add_tag("key2", "value2") .build() - - Update fields of the workload: +- Update fields of the workload: + .. code-block:: python + workload.update_agent_name("agent_B") - - Update dependencies: +- Update dependencies: + .. code-block:: python + deps = workload.get_dependencies() deps["other_workload"] = "ADD_COND_SUCCEEDED" workload.update_dependencies(deps) - - Update tags: +- Update tags: + .. code-block:: python + tags = workload.get_tags() tags.pop("key1") workload.update_tags(tags) - - Print the updated workload: +- Print the updated workload: + .. code-block:: python + print(workload) """ + __all__ = ["Workload", "WorkloadBuilder"] @@ -70,6 +85,7 @@ class Workload: def __init__(self, name: str) -> None: """ Initialize a Workload object. + The Workload object should be created using the Workload.builder() method. @@ -153,7 +169,7 @@ def update_runtime_config_from_file(self, config_file: str) -> None: def update_restart_policy(self, policy: str) -> None: """ Set the restart policy for the workload. - Supported values: 'NEVER', 'ON_FAILURE', 'ALWAYS'. + Supported values: `NEVER`, `ON_FAILURE`, `ALWAYS`. Args: policy (str): The restart policy to update. @@ -170,8 +186,8 @@ def update_restart_policy(self, policy: str) -> None: def add_dependency(self, workload_name: str, condition: str) -> None: """ Add a dependency to the workload. - Supported values: 'ADD_COND_RUNNING', 'ADD_COND_SUCCEEDED', - 'ADD_COND_FAILED'. + Supported values: `ADD_COND_RUNNING`, `ADD_COND_SUCCEEDED`, + `ADD_COND_FAILED`. Args: workload_name (str): The name of the dependent workload. @@ -192,7 +208,7 @@ def get_dependencies(self) -> dict: Return the dependencies of the workload. Returns: - dict: A dictionary of dependencies with workload names + dict: A dictionary of dependencies with workload names \ as keys and conditions as values. """ deps = dict(self._workload.dependencies.dependencies) @@ -253,10 +269,10 @@ def add_allow_rule( ) -> None: """ Add an allow rule to the workload. + Supported values: `Nothing`, `Write`, `Read`, `ReadWrite`. Args: operation (str): The operation the rule allows. - Allowed values: 'Nothing', 'Write', 'Read', 'ReadWrite'. filter_masks (list): The list of filter masks. Raises: @@ -323,10 +339,10 @@ def add_deny_rule( ) -> None: """ Add a deny rule to the workload. + Supported values: `Nothing`, `Write`, `Read`, `ReadWrite`. Args: operation (str): The operation the rule denies. - Allowed values: 'Nothing', 'Write', 'Read', 'ReadWrite'. filter_masks (list): The list of filter masks. Raises: diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index efc83e7..88e1b02 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -18,25 +18,39 @@ and sub-states of workloads, including converting between different representations and handling collections of workload states. -Classes: - - WorkloadExecutionState: Represents the execution state and - sub-state of a workload. - - WorkloadInstanceName: Represents the name of a workload instance. - - WorkloadState: Represents the state of a workload - (execution state and name). - - WorkloadStateCollection: A collection of workload states. - -Enums: - - WorkloadStateEnum: Enumeration for different states of a workload. - - WorkloadSubStateEnum: Enumeration for different sub-states of a workload. - -Usage: - - Get all workload states: +Classes +------- + +- WorkloadExecutionState + Represents the execution state and sub-state of a workload. +- WorkloadInstanceName: + Represents the name of a workload instance. +- WorkloadState: + Represents the state of a workload (execution state and name). +- WorkloadStateCollection: + A collection of workload states. + +Enums +----- + +- WorkloadStateEnum: + Enumeration for different states of a workload. +- WorkloadSubStateEnum: + Enumeration for different sub-states of a workload. + +Usage +----- + +- Get all workload states: + .. code-block:: python + workload_state_collection = WorkloadStateCollection() list_of_workload_states = workload_state_collection.get_as_list() dict_of_workload_states = workload_state_collection.get_as_dict() - - Unpack a workload state: +- Unpack a workload state: + .. code-block:: python + workload_state = WorkloadState() agent_name = workload_state.workload_instance_name.agent_name workload_name = workload_state.workload_instance_name.workload_name @@ -55,27 +69,23 @@ class WorkloadStateEnum(Enum): - """ - Enumeration for different states of a workload. - - Attributes: - AgentDisconnected (int): The agent is disconnected. - Pending (int): The workload is pending. - Running (int): The workload is running. - Stopping (int): The workload is stopping. - Succeeded (int): The workload has succeeded. - Failed (int): The workload has failed. - NotScheduled (int): The workload is not scheduled. - Removed (int): The workload has been removed. - """ + """ Enumeration for different states of a workload. """ AGENT_DISCONNECTED: int = 0 + "(int): The agent is disconnected." PENDING: int = 1 + "(int): The workload is pending." RUNNING: int = 2 + "(int): The workload is running." STOPPING: int = 3 + "(int): The workload is stopping." SUCCEEDED: int = 4 + "(int): The workload has succeeded." FAILED: int = 5 + "(int): The workload has failed." NOT_SCHEDULED: int = 6 + "(int): The workload is not scheduled." REMOVED: int = 7 + "(int): The workload has been removed." def __str__(self) -> str: """ @@ -111,44 +121,39 @@ def _get(field: str) -> "WorkloadStateEnum": class WorkloadSubStateEnum(Enum): - """ - Enumeration for different sub-states of a workload. - - Attributes: - AGENT_DISCONNECTED (int): The agent is disconnected. - PENDING_INITIAL (int): The workload is in the initial pending state. - PENDING_WAITING_TO_START (int): The workload is waiting to start. - PENDING_STARTING (int): The workload is starting. - PENDING_STARTING_FAILED (int): The workload failed to start. - RUNNING_OK (int): The workload is running successfully. - STOPPING (int): The workload is stopping. - STOPPING_WAITING_TO_STOP (int): The workload is waiting to stop. - STOPPING_REQUESTED_AT_RUNTIME (int): The workload stop was - requested at runtime. - STOPPING_DELETE_FAILED (int): The workload stop failed to delete. - SUCCEEDED_OK (int): The workload succeeded successfully. - FAILED_EXEC_FAILED (int): The workload failed due to execution failure. - FAILED_UNKNOWN (int): The workload failed due to an unknown reason. - FAILED_LOST (int): The workload failed because it was lost. - NOT_SCHEDULED (int): The workload is not scheduled. - REMOVED (int): The workload has been removed. - """ + """ Enumeration for different sub-states of a workload. """ AGENT_DISCONNECTED: int = 0 + "(int): The agent is disconnected." PENDING_INITIAL: int = 1 + "(int): The workload is in the initial pending state." PENDING_WAITING_TO_START: int = 2 + "(int): The workload is waiting to start." PENDING_STARTING: int = 3 + "(int): The workload is starting." PENDING_STARTING_FAILED: int = 4 + "(int): The workload failed to start." RUNNING_OK: int = 5 + "(int): The workload is running successfully." STOPPING: int = 6 + "(int): The workload is stopping." STOPPING_WAITING_TO_STOP: int = 7 + "(int): The workload is waiting to stop." STOPPING_REQUESTED_AT_RUNTIME: int = 8 + "(int): The workload stop was requested at runtime." STOPPING_DELETE_FAILED: int = 9 + "(int): The workload stop failed to delete." SUCCEEDED_OK: int = 10 + "(int): The workload succeeded successfully." FAILED_EXEC_FAILED: int = 11 + "(int): The workload failed due to execution failure." FAILED_UNKNOWN: int = 12 + "(int): The workload failed due to an unknown reason." FAILED_LOST: int = 13 + "(int): The workload failed because it was lost." NOT_SCHEDULED: int = 14 + "(int): The workload is not scheduled." REMOVED: int = 15 + "(int): The workload has been removed." def __str__(self) -> str: """ diff --git a/ankaios_sdk/_protos/__init__.py b/ankaios_sdk/_protos/__init__.py index 191fec6..c0a0c94 100644 --- a/ankaios_sdk/_protos/__init__.py +++ b/ankaios_sdk/_protos/__init__.py @@ -16,9 +16,13 @@ This module contains the ankaios_sdk protobuf components. It contains the proto files and the generated protobuf classes. -Imports: - ank_base_pb2: Used for general grpc messages. - control_api_pb2: Used for exchanging messages with the control interface. +Imports +------- + +- ank_base_pb2: + Used for general grpc messages. +- control_api_pb2: + Used for exchanging messages with the control interface. """ try: diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index dea5530..69e8fe6 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -16,43 +16,76 @@ This script defines the Ankaios class for interacting with the Ankaios control interface. -Classes: - - Ankaios: Handles the interaction with the Ankaios control interface. +Classes +------- + +- Ankaios: + Handles the interaction with the Ankaios control interface. + +Enums +----- + +- AnkaiosLogLevel: + Represents the log levels for the Ankaios class. + +Usage +----- + +- Create an Ankaios object and connect to the control interface: + .. code-block:: python -Usage: - - Create an Ankaios object and connect to the control interface: with Ankaios() as ankaios: pass - - Apply a manifest: +- Apply a manifest: + .. code-block:: python + ankaios.apply_manifest(manifest) - - Delete a manifest: +- Delete a manifest: + .. code-block:: python + ankaios.delete_manifest(manifest) - - Run a workload: +- Run a workload: + .. code-block:: python + ankaios.run_workload(workload) - - Delete a workload: +- Delete a workload: + .. code-block:: python + ankaios.delete_workload(workload_name) - - Get a workload: +- Get a workload: + .. code-block:: python + workload = ankaios.get_workload(workload_name) - - Get the state: +- Get the state: + .. code-block:: python + state = ankaios.get_state() - - Get the agents: +- Get the agents: + .. code-block:: python + agents = ankaios.get_agents() - - Get the workload states: +- Get the workload states: + .. code-block:: python + workload_states = ankaios.get_workload_states() - - Get the workload states on an agent: +- Get the workload states on an agent: + .. code-block:: python + workload_states = ankaios.get_workload_states_on_agent(agent_name) - - Get the workload states on a workload name: - workload_states = \ +- Get the workload states on a workload name: + .. code-block:: python + + workload_states = ankaios.get_workload_states_on_workload_name(workload_name) """ @@ -71,21 +104,17 @@ class AnkaiosLogLevel(Enum): - """ - Ankaios log levels. - - Attributes: - FATAL (int): Fatal log level. - ERROR (int): Error log level. - WARN (int): Warning log level. - INFO (int): Info log level. - DEBUG (int): Debug log level. - """ + """ Ankaios log levels. """ FATAL = logging.FATAL + "(int): Fatal log level." ERROR = logging.ERROR + "(int): Error log level." WARN = logging.WARN + "(int): Warning log level." INFO = logging.INFO + "(int): Info log level." DEBUG = logging.DEBUG + "(int): Debug log level." # pylint: disable=too-many-public-methods @@ -96,14 +125,14 @@ class Ankaios: by sending requests. Attributes: - ANKAIOS_CONTROL_INTERFACE_BASE_PATH (str): The base path for the - Ankaios control interface. - DEFAULT_TIMEOUT (int): The default timeout, if not manually provided. logger (logging.Logger): The logger for the Ankaios class. path (str): The path to the control interface. """ ANKAIOS_CONTROL_INTERFACE_BASE_PATH = "/run/ankaios/control_interface" + "(str): The base path for the Ankaios control interface." + DEFAULT_TIMEOUT = 5.0 + "(float): The default timeout, if not manually provided." def __init__(self) -> None: """Initialize the Ankaios object.""" diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..49fddba --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,23 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +open: + python3 -m http.server -d build/html 8000 diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_static/.gitkeep b/docs/source/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/_templates/.gitkeep b/docs/source/_templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/ankaios.rst b/docs/source/ankaios.rst new file mode 100644 index 0000000..e502df4 --- /dev/null +++ b/docs/source/ankaios.rst @@ -0,0 +1,21 @@ +Ankaios +======= + +.. automodule:: ankaios_sdk.ankaios + +Ankaios Class +------------- + +.. autoclass:: ankaios_sdk.ankaios.Ankaios + :special-members: __init__, __enter__, __exit__ + :members: + :undoc-members: + :show-inheritance: + +AnkaiosLogLevel Enum +--------------------- + +.. autoclass:: ankaios_sdk.ankaios.AnkaiosLogLevel + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/complete_state.rst b/docs/source/complete_state.rst new file mode 100644 index 0000000..510eca3 --- /dev/null +++ b/docs/source/complete_state.rst @@ -0,0 +1,13 @@ +Complete State +============== + +.. automodule:: ankaios_sdk._components.complete_state + +CompleteState Class +------------------- + +.. autoclass:: ankaios_sdk._components.complete_state.CompleteState + :special-members: __init__, __str__ + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..b2d884b --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,57 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) + +# -- Read the setup.cfg file ------------------------------------------------- +import configparser + +config = configparser.ConfigParser() +config.read(os.path.join(os.path.dirname(__file__), '..', '..', 'setup.cfg')) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = config['metadata']['name'] +author = config['metadata']['author'] +copyright = f'2024, {author}' +version = config['metadata']['version'] +license = config['metadata']['license'] +language = 'en' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', # Automatically documents your Python code + 'sphinx.ext.napoleon', # Supports NumPy and Google-style docstrings + 'sphinx_autodoc_typehints', # Handles type hints + 'sphinx.ext.viewcode', # Adds links to the source code in the documentation + 'sphinx_mdinclude', # For reading md files +] + +templates_path = ['_templates'] +exclude_patterns = [] +autodoc_member_order = 'bysource' + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# -- Prepare the ReadMe file - skip the image ---------------------------------- +read_me_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'README.md')) +read_me_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'README.md')) +with open(read_me_in, 'r') as f: + readme = f.readlines() + +for i, line in enumerate(readme): + if "" in line: + with open(read_me_out, 'w') as f: + f.writelines(readme[(i+1):]) + break diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..c9810cf --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,34 @@ +.. raw:: html + + + + Shows Ankaios logo + + +.. mdinclude:: ../build/README.md + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + ankaios + complete_state + workload + workload_state + manifest + request + response + +.. toctree:: + :maxdepth: 2 + :caption: Links: + + Ankaios Github + Ankaios Python SDK Github + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/manifest.rst b/docs/source/manifest.rst new file mode 100644 index 0000000..37aec02 --- /dev/null +++ b/docs/source/manifest.rst @@ -0,0 +1,13 @@ +Manifest +======== + +.. automodule:: ankaios_sdk._components.manifest + +Manifest Class +-------------- + +.. autoclass:: ankaios_sdk._components.manifest.Manifest + :special-members: __init__ + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/request.rst b/docs/source/request.rst new file mode 100644 index 0000000..59f28d1 --- /dev/null +++ b/docs/source/request.rst @@ -0,0 +1,13 @@ +Request +======= + +.. automodule:: ankaios_sdk._components.request + +Request Class +------------- + +.. autoclass:: ankaios_sdk._components.request.Request + :special-members: __init__, __str__ + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/response.rst b/docs/source/response.rst new file mode 100644 index 0000000..3f18e18 --- /dev/null +++ b/docs/source/response.rst @@ -0,0 +1,22 @@ +Response +======== + +.. automodule:: ankaios_sdk._components.response + +Response Class +-------------- + +.. autoclass:: ankaios_sdk._components.response.Response + :special-members: __init__ + :members: + :undoc-members: + :show-inheritance: + +ResponseEvent Class +------------------- + +.. autoclass:: ankaios_sdk._components.response.ResponseEvent + :special-members: __init__ + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/workload.rst b/docs/source/workload.rst new file mode 100644 index 0000000..03b9bb3 --- /dev/null +++ b/docs/source/workload.rst @@ -0,0 +1,24 @@ +Workload +======== + +.. automodule:: ankaios_sdk._components.workload + +Workload Class +-------------- +.. _workload-class: + +.. autoclass:: ankaios_sdk._components.workload.Workload + :special-members: __init__, __str__ + :members: + :undoc-members: + :show-inheritance: + +WorkloadBuilder Class +--------------------- +.. _workloadbuilder-class: + +.. autoclass:: ankaios_sdk._components.workload.WorkloadBuilder + :special-members: __init__ + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/workload_state.rst b/docs/source/workload_state.rst new file mode 100644 index 0000000..96948f1 --- /dev/null +++ b/docs/source/workload_state.rst @@ -0,0 +1,58 @@ +Workload State +============== + +.. automodule:: ankaios_sdk._components.workload_state + +WorkloadExecutionState Class +---------------------------- + +.. autoclass:: ankaios_sdk._components.workload_state.WorkloadExecutionState + :special-members: __init__ + :members: + :undoc-members: + :show-inheritance: + +WorkloadInstanceName Class +-------------------------- + +.. autoclass:: ankaios_sdk._components.workload_state.WorkloadInstanceName + :special-members: __init__, __str__ + :members: + :undoc-members: + :show-inheritance: + +WorkloadState Class +------------------- + +.. autoclass:: ankaios_sdk._components.workload_state.WorkloadState + :special-members: __init__ + :members: + :undoc-members: + :show-inheritance: + +WorkloadStateCollection Class +----------------------------- + +.. autoclass:: ankaios_sdk._components.workload_state.WorkloadStateCollection + :special-members: __init__ + :members: + :undoc-members: + :show-inheritance: + +WorkloadStateEnum Enum +---------------------- + +.. autoclass:: ankaios_sdk._components.workload_state.WorkloadStateEnum + :special-members: __str__ + :members: + :undoc-members: + :show-inheritance: + +WorkloadSubStateEnum Enum +------------------------- + +.. autoclass:: ankaios_sdk._components.workload_state.WorkloadSubStateEnum + :special-members: __str__ + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/generate_docs.sh b/generate_docs.sh new file mode 100755 index 0000000..d039889 --- /dev/null +++ b/generate_docs.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +python3 -m venv docs_env +source docs_env/bin/activate + +pip install .[docs] + +cd docs + +# make html +sphinx-build -b html source build + +cd .. + +deactivate +rm -rf docs_env + +echo "Documentation generated successfully in docs/build" diff --git a/setup.cfg b/setup.cfg index 09a517f..361ca7a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,10 @@ +[metadata] +name = ankaios-sdk +version = 0.1.0 +author= Elektrobit Automotive GmbH and Ankaios contributors +# author_email = +license = Apache-2.0 + [tool:pytest] testpaths = tests diff --git a/setup.py b/setup.py index b9879ec..4d59b8e 100644 --- a/setup.py +++ b/setup.py @@ -15,14 +15,14 @@ import os from setuptools import setup, find_packages -PROJECT_NAME = "ankaios_sdk" +PROJECT_DIR = "ankaios_sdk" def generate_protos(): """Generate python protobuf files from the proto files.""" from grpc_tools import protoc - protos_dir = f"{PROJECT_NAME}/_protos" + protos_dir = f"{PROJECT_DIR}/_protos" proto_files = ["ank_base.proto", "control_api.proto"] for proto_file in proto_files: @@ -54,11 +54,6 @@ def generate_protos(): setup( - name=PROJECT_NAME.replace("_", "-"), - version="0.1.0", - license="Apache-2.0", - author="Elektrobit Automotive GmbH and Ankaios contributors", - # author_email="", description="Eclipse Ankaios Python SDK - provides a convenient Python interface for interacting with the Ankaios platform.", long_description=open('README.md').read(), long_description_content_type="text/markdown", @@ -93,6 +88,14 @@ def generate_protos(): 'pylint', 'pycodestyle', ], + # Documentation dependencies + 'docs': [ + 'sphinx', + 'sphinx-rtd-theme', + 'sphinx-autodoc-typehints', + 'sphinx-mdinclude', + 'google-api-python-client', + ], }, ) From 885cf2d51a02020885c18b2f0709ab5121158bbc Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 10 Oct 2024 10:15:32 +0300 Subject: [PATCH 30/72] Fix findings --- .gitignore | 2 +- ankaios_sdk/_components/request.py | 12 +++ ankaios_sdk/ankaios.py | 114 ++++++++++++++++++++--------- setup.cfg | 2 +- tests/test_ankaios.py | 42 +++++------ 5 files changed, 113 insertions(+), 59 deletions(-) diff --git a/.gitignore b/.gitignore index dfbf9c4..cf49b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ build/ dist/ # Docs build dir -docs/build/ \ No newline at end of file +docs/build/ diff --git a/ankaios_sdk/_components/request.py b/ankaios_sdk/_components/request.py index 0c87696..aa1aeb4 100644 --- a/ankaios_sdk/_components/request.py +++ b/ankaios_sdk/_components/request.py @@ -127,6 +127,18 @@ def add_mask(self, mask: str) -> None: elif self._request_type == "get_state": self._request.completeStateRequest.fieldMask.append(mask) + def set_masks(self, masks: list) -> None: + """ + Sets the update masks for the request. + + Args: + masks (list): The masks to set for the request. + """ + if self._request_type == "update_state": + self._request.updateStateRequest.updateMask[:] = masks + elif self._request_type == "get_state": + self._request.completeStateRequest.fieldMask[:] = masks + def _to_proto(self) -> _ank_base.Request: """ Converts the Request object to a proto message. diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 69e8fe6..47a14f1 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -40,22 +40,26 @@ - Apply a manifest: .. code-block:: python - ankaios.apply_manifest(manifest) + if ankaios.apply_manifest(manifest): + print("Manifest applied successfully.") - Delete a manifest: .. code-block:: python - ankaios.delete_manifest(manifest) + if ankaios.delete_manifest(manifest): + print("Manifest deleted successfully.") - Run a workload: .. code-block:: python - ankaios.run_workload(workload) + if ankaios.run_workload(workload): + print("Workload started successfully.") - Delete a workload: .. code-block:: python - ankaios.delete_workload(workload_name) + if ankaios.delete_workload(workload_name): + print("Workload deleted successfully.") - Get a workload: .. code-block:: python @@ -120,9 +124,9 @@ class AnkaiosLogLevel(Enum): # pylint: disable=too-many-public-methods class Ankaios: """ - A class to interact with the Ankaios control interface. It provides - the functionality to interact with the Ankaios control interface - by sending requests. + This class is used to interact with the Ankaios using an intuitive API. + The class automatically handles the session creation and the requests + and responses sent and received over the Ankaios Control Interface. Attributes: logger (logging.Logger): The logger for the Ankaios class. @@ -153,7 +157,7 @@ def __enter__(self) -> "Ankaios": Returns: Ankaios: The Ankaios object. """ - self.connect() + self._connect() return self def __exit__(self, exc_type, exc_value, traceback) -> None: @@ -168,7 +172,7 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: if exc_type is not None: # pragma: no cover self.logger.error("An exception occurred: %s, %s, %s", exc_type, exc_value, traceback) - self.disconnect() + self._disconnect() def _create_logger(self) -> None: """Create a logger with custom format and default log level.""" @@ -308,7 +312,7 @@ def set_logger_level(self, level: AnkaiosLogLevel) -> None: """ self.logger.setLevel(level.value) - def connect(self) -> None: + def _connect(self) -> None: """ Connect to the control interface by starting to read from the input fifo. @@ -324,7 +328,7 @@ def connect(self) -> None: ) self._read_thread.start() - def disconnect(self) -> None: + def _disconnect(self) -> None: """ Disconnect from the control interface by stopping to read from the input fifo. @@ -337,24 +341,27 @@ def disconnect(self) -> None: self._connected = False self._read_thread.join() - def apply_manifest(self, manifest: Manifest) -> None: + def apply_manifest(self, manifest: Manifest) -> bool: """ Send a request to apply a manifest. Args: manifest (Manifest): The manifest object to be applied. + + Returns: + bool: True if the manifest was applied successfully, + False otherwise. """ request = Request(request_type="update_state") request.set_complete_state(manifest.generate_complete_state()) - for mask in manifest._calculate_masks(): - request.add_mask(mask) + request.set_masks(manifest._calculate_masks()) # Send request try: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return + return False # Interpret response (content_type, content) = response.get_content() @@ -367,25 +374,30 @@ def apply_manifest(self, manifest: Manifest) -> None: + "%s deleted workloads.", content["added_workloads"], content["deleted_workloads"] ) + return True + return False - def delete_manifest(self, manifest: Manifest) -> None: + def delete_manifest(self, manifest: Manifest) -> bool: """ Send a request to delete a manifest. Args: manifest (Manifest): The manifest object to be deleted. + + Returns: + bool: True if the manifest was deleted successfully, + False otherwise. """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) - for mask in manifest._calculate_masks(): - request.add_mask(mask) + request.set_masks(manifest._calculate_masks()) # Send request try: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return + return False # Interpret response (content_type, content) = response.get_content() @@ -398,13 +410,18 @@ def delete_manifest(self, manifest: Manifest) -> None: + "%s deleted workloads.", content["added_workloads"], content["deleted_workloads"] ) + return True + return False - def run_workload(self, workload: Workload) -> None: + def run_workload(self, workload: Workload) -> bool: """ Send a request to run a workload. Args: workload (Workload): The workload object to be run. + + Returns: + bool: True if the workload was run successfully, False otherwise. """ complete_state = CompleteState() complete_state.set_workload(workload) @@ -412,15 +429,14 @@ def run_workload(self, workload: Workload) -> None: # Create the request request = Request(request_type="update_state") request.set_complete_state(complete_state) - for mask in workload.masks: - request.add_mask(mask) + request.set_masks(workload.masks) # Send request try: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return + return False # Interpret response (content_type, content) = response.get_content() @@ -433,13 +449,19 @@ def run_workload(self, workload: Workload) -> None: + "%s deleted workloads.", content["added_workloads"], content["deleted_workloads"] ) + return True + return False - def delete_workload(self, workload_name: str) -> None: + def delete_workload(self, workload_name: str) -> bool: """ Send a request to delete a workload. Args: workload_name (str): The name of the workload to be deleted. + + Returns: + bool: True if the workload was deleted successfully, + False otherwise. """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) @@ -449,7 +471,7 @@ def delete_workload(self, workload_name: str) -> None: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return + return False # Interpret response (content_type, content) = response.get_content() @@ -462,6 +484,8 @@ def delete_workload(self, workload_name: str) -> None: + "%s deleted workloads.", content["added_workloads"], content["deleted_workloads"] ) + return True + return False def get_workload(self, workload_name: str, state: CompleteState = None, @@ -484,7 +508,7 @@ def get_workload(self, workload_name: str, ) return state.get_workload(workload_name) if state is not None else None - def set_configs_from_file(self, configs_path: str) -> None: + def set_configs_from_file(self, configs_path: str) -> bool: """ Set the configs from a file. The configs file should have a dictionary as the top level object. @@ -492,6 +516,9 @@ def set_configs_from_file(self, configs_path: str) -> None: Args: config_path (str): The path to the configs file. + + Returns: + bool: True if the configs were set successfully, False otherwise. """ # with open(configs_path, "r", encoding="utf-8") as f: # configs = f.read() @@ -500,16 +527,19 @@ def set_configs_from_file(self, configs_path: str) -> None: "set_configs_from_file is not implemented yet." ) - def set_configs(self, configs: dict) -> None: + def set_configs(self, configs: dict) -> bool: """ Set the configs. The names will be the keys of the dictionary. Args: configs (dict): The configs dictionary. + + Returns: + bool: True if the configs were set successfully, False otherwise. """ raise NotImplementedError("set_configs is not implemented yet.") - def set_config_from_file(self, name: str, config_path: str) -> None: + def set_config_from_file(self, name: str, config_path: str) -> bool: """ Set the config from a file, with the provided name. If the config exists, it will be replaced. @@ -517,12 +547,15 @@ def set_config_from_file(self, name: str, config_path: str) -> None: Args: name (str): The name of the config. config_path (str): The path to the config file. + + Returns: + bool: True if the config was set successfully, False otherwise. """ raise NotImplementedError( "set_config_from_file is not implemented yet." ) - def set_config(self, name: str, config: Union[dict, list, str]) -> None: + def set_config(self, name: str, config: Union[dict, list, str]) -> bool: """ Set the config with the provided name. If the config exists, it will eb replaced. @@ -530,6 +563,9 @@ def set_config(self, name: str, config: Union[dict, list, str]) -> None: Args: name (str): The name of the config. config (Union[dict, list, str]): The config dictionary. + + Returns: + bool: True if the config was set successfully, False otherwise. """ raise NotImplementedError("set_config is not implemented yet.") @@ -554,39 +590,45 @@ def get_config(self, name: str) -> Union[dict, list, str]: """ raise NotImplementedError("get_config is not implemented yet.") - def delete_configs(self) -> None: + def delete_configs(self) -> bool: """ Delete all the configs. + + Returns: + bool: True if the configs were deleted successfully, + False otherwise. """ raise NotImplementedError("delete_configs is not implemented yet.") - def delete_config(self, name: str) -> None: + def delete_config(self, name: str) -> bool: """ Delete the config. Args: name (str): The name of the config. + + Returns: + bool: True if the config was deleted successfully, False otherwise. """ raise NotImplementedError("delete_config is not implemented yet.") def get_state(self, timeout: float = DEFAULT_TIMEOUT, - field_mask: list[str] = None) -> CompleteState: + field_masks: list[str] = None) -> CompleteState: """ Send a request to get the complete state. Args: timeout (float): The maximum time to wait for the response, in seconds. - field_mask (list[str]): The list of field masks to filter + field_masks (list[str]): The list of field masks to filter the state. Returns: CompleteState: The complete state object. """ request = Request(request_type="get_state") - if field_mask is not None: - for mask in field_mask: - request.add_mask(mask) + if field_masks is not None: + request.set_masks(field_masks) try: response = self._send_request(request, timeout) except TimeoutError as e: diff --git a/setup.cfg b/setup.cfg index 361ca7a..a2b15b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,4 +25,4 @@ exclude_lines = directory = reports/coverage/html [coverage:xml] -output = reports/coverage/cov_report.xml \ No newline at end of file +output = reports/coverage/cov_report.xml diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 14f75bf..25ad378 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -60,7 +60,7 @@ def test_connection(): mock_thread_instance = MagicMock() mock_thread.return_value = mock_thread_instance - ankaios.connect() + ankaios._connect() mock_thread.assert_called_once_with( target=ankaios._read_from_control_interface ) @@ -68,14 +68,14 @@ def test_connection(): assert ankaios._connected with pytest.raises(ValueError, match="Already connected."): - ankaios.connect() + ankaios._connect() - ankaios.disconnect() + ankaios._disconnect() mock_thread_instance.join.assert_called_once() assert not ankaios._connected with pytest.raises(ValueError, match="Already disconnected."): - ankaios.disconnect() + ankaios._disconnect() with Ankaios() as ank: assert ank._connected @@ -97,10 +97,10 @@ def test_read_from_control_interface(): ankaios = Ankaios() # will call _read_from_control_interface - ankaios.connect() + ankaios._connect() # will stop the thread after reading the message - ankaios.disconnect() + ankaios._disconnect() mock_file.assert_called_once_with( f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") @@ -117,10 +117,10 @@ def test_read_from_control_interface(): ankaios._responses["1234"] = ResponseEvent() # will call _read_from_control_interface - ankaios.connect() + ankaios._connect() # will stop the thread after reading the message - ankaios.disconnect() + ankaios._disconnect() mock_file.assert_called_once_with( f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") @@ -207,21 +207,21 @@ def test_apply_manifest(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - ankaios.apply_manifest(manifest) + assert ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - ankaios.apply_manifest(manifest) + assert not ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - ankaios.apply_manifest(manifest) + assert not ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -238,21 +238,21 @@ def test_delete_manifest(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - ankaios.delete_manifest(manifest) + assert ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - ankaios.delete_manifest(manifest) + assert not ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - ankaios.delete_manifest(manifest) + assert not ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -269,21 +269,21 @@ def test_run_workload(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - ankaios.run_workload(workload) + assert ankaios.run_workload(workload) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - ankaios.run_workload(workload) + assert not ankaios.run_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - ankaios.run_workload(workload) + assert not ankaios.run_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -299,21 +299,21 @@ def test_delete_workload(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - ankaios.delete_workload("nginx") + assert ankaios.delete_workload("nginx") mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - ankaios.delete_workload("nginx") + assert not ankaios.delete_workload("nginx") mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - ankaios.delete_workload("nginx") + assert not ankaios.delete_workload("nginx") mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -408,7 +408,7 @@ def test_get_state(): # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - result = ankaios.get_state(field_mask=["invalid_mask"]) + result = ankaios.get_state(field_masks=["invalid_mask"]) mock_send_request.assert_called_once() assert result is None ankaios.logger.error.assert_called() From 276e508c9835e34dd23cac44c9915d8f4a14d00a Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 10 Oct 2024 10:59:51 +0300 Subject: [PATCH 31/72] Add automatic fetching of proto files from release --- setup.cfg | 1 + setup.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a2b15b8..d61aa56 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [metadata] name = ankaios-sdk version = 0.1.0 +ankaios_version = 0.5.0 author= Elektrobit Automotive GmbH and Ankaios contributors # author_email = license = Apache-2.0 diff --git a/setup.py b/setup.py index 4d59b8e..4b23e8d 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,35 @@ import os from setuptools import setup, find_packages +import configparser PROJECT_DIR = "ankaios_sdk" +ANKAIOS_RELEASE_LINK = "https://github.com/eclipse-ankaios/ankaios/releases/download/v{version}/{file}" +PROTO_FILES = ["ank_base.proto", "control_api.proto"] + +config = configparser.ConfigParser() +config.read(os.path.join(os.path.dirname(__file__), 'setup.cfg')) + + +def extract_the_proto_files(): + """ Download the proto files from the ankaios release branch. """ + import requests + + ankaios_version = config['metadata']['ankaios_version'] + + for file in PROTO_FILES: + file_url = ANKAIOS_RELEASE_LINK.format(version=ankaios_version, file=file) + file_path = f"{PROJECT_DIR}/_protos/{file}" + if os.path.exists(file_path): + continue + try: + response = requests.get(file_url) + response.raise_for_status() + with open(file_path, 'w', encoding="utf-8") as f: + f.write(response.text) + except requests.exceptions.RequestException as e: + print(f"Error: Failed to download {file} from {file_url}.") + raise e def generate_protos(): @@ -23,10 +50,12 @@ def generate_protos(): from grpc_tools import protoc protos_dir = f"{PROJECT_DIR}/_protos" - proto_files = ["ank_base.proto", "control_api.proto"] + proto_files = PROTO_FILES for proto_file in proto_files: proto_path = os.path.join(protos_dir, proto_file) + if not os.path.exists(proto_path): + raise Exception(f"Error: {proto_file} not found.") output_file = proto_path.replace('.proto', '_pb2.py') if not os.path.exists(output_file) or os.path.getmtime(proto_path) > os.path.getmtime(output_file): @@ -100,4 +129,5 @@ def generate_protos(): ) +extract_the_proto_files() generate_protos() From 325c18027ce3bddf50e3b26c522ac4d85e107885 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 10 Oct 2024 11:04:59 +0300 Subject: [PATCH 32/72] Fix dependencies --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 4b23e8d..628ade9 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,7 @@ def generate_protos(): setup_requires=[ "protobuf==5.27.2", "grpcio-tools>=1.56.2", + "requests", ], extras_require={ # Development dependencies From 1d5fc6bbddae20e1fa726135023d39da24f3bd87 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 10 Oct 2024 11:31:01 +0300 Subject: [PATCH 33/72] Fix get_workload_states_for_name --- ankaios_sdk/ankaios.py | 14 +++++++------- tests/test_ankaios.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 47a14f1..b99ee50 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -90,7 +90,7 @@ .. code-block:: python workload_states = - ankaios.get_workload_states_on_workload_name(workload_name) + ankaios.get_workload_states_for_name(workload_name) """ __all__ = ["Ankaios", "AnkaiosLogLevel"] @@ -706,13 +706,13 @@ def get_workload_states_on_agent(self, agent_name: str, state = self.get_state(timeout, ["workloadStates." + agent_name]) return state.get_workload_states() if state is not None else None - def get_workload_states_on_workload_name(self, workload_name: str, - state: CompleteState = None, - timeout: float = DEFAULT_TIMEOUT - ) -> WorkloadStateCollection: + def get_workload_states_for_name(self, workload_name: str, + state: CompleteState = None, + timeout: float = DEFAULT_TIMEOUT + ) -> WorkloadStateCollection: """ - Get the workload states on a specific workload name from the requested - complete state. + Get the workload states for a specific workload name from the + requested complete state. If a state is not provided, it will be requested. Args: diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 25ad378..aea927a 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -490,9 +490,9 @@ def test_get_workload_states_on_agent(): mock_state_get_workload_states.assert_called_once() -def test_get_workload_states_on_workload_name(): +def test_get_workload_states_for_name(): """ - Test the get workload states on workload name method of the Ankaios class. + Test the get workload states for workload name method of the Ankaios class. """ ankaios = Ankaios() @@ -500,7 +500,7 @@ def test_get_workload_states_on_workload_name(): patch("ankaios_sdk.CompleteState.get_workload_states") \ as mock_state_get_workload_states: mock_get_state.return_value = CompleteState() - ankaios.get_workload_states_on_workload_name("nginx") + ankaios.get_workload_states_for_name("nginx") mock_get_state.assert_called_once_with( Ankaios.DEFAULT_TIMEOUT, ["workloadStates.nginx"] ) @@ -509,7 +509,7 @@ def test_get_workload_states_on_workload_name(): with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ as mock_state_get_workload_states: - ankaios.get_workload_states_on_workload_name( + ankaios.get_workload_states_for_name( "nginx", state=CompleteState() ) mock_get_state.assert_not_called() From 0c06ad4d514f15e8f83d10438cf2bcf37ef17ba0 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 11 Oct 2024 09:17:48 +0300 Subject: [PATCH 34/72] Additional fixes to the docs --- README.md | 3 +-- docs/source/code_of_conduct.rst | 1 + docs/source/conf.py | 23 ++++++++++++++++++++--- docs/source/contributing.rst | 1 + docs/source/index.rst | 8 ++++++++ 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 docs/source/code_of_conduct.rst create mode 100644 docs/source/contributing.rst diff --git a/README.md b/README.md index 157961d..4ae1e3a 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,7 @@ pip install -e .[dev] ## Usage After installation, you can use the Ankaios SDK to configure and run workloads and request -the state of the Ankaios system and the connected agents. For more information, you can check -the documentation (TBD). +the state of the Ankaios system and the connected agents. Example: ```python diff --git a/docs/source/code_of_conduct.rst b/docs/source/code_of_conduct.rst new file mode 100644 index 0000000..3b9954b --- /dev/null +++ b/docs/source/code_of_conduct.rst @@ -0,0 +1 @@ +.. mdinclude:: ../../CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index b2d884b..fa485f3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -44,14 +44,31 @@ html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] -# -- Prepare the ReadMe file - skip the image ---------------------------------- +# -- Prepare the Contributing file - coc link ----------------------------------------- +contrib_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'CONTRIBUTING.md')) +contrib_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'CONTRIBUTING.md')) +with open(contrib_in, 'r') as f: + contrib = f.readlines() + +for i, line in enumerate(contrib): + if "./CODE_OF_CONDUCT.md" in line: + contrib[i] = line.replace("./CODE_OF_CONDUCT.md", "./code_of_conduct.html") + break +with open(contrib_out, 'w') as f: + f.writelines(contrib) + +# -- Prepare the ReadMe file - skip the image and the contributing + license ------------- read_me_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'README.md')) read_me_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'README.md')) with open(read_me_in, 'r') as f: readme = f.readlines() +start = stop = 0 for i, line in enumerate(readme): if "" in line: - with open(read_me_out, 'w') as f: - f.writelines(readme[(i+1):]) + start = i+1 + if "## Contributing" in line: + stop = i break +with open(read_me_out, 'w') as f: + f.writelines(readme[start:stop]) diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 0000000..a94705e --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1 @@ +.. mdinclude:: ../build/CONTRIBUTING.md \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index c9810cf..61d98a8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -26,6 +26,14 @@ Ankaios Github Ankaios Python SDK Github +.. toctree:: + :maxdepth: 2 + :caption: Other: + + contributing + code_of_conduct + License + Indices and tables ================== From 469109d0a812595e4d5d9af6cbf6728f8834d9d2 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 11 Oct 2024 14:53:38 +0300 Subject: [PATCH 35/72] Add doc generation to build workflow --- .github/workflows/build.yml | 35 +++++++++++++++++++++++++++++++++ docs/Makefile | 13 ++++++++---- docs/source/code_of_conduct.rst | 2 +- docs/source/conf.py | 12 +++++++++++ generate_docs.sh | 2 +- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af9be7f..4dd8134 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -171,3 +171,38 @@ jobs: with: name: codestyle-report path: reports/codestyle + + docs: + needs: setup + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Restore cache + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + pip install .[docs] + + - name: Generate documentation + run: | + cd docs + make html + cd .. + + - name: Upload documentation + uses: actions/upload-artifact@v4 + with: + name: docs_html + path: docs/build/html diff --git a/docs/Makefile b/docs/Makefile index 49fddba..56f90cb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build @@ -12,12 +12,17 @@ BUILDDIR = build help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# Catch the html argument, so that we can use the -W flag to turn warnings into errors +html: + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -W --keep-going + +# Open a server with the documentation +open: + python3 -m http.server -d build/html 8000 + .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -open: - python3 -m http.server -d build/html 8000 diff --git a/docs/source/code_of_conduct.rst b/docs/source/code_of_conduct.rst index 3b9954b..af5d0c6 100644 --- a/docs/source/code_of_conduct.rst +++ b/docs/source/code_of_conduct.rst @@ -1 +1 @@ -.. mdinclude:: ../../CODE_OF_CONDUCT.md \ No newline at end of file +.. mdinclude:: ../build/CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index fa485f3..0d94260 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -57,6 +57,18 @@ with open(contrib_out, 'w') as f: f.writelines(contrib) + +# -- Prepare the Code of Conduct file - foot note warning ----------------------------------------- +coc_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'CODE_OF_CONDUCT.md')) +coc_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'CODE_OF_CONDUCT.md')) +with open(coc_in, 'r') as f: + coc = f.readlines() +for i, line in enumerate(coc): + if "Committers[^1]" in line: + coc[i] = line.replace("Committers[^1]", "Committers") +with open(coc_out, 'w') as f: + f.writelines(coc[:-2]) + # -- Prepare the ReadMe file - skip the image and the contributing + license ------------- read_me_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'README.md')) read_me_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'README.md')) diff --git a/generate_docs.sh b/generate_docs.sh index d039889..f9843c3 100755 --- a/generate_docs.sh +++ b/generate_docs.sh @@ -8,7 +8,7 @@ pip install .[docs] cd docs # make html -sphinx-build -b html source build +sphinx-build -b html source build -W --keep-going cd .. From 0e5029de9b6a37d27360216224cfc36c6f8bcf63 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 11 Oct 2024 14:59:00 +0300 Subject: [PATCH 36/72] Fix docs build dir generation --- docs/source/conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0d94260..5aadf33 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -44,6 +44,11 @@ html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] +# -- Ensure that the build dir exists ----------------------------------------- +build_dir_path = os.path.join(os.path.dirname(__file__), '..', 'build') +if not os.path.exists(build_dir_path): + os.makedirs(build_dir_path) + # -- Prepare the Contributing file - coc link ----------------------------------------- contrib_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'CONTRIBUTING.md')) contrib_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'CONTRIBUTING.md')) From fbfc1c4caafaac44c296d62a93b649188e4e79dc Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 11 Oct 2024 15:03:03 +0300 Subject: [PATCH 37/72] Simplify docs conf.py code --- docs/source/conf.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5aadf33..aac58a6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,11 +7,14 @@ import sys sys.path.insert(0, os.path.abspath('../..')) +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +BUILD_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build')) + # -- Read the setup.cfg file ------------------------------------------------- import configparser config = configparser.ConfigParser() -config.read(os.path.join(os.path.dirname(__file__), '..', '..', 'setup.cfg')) +config.read(os.path.join(ROOT_DIR, 'setup.cfg')) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information @@ -45,13 +48,12 @@ html_static_path = ['_static'] # -- Ensure that the build dir exists ----------------------------------------- -build_dir_path = os.path.join(os.path.dirname(__file__), '..', 'build') -if not os.path.exists(build_dir_path): - os.makedirs(build_dir_path) +if not os.path.exists(BUILD_DIR): + os.makedirs(BUILD_DIR) # -- Prepare the Contributing file - coc link ----------------------------------------- -contrib_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'CONTRIBUTING.md')) -contrib_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'CONTRIBUTING.md')) +contrib_in = os.path.abspath(os.path.join(ROOT_DIR, 'CONTRIBUTING.md')) +contrib_out = os.path.abspath(os.path.join(BUILD_DIR, 'CONTRIBUTING.md')) with open(contrib_in, 'r') as f: contrib = f.readlines() @@ -64,8 +66,8 @@ # -- Prepare the Code of Conduct file - foot note warning ----------------------------------------- -coc_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'CODE_OF_CONDUCT.md')) -coc_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'CODE_OF_CONDUCT.md')) +coc_in = os.path.abspath(os.path.join(ROOT_DIR, 'CODE_OF_CONDUCT.md')) +coc_out = os.path.abspath(os.path.join(BUILD_DIR, 'CODE_OF_CONDUCT.md')) with open(coc_in, 'r') as f: coc = f.readlines() for i, line in enumerate(coc): @@ -75,8 +77,8 @@ f.writelines(coc[:-2]) # -- Prepare the ReadMe file - skip the image and the contributing + license ------------- -read_me_in = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'README.md')) -read_me_out = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'build', 'README.md')) +read_me_in = os.path.abspath(os.path.join(ROOT_DIR, 'README.md')) +read_me_out = os.path.abspath(os.path.join(BUILD_DIR, 'README.md')) with open(read_me_in, 'r') as f: readme = f.readlines() From 2f451769697ccd0ad0d03c5ee7fac441f0010a6d Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 18 Oct 2024 14:38:36 +0300 Subject: [PATCH 38/72] Findings: Return value is WorkloadInstanceName --- .github/workflows/build.yml | 8 +- CONTRIBUTING.md | 3 +- README.md | 49 +-- SECURITY.md | 7 + ankaios_sdk/__init__.py | 2 +- ankaios_sdk/_components/complete_state.py | 12 +- ankaios_sdk/_components/manifest.py | 6 + ankaios_sdk/_components/response.py | 29 +- ankaios_sdk/_components/workload.py | 165 +++++------ ankaios_sdk/_components/workload_state.py | 53 +++- ankaios_sdk/_protos/.gitignore | 2 +- ankaios_sdk/ankaios.py | 278 ++++++++++-------- docs/Makefile | 4 +- docs/make.bat | 35 --- run_tests.py => run_checks.py | 8 +- setup.cfg | 3 +- setup.py | 28 +- tests/response/test_response.py | 14 +- tests/test_ankaios.py | 152 +++++++--- tests/test_manifest.py | 4 + tests/workload/test_workload.py | 66 ++--- tests/workload/test_workload_builder.py | 22 ++ .../test_workload_instance_name.py | 28 +- .../test_workload_state_collection.py | 16 +- 24 files changed, 596 insertions(+), 398 deletions(-) create mode 100644 SECURITY.md delete mode 100644 docs/make.bat rename run_tests.py => run_checks.py (95%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4dd8134..1647ecc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,7 @@ jobs: pip install .[dev] - name: Run unit tests - run: python3 run_tests.py --utest + run: python3 run_checks.py --utest continue-on-error: true - name: Upload unit test report @@ -97,7 +97,7 @@ jobs: pip install .[dev] - name: Run coverage - run: python3 run_tests.py --cov + run: python3 run_checks.py --cov continue-on-error: true - name: Upload coverage report @@ -130,7 +130,7 @@ jobs: pip install .[dev] - name: Run lint - run: python3 run_tests.py --lint + run: python3 run_checks.py --lint continue-on-error: true - name: Upload lint report @@ -163,7 +163,7 @@ jobs: pip install .[dev] - name: Run pep8 codestyle check - run: python3 run_tests.py --pep8 + run: python3 run_checks.py --pep8 continue-on-error: true - name: Upload codestyle report diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 54b0c34..780eed5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Please observe our [Community Code of Conduct](./CODE_OF_CONDUCT.md). ## How to Contribute This project welcomes contributions and suggestions. -You'll also need to create an [Eclipse Foundation account](https://accounts.eclipse.org/) and agree to the [Eclipse Contributor Agreement](https://www.eclipse.org/legal/ECA.php). See more info at . +For contributions, you'll also need to create an [Eclipse Foundation account](https://accounts.eclipse.org/) and agree to the [Eclipse Contributor Agreement](https://www.eclipse.org/legal/ECA.php). See more info at . If you have a bug to report or a feature to suggest, please use the New Issue button on the Issues page to access templates for these items. @@ -23,3 +23,4 @@ besides a technical review: Please join our [developer mailing list](https://accounts.eclipse.org/mailing-list/ankaios-dev) for up to date information or use the Ankaios [discussion forum](https://github.com/eclipse-ankaios/ankaios/discussions). If you are looking for the main project, you can find it [here](https://github.com/eclipse-ankaios/ankaios/tree/main). +You can also join the conversion with the community in the [Ankaios Slack workspace](https://github.com/eclipse-ankaios/ankaios/wiki#slack). diff --git a/README.md b/README.md index 4ae1e3a..d71719e 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ # Ankaios Python SDK for Eclipse Ankaios Eclipse Ankaios provides workload and container orchestration for automotive -High Performance Computing Software (HPCs). While it can be used for various -fields of applications, it is developed from scratch for automotive use cases -and provides a slim yet powerful solution to manage containerized applications. +High Performance Computers (HPCs). While it can be used for various fields of +applications, it is developed from scratch for automotive use cases and provides +a slim yet powerful solution to manage containerized applications. The Python SDK provides easy access from the container (workload) point-of-view to manage the Ankaios system. A workload can use the Python SDK to run other workloads @@ -58,27 +58,40 @@ with Ankaios() as ankaios: .build() # Run the workload - ankaios.run_workload(workload) - - # Request the state of the system, filtered with the current workload + ret = ankaios.run_workload(workload) + + # Check if the workload is scheduled and get the WorkloadInstanceName + if ret is not None: + workload_instance_name = ret["added_workloads"][0] + + # Wait until the workload raches the running state + ret = ankaios.wait_for_workload_to_reach_state( + workload_instance_name, + state=WorkloadStateEnum.RUNNING, + timeout=5 + ) + if ret: + print("Workload reached the RUNNING state.") + + # Request the workload state based on the workload instance name + ret = ankaios.get_workload_state_for_instance_name(workload_instance_name) + if ret is not None: + print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.info}") + + # Request the state of the system, filtered with the agent name complete_state = ankaios.get_state( timeout=5, - field_mask=["workloadStates.agent_A.dynamic_nginx"]) + field_mask=["workloadStates.agent_A"]) # Get the workload states present in the complete_state workload_states_dict = complete_state.get_workload_states().get_as_dict() - # Get the state of the desired workload - dynamic_nginx_state = workload_states_dict["agent_A"]["dynamic_nginx"].values()[0] - - # Check state - if dynamic_nginx_state.state == WorkloadStateEnum.RUNNING and - dynamic_nginx_state.substate == WorkloadSubStateEnum.RUNNING_OK: - print("Workload started running succesfully") - elif dynamic_nginx_state.state == WorkloadStateEnum.FAILED: - print("Workload failed with the following substate: {}".format( - dynamic_nginx_state.substate.name - )) + # Print the states of the workloads: + for workload_name in workload_states_dict["agent_A"]: + for workload_id in workload_states_dict["agent_A"][workload_name]: + print(f"Workload {workload_name} with id {workload_id} has the state " + + str(workload_states_dict["agent_A"] \ + [workload_name][workload_id].state)) ``` ## Contributing diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..706a6f2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +## Reporting a Vulnerability + +If you want to report a vulnerability in Ankaios or one of the SDKs, you can create a [private vulnerability report using Github](https://github.com/eclipse-ankaios/ankaios/security/advisories/new). +In this case the Ankaios committers will be informed and only you and the committers will have access to this vulnerability report and you will get feedback there once the report has been analyzed. +In the form to fill out, only the title and description are required, the rest of the fields are optional. \ No newline at end of file diff --git a/ankaios_sdk/__init__.py b/ankaios_sdk/__init__.py index e31b39a..a0b3de8 100644 --- a/ankaios_sdk/__init__.py +++ b/ankaios_sdk/__init__.py @@ -31,7 +31,7 @@ - Manifest: Represents a workload manifest. - CompleteState: - Represents the complete state of the system. + Represents the complete state of the Ankaios cluster. """ from .ankaios import * diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 5be64ef..5c16d83 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -14,13 +14,13 @@ """ This script defines the CompleteState class for managing -the state of the system. +the state of the Ankaios cluster. Classes ------- - CompleteState: - Represents the complete state of the system. + Represents the complete state of the Ankaios cluster. Usage ----- @@ -45,7 +45,7 @@ workload = complete_state.get_workload("nginx") -- Get a list of workloads from the complete state: +- Get the entire list of workloads from the complete state: .. code-block:: python workloads = complete_state.get_workloads() @@ -68,16 +68,16 @@ from .workload_state import WorkloadStateCollection -DEFAULT_API_VERSION = "v0.1" +SUPPORTED_API_VERSION = "v0.1" class CompleteState: """ A class to represent the complete state. """ - def __init__(self, api_version: str = DEFAULT_API_VERSION) -> None: + def __init__(self, api_version: str = SUPPORTED_API_VERSION) -> None: """ - Initializes a CompleteState instance with the given API version. + Initializes an empty CompleteState instance with the given API version. Args: api_version (str): The API version to set for the complete state. diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index c93b136..387f6cf 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -137,10 +137,16 @@ def check(self) -> bool: wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", "dependencies", "tags", "controlInterfaceAccess"] + wl_mandatory_keys = ["runtime", "runtimeConfig", "agent"] for wl_name in self._manifest["workloads"]: + # Check allowed keys for key in self._manifest["workloads"][wl_name].keys(): if key not in wl_allowed_keys: return False + # Check mandatory keys + for key in wl_mandatory_keys: + if key not in self._manifest["workloads"][wl_name].keys(): + return False return True def _calculate_masks(self) -> list[str]: diff --git a/ankaios_sdk/_components/response.py b/ankaios_sdk/_components/response.py index 4fe0399..7ebbb29 100644 --- a/ankaios_sdk/_components/response.py +++ b/ankaios_sdk/_components/response.py @@ -47,6 +47,7 @@ from threading import Event from .._protos import _control_api from .complete_state import CompleteState +from .workload_state import WorkloadInstanceName class Response: @@ -107,13 +108,28 @@ def _from_proto(self) -> None: self.content = CompleteState() self.content._from_proto(self._response.completeState) elif self._response.HasField("UpdateStateSuccess"): + update_state_msg = self._response.UpdateStateSuccess self.content_type = "update_state_success" self.content = { - "added_workloads": - self._response.UpdateStateSuccess.addedWorkloads, - "deleted_workloads": - self._response.UpdateStateSuccess.deletedWorkloads, + "added_workloads": [], + "deleted_workloads": [], } + for workload in update_state_msg.addedWorkloads: + workload_name, workload_id, agent_name = \ + workload.split(".") + self.content["added_workloads"].append( + WorkloadInstanceName( + agent_name, workload_name, workload_id + ) + ) + for workload in update_state_msg.deletedWorkloads: + workload_name, workload_id, agent_name = \ + workload.split(".") + self.content["deleted_workloads"].append( + WorkloadInstanceName( + agent_name, workload_name, workload_id + ) + ) else: raise ValueError("Invalid response type.") @@ -143,8 +159,9 @@ def get_content(self) -> tuple[str, Union[str, CompleteState, dict]]: Gets the content of the response. Returns: - (tuple[str, Union[str, CompleteState, dict]]): A tuple containing - the content type and the content of the response. + tuple[str, str]: in case of an error response. + tuple[str, CompleteState]: in case of a complete state response. + tuple[str, dict]: in case of an update state success response. """ return (self.content_type, self.content) diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index ea6ecc6..3c1ceeb 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -183,26 +183,6 @@ def update_restart_policy(self, policy: str) -> None: self._workload.restartPolicy = _ank_base.RestartPolicy.Value(policy) self._add_mask(f"{self._main_mask}.restartPolicy") - def add_dependency(self, workload_name: str, condition: str) -> None: - """ - Add a dependency to the workload. - Supported values: `ADD_COND_RUNNING`, `ADD_COND_SUCCEEDED`, - `ADD_COND_FAILED`. - - Args: - workload_name (str): The name of the dependent workload. - condition (str): The condition for the dependency. - - Raises: - ValueError: If an invalid condition is provided. - """ - if condition not in _ank_base.AddCondition.keys(): - raise ValueError("Invalid condition. Supported values: " - + ", ".join(_ank_base.AddCondition.keys()) + ".") - self._workload.dependencies.dependencies[workload_name] = \ - _ank_base.AddCondition.Value(condition) - self._add_mask(f"{self._main_mask}.dependencies") - def get_dependencies(self) -> dict: """ Return the dependencies of the workload. @@ -216,17 +196,29 @@ def get_dependencies(self) -> dict: deps[dep] = _ank_base.AddCondition.Name(deps[dep]) return deps - def update_dependencies(self, dependencies: dict) -> None: + def update_dependencies(self, dependencies: dict[str, str]) -> None: """ Update the dependencies of the workload. + Supported conditions: `ADD_COND_RUNNING`, `ADD_COND_SUCCEEDED`, + `ADD_COND_FAILED`. Args: dependencies (dict): A dictionary of dependencies with - workload names and values. + workload names and condition as values. + + Raises: + ValueError: If an invalid condition is provided. """ self._workload.dependencies.dependencies.clear() for workload_name, condition in dependencies.items(): - self.add_dependency(workload_name, condition) + if condition not in _ank_base.AddCondition.keys(): + raise ValueError( + f"Invalid condition for workload {workload_name}. " + + "Supported values: " + + ", ".join(_ank_base.AddCondition.keys()) + ".") + self._workload.dependencies.dependencies[workload_name] = \ + _ank_base.AddCondition.Value(condition) + self._add_mask(f"{self._main_mask}.dependencies") def add_tag(self, key: str, value: str) -> None: """ @@ -238,7 +230,8 @@ def add_tag(self, key: str, value: str) -> None: """ tag = _ank_base.Tag(key=key, value=value) self._workload.tags.tags.append(tag) - self._add_mask(f"{self._main_mask}.tags") + if f"{self._main_mask}.tags" not in self.masks: + self._add_mask(f"{self._main_mask}.tags.{key}") def get_tags(self) -> list[tuple[str, str]]: """ @@ -262,21 +255,29 @@ def update_tags(self, tags: list) -> None: while len(self._workload.tags.tags) > 0: self._workload.tags.tags.pop() for key, value in tags: - self.add_tag(key, value) + tag = _ank_base.Tag(key=key, value=value) + self._workload.tags.tags.append(tag) + self.masks = [mask for mask in self.masks if not mask.startswith( + f"{self._main_mask}.tags" + )] + self._add_mask(f"{self._main_mask}.tags") - def add_allow_rule( - self, operation: str, filter_masks: list[str] - ) -> None: + def _generate_access_right_rule(self, + operation: str, + filter_masks: list[str] + ) -> _ank_base.AccessRightsRule: """ - Add an allow rule to the workload. - Supported values: `Nothing`, `Write`, `Read`, `ReadWrite`. + Generate an access rights rule for the workload. Args: operation (str): The operation the rule allows. filter_masks (list): The list of filter masks. + Returns: + _ank_base.AccessRightsRule: The access rights rule generated. + Raises: - ValueError: If an invalid operation is provided + ValueError: If an invalid operation is provided. """ enum_mapper = { "Nothing": _ank_base.ReadWriteEnum.RW_NOTHING, @@ -290,22 +291,24 @@ def add_allow_rule( + "Supported values: " + ", ".join(enum_mapper.keys()) + "." ) - self._workload.controlInterfaceAccess.allowRules.append( - _ank_base.AccessRightsRule( - stateRule=_ank_base.StateRule( - operation=enum_mapper[operation], - filterMasks=filter_masks - ) + return _ank_base.AccessRightsRule( + stateRule=_ank_base.StateRule( + operation=enum_mapper[operation], + filterMasks=filter_masks ) ) - self._add_mask(f"{self._main_mask}.controlInterfaceAccess") - def get_allow_rules(self) -> list[tuple[str, list[str]]]: + def _access_right_rule_to_str(self, rule: _ank_base.AccessRightsRule + ) -> tuple[str, list[str]]: """ - Return the allow rules of the workload. + Convert an access rights rule to a tuple. + + Args: + rule (_ank_base.AccessRightsRule): The access + rights rule to convert. Returns: - list: A list of tuples containing operation and filter masks. + tuple: A tuple containing operation and filter masks. """ enum_mapper = { _ank_base.ReadWriteEnum.RW_NOTHING: "Nothing", @@ -313,62 +316,42 @@ def get_allow_rules(self) -> list[tuple[str, list[str]]]: _ank_base.ReadWriteEnum.RW_READ: "Read", _ank_base.ReadWriteEnum.RW_READ_WRITE: "ReadWrite", } + return ( + enum_mapper[rule.stateRule.operation], + rule.stateRule.filterMasks + ) + + def get_allow_rules(self) -> list[tuple[str, list[str]]]: + """ + Return the allow rules of the workload. + + Returns: + list: A list of tuples containing operation and filter masks. + """ rules = [] for rule in self._workload.controlInterfaceAccess.allowRules: - rules.append(( - enum_mapper[rule.stateRule.operation], - rule.stateRule.filterMasks - )) + rules.append(self._access_right_rule_to_str(rule)) return rules def update_allow_rules(self, rules: list[tuple[str, list[str]]]) -> None: """ Update the allow rules of the workload. + Supported values: `Nothing`, `Write`, `Read`, `ReadWrite`. Args: rules (list): A list of tuples containing operation and filter masks. - """ - while len(self._workload.controlInterfaceAccess.allowRules) > 0: - self._workload.controlInterfaceAccess.allowRules.pop() - for operation, filter_masks in rules: - self.add_allow_rule(operation, filter_masks) - - def add_deny_rule( - self, operation: str, filter_masks: list[str] - ) -> None: - """ - Add a deny rule to the workload. - Supported values: `Nothing`, `Write`, `Read`, `ReadWrite`. - - Args: - operation (str): The operation the rule denies. - filter_masks (list): The list of filter masks. Raises: ValueError: If an invalid operation is provided """ - enum_mapper = { - "Nothing": _ank_base.ReadWriteEnum.RW_NOTHING, - "Write": _ank_base.ReadWriteEnum.RW_WRITE, - "Read": _ank_base.ReadWriteEnum.RW_READ, - "ReadWrite": _ank_base.ReadWriteEnum.RW_READ_WRITE, - } - if operation not in enum_mapper: - raise ValueError( - f"Invalid operation {operation}. " - + "Supported values: " - + ", ".join(enum_mapper.keys()) + "." - ) - self._workload.controlInterfaceAccess.denyRules.append( - _ank_base.AccessRightsRule( - stateRule=_ank_base.StateRule( - operation=enum_mapper[operation], - filterMasks=filter_masks - ) + while len(self._workload.controlInterfaceAccess.allowRules) > 0: + self._workload.controlInterfaceAccess.allowRules.pop() + for operation, filter_masks in rules: + self._workload.controlInterfaceAccess.allowRules.append( + self._generate_access_right_rule(operation, filter_masks) ) - ) - self._add_mask(f"{self._main_mask}.controlInterfaceAccess") + self._add_mask(f"{self._main_mask}.controlInterfaceAccess.allowRules") def get_deny_rules(self) -> list[tuple[str, list[str]]]: """ @@ -377,32 +360,30 @@ def get_deny_rules(self) -> list[tuple[str, list[str]]]: Returns: list: A list of tuples containing operation and filter masks. """ - enum_mapper = { - _ank_base.ReadWriteEnum.RW_NOTHING: "Nothing", - _ank_base.ReadWriteEnum.RW_WRITE: "Write", - _ank_base.ReadWriteEnum.RW_READ: "Read", - _ank_base.ReadWriteEnum.RW_READ_WRITE: "ReadWrite", - } rules = [] for rule in self._workload.controlInterfaceAccess.denyRules: - rules.append(( - enum_mapper[rule.stateRule.operation], - rule.stateRule.filterMasks - )) + rules.append(self._access_right_rule_to_str(rule)) return rules def update_deny_rules(self, rules: list[tuple[str, list[str]]]) -> None: """ Update the deny rules of the workload. + Supported values: `Nothing`, `Write`, `Read`, `ReadWrite`. Args: rules (list): A list of tuples containing operation and filter masks. + + Raises: + ValueError: If an invalid operation is provided """ while len(self._workload.controlInterfaceAccess.denyRules) > 0: self._workload.controlInterfaceAccess.denyRules.pop() for operation, filter_masks in rules: - self.add_deny_rule(operation, filter_masks) + self._workload.controlInterfaceAccess.denyRules.append( + self._generate_access_right_rule(operation, filter_masks) + ) + self._add_mask(f"{self._main_mask}.controlInterfaceAccess.denyRules") def add_config(self, alias: str, name: str) -> None: """ diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index 88e1b02..0f7b7dd 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -63,7 +63,7 @@ "WorkloadInstanceName", "WorkloadExecutionState", "WorkloadStateEnum", "WorkloadSubStateEnum"] -from typing import TypeAlias +from typing import Optional, TypeAlias from enum import Enum from .._protos import _ank_base @@ -279,6 +279,23 @@ def __init__(self, agent_name: str, self.workload_name = workload_name self.workload_id = workload_id + def __eq__(self, other: "WorkloadInstanceName") -> bool: + """ + Checks if two workload instance names are equal. + + Args: + other (WorkloadInstanceName): The instance name to compare with. + + Returns: + bool: True if the workload instance names are equal, + False otherwise. + """ + if isinstance(other, WorkloadInstanceName): + return (self.agent_name == other.agent_name + and self.workload_name == other.workload_name + and self.workload_id == other.workload_id) + return NotImplemented + def __str__(self) -> str: """ Returns the string representation of the workload instance name. @@ -286,7 +303,17 @@ def __str__(self) -> str: Returns: str: The string representation of the workload instance name. """ - return f"{self.agent_name}.{self.workload_name}.{self.workload_id}" + return f"{self.workload_name}.{self.workload_id}.{self.agent_name}" + + def get_filter_mask(self) -> str: + """ + Returns the filter mask for the workload instance name. + + Returns: + str: The filter mask for the workload instance name. + """ + return f"workloadStates.{self.agent_name}." \ + + f"{self.workload_name}.{self.workload_id}" # pylint: disable=too-few-public-methods @@ -343,10 +370,10 @@ def add_workload_state(self, state: WorkloadState) -> None: def get_as_dict(self) -> WorkloadStatesMap: """ - Returns the workload states as a list. + Returns the workload states as a dict. Returns: - list[WorkloadState]: A list of workload states. + WorkloadStatesMap: A dict of workload states. """ return_dict = self.WorkloadStatesMap() for state in self._workload_states: @@ -374,6 +401,24 @@ def get_as_list(self) -> list[WorkloadState]: """ return self._workload_states + def get_for_instance_name(self, instance_name: WorkloadInstanceName + ) -> Optional[WorkloadState]: + """ + Returns the workload state for the given workload instance name. + + Args: + instance_name (WorkloadInstanceName): The workload instance name + to look up. + + Returns: + WorkloadState: The workload state for the given instance name. + None: If no workload state was found. + """ + for state in self._workload_states: + if state.workload_instance_name == instance_name: + return state + return None + def _from_proto(self, state: _ank_base.WorkloadStatesMap) -> None: """ Populates the collection from a proto message. diff --git a/ankaios_sdk/_protos/.gitignore b/ankaios_sdk/_protos/.gitignore index 6c182e7..daed7b0 100644 --- a/ankaios_sdk/_protos/.gitignore +++ b/ankaios_sdk/_protos/.gitignore @@ -1,2 +1,2 @@ *pb2.py -*pb2_grpc.py \ No newline at end of file +*pb2_grpc.py diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index b99ee50..61a0517 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -40,26 +40,35 @@ - Apply a manifest: .. code-block:: python - if ankaios.apply_manifest(manifest): + ret = ankaios.apply_manifest(manifest) + if ret is not None: print("Manifest applied successfully.") + print(ret["added_workloads"]) + print(ret["deleted_workloads"]) - Delete a manifest: .. code-block:: python - if ankaios.delete_manifest(manifest): + ret = ankaios.delete_manifest(manifest) + if ret is not None: print("Manifest deleted successfully.") + print(ret["deleted_workloads"]) - Run a workload: .. code-block:: python - if ankaios.run_workload(workload): + ret = ankaios.run_workload(workload) + if ret is not None: print("Workload started successfully.") + print(ret["added_workloads"]) - Delete a workload: .. code-block:: python - if ankaios.delete_workload(workload_name): + ret = ankaios.delete_workload(workload_name) + if ret is not None: print("Workload deleted successfully.") + print(ret["deleted_workloads"]) - Get a workload: .. code-block:: python @@ -81,22 +90,34 @@ workload_states = ankaios.get_workload_states() -- Get the workload states on an agent: +- Get the workload states: + .. code-block:: python + + workload_states = ankaios.get_workload_states() + +- Get the workload execution state for instance name: .. code-block:: python - workload_states = ankaios.get_workload_states_on_agent(agent_name) + ret = ankaios.get_execution_state_for_instance_name(instance_name) + if ret is not None: + print(f"State: {ret.state}, substate: {ret.substate}") -- Get the workload states on a workload name: +- Wait for a workload to reach a state: .. code-block:: python - workload_states = - ankaios.get_workload_states_for_name(workload_name) + ret = ankaios.wait_for_workload_to_reach_state( + instance_name, + WorkloadStateEnum.RUNNING + ) + if ret: + print(f"State reached.") """ __all__ = ["Ankaios", "AnkaiosLogLevel"] import logging -from typing import Union +import time +from typing import Optional, Union from enum import Enum import threading from google.protobuf.internal.encoder import _VarintBytes @@ -104,7 +125,9 @@ from ._protos import _control_api from ._components import Workload, CompleteState, Request, Response, \ - ResponseEvent, WorkloadStateCollection, Manifest + ResponseEvent, WorkloadStateCollection, Manifest, \ + WorkloadInstanceName, WorkloadStateEnum, \ + WorkloadExecutionState class AnkaiosLogLevel(Enum): @@ -191,16 +214,17 @@ def _read_from_control_interface(self) -> None: It reads the response from the control interface and saves it in the responses dictionary, by triggering the corresponding ResponseEvent. """ - # pylint: disable=consider-using-with - f = open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") - try: + # pylint: disable=consider-using-with + input_fifo = open( + f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") + while self._connected: # Buffer for reading in the byte size of the proto msg varint_buffer = bytearray() while True: # Consume byte for byte - next_byte = f.read(1) + next_byte = input_fifo.read(1) if not next_byte: # pragma: no cover break varint_buffer += next_byte @@ -215,7 +239,7 @@ def _read_from_control_interface(self) -> None: msg_buf = bytearray() for _ in range(msg_len): # Read the message according to the length - next_byte = f.read(1) + next_byte = input_fifo.read(1) if not next_byte: # pragma: no cover break msg_buf += next_byte @@ -236,7 +260,7 @@ def _read_from_control_interface(self) -> None: except Exception as e: # pylint: disable=broad-exception-caught self.logger.error("Error while reading fifo file: %s", e) finally: - f.close() + input_fifo.close() def _get_response_by_id(self, request_id: str, timeout: float = DEFAULT_TIMEOUT) -> Response: @@ -274,9 +298,9 @@ def _write_to_pipe(self, request: Request) -> None: request_to_ankaios = _control_api.ToAnkaios( request=request._to_proto() ) - # Send the byte length of the proto msg + # Adds the byte length of the proto msg f.write(_VarintBytes(request_to_ankaios.ByteSize())) - # Send the proto msg itself + # Adds the proto msg itself f.write(request_to_ankaios.SerializeToString()) f.flush() @@ -341,7 +365,7 @@ def _disconnect(self) -> None: self._connected = False self._read_thread.join() - def apply_manifest(self, manifest: Manifest) -> bool: + def apply_manifest(self, manifest: Manifest) -> Optional[dict]: """ Send a request to apply a manifest. @@ -349,8 +373,8 @@ def apply_manifest(self, manifest: Manifest) -> bool: manifest (Manifest): The manifest object to be applied. Returns: - bool: True if the manifest was applied successfully, - False otherwise. + dict: a dict with the added and deleted workloads. + None: If the manifest was not applied successfully. """ request = Request(request_type="update_state") request.set_complete_state(manifest.generate_complete_state()) @@ -361,7 +385,7 @@ def apply_manifest(self, manifest: Manifest) -> bool: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return False + return None # Interpret response (content_type, content) = response.get_content() @@ -372,12 +396,13 @@ def apply_manifest(self, manifest: Manifest) -> bool: self.logger.info( "Update successfull: %s added workloads, " + "%s deleted workloads.", - content["added_workloads"], content["deleted_workloads"] + len(content["added_workloads"]), + len(content["deleted_workloads"]) ) - return True - return False + return content + return None - def delete_manifest(self, manifest: Manifest) -> bool: + def delete_manifest(self, manifest: Manifest) -> Optional[dict]: """ Send a request to delete a manifest. @@ -385,8 +410,8 @@ def delete_manifest(self, manifest: Manifest) -> bool: manifest (Manifest): The manifest object to be deleted. Returns: - bool: True if the manifest was deleted successfully, - False otherwise. + dict: a dict with the added and deleted workloads. + None: If the manifest was not deleted successfully. """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) @@ -397,7 +422,7 @@ def delete_manifest(self, manifest: Manifest) -> bool: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return False + return None # Interpret response (content_type, content) = response.get_content() @@ -408,12 +433,13 @@ def delete_manifest(self, manifest: Manifest) -> bool: self.logger.info( "Update successfull: %s added workloads, " + "%s deleted workloads.", - content["added_workloads"], content["deleted_workloads"] + len(content["added_workloads"]), + len(content["deleted_workloads"]) ) - return True - return False + return content + return None - def run_workload(self, workload: Workload) -> bool: + def run_workload(self, workload: Workload) -> Optional[dict]: """ Send a request to run a workload. @@ -421,7 +447,8 @@ def run_workload(self, workload: Workload) -> bool: workload (Workload): The workload object to be run. Returns: - bool: True if the workload was run successfully, False otherwise. + dict: a dict with the added and deleted workloads. + None: If the workload was not run successfully. """ complete_state = CompleteState() complete_state.set_workload(workload) @@ -436,7 +463,7 @@ def run_workload(self, workload: Workload) -> bool: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return False + return None # Interpret response (content_type, content) = response.get_content() @@ -447,12 +474,13 @@ def run_workload(self, workload: Workload) -> bool: self.logger.info( "Update successfull: %s added workloads, " + "%s deleted workloads.", - content["added_workloads"], content["deleted_workloads"] + len(content["added_workloads"]), + len(content["deleted_workloads"]) ) - return True - return False + return content + return None - def delete_workload(self, workload_name: str) -> bool: + def delete_workload(self, workload_name: str) -> Optional[dict]: """ Send a request to delete a workload. @@ -460,8 +488,8 @@ def delete_workload(self, workload_name: str) -> bool: workload_name (str): The name of the workload to be deleted. Returns: - bool: True if the workload was deleted successfully, - False otherwise. + dict: a dict with the added and deleted workloads. + None: If the workload was not deleted successfully. """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) @@ -471,7 +499,7 @@ def delete_workload(self, workload_name: str) -> bool: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return False + return None # Interpret response (content_type, content) = response.get_content() @@ -482,10 +510,11 @@ def delete_workload(self, workload_name: str) -> bool: self.logger.info( "Update successfull: %s added workloads, " + "%s deleted workloads.", - content["added_workloads"], content["deleted_workloads"] + len(content["added_workloads"]), + len(content["deleted_workloads"]) ) - return True - return False + return content + return None def get_workload(self, workload_name: str, state: CompleteState = None, @@ -508,25 +537,6 @@ def get_workload(self, workload_name: str, ) return state.get_workload(workload_name) if state is not None else None - def set_configs_from_file(self, configs_path: str) -> bool: - """ - Set the configs from a file. - The configs file should have a dictionary as the top level object. - The names will be the keys of the dictionary. - - Args: - config_path (str): The path to the configs file. - - Returns: - bool: True if the configs were set successfully, False otherwise. - """ - # with open(configs_path, "r", encoding="utf-8") as f: - # configs = f.read() - # self.set_configs(configs) - raise NotImplementedError( - "set_configs_from_file is not implemented yet." - ) - def set_configs(self, configs: dict) -> bool: """ Set the configs. The names will be the keys of the dictionary. @@ -539,26 +549,10 @@ def set_configs(self, configs: dict) -> bool: """ raise NotImplementedError("set_configs is not implemented yet.") - def set_config_from_file(self, name: str, config_path: str) -> bool: - """ - Set the config from a file, with the provided name. - If the config exists, it will be replaced. - - Args: - name (str): The name of the config. - config_path (str): The path to the config file. - - Returns: - bool: True if the config was set successfully, False otherwise. - """ - raise NotImplementedError( - "set_config_from_file is not implemented yet." - ) - def set_config(self, name: str, config: Union[dict, list, str]) -> bool: """ Set the config with the provided name. - If the config exists, it will eb replaced. + If the config exists, it will be replaced. Args: name (str): The name of the config. @@ -590,7 +584,7 @@ def get_config(self, name: str) -> Union[dict, list, str]: """ raise NotImplementedError("get_config is not implemented yet.") - def delete_configs(self) -> bool: + def delete_all_configs(self) -> bool: """ Delete all the configs. @@ -598,7 +592,7 @@ def delete_configs(self) -> bool: bool: True if the configs were deleted successfully, False otherwise. """ - raise NotImplementedError("delete_configs is not implemented yet.") + raise NotImplementedError("delete_all_configs is not implemented yet.") def delete_config(self, name: str) -> bool: """ @@ -613,7 +607,7 @@ def delete_config(self, name: str) -> bool: raise NotImplementedError("delete_config is not implemented yet.") def get_state(self, timeout: float = DEFAULT_TIMEOUT, - field_masks: list[str] = None) -> CompleteState: + field_masks: list[str] = None) -> Optional[CompleteState]: """ Send a request to get the complete state. @@ -625,6 +619,7 @@ def get_state(self, timeout: float = DEFAULT_TIMEOUT, Returns: CompleteState: The complete state object. + None: If the state was not retrieved successfully. """ request = Request(request_type="get_state") if field_masks is not None: @@ -644,90 +639,141 @@ def get_state(self, timeout: float = DEFAULT_TIMEOUT, return content - def get_agents(self, state: CompleteState = None, - timeout: float = DEFAULT_TIMEOUT) -> list[str]: + def get_agents( + self, timeout: float = DEFAULT_TIMEOUT + ) -> Optional[list[str]]: """ Get the agents from the requested complete state. Args: - state (CompleteState): The complete state to get the agents from. timeout (float): The maximum time to wait for the response, in seconds. Returns: list[str]: The list of agent names. + None: If the state was not retrieved successfully. """ - if state is None: - state = self.get_state(timeout) + state = self.get_state(timeout) return state.get_agents() if state is not None else None def get_workload_states(self, - state: CompleteState = None, timeout: float = DEFAULT_TIMEOUT - ) -> WorkloadStateCollection: + ) -> Optional[WorkloadStateCollection]: """ Get the workload states from the requested complete state. - If a state is not provided, it will be requested. Args: - state (CompleteState): The complete state to get - the workload states from. timeout (float): The maximum time to wait for the response, in seconds. Returns: WorkloadStateCollection: The collection of workload states. + None: If the state was not retrieved successfully. """ - if state is None: - state = self.get_state(timeout) + state = self.get_state(timeout) return state.get_workload_states() if state is not None else None + def get_execution_state_for_instance_name( + self, + instance_name: WorkloadInstanceName, + timeout: float = DEFAULT_TIMEOUT + ) -> Optional[WorkloadExecutionState]: + """ + Get the workload states for a specific workload instance name from the + requested complete state. + + Args: + instance_name (WorkloadInstanceName): The instance name of the + workload. + timeout (float): The maximum time to wait for the response, + in seconds. + + Returns: + WorkloadExecutionState: The specified workload's execution state. + None: If the state was not retrieved successfully. + """ + state = self.get_state(timeout, [instance_name.get_filter_mask()]) + if state is not None: + workload_states = state.get_workload_states().get_as_list() + if len(workload_states) != 1: + self.logger.error("Expected exactly one workload state " + + "for instance name %s, but got %s", + instance_name, len(workload_states)) + return None + return workload_states[0].execution_state + return None + def get_workload_states_on_agent(self, agent_name: str, - state: CompleteState = None, timeout: float = DEFAULT_TIMEOUT - ) -> WorkloadStateCollection: + ) -> Optional[WorkloadStateCollection]: """ Get the workload states on a specific agent from the requested complete state. - If a state is not provided, it will be requested. Args: agent_name (str): The name of the agent. - state (CompleteState): The complete state to get - the workload states from. timeout (float): The maximum time to wait for the response, in seconds. Returns: - WorkloadStateCollection: The collection of workload states on the - specified agent. + WorkloadStateCollection: The collection of workload states. + None: If the state was not retrieved successfully """ - if state is None: - state = self.get_state(timeout, ["workloadStates." + agent_name]) + state = self.get_state(timeout, ["workloadStates." + agent_name]) return state.get_workload_states() if state is not None else None def get_workload_states_for_name(self, workload_name: str, - state: CompleteState = None, timeout: float = DEFAULT_TIMEOUT - ) -> WorkloadStateCollection: + ) -> Optional[WorkloadStateCollection]: """ Get the workload states for a specific workload name from the requested complete state. - If a state is not provided, it will be requested. Args: workload_name (str): The name of the workload. - state (CompleteState): The complete state to get - the workload states from. timeout (float): The maximum time to wait for the response, in seconds. Returns: - WorkloadStateCollection: The collection of workload states on the - specified workload name. + WorkloadStateCollection: The collection of workload states. + None: If the state was not retrieved successfully. """ + state = self.get_state( + timeout, ["workloadStates"] + ) if state is None: - state = self.get_state( - timeout, ["workloadStates." + workload_name] + return None + workload_states = state.get_workload_states().get_as_list() + workload_states_for_name = WorkloadStateCollection() + for workload_state in workload_states: + if workload_state.name == workload_name: + workload_states_for_name.add_workload_state(workload_state) + return workload_states_for_name + + def wait_for_workload_to_reach_state(self, + instance_name: WorkloadInstanceName, + state: WorkloadStateEnum, + timeout: float = DEFAULT_TIMEOUT + ) -> bool: + """ + Waits for the workload to reach the specified state. + + Args: + instance_name (WorkloadInstanceName): The instance name of the + workload. + state (WorkloadStateEnum): The state to wait for. + timeout (float): The maximum time to wait for the response, + in seconds. + + Returns: + bool: True if the workload reached the state, False otherwise. + """ + start_time = time.time() + while time.time() - start_time < timeout: + workload_state = self.get_execution_state_for_instance_name( + instance_name ) - return state.get_workload_states() if state is not None else None + if workload_state is not None and workload_state.state == state: + return True + time.sleep(0.1) + return False diff --git a/docs/Makefile b/docs/Makefile index 56f90cb..0b84051 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,5 @@ # Minimal makefile for Sphinx documentation -# +# This file was auto-generated by sphinx-quickstart # You can set these variables from the command line, and also # from the environment for the first two. @@ -18,7 +18,7 @@ html: # Open a server with the documentation open: - python3 -m http.server -d build/html 8000 + python3 -m http.server -d build/html 8001 .PHONY: help Makefile diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 747ffb7..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/run_tests.py b/run_checks.py similarity index 95% rename from run_tests.py rename to run_checks.py index 635c3e0..97a9e4b 100644 --- a/run_tests.py +++ b/run_checks.py @@ -13,13 +13,13 @@ # SPDX-License-Identifier: Apache-2.0 """ -Run tests for ankaios_sdk Python package. -This script runs unit tests, coverage and pylint and -saves the results in the reports directory. +Run checks for ankaios_sdk Python package. +This script runs unit tests, coverage, pylint and pycodestyle +checks and saves the results in the reports directory. Example usage: # This will run the unit tests with the --full-trace option - python3 run_tests.py -u --full-trace + python3 run_checks.py -u --full-trace """ import os diff --git a/setup.cfg b/setup.cfg index d61aa56..d643be0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,8 +2,7 @@ name = ankaios-sdk version = 0.1.0 ankaios_version = 0.5.0 -author= Elektrobit Automotive GmbH and Ankaios contributors -# author_email = +author = Elektrobit Automotive GmbH and Ankaios contributors license = Apache-2.0 [tool:pytest] diff --git a/setup.py b/setup.py index 628ade9..1a7ee9a 100644 --- a/setup.py +++ b/setup.py @@ -102,29 +102,29 @@ def generate_protos(): "Bug Tracker": "https://github.com/eclipse-ankaios/ank-sdk-python/issues", }, install_requires=[ - "protobuf==5.27.2", - "PyYAML", + "protobuf==5.27.2", # Protocol Buffers + "PyYAML", # Used to parse manifest files ], setup_requires=[ - "protobuf==5.27.2", - "grpcio-tools>=1.56.2", - "requests", + "protobuf==5.27.2", # Protocol Buffers + "grpcio-tools>=1.56.2", # Needed for an OS independent protoc + "requests", # Used to download the proto files ], extras_require={ # Development dependencies 'dev': [ - 'pytest', - 'pytest-cov', - 'pylint', - 'pycodestyle', + 'pytest', # Testing framework + 'pytest-cov', # Coverage plugin + 'pylint', # Linter + 'pycodestyle', # Style guide checker ], # Documentation dependencies 'docs': [ - 'sphinx', - 'sphinx-rtd-theme', - 'sphinx-autodoc-typehints', - 'sphinx-mdinclude', - 'google-api-python-client', + 'sphinx', # Documentation generator + 'sphinx-rtd-theme', # Read the Docs theme + 'sphinx-autodoc-typehints', # Type hints support + 'sphinx-mdinclude', # Markdown include support + 'google-api-python-client', # Required for the Google API docstring extension ], }, ) diff --git a/tests/response/test_response.py b/tests/response/test_response.py index 16e664e..d8ed7de 100644 --- a/tests/response/test_response.py +++ b/tests/response/test_response.py @@ -49,8 +49,8 @@ response=_ank_base.Response( requestId="1234", UpdateStateSuccess=_ank_base.UpdateStateSuccess( - addedWorkloads=["new_nginx"], - deletedWorkloads=["old_nginx"], + addedWorkloads=["new_nginx.12345.agent_A"], + deletedWorkloads=["old_nginx.54321.agent_A"], ) ) ) @@ -85,10 +85,12 @@ def test_initialisation(): # Test UpdateStateSuccess message response = Response(MESSAGE_BUFFER_UPDATE_SUCCESS) assert response.content_type == "update_state_success" - assert response.content == { - "added_workloads": ["new_nginx"], - "deleted_workloads": ["old_nginx"], - } + added_workloads = response.content["added_workloads"] + deleted_workloads = response.content["deleted_workloads"] + assert len(added_workloads) == 1 + assert len(deleted_workloads) == 1 + assert str(added_workloads[0]) == "new_nginx.12345.agent_A" + assert str(deleted_workloads[0]) == "old_nginx.54321.agent_A" # Test invalid buffer with pytest.raises(ValueError, match="Invalid response, parsing error"): diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index aea927a..d752411 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -21,7 +21,8 @@ from unittest.mock import patch, mock_open, MagicMock import pytest from ankaios_sdk import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, \ - Manifest, CompleteState + Manifest, CompleteState, WorkloadInstanceName, WorkloadStateCollection, \ + WorkloadStateEnum from tests.workload.test_workload import generate_test_workload from tests.test_request import generate_test_request from tests.response.test_response import MESSAGE_BUFFER_ERROR, \ @@ -349,31 +350,9 @@ def test_configs(): """ ankaios = Ankaios() - # Note for the set from file tests: - # with patch("builtins.open", mock_open()) as mock_file, \ - # patch("ankaios_sdk.Ankaios.set_config") as mock_set_config: - # mock_file().read.return_value = {'config_test': 'value'} - # ankaios.set_config_from_file(name="config_test", - # config_path=r"path/to/config") - - # mock_file.assert_called_with( - # r"path/to/config", "r", encoding="utf-8" - # ) - # mock_file().read.assert_called_once() - # mock_set_config.assert_called_once_with( - # "config_test", {'config_test': 'value'} - # ) - - with pytest.raises(NotImplementedError, match="not implemented yet"): - ankaios.set_configs_from_file(configs_path=r"path/to/configs") - with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.set_configs(configs={'name': 'config'}) - with pytest.raises(NotImplementedError, match="not implemented yet"): - ankaios.set_config_from_file(name="config_test", - config_path=r"path/to/config") - with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.set_config(name="config_test", config={'config_test': 'value'}) @@ -384,7 +363,7 @@ def test_configs(): ankaios.get_config(name="config_test") with pytest.raises(NotImplementedError, match="not implemented yet"): - ankaios.delete_configs() + ankaios.delete_all_configs() with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.delete_config(name="config_test") @@ -436,13 +415,6 @@ def test_get_agents(): mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) mock_state_get_agents.assert_called_once() - with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_agents") \ - as mock_state_get_agents: - ankaios.get_agents(state=CompleteState()) - mock_get_state.assert_not_called() - mock_state_get_agents.assert_called_once() - def test_get_workload_states(): """ @@ -458,12 +430,57 @@ def test_get_workload_states(): mock_get_state.assert_called_once_with(Ankaios.DEFAULT_TIMEOUT) mock_state_get_workload_states.assert_called_once() + +def test_get_workload_states_for_instance_name(): + """ + Test the get workload states for instance name method of the Ankaios class. + """ + ankaios = Ankaios() + ankaios.logger = MagicMock() + workload_instance_name = WorkloadInstanceName( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234" + ) + + # State is None with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ as mock_state_get_workload_states: - ankaios.get_workload_states(state=CompleteState()) - mock_get_state.assert_not_called() + mock_get_state.return_value = None + assert ankaios.get_execution_state_for_instance_name( + workload_instance_name + ) is None + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, [workload_instance_name.get_filter_mask()] + ) + mock_state_get_workload_states.assert_not_called() + + # State does not contain the required workload state + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: + mock_get_state.return_value = CompleteState() + assert ankaios.get_execution_state_for_instance_name( + workload_instance_name + ) is None mock_state_get_workload_states.assert_called_once() + ankaios.logger.error.assert_called() + + # State contains the required workload state + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states, \ + patch("ankaios_sdk.WorkloadStateCollection.get_as_list") \ + as mock_state_get_as_list: + mock_get_state.return_value = CompleteState() + mock_state_get_workload_states.return_value = WorkloadStateCollection() + workload_state = MagicMock() + workload_state.execution_state = MagicMock() + mock_state_get_as_list.return_value = [workload_state] + assert ankaios.get_execution_state_for_instance_name( + workload_instance_name + ) == workload_state.execution_state def test_get_workload_states_on_agent(): @@ -482,13 +499,6 @@ def test_get_workload_states_on_agent(): ) mock_state_get_workload_states.assert_called_once() - with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_workload_states") \ - as mock_state_get_workload_states: - ankaios.get_workload_states_on_agent("agent_A", state=CompleteState()) - mock_get_state.assert_not_called() - mock_state_get_workload_states.assert_called_once() - def test_get_workload_states_for_name(): """ @@ -496,21 +506,71 @@ def test_get_workload_states_for_name(): """ ankaios = Ankaios() + # Invalid state + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workload_states") \ + as mock_state_get_workload_states: + mock_get_state.return_value = None + assert ankaios.get_workload_states_for_name("nginx") is None + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, ["workloadStates"] + ) + + # Valid state, workload not found with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ as mock_state_get_workload_states: mock_get_state.return_value = CompleteState() - ankaios.get_workload_states_for_name("nginx") + ret = ankaios.get_workload_states_for_name("nginx") + assert isinstance(ret, WorkloadStateCollection) mock_get_state.assert_called_once_with( - Ankaios.DEFAULT_TIMEOUT, ["workloadStates.nginx"] + Ankaios.DEFAULT_TIMEOUT, ["workloadStates"] ) mock_state_get_workload_states.assert_called_once() + # Valid state, workload found with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ as mock_state_get_workload_states: - ankaios.get_workload_states_for_name( - "nginx", state=CompleteState() + mock_get_state.return_value = CompleteState() + wl_state_collection = WorkloadStateCollection() + wl_state = MagicMock() + wl_state.name = "nginx" + wl_state_collection.add_workload_state(wl_state) + mock_state_get_workload_states.return_value = wl_state_collection + ret = ankaios.get_workload_states_for_name("nginx") + assert isinstance(ret, WorkloadStateCollection) + assert wl_state in ret.get_as_list() + + +def test_wait_for_workload_to_reach_state(): + """ + Test the wait for workload to reach state method of the Ankaios class. + """ + ankaios = Ankaios() + instance_name = WorkloadInstanceName( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234" + ) + + # Test timeout + with patch("ankaios_sdk.Ankaios.get_execution_state_for_instance_name") \ + as mock_get_state: + mock_get_state.return_value = MagicMock() + mock_get_state().state = WorkloadStateEnum.FAILED + assert not ankaios.wait_for_workload_to_reach_state( + instance_name, WorkloadStateEnum.RUNNING, + timeout=0.01 ) - mock_get_state.assert_not_called() - mock_state_get_workload_states.assert_called_once() + mock_get_state.assert_called() + + # Test success + with patch("ankaios_sdk.Ankaios.get_execution_state_for_instance_name") \ + as mock_get_state: + mock_get_state.return_value = MagicMock() + mock_get_state().state = WorkloadStateEnum.RUNNING + assert ankaios.wait_for_workload_to_reach_state( + instance_name, WorkloadStateEnum.RUNNING + ) + mock_get_state.assert_called() diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 9234d2f..a0d2a4a 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -98,6 +98,10 @@ def test_check(): with pytest.raises(ValueError, match="Invalid manifest"): manifest = Manifest({'apiVersion': 'v0.1'}) + with pytest.raises(ValueError, match="Invalid manifest"): + manifest = Manifest({'apiVersion': 'v0.1', 'workloads': + {'nginx_test': {}}}) + with pytest.raises(ValueError, match="Invalid manifest"): manifest = Manifest({'apiVersion': 'v0.1', 'workloads': {'nginx_test': {'invalid_key': ''}}}) diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index 2e8a4c1..43dc046 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -119,22 +119,21 @@ def test_dependencies( Args: workload (Workload): The Workload fixture. """ - assert len(workload.get_dependencies()) == 1 + deps = workload.get_dependencies() + assert len(deps) == 1 + deps["other_workload_test"] = "ADD_COND_SUCCEEDED" with pytest.raises(ValueError): - workload.add_dependency("other_workload_test", "ADD_COND_DANCING") + workload.update_dependencies( + {"other_workload_test": "ADD_COND_DANCING"} + ) - workload.add_dependency("other_workload_test", "ADD_COND_SUCCEEDED") + workload.update_dependencies(deps) assert len(workload.get_dependencies()) == 2 - workload.add_dependency("another_workload_test", "ADD_COND_FAILED") - - deps = workload.get_dependencies() - assert len(deps) == 3 deps.pop("other_workload_test") - workload.update_dependencies(deps) - assert len(workload.get_dependencies()) == 2 + assert len(workload.get_dependencies()) == 1 def test_tags(workload: Workload): # pylint: disable=redefined-outer-name @@ -164,34 +163,25 @@ def test_rules(workload: Workload): # pylint: disable=redefined-outer-name Args: workload (Workload): The Workload fixture. """ - assert len(workload.get_allow_rules()) == 1 - assert len(workload.get_deny_rules()) == 1 + allow_rules = workload.get_allow_rules() + deny_rules = workload.get_deny_rules() + assert len(allow_rules) == 1 + assert len(deny_rules) == 1 with pytest.raises(ValueError): - workload.add_allow_rule("Invalid", ["mask"]) + workload.update_allow_rules([("Invalid", ["mask"])]) with pytest.raises(ValueError): - workload.add_deny_rule("Invalid", ["mask"]) - - workload.add_allow_rule( - "Write", ["desiredState.workloads.another_workload"] - ) - assert len(workload.get_allow_rules()) == 2 + workload.update_deny_rules([("Invalid", ["mask"])]) - workload.add_deny_rule( - "Read", ["workloadStates.agent_Test.another_workload"] - ) - assert len(workload.get_deny_rules()) == 2 + allow_rules.append(("Write", ["desiredState.workloads.another_workload"])) + deny_rules.append(("Read", ["workloadStates.agent_Test.another_workload"])) - rules = workload.get_allow_rules() - rules = rules[1:] - workload.update_allow_rules(rules) - assert len(workload.get_allow_rules()) == 1 + workload.update_allow_rules(allow_rules) + workload.update_deny_rules(deny_rules) - rules = workload.get_deny_rules() - rules = rules[1:] - workload.update_deny_rules(rules) - assert len(workload.get_deny_rules()) == 1 + assert len(workload.get_allow_rules()) == 2 + assert len(workload.get_deny_rules()) == 2 def test_configs(workload: Workload): # pylint: disable=redefined-outer-name @@ -296,15 +286,19 @@ def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name "desiredState.workloads.workload_test.restartPolicy"), ("update_runtime_config", {"config": "config_test"}, "desiredState.workloads.workload_test.runtimeConfig"), - ("add_dependency", {"workload_name": "workload_test_other", - "condition": "ADD_COND_RUNNING"}, + ("update_dependencies", {"dependencies": + {"workload_test_other": "ADD_COND_RUNNING"}}, "desiredState.workloads.workload_test.dependencies"), ("add_tag", {"key": "key1", "value": "value1"}, + "desiredState.workloads.workload_test.tags.key1"), + ("update_tags", {"tags": [("key1", "value1"), ("key2", "value")]}, "desiredState.workloads.workload_test.tags"), - ("add_allow_rule", {"operation": "Write", "filter_masks": ["mask"]}, - "desiredState.workloads.workload_test.controlInterfaceAccess"), - ("add_deny_rule", {"operation": "Write", "filter_masks": ["mask"]}, - "desiredState.workloads.workload_test.controlInterfaceAccess"), + ("update_allow_rules", {"rules": [("Write", ["mask"])]}, + "desiredState.workloads.workload_test." + + "controlInterfaceAccess.allowRules"), + ("update_deny_rules", {"rules": [("Write", ["mask"])]}, + "desiredState.workloads.workload_test." + + "controlInterfaceAccess.denyRules"), ]) def test_mask_generation(function_name: str, data: dict, mask: str): """ diff --git a/tests/workload/test_workload_builder.py b/tests/workload/test_workload_builder.py index e0e95d0..6e6640f 100644 --- a/tests/workload/test_workload_builder.py +++ b/tests/workload/test_workload_builder.py @@ -23,6 +23,7 @@ class in the ankaios_sdk. from unittest.mock import patch, mock_open import pytest from ankaios_sdk import Workload, WorkloadBuilder +from ankaios_sdk._protos import _ank_base @pytest.fixture @@ -178,7 +179,28 @@ def test_build( .restart_policy("NEVER") \ .add_dependency("workload_test_other", "ADD_COND_RUNNING") \ .add_tag("key_test", "abc") \ + .add_allow_rule("Write", ["mask"]) \ .build() assert workload is not None assert isinstance(workload, Workload) + + assert workload.name == "workload_test" + assert workload._workload.agent == "agent_Test" + assert workload._workload.runtime == "runtime_test" + assert workload._workload.runtimeConfig == "config_test" + assert workload._workload.restartPolicy == _ank_base.RestartPolicy.NEVER + assert workload._workload.dependencies == _ank_base.Dependencies( + dependencies={"workload_test_other": + _ank_base.AddCondition.ADD_COND_RUNNING} + ) + assert workload._workload.tags == _ank_base.Tags( + tags=[_ank_base.Tag(key="key_test", value="abc")] + ) + assert workload._workload.controlInterfaceAccess.allowRules == [ + _ank_base.AccessRightsRule( + stateRule=_ank_base.StateRule( + operation=_ank_base.ReadWriteEnum.RW_WRITE, + filterMasks=["mask"] + ) + )] diff --git a/tests/workload_state/test_workload_instance_name.py b/tests/workload_state/test_workload_instance_name.py index 3b540d1..a81e3f5 100644 --- a/tests/workload_state/test_workload_instance_name.py +++ b/tests/workload_state/test_workload_instance_name.py @@ -20,9 +20,9 @@ class in the ankaios_sdk. from ankaios_sdk import WorkloadInstanceName -def test_creation(): +def test_methods(): """ - Test the creation of a WorkloadInstanceName instance, + Test the methods of a WorkloadInstanceName instance, ensuring it is correctly initialized with the provided attributes. """ workload_instance_name = WorkloadInstanceName( @@ -34,4 +34,26 @@ def test_creation(): assert workload_instance_name.agent_name == "agent_Test" assert workload_instance_name.workload_name == "workload_Test" assert workload_instance_name.workload_id == "1234" - assert str(workload_instance_name) == "agent_Test.workload_Test.1234" + assert str(workload_instance_name) == "workload_Test.1234.agent_Test" + assert workload_instance_name.get_filter_mask() == \ + "workloadStates.agent_Test.workload_Test.1234" + + +def test_equality(): + """ + Test the equality of two WorkloadInstanceName instances. + """ + workload_instance_name = WorkloadInstanceName( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234" + ) + other_workload_instance_name = WorkloadInstanceName( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234" + ) + assert workload_instance_name == other_workload_instance_name + other_workload_instance_name.workload_id = "5678" + assert workload_instance_name != other_workload_instance_name + assert workload_instance_name != "" diff --git a/tests/workload_state/test_workload_state_collection.py b/tests/workload_state/test_workload_state_collection.py index 0230a42..5c00a97 100644 --- a/tests/workload_state/test_workload_state_collection.py +++ b/tests/workload_state/test_workload_state_collection.py @@ -18,7 +18,7 @@ class in the ankaios_sdk. """ from ankaios_sdk import WorkloadStateCollection, WorkloadState, \ - WorkloadExecutionState + WorkloadExecutionState, WorkloadInstanceName from ankaios_sdk._protos import _ank_base @@ -62,6 +62,20 @@ def test_get(): WorkloadExecutionState ) + # Test get_for_instance_name + workload_instance_name = WorkloadInstanceName( + agent_name="agent_Test", + workload_name="workload_Test", + workload_id="1234" + ) + assert workload_state_collection.get_for_instance_name( + workload_instance_name + ) == workload_state + workload_instance_name.workload_id = "5678" + assert workload_state_collection.get_for_instance_name( + workload_instance_name + ) is None + def test_from_proto(): """ From cd499b19d1cc3daf447897446a3668619c7795bc Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 18 Oct 2024 17:47:10 +0300 Subject: [PATCH 39/72] Add custom exceptions --- ankaios_sdk/__init__.py | 1 + ankaios_sdk/_components/manifest.py | 22 +-- ankaios_sdk/_components/request.py | 13 +- ankaios_sdk/_components/response.py | 9 +- ankaios_sdk/_components/workload.py | 49 ++--- ankaios_sdk/ankaios.py | 239 +++++++++++++----------- ankaios_sdk/exceptions.py | 72 +++++++ docs/source/exceptions.rst | 7 + docs/source/index.rst | 1 + tests/response/test_response.py | 6 +- tests/test_ankaios.py | 150 +++++++-------- tests/test_manifest.py | 41 ++-- tests/test_request.py | 6 +- tests/workload/test_workload.py | 10 +- tests/workload/test_workload_builder.py | 10 +- 15 files changed, 369 insertions(+), 267 deletions(-) create mode 100644 ankaios_sdk/exceptions.py create mode 100644 docs/source/exceptions.rst diff --git a/ankaios_sdk/__init__.py b/ankaios_sdk/__init__.py index a0b3de8..752b169 100644 --- a/ankaios_sdk/__init__.py +++ b/ankaios_sdk/__init__.py @@ -34,6 +34,7 @@ Represents the complete state of the Ankaios cluster. """ +from .exceptions import * from .ankaios import * from ._components import * diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index 387f6cf..6b5191f 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -47,6 +47,7 @@ """ import yaml +from ..exceptions import InvalidManifestException from .complete_state import CompleteState @@ -66,9 +67,7 @@ def __init__(self, manifest: dict) -> None: ValueError: If the manifest data is invalid. """ self._manifest: dict = manifest - - if not self.check(): - raise ValueError("Invalid manifest") + self.check() @staticmethod def from_file(file_path: str) -> 'Manifest': @@ -123,17 +122,17 @@ def from_dict(manifest: dict) -> 'Manifest': """ return Manifest(manifest) - def check(self) -> bool: + def check(self) -> None: """ Validates the manifest data. - Returns: - bool: True if the manifest data is valid, False otherwise. + Raises: + InvalidManifestException: If the manifest is invalid. """ if "apiVersion" not in self._manifest.keys(): - return False + raise InvalidManifestException("apiVersion is missing.") if "workloads" not in self._manifest.keys(): - return False + raise InvalidManifestException("workloads is missing.") wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", "dependencies", "tags", "controlInterfaceAccess"] @@ -142,12 +141,13 @@ def check(self) -> bool: # Check allowed keys for key in self._manifest["workloads"][wl_name].keys(): if key not in wl_allowed_keys: - return False + raise InvalidManifestException( + f"Invalid key in workload {wl_name}: {key}") # Check mandatory keys for key in wl_mandatory_keys: if key not in self._manifest["workloads"][wl_name].keys(): - return False - return True + raise InvalidManifestException( + f"Mandatory key {key} missing in workload {wl_name}") def _calculate_masks(self) -> list[str]: """ diff --git a/ankaios_sdk/_components/request.py b/ankaios_sdk/_components/request.py index aa1aeb4..7c77084 100644 --- a/ankaios_sdk/_components/request.py +++ b/ankaios_sdk/_components/request.py @@ -52,6 +52,7 @@ import uuid from .._protos import _ank_base +from ..exceptions import RequestException from .complete_state import CompleteState @@ -68,15 +69,15 @@ def __init__(self, request_type: str) -> None: either "update_state" or "get_state". Raises: - ValueError: If the request type is invalid. + RequestException: If the request type is invalid. """ self._request = _ank_base.Request() self._request.requestId = str(uuid.uuid4()) self._request_type = request_type if request_type not in ["update_state", "get_state"]: - raise ValueError("Invalid request type. Supported values: " - + "'update_state', 'get_state'.") + raise RequestException("Invalid request type. Supported values: " + + "'update_state', 'get_state'.") def __str__(self) -> str: """ @@ -105,11 +106,11 @@ def set_complete_state(self, complete_state: CompleteState) -> None: set for the request. Raises: - ValueError: If the request type is not "update_state". + RequestException: If the request type is not "update_state". """ if self._request_type != "update_state": - raise ValueError("Complete state can only be set " - + "for an update state request.") + raise RequestException("Complete state can only be set " + + "for an update state request.") self._request.updateStateRequest.newState.CopyFrom( complete_state._to_proto() diff --git a/ankaios_sdk/_components/response.py b/ankaios_sdk/_components/response.py index 7ebbb29..78924d2 100644 --- a/ankaios_sdk/_components/response.py +++ b/ankaios_sdk/_components/response.py @@ -46,6 +46,7 @@ from typing import Union from threading import Event from .._protos import _control_api +from ..exceptions import ResponseException from .complete_state import CompleteState from .workload_state import WorkloadInstanceName @@ -81,14 +82,14 @@ def _parse_response(self) -> None: Parses the received message buffer into a protobuf response message. Raises: - ValueError: If there is an error parsing the message buffer. + ResponseException: If there is an error parsing the message buffer. """ from_ankaios = _control_api.FromAnkaios() try: # Deserialize the received proto msg from_ankaios.ParseFromString(self.buffer) except Exception as e: - raise ValueError(f"Invalid response, parsing error: '{e}'") from e + raise ResponseException(f"Parsing error: '{e}'") from e self._response = from_ankaios.response def _from_proto(self) -> None: @@ -98,7 +99,7 @@ def _from_proto(self) -> None: or an update state success. Raises: - ValueError: If the response type is invalid. + ResponseException: If the response type is invalid. """ if self._response.HasField("error"): self.content_type = "error" @@ -131,7 +132,7 @@ def _from_proto(self) -> None: ) ) else: - raise ValueError("Invalid response type.") + raise ResponseException("Invalid response type.") def get_request_id(self) -> str: """ diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index 3c1ceeb..a1c1a9a 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -72,6 +72,7 @@ from .._protos import _ank_base +from ..exceptions import WorkloadFieldException, WorkloadBuilderException # pylint: disable=too-many-public-methods @@ -175,11 +176,12 @@ def update_restart_policy(self, policy: str) -> None: policy (str): The restart policy to update. Raises: - ValueError: If an invalid restart policy is provided. + WorkloadFieldException: If an invalid restart policy is provided. """ if policy not in _ank_base.RestartPolicy.keys(): - raise ValueError("Invalid restart policy. Supported values: " - + ", ".join(_ank_base.RestartPolicy.keys()) + ".") + raise WorkloadFieldException( + "restart policy", policy, _ank_base.RestartPolicy.keys() + ) self._workload.restartPolicy = _ank_base.RestartPolicy.Value(policy) self._add_mask(f"{self._main_mask}.restartPolicy") @@ -207,15 +209,15 @@ def update_dependencies(self, dependencies: dict[str, str]) -> None: workload names and condition as values. Raises: - ValueError: If an invalid condition is provided. + WorkloadFieldException: If an invalid condition is provided. """ self._workload.dependencies.dependencies.clear() for workload_name, condition in dependencies.items(): if condition not in _ank_base.AddCondition.keys(): - raise ValueError( - f"Invalid condition for workload {workload_name}. " - + "Supported values: " - + ", ".join(_ank_base.AddCondition.keys()) + ".") + raise WorkloadFieldException( + "dependency condition", condition, + _ank_base.AddCondition.keys() + ) self._workload.dependencies.dependencies[workload_name] = \ _ank_base.AddCondition.Value(condition) self._add_mask(f"{self._main_mask}.dependencies") @@ -277,7 +279,7 @@ def _generate_access_right_rule(self, _ank_base.AccessRightsRule: The access rights rule generated. Raises: - ValueError: If an invalid operation is provided. + WorkloadFieldException: If an invalid operation is provided. """ enum_mapper = { "Nothing": _ank_base.ReadWriteEnum.RW_NOTHING, @@ -286,11 +288,9 @@ def _generate_access_right_rule(self, "ReadWrite": _ank_base.ReadWriteEnum.RW_READ_WRITE, } if operation not in enum_mapper: - raise ValueError( - f"Invalid operation {operation}. " - + "Supported values: " - + ", ".join(enum_mapper.keys()) + "." - ) + raise WorkloadFieldException( + "rule operation", operation, enum_mapper.keys() + ) return _ank_base.AccessRightsRule( stateRule=_ank_base.StateRule( operation=enum_mapper[operation], @@ -343,7 +343,7 @@ def update_allow_rules(self, rules: list[tuple[str, list[str]]]) -> None: operation and filter masks. Raises: - ValueError: If an invalid operation is provided + WorkloadFieldException: If an invalid operation is provided """ while len(self._workload.controlInterfaceAccess.allowRules) > 0: self._workload.controlInterfaceAccess.allowRules.pop() @@ -375,7 +375,7 @@ def update_deny_rules(self, rules: list[tuple[str, list[str]]]) -> None: operation and filter masks. Raises: - ValueError: If an invalid operation is provided + WorkloadFieldException: If an invalid operation is provided """ while len(self._workload.controlInterfaceAccess.denyRules) > 0: self._workload.controlInterfaceAccess.denyRules.pop() @@ -684,22 +684,23 @@ def build(self) -> Workload: Workload: The built Workload object. Raises: - ValueError: If required fields are not set. + WorkloadBuilderException: If required fields are not set. """ if self.wl_name is None: - raise ValueError("Workload can not be built without a name.") + raise WorkloadBuilderException( + "Workload can not be built without a name.") workload = Workload(self.wl_name) if self.wl_agent_name is None: - raise ValueError("Workload can not be built without an " - + "agent name.") + raise WorkloadBuilderException( + "Workload can not be built without an agent name.") if self.wl_runtime is None: - raise ValueError("Workload can not be built without a " - + "runtime.") + raise WorkloadBuilderException( + "Workload can not be built without a runtime.") if self.wl_runtime_config is None: - raise ValueError("Workload can not be built without a " - + "runtime configuration.") + raise WorkloadBuilderException( + "Workload can not be built without a runtime configuration.") workload.update_agent_name(self.wl_agent_name) workload.update_runtime(self.wl_runtime) diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 61a0517..e81dbd7 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -117,13 +117,15 @@ import logging import time -from typing import Optional, Union +from typing import Union from enum import Enum import threading from google.protobuf.internal.encoder import _VarintBytes from google.protobuf.internal.decoder import _DecodeVarint from ._protos import _control_api +from .exceptions import AnkaiosConnectionException, AnkaiosException, \ + ResponseException from ._components import Workload, CompleteState, Request, Response, \ ResponseEvent, WorkloadStateCollection, Manifest, \ WorkloadInstanceName, WorkloadStateEnum, \ @@ -191,11 +193,16 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: exc_type (type): The exception type. exc_value (Exception): The exception instance. traceback (traceback): The traceback object. + + Raises: + AnkaiosConnectionException: If an exception occurred. """ + self._disconnect() if exc_type is not None: # pragma: no cover self.logger.error("An exception occurred: %s, %s, %s", exc_type, exc_value, traceback) - self._disconnect() + raise AnkaiosConnectionException( + f"An exception occurred: {exc_type}, {exc_value}, {traceback}") def _create_logger(self) -> None: """Create a logger with custom format and default log level.""" @@ -213,6 +220,10 @@ def _read_from_control_interface(self) -> None: This is meant to be run in a separate thread. It reads the response from the control interface and saves it in the responses dictionary, by triggering the corresponding ResponseEvent. + + Raises: + AnkaiosConnectionException: If an error occurs + while reading the fifo. """ try: # pylint: disable=consider-using-with @@ -246,7 +257,7 @@ def _read_from_control_interface(self) -> None: try: response = Response(bytes(msg_buf)) - except ValueError as e: # pragma: no cover + except ResponseException as e: # pragma: no cover self.logger.error("Error while reading: %s", e) continue @@ -273,11 +284,15 @@ def _get_response_by_id(self, request_id: str, in seconds. Returns: - Response: The response object. + AnkaiosConnectionException: The response object. + + Raises: + AnkaiosConnectionException: If reading from the control interface + is not started. """ if not self._connected: - raise ValueError("Reading from the control interface " - + "is not started.") + raise AnkaiosConnectionException( + "Reading from the control interface is not started.") with self._responses_lock: if request_id in self._responses: @@ -316,9 +331,14 @@ def _send_request(self, request: Request, Returns: Response: The response object. + + Raises: + AnkaiosConnectionException: If not connected. """ if not self._connected: - raise ValueError("Cannot request if not connected.") + raise AnkaiosConnectionException( + "Cannot request if not connected." + ) self._write_to_pipe(request) try: @@ -342,10 +362,10 @@ def _connect(self) -> None: from the input fifo. Raises: - ValueError: If already connected. + AnkaiosConnectionException: If already connected. """ if self._connected: - raise ValueError("Already connected.") + raise AnkaiosConnectionException("Already connected.") self._connected = True self._read_thread = threading.Thread( target=self._read_from_control_interface @@ -358,14 +378,14 @@ def _disconnect(self) -> None: from the input fifo. Raises: - ValueError: If already disconnected. + AnkaiosConnectionException: If already disconnected. """ if not self._connected: - raise ValueError("Already disconnected.") + raise AnkaiosConnectionException("Already disconnected.") self._connected = False self._read_thread.join() - def apply_manifest(self, manifest: Manifest) -> Optional[dict]: + def apply_manifest(self, manifest: Manifest) -> dict: """ Send a request to apply a manifest. @@ -374,7 +394,11 @@ def apply_manifest(self, manifest: Manifest) -> Optional[dict]: Returns: dict: a dict with the added and deleted workloads. - None: If the manifest was not applied successfully. + + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred while applying + the manifest. """ request = Request(request_type="update_state") request.set_complete_state(manifest.generate_complete_state()) @@ -385,24 +409,23 @@ def apply_manifest(self, manifest: Manifest) -> Optional[dict]: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return None + raise e # Interpret response (content_type, content) = response.get_content() if content_type == "error": self.logger.error("Error while trying to apply manifest: %s", content) - elif content_type == "update_state_success": - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content - return None + raise AnkaiosException(f"Received error: {content}") + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content - def delete_manifest(self, manifest: Manifest) -> Optional[dict]: + def delete_manifest(self, manifest: Manifest) -> dict: """ Send a request to delete a manifest. @@ -411,7 +434,11 @@ def delete_manifest(self, manifest: Manifest) -> Optional[dict]: Returns: dict: a dict with the added and deleted workloads. - None: If the manifest was not deleted successfully. + + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred while deleting + the manifest. """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) @@ -422,24 +449,23 @@ def delete_manifest(self, manifest: Manifest) -> Optional[dict]: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return None + raise e # Interpret response (content_type, content) = response.get_content() if content_type == "error": self.logger.error("Error while trying to delete manifest: %s", content) - elif content_type == "update_state_success": - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content - return None + raise AnkaiosException(f"Received error: {content}") + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content - def run_workload(self, workload: Workload) -> Optional[dict]: + def run_workload(self, workload: Workload) -> dict: """ Send a request to run a workload. @@ -448,7 +474,10 @@ def run_workload(self, workload: Workload) -> Optional[dict]: Returns: dict: a dict with the added and deleted workloads. - None: If the workload was not run successfully. + + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred while running the workload. """ complete_state = CompleteState() complete_state.set_workload(workload) @@ -463,24 +492,23 @@ def run_workload(self, workload: Workload) -> Optional[dict]: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return None + raise e # Interpret response (content_type, content) = response.get_content() if content_type == "error": self.logger.error("Error while trying to run workload: %s", content) - elif content_type == "update_state_success": - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content - return None + raise AnkaiosException(f"Received error: {content}") + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content - def delete_workload(self, workload_name: str) -> Optional[dict]: + def delete_workload(self, workload_name: str) -> dict: """ Send a request to delete a workload. @@ -489,7 +517,10 @@ def delete_workload(self, workload_name: str) -> Optional[dict]: Returns: dict: a dict with the added and deleted workloads. - None: If the workload was not deleted successfully. + + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred while deleting the workload. """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) @@ -499,43 +530,41 @@ def delete_workload(self, workload_name: str) -> Optional[dict]: response = self._send_request(request) except TimeoutError as e: self.logger.error("%s", e) - return None + raise e # Interpret response (content_type, content) = response.get_content() if content_type == "error": self.logger.error("Error while trying to delete workload: %s", content) - elif content_type == "update_state_success": - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content - return None + raise AnkaiosException(f"Received error: {content}") + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content - def get_workload(self, workload_name: str, - state: CompleteState = None, - timeout: float = DEFAULT_TIMEOUT) -> Workload: + def get_workload_with_instance_name( + self, instance_name: WorkloadInstanceName, + timeout: float = DEFAULT_TIMEOUT + ) -> Workload: """ - Get the workload from the requested complete state. + Get the workload from the requested complete state, filtered + with the provided instance name. Args: - workload_name (str): The name of the workload. - state (CompleteState): The complete state to get the workload from. + instance_name (instance_name): The instance name of the workload. timeout (float): The maximum time to wait for the response, in seconds. Returns: Workload: The workload object. """ - if state is None: - state = self.get_state( - timeout, [f"desiredState.workloads.{workload_name}"] - ) - return state.get_workload(workload_name) if state is not None else None + return self.get_state( + timeout, [f"desiredState.workloads.{str(instance_name)}"] + ).get_workloads()[0] def set_configs(self, configs: dict) -> bool: """ @@ -607,7 +636,7 @@ def delete_config(self, name: str) -> bool: raise NotImplementedError("delete_config is not implemented yet.") def get_state(self, timeout: float = DEFAULT_TIMEOUT, - field_masks: list[str] = None) -> Optional[CompleteState]: + field_masks: list[str] = None) -> CompleteState: """ Send a request to get the complete state. @@ -619,7 +648,10 @@ def get_state(self, timeout: float = DEFAULT_TIMEOUT, Returns: CompleteState: The complete state object. - None: If the state was not retrieved successfully. + + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred while getting the state. """ request = Request(request_type="get_state") if field_masks is not None: @@ -628,20 +660,20 @@ def get_state(self, timeout: float = DEFAULT_TIMEOUT, response = self._send_request(request, timeout) except TimeoutError as e: self.logger.error("%s", e) - return None + raise e # Interpret response (content_type, content) = response.get_content() if content_type == "error": self.logger.error("Error while trying to get the state: %s", content) - return None + raise AnkaiosException(f"Received error: {content}") return content def get_agents( self, timeout: float = DEFAULT_TIMEOUT - ) -> Optional[list[str]]: + ) -> list[str]: """ Get the agents from the requested complete state. @@ -651,14 +683,12 @@ def get_agents( Returns: list[str]: The list of agent names. - None: If the state was not retrieved successfully. """ - state = self.get_state(timeout) - return state.get_agents() if state is not None else None + return self.get_state(timeout).get_agents() def get_workload_states(self, timeout: float = DEFAULT_TIMEOUT - ) -> Optional[WorkloadStateCollection]: + ) -> WorkloadStateCollection: """ Get the workload states from the requested complete state. @@ -668,16 +698,14 @@ def get_workload_states(self, Returns: WorkloadStateCollection: The collection of workload states. - None: If the state was not retrieved successfully. """ - state = self.get_state(timeout) - return state.get_workload_states() if state is not None else None + return self.get_state(timeout).get_workload_states() def get_execution_state_for_instance_name( self, instance_name: WorkloadInstanceName, timeout: float = DEFAULT_TIMEOUT - ) -> Optional[WorkloadExecutionState]: + ) -> WorkloadExecutionState: """ Get the workload states for a specific workload instance name from the requested complete state. @@ -690,22 +718,25 @@ def get_execution_state_for_instance_name( Returns: WorkloadExecutionState: The specified workload's execution state. - None: If the state was not retrieved successfully. + + Raises: + AnkaiosException: If the workload state was not + retrieved successfully. """ state = self.get_state(timeout, [instance_name.get_filter_mask()]) - if state is not None: - workload_states = state.get_workload_states().get_as_list() - if len(workload_states) != 1: - self.logger.error("Expected exactly one workload state " - + "for instance name %s, but got %s", - instance_name, len(workload_states)) - return None - return workload_states[0].execution_state - return None + workload_states = state.get_workload_states().get_as_list() + if len(workload_states) != 1: + self.logger.error("Expected exactly one workload state " + + "for instance name %s, but got %s", + instance_name, len(workload_states)) + raise AnkaiosException( + "Expected exactly one workload state for instance name " + + f"{instance_name}, but got {len(workload_states)}") + return workload_states[0].execution_state def get_workload_states_on_agent(self, agent_name: str, timeout: float = DEFAULT_TIMEOUT - ) -> Optional[WorkloadStateCollection]: + ) -> WorkloadStateCollection: """ Get the workload states on a specific agent from the requested complete state. @@ -717,14 +748,13 @@ def get_workload_states_on_agent(self, agent_name: str, Returns: WorkloadStateCollection: The collection of workload states. - None: If the state was not retrieved successfully """ state = self.get_state(timeout, ["workloadStates." + agent_name]) - return state.get_workload_states() if state is not None else None + return state.get_workload_states() def get_workload_states_for_name(self, workload_name: str, timeout: float = DEFAULT_TIMEOUT - ) -> Optional[WorkloadStateCollection]: + ) -> WorkloadStateCollection: """ Get the workload states for a specific workload name from the requested complete state. @@ -736,13 +766,10 @@ def get_workload_states_for_name(self, workload_name: str, Returns: WorkloadStateCollection: The collection of workload states. - None: If the state was not retrieved successfully. """ state = self.get_state( timeout, ["workloadStates"] ) - if state is None: - return None workload_states = state.get_workload_states().get_as_list() workload_states_for_name = WorkloadStateCollection() for workload_state in workload_states: @@ -754,7 +781,7 @@ def wait_for_workload_to_reach_state(self, instance_name: WorkloadInstanceName, state: WorkloadStateEnum, timeout: float = DEFAULT_TIMEOUT - ) -> bool: + ) -> None: """ Waits for the workload to reach the specified state. @@ -765,8 +792,8 @@ def wait_for_workload_to_reach_state(self, timeout (float): The maximum time to wait for the response, in seconds. - Returns: - bool: True if the workload reached the state, False otherwise. + Raises: + TimeoutError: If the state was not reached in time. """ start_time = time.time() while time.time() - start_time < timeout: @@ -774,6 +801,8 @@ def wait_for_workload_to_reach_state(self, instance_name ) if workload_state is not None and workload_state.state == state: - return True + return time.sleep(0.1) - return False + raise TimeoutError( + "Timeout while waiting for workload to reach state." + ) diff --git a/ankaios_sdk/exceptions.py b/ankaios_sdk/exceptions.py new file mode 100644 index 0000000..a3b76be --- /dev/null +++ b/ankaios_sdk/exceptions.py @@ -0,0 +1,72 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This script defines the exceptions used in the ankaios_sdk module. + +Exceptions +---------- +- WorkloadFieldException: Raised when the workload field is invalid. +- WorkloadBuilderException: Raised when the workload builder is invalid. +- InvalidManifestException: Raised when the manifest file is invalid. +- RequestException: Raised when the request is invalid. +- ResponseException: Raised when the response is invalid. +- AnkaiosConnectionException: Raised when an operation on the connection fails. +- AnkaiosException: Raised when an update operation fails. +""" + +import inspect + +__all__ = ['WorkloadFieldException', 'WorkloadBuilderException', + 'InvalidManifestException', 'RequestException', 'ResponseException', + 'AnkaiosConnectionException', 'AnkaiosException'] + + +class AnkaiosBaseException(Exception): + """Base class for exceptions in this module.""" + + +class WorkloadFieldException(AnkaiosBaseException): + """Raised when the workload field is invalid""" + def __init__(self, field: str, value: str, accepted_values: list) -> None: + message = f"Invalid value for {field}: \"{value}\"." + message += "Accepted values are: " + ", ".join(accepted_values) + super().__init__(message) + + +class WorkloadBuilderException(AnkaiosBaseException): + """Raised when the workload builder is invalid.""" + + +class InvalidManifestException(AnkaiosBaseException): + """Raised when the manifest file is invalid.""" + + +class RequestException(AnkaiosBaseException): + """Raised when the request is invalid.""" + + +class ResponseException(AnkaiosBaseException): + """Raised when the response is invalid.""" + + +class AnkaiosConnectionException(AnkaiosBaseException): + """Raised when an operation on the connection fails.""" + + +class AnkaiosException(AnkaiosBaseException): + """Raised when an update operation fails.""" + def __init__(self, message): + function_name = inspect.stack()[1].function + super().__init__(f"{function_name}: {message}") diff --git a/docs/source/exceptions.rst b/docs/source/exceptions.rst new file mode 100644 index 0000000..5594363 --- /dev/null +++ b/docs/source/exceptions.rst @@ -0,0 +1,7 @@ +Exceptions +========== + +.. automodule:: ankaios_sdk.exceptions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 61d98a8..0df0280 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,6 +18,7 @@ manifest request response + exceptions .. toctree:: :maxdepth: 2 diff --git a/tests/response/test_response.py b/tests/response/test_response.py index d8ed7de..87034a3 100644 --- a/tests/response/test_response.py +++ b/tests/response/test_response.py @@ -18,7 +18,7 @@ import pytest from google.protobuf.internal.encoder import _VarintBytes -from ankaios_sdk import Response, CompleteState +from ankaios_sdk import Response, CompleteState, ResponseException from ankaios_sdk._protos import _ank_base, _control_api @@ -93,11 +93,11 @@ def test_initialisation(): assert str(deleted_workloads[0]) == "old_nginx.54321.agent_A" # Test invalid buffer - with pytest.raises(ValueError, match="Invalid response, parsing error"): + with pytest.raises(ResponseException, match="Parsing error"): _ = Response(b"invalid_buffer{") # Test invalid response type - with pytest.raises(ValueError, match="Invalid response type"): + with pytest.raises(ResponseException, match="Invalid response type"): response = Response(MESSAGE_BUFFER_INVALID_RESPONSE) diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index d752411..72007a6 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -22,7 +22,7 @@ import pytest from ankaios_sdk import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, \ Manifest, CompleteState, WorkloadInstanceName, WorkloadStateCollection, \ - WorkloadStateEnum + WorkloadStateEnum, AnkaiosConnectionException, AnkaiosException from tests.workload.test_workload import generate_test_workload from tests.test_request import generate_test_request from tests.response.test_response import MESSAGE_BUFFER_ERROR, \ @@ -68,18 +68,21 @@ def test_connection(): mock_thread_instance.start.assert_called_once() assert ankaios._connected - with pytest.raises(ValueError, match="Already connected."): + with pytest.raises(AnkaiosConnectionException, + match="Already connected."): ankaios._connect() ankaios._disconnect() mock_thread_instance.join.assert_called_once() assert not ankaios._connected - with pytest.raises(ValueError, match="Already disconnected."): + with pytest.raises(AnkaiosConnectionException, + match="Already disconnected."): ankaios._disconnect() with Ankaios() as ank: assert ank._connected + assert not ank._connected def test_read_from_control_interface(): @@ -135,7 +138,7 @@ def test_get_reponse_by_id(): """ ankaios = Ankaios() with pytest.raises( - ValueError, + AnkaiosConnectionException, match="Reading from the control interface is not started." ): ankaios._get_response_by_id("1234") @@ -173,7 +176,8 @@ def test_send_request(): Test the _send_request method of the Ankaios class. """ ankaios = Ankaios() - with pytest.raises(ValueError, match="Cannot request if not connected."): + with pytest.raises(AnkaiosConnectionException, + match="Cannot request if not connected."): ankaios._send_request(None) ankaios._connected = True @@ -208,21 +212,24 @@ def test_apply_manifest(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - assert ankaios.apply_manifest(manifest) + ret = ankaios.apply_manifest(manifest) + assert isinstance(ret, dict) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - assert not ankaios.apply_manifest(manifest) + with pytest.raises(AnkaiosException): + ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - assert not ankaios.apply_manifest(manifest) + with pytest.raises(TimeoutError): + ankaios.apply_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -239,21 +246,24 @@ def test_delete_manifest(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - assert ankaios.delete_manifest(manifest) + ret = ankaios.delete_manifest(manifest) + assert isinstance(ret, dict) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - assert not ankaios.delete_manifest(manifest) + with pytest.raises(AnkaiosException): + ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - assert not ankaios.delete_manifest(manifest) + with pytest.raises(TimeoutError): + ankaios.delete_manifest(manifest) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -270,21 +280,24 @@ def test_run_workload(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - assert ankaios.run_workload(workload) + ret = ankaios.run_workload(workload) + assert isinstance(ret, dict) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - assert not ankaios.run_workload(workload) + with pytest.raises(AnkaiosException): + ankaios.run_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - assert not ankaios.run_workload(workload) + with pytest.raises(TimeoutError): + ankaios.run_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -300,48 +313,54 @@ def test_delete_workload(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - assert ankaios.delete_workload("nginx") + ret = ankaios.delete_workload("nginx") + assert isinstance(ret, dict) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - assert not ankaios.delete_workload("nginx") + with pytest.raises(AnkaiosException): + ankaios.delete_workload("nginx") mock_send_request.assert_called_once() ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - assert not ankaios.delete_workload("nginx") + with pytest.raises(TimeoutError): + ankaios.delete_workload("nginx") mock_send_request.assert_called_once() ankaios.logger.error.assert_called() -def test_get_workload(): +def test_get_workload_with_instance_name(): """ - Test the get workload method of the Ankaios class. + Test the get workload with instance name of the Ankaios class. """ ankaios = Ankaios() + workload_instance_name = WorkloadInstanceName( + agent_name="agent_Test", + workload_name="nginx", + workload_id="1234" + ) + workload = generate_test_workload( + workload_name="nginx" + ) with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_workload") \ - as mock_state_get_workload: + patch("ankaios_sdk.CompleteState.get_workloads") \ + as mock_state_get_workloads: mock_get_state.return_value = CompleteState() - ankaios.get_workload("nginx") + mock_state_get_workloads.return_value = [workload] + ret = ankaios.get_workload_with_instance_name(workload_instance_name) + assert ret == workload mock_get_state.assert_called_once_with( Ankaios.DEFAULT_TIMEOUT, - ["desiredState.workloads.nginx"] + ["desiredState.workloads.nginx.1234.agent_Test"] ) - mock_state_get_workload.assert_called_once_with("nginx") - - with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_workload") \ - as mock_state_get_workload: - ankaios.get_workload("nginx", state=CompleteState()) - mock_get_state.assert_not_called() - mock_state_get_workload.assert_called_once_with("nginx") + mock_state_get_workloads.assert_called_once() def test_configs(): @@ -380,24 +399,24 @@ def test_get_state(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_COMPLETE_STATE) - result = ankaios.get_state() + ret = ankaios.get_state() mock_send_request.assert_called_once() - assert isinstance(result, CompleteState) + assert isinstance(ret, CompleteState) # Test error with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) - result = ankaios.get_state(field_masks=["invalid_mask"]) + with pytest.raises(AnkaiosException): + ankaios.get_state(field_masks=["invalid_mask"]) mock_send_request.assert_called_once() - assert result is None ankaios.logger.error.assert_called() # Test timeout with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() - result = ankaios.get_state() + with pytest.raises(TimeoutError): + ankaios.get_state() mock_send_request.assert_called_once() - assert result is None ankaios.logger.error.assert_called() @@ -431,9 +450,9 @@ def test_get_workload_states(): mock_state_get_workload_states.assert_called_once() -def test_get_workload_states_for_instance_name(): +def test_get_workload_state_for_instance_name(): """ - Test the get workload states for instance name method of the Ankaios class. + Test the get workload state for instance name method of the Ankaios class. """ ankaios = Ankaios() ankaios.logger = MagicMock() @@ -443,27 +462,14 @@ def test_get_workload_states_for_instance_name(): workload_id="1234" ) - # State is None - with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_workload_states") \ - as mock_state_get_workload_states: - mock_get_state.return_value = None - assert ankaios.get_execution_state_for_instance_name( - workload_instance_name - ) is None - mock_get_state.assert_called_once_with( - Ankaios.DEFAULT_TIMEOUT, [workload_instance_name.get_filter_mask()] - ) - mock_state_get_workload_states.assert_not_called() - # State does not contain the required workload state with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ as mock_state_get_workload_states: mock_get_state.return_value = CompleteState() - assert ankaios.get_execution_state_for_instance_name( - workload_instance_name - ) is None + with pytest.raises(AnkaiosException): + ankaios.get_execution_state_for_instance_name( + workload_instance_name) mock_state_get_workload_states.assert_called_once() ankaios.logger.error.assert_called() @@ -506,29 +512,6 @@ def test_get_workload_states_for_name(): """ ankaios = Ankaios() - # Invalid state - with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_workload_states") \ - as mock_state_get_workload_states: - mock_get_state.return_value = None - assert ankaios.get_workload_states_for_name("nginx") is None - mock_get_state.assert_called_once_with( - Ankaios.DEFAULT_TIMEOUT, ["workloadStates"] - ) - - # Valid state, workload not found - with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_workload_states") \ - as mock_state_get_workload_states: - mock_get_state.return_value = CompleteState() - ret = ankaios.get_workload_states_for_name("nginx") - assert isinstance(ret, WorkloadStateCollection) - mock_get_state.assert_called_once_with( - Ankaios.DEFAULT_TIMEOUT, ["workloadStates"] - ) - mock_state_get_workload_states.assert_called_once() - - # Valid state, workload found with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ as mock_state_get_workload_states: @@ -559,10 +542,11 @@ def test_wait_for_workload_to_reach_state(): as mock_get_state: mock_get_state.return_value = MagicMock() mock_get_state().state = WorkloadStateEnum.FAILED - assert not ankaios.wait_for_workload_to_reach_state( - instance_name, WorkloadStateEnum.RUNNING, - timeout=0.01 - ) + with pytest.raises(TimeoutError): + ankaios.wait_for_workload_to_reach_state( + instance_name, WorkloadStateEnum.RUNNING, + timeout=0.01 + ) mock_get_state.assert_called() # Test success @@ -570,7 +554,7 @@ def test_wait_for_workload_to_reach_state(): as mock_get_state: mock_get_state.return_value = MagicMock() mock_get_state().state = WorkloadStateEnum.RUNNING - assert ankaios.wait_for_workload_to_reach_state( + ankaios.wait_for_workload_to_reach_state( instance_name, WorkloadStateEnum.RUNNING ) mock_get_state.assert_called() diff --git a/tests/test_manifest.py b/tests/test_manifest.py index a0d2a4a..8337168 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -18,7 +18,7 @@ from unittest.mock import patch, mock_open import pytest -from ankaios_sdk import Manifest, CompleteState +from ankaios_sdk import Manifest, CompleteState, InvalidManifestException MANIFEST_CONTENT = """apiVersion: v0.1 @@ -80,7 +80,7 @@ def test_from_dict(): manifest = Manifest.from_dict(MANIFEST_DICT) assert manifest._manifest == MANIFEST_DICT - with pytest.raises(ValueError, match="Invalid manifest"): + with pytest.raises(InvalidManifestException): _ = Manifest.from_dict({}) @@ -89,22 +89,27 @@ def test_check(): Test the check method of the Manifest class, ensuring it correctly validates the manifest data and handles errors. """ - manifest = Manifest(MANIFEST_DICT) - assert manifest.check() - - with pytest.raises(ValueError, match="Invalid manifest"): - manifest = Manifest({}) - - with pytest.raises(ValueError, match="Invalid manifest"): - manifest = Manifest({'apiVersion': 'v0.1'}) - - with pytest.raises(ValueError, match="Invalid manifest"): - manifest = Manifest({'apiVersion': 'v0.1', 'workloads': - {'nginx_test': {}}}) - - with pytest.raises(ValueError, match="Invalid manifest"): - manifest = Manifest({'apiVersion': 'v0.1', 'workloads': - {'nginx_test': {'invalid_key': ''}}}) + with patch("ankaios_sdk.InvalidManifestException") as mock_exception: + _ = Manifest(MANIFEST_DICT) + assert not mock_exception.called + + with pytest.raises(InvalidManifestException, + match="apiVersion is missing."): + _ = Manifest({}) + + with pytest.raises(InvalidManifestException, + match="workloads is missing."): + _ = Manifest({'apiVersion': 'v0.1'}) + + with pytest.raises(InvalidManifestException, + match="Mandatory key"): + _ = Manifest({'apiVersion': 'v0.1', 'workloads': + {'nginx_test': {}}}) + + with pytest.raises(InvalidManifestException, + match="Invalid key"): + _ = Manifest({'apiVersion': 'v0.1', 'workloads': + {'nginx_test': {'invalid_key': ''}}}) def test_calculate_masks(): diff --git a/tests/test_request.py b/tests/test_request.py index 3d5220f..47b248e 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -17,7 +17,7 @@ """ import pytest -from ankaios_sdk import Request, CompleteState +from ankaios_sdk import Request, CompleteState, RequestException from tests.workload.test_workload import generate_test_workload @@ -41,7 +41,7 @@ def test_general_functionality(): """ Test general functionality of the Request class. """ - with pytest.raises(ValueError, match="Invalid request type."): + with pytest.raises(RequestException, match="Invalid request type."): Request("invalid") request = Request("update_state") @@ -63,7 +63,7 @@ def test_update_state(): assert request._request.updateStateRequest.updateMask == ["test_mask"] with pytest.raises( - ValueError, + RequestException, match="Complete state can only be set for an update state request." ): Request("get_state").set_complete_state(CompleteState()) diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index 43dc046..da48a80 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -25,7 +25,7 @@ from unittest.mock import patch, mock_open import pytest -from ankaios_sdk import Workload, WorkloadBuilder +from ankaios_sdk import Workload, WorkloadBuilder, WorkloadFieldException from ankaios_sdk._protos import _ank_base @@ -104,7 +104,7 @@ def test_update_fields( workload.update_runtime_config_from_file("new_config_test_from_file") assert workload._workload.runtimeConfig == "new_config_test_from_file" - with pytest.raises(ValueError): + with pytest.raises(WorkloadFieldException): workload.update_restart_policy("INVALID_POLICY") workload.update_restart_policy("ON_FAILURE") assert workload._workload.restartPolicy == _ank_base.ON_FAILURE @@ -123,7 +123,7 @@ def test_dependencies( assert len(deps) == 1 deps["other_workload_test"] = "ADD_COND_SUCCEEDED" - with pytest.raises(ValueError): + with pytest.raises(WorkloadFieldException): workload.update_dependencies( {"other_workload_test": "ADD_COND_DANCING"} ) @@ -168,10 +168,10 @@ def test_rules(workload: Workload): # pylint: disable=redefined-outer-name assert len(allow_rules) == 1 assert len(deny_rules) == 1 - with pytest.raises(ValueError): + with pytest.raises(WorkloadFieldException): workload.update_allow_rules([("Invalid", ["mask"])]) - with pytest.raises(ValueError): + with pytest.raises(WorkloadFieldException): workload.update_deny_rules([("Invalid", ["mask"])]) allow_rules.append(("Write", ["desiredState.workloads.another_workload"])) diff --git a/tests/workload/test_workload_builder.py b/tests/workload/test_workload_builder.py index 6e6640f..c77f294 100644 --- a/tests/workload/test_workload_builder.py +++ b/tests/workload/test_workload_builder.py @@ -22,7 +22,7 @@ class in the ankaios_sdk. from unittest.mock import patch, mock_open import pytest -from ankaios_sdk import Workload, WorkloadBuilder +from ankaios_sdk import Workload, WorkloadBuilder, WorkloadBuilderException from ankaios_sdk._protos import _ank_base @@ -149,28 +149,28 @@ def test_build( builder (WorkloadBuilder): The WorkloadBuilder fixture. """ with pytest.raises( - ValueError, + WorkloadBuilderException, match="Workload can not be built without a name." ): builder.build() builder = builder.workload_name("workload_test") with pytest.raises( - ValueError, + WorkloadBuilderException, match="Workload can not be built without an agent name." ): builder.build() builder = builder.agent_name("agent_Test") with pytest.raises( - ValueError, + WorkloadBuilderException, match="Workload can not be built without a runtime." ): builder.build() builder = builder.runtime("runtime_test") with pytest.raises( - ValueError, + WorkloadBuilderException, match="Workload can not be built without a runtime configuration." ): builder.build() From 2eabc4495cbe2f0b85fe486325941e8939886044 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 21 Oct 2024 10:42:43 +0300 Subject: [PATCH 40/72] Update protos. Config implementation --- ankaios_sdk/_components/complete_state.py | 80 ++++++++++++++++-- ankaios_sdk/_components/manifest.py | 2 +- ankaios_sdk/_components/workload.py | 33 +++++--- ankaios_sdk/_protos/ank_base.proto | 60 +++++++++++++- tests/test_complete_state.py | 81 +++++++++++++++++-- tests/test_manifest.py | 17 +++- tests/workload/test_workload.py | 20 +++-- tests/workload/test_workload_builder.py | 6 +- .../test_workload_state_collection.py | 19 ++++- 9 files changed, 280 insertions(+), 38 deletions(-) diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 5c16d83..7d7b2df 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -63,6 +63,7 @@ __all__ = ["CompleteState"] +from typing import Union from .._protos import _ank_base from .workload import Workload from .workload_state import WorkloadStateCollection @@ -86,6 +87,7 @@ def __init__(self, api_version: str = SUPPORTED_API_VERSION) -> None: self._set_api_version(api_version) self._workloads: list[Workload] = [] self._workload_state_collection = WorkloadStateCollection() + self._configs = {} def __str__(self) -> str: """ @@ -157,15 +159,38 @@ def get_workload_states(self) -> WorkloadStateCollection: """ return self._workload_state_collection - def get_agents(self) -> list[str]: + def get_agents(self) -> dict[str, dict]: """ - Gets the connected agents. + Gets the connected agents and their attributes. Returns: - list[str]: A list of connected agents. + dict[str, dict]: A dict with the agents and their attributes. """ - # "AgentAttributes" does not contain anything at the moment - return list(self._complete_state.agents.agents.keys()) + agents = {} + for name, attributes in self._complete_state.agents.agents.items(): + agents[name] = { + "cpu_usage": int(attributes.cpu_usage.cpu_usage), + "free_memory": attributes.free_memory.free_memory, + } + return agents + + def set_configs(self, configs: dict) -> None: + """ + Sets the configurations in the complete state. + + Args: + configs (dict): The configurations to set in the complete state. + """ + self._configs = configs + + def get_configs(self) -> dict: + """ + Gets the configurations from the complete state. + + Returns: + dict: The configurations from the complete state + """ + return self._configs def _from_dict(self, dict_state: dict) -> None: """ @@ -186,6 +211,8 @@ def _from_dict(self, dict_state: dict) -> None: self._workloads.append( Workload._from_dict(workload_name, workload_dict) ) + if dict_state.get("configs") is not None: + self._configs = dict_state.get("configs") def _to_proto(self) -> _ank_base.CompleteState: """ @@ -195,10 +222,31 @@ def _to_proto(self) -> _ank_base.CompleteState: _ank_base.CompleteState: The protobuf message representing the complete state. """ - # Clear previous workloads + def _to_config_item(item: Union[str, list, dict] + ) -> _ank_base.ConfigItem: + config_item = _ank_base.ConfigItem() + if isinstance(item, str): + config_item.String = item + elif isinstance(item, list): + for value in [_to_config_item(value) for value in item]: + config_item.array.values.append(value) + elif isinstance(item, dict): + for key, value in item.items(): + config_item.object.fields[key]. \ + CopyFrom(_to_config_item(value)) + return config_item + + if len(self._complete_state.desiredState.workloads.workloads) != 0: + self._complete_state.desiredState.workloads.workloads.clear() for workload in self._workloads: self._complete_state.desiredState.workloads.\ workloads[workload.name].CopyFrom(workload._to_proto()) + if len(self._complete_state.desiredState.configs.configs) != 0: + self._complete_state.desiredState.configs.configs.clear() + for key, value in self._configs.items(): + self._complete_state.desiredState.configs.configs[key].CopyFrom( + _to_config_item(value) + ) return self._complete_state def _from_proto(self, proto: _ank_base.CompleteState) -> None: @@ -209,13 +257,29 @@ def _from_proto(self, proto: _ank_base.CompleteState) -> None: proto (_ank_base.CompleteState): The protobuf message representing the complete state. """ + def _from_config_item(item: _ank_base.ConfigItem + ) -> Union[str, list, dict]: + if item.HasField("String"): + return item.String + if item.HasField("array"): + return [_from_config_item(value) + for value in item.array.values] + if item.HasField("object"): + return {key: _from_config_item(value) + for key, value in item.object.fields.items()} + return None # pragma: no cover + self._complete_state = proto self._workloads = [] - for workload_name, proto_workload in self._complete_state.desiredState\ - .workloads.workloads.items(): + for workload_name, proto_workload in self._complete_state. \ + desiredState.workloads.workloads.items(): workload = Workload(workload_name) workload._from_proto(proto_workload) self._workloads.append(workload) self._workload_state_collection._from_proto( self._complete_state.workloadStates ) + self._configs = {} + for key, value in self._complete_state.desiredState. \ + configs.configs.items(): + self._configs[key] = _from_config_item(value) diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index 6b5191f..4867596 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -135,7 +135,7 @@ def check(self) -> None: raise InvalidManifestException("workloads is missing.") wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", "dependencies", "tags", - "controlInterfaceAccess"] + "controlInterfaceAccess", "configs"] wl_mandatory_keys = ["runtime", "runtimeConfig", "agent"] for wl_name in self._manifest["workloads"]: # Check allowed keys diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index a1c1a9a..ae6e0f2 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -393,26 +393,34 @@ def add_config(self, alias: str, name: str) -> None: alias (str): The alias of the configuration. name (str): The name of the configuration. """ - raise NotImplementedError("add_config is not implemented yet.") + self._workload.configs.configs[alias] = name + # Currently the mask is for all configs, not for individual aliases + self._add_mask(f"{self._main_mask}.configs") - def get_configs(self) -> tuple[tuple[str, str]]: + def get_configs(self) -> dict[str, str]: """ Return the configurations linked to the workload. Returns: - tuple: A tuple containing the alias and name of the configurations. + dict[str, str]: A dict containing the alias as key and name of the + configuration as value. """ - raise NotImplementedError("get_configs is not implemented yet.") + config_mappings = {} + for alias, name in self._workload.configs.configs.items(): + config_mappings[alias] = name + return config_mappings - def update_configs(self, configs: tuple[tuple[str, str]]) -> None: + def update_configs(self, configs: dict[str, str]) -> None: """ Update the configurations linked to the workload. Args: - configs (tuple): A tuple containing the alias and + configs (dict[str, str]): A tuple containing the alias and name of the configurations. """ - raise NotImplementedError("update_configs is not implemented yet.") + self._workload.configs.configs.clear() + for alias, name in configs.items(): + self.add_config(alias, name) def _add_mask(self, mask: str) -> None: """ @@ -469,6 +477,9 @@ def _from_dict(workload_name: str, dict_workload: dict) -> "Workload": workload = workload.add_deny_rule( rule["operation"], rule["filterMask"] ) + if "configs" in dict_workload: + for alias, name in dict_workload["configs"].items(): + workload = workload.add_config(alias, name) return workload.build() @@ -520,6 +531,7 @@ def __init__(self) -> None: self.tags = [] self.allow_rules = [] self.deny_rules = [] + self.configs = {} def workload_name(self, workload_name: str) -> "WorkloadBuilder": """ @@ -664,7 +676,7 @@ def add_deny_rule( self.deny_rules.append((operation, filter_masks)) return self - def add_config(self, alias: str, name: str) -> None: + def add_config(self, alias: str, name: str) -> "WorkloadBuilder": """ Link a configuration to the workload. @@ -672,7 +684,8 @@ def add_config(self, alias: str, name: str) -> None: alias (str): The alias of the configuration. name (str): The name of the configuration. """ - raise NotImplementedError("add_config is not implemented yet.") + self.configs[alias] = name + return self def build(self) -> Workload: """ @@ -716,5 +729,7 @@ def build(self) -> Workload: workload.update_allow_rules(self.allow_rules) if len(self.deny_rules) > 0: workload.update_deny_rules(self.deny_rules) + if len(self.configs) > 0: + workload.update_configs(self.configs) return workload diff --git a/ankaios_sdk/_protos/ank_base.proto b/ankaios_sdk/_protos/ank_base.proto index e7120d8..d872d42 100644 --- a/ankaios_sdk/_protos/ank_base.proto +++ b/ankaios_sdk/_protos/ank_base.proto @@ -199,9 +199,26 @@ message AgentMap { } /** -* A message that has not yet been implemented but will contain attributes of the agent in the future. +* A message containing the CPU usage information of the agent. */ -message AgentAttributes {} +message CpuUsage { + uint32 cpu_usage = 1; // expressed in percent, the formula for calculating: cpu_usage = (new_work_time - old_work_time) / (new_total_time - old_total_time) * 100 +} + +/** +* A message containing the amount of free memory of the agent. +*/ +message FreeMemory { + uint64 free_memory = 1; // expressed in bytes +} + +/** +* A message that contains attributes of the agent. +*/ +message AgentAttributes { + CpuUsage cpu_usage = 1; /// The cpu usage of the agent. + FreeMemory free_memory = 2; /// The amount of free memory of the agent. +} /** * A message containing the information about the workload state. @@ -223,10 +240,12 @@ message WorkloadInstanceName { message State { string apiVersion = 1; /// The current version of the API. WorkloadMap workloads = 2; /// A mapping from workload names to workload configurations. + ConfigMap configs = 3; /// Configuration values which can be referenced in workload configurations. } /** * This is a workaround for proto not supporing optional maps +* Workload names shall not be shorter than 1 symbol longer then 63 symbols and can contain only regular characters, digits, the "-" and "_" symbols. */ message WorkloadMap { map workloads = 1; @@ -243,6 +262,7 @@ message Workload { optional string runtime = 5; /// The name of the runtime e.g. podman. optional string runtimeConfig = 6; /// The configuration information specific to the runtime. ControlInterfaceAccess controlInterfaceAccess = 7; + ConfigMappings configs = 8; /// A mapping containing the configurations assigned to the workload. } /** @@ -318,3 +338,39 @@ enum ReadWriteEnum { RW_READ_WRITE = 5; // Allow read and write } +/** +* This is a workaround for proto not supporing optional maps +*/ +message ConfigMappings { + map configs = 1; +} + + +/** +* This is a workaround for proto not supporing optional maps +*/ +message ConfigMap { + map configs = 1; +} + + + +/** +* An enum type describing possible configuration objects. +*/ +message ConfigItem { + oneof ConfigItem { + string String = 1; + ConfigArray array = 2; + ConfigObject object = 3; + } +} + + +message ConfigArray { + repeated ConfigItem values = 1; +} + +message ConfigObject { + map fields = 1; +} diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index 4199f3c..e6fc31b 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -21,6 +21,20 @@ from tests.workload.test_workload import generate_test_workload +def generate_test_config(): + """ + Generate a test configuration. + """ + return { + "config_1": "val_1", + "config_2": ["val_2", "val_3"], + "config_3": { + "key_1": "val_4", + "key_2": "val_5" + } + } + + def test_general_functionality(): """ Test general functionality of the CompleteState class. @@ -93,14 +107,53 @@ def test_get_agents(): """ complete_state = CompleteState() complete_state._from_proto(_ank_base.CompleteState( - agents=_ank_base.AgentMap( - agents={"agent_A": _ank_base.AgentAttributes(), - "agent_B": _ank_base.AgentAttributes()} + agents=_ank_base.AgentMap(agents={ + "agent_A": _ank_base.AgentAttributes( + cpu_usage=_ank_base.CpuUsage(cpu_usage=50), + free_memory=_ank_base.FreeMemory(free_memory=1024) + ) + }) + )) + agents = complete_state.get_agents() + assert len(agents) == 1 + assert "agent_A" in agents + assert agents["agent_A"]["cpu_usage"] == 50 + assert agents["agent_A"]["free_memory"] == 1024 + + +def test_get_configs(): + """ + Test the get_configs method of the CompleteState class. + """ + complete_state = CompleteState() + complete_state._from_proto(_ank_base.CompleteState( + desiredState=_ank_base.State( + configs=_ank_base.ConfigMap( + configs={ + "config_1": _ank_base.ConfigItem( + String="val_1" + ), + "config_2": _ank_base.ConfigItem( + array=_ank_base.ConfigArray( + values=[ + _ank_base.ConfigItem(String="val_2"), + _ank_base.ConfigItem(String="val_3") + ] + ) + ), + "config_3": _ank_base.ConfigItem( + object=_ank_base.ConfigObject( + fields={ + "key_1": _ank_base.ConfigItem(String="val_4"), + "key_2": _ank_base.ConfigItem(String="val_5") + } + ) + ) + } + ) ) )) - assert len(complete_state.get_agents()) == 2 - assert "agent_A" in complete_state.get_agents() - assert "agent_B" in complete_state.get_agents() + assert complete_state.get_configs() == generate_test_config() def test_from_dict(): @@ -115,12 +168,25 @@ def test_from_dict(): "runtime": "podman", "restartPolicy": "NEVER", "agent": "agent_A", + "configs": { + "ports": "test_ports" + }, "runtimeConfig": "config", } + }, + 'configs': { + "test_ports": { + "port": "8081" + } } }) assert complete_state.get_api_version() == "v0.1" assert len(complete_state.get_workloads()) == 1 + assert complete_state.get_configs() == { + "test_ports": { + "port": "8081" + } + } complete_state._from_dict({ "apiVersion": "v0.2", @@ -135,7 +201,10 @@ def test_proto(): """ complete_state = CompleteState(api_version="v0.1") wl_nginx = generate_test_workload("nginx_test") + config = generate_test_config() + complete_state.set_workload(wl_nginx) + complete_state.set_configs(config) new_complete_state = CompleteState() new_complete_state._from_proto(complete_state._to_proto()) diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 8337168..a1e8c45 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -27,8 +27,13 @@ runtime: podman restartPolicy: NEVER agent: agent_A + configs: + ports: test_ports runtimeConfig: | - image: image/test""" + image: image/test +configs: + test_ports: + port: \"8081\"""" MANIFEST_DICT = { 'apiVersion': 'v0.1', @@ -37,7 +42,15 @@ 'runtime': 'podman', 'restartPolicy': 'NEVER', 'agent': 'agent_A', - 'runtimeConfig': 'image: image/test' + "configs": { + "ports": "test_ports" + }, + 'runtimeConfig': 'image: image/test\n' + } + }, + 'configs': { + "test_ports": { + "port": "8081" } } } diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index da48a80..f2ccc59 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -49,6 +49,7 @@ def generate_test_workload(workload_name: str = "workload_test") -> Workload: ["desiredState.workloads.another_workload"]) \ .add_deny_rule("Read", ["workloadStates.agent_Test.another_workload"]) \ + .add_config("alias_test", "config1") \ .build() @@ -191,14 +192,16 @@ def test_configs(workload: Workload): # pylint: disable=redefined-outer-name Args: workload (Workload): The Workload fixture. """ - with pytest.raises(NotImplementedError, match="not implemented yet"): - workload.add_config(alias="alias_test", name="config_test") + assert len(workload.get_configs()) == 1 - with pytest.raises(NotImplementedError, match="not implemented yet"): - workload.get_configs() + workload.add_config("alias_other", "config2") + configs = workload.get_configs() + assert len(configs) == 2 - with pytest.raises(NotImplementedError, match="not implemented yet"): - workload.update_configs(configs=[["alias_test", "config_test"]]) + configs["alias_new"] = "config3" + workload.update_configs(configs) + + assert len(workload.get_configs()) == 3 def test_to_proto(workload: Workload): # pylint: disable=redefined-outer-name @@ -267,6 +270,9 @@ def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name "operation": "Read", "filterMask": ["workloadStates.agent_Test.another_workload"] }] + }, + "configs": { + "alias_test": "config1" } } @@ -299,6 +305,8 @@ def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name ("update_deny_rules", {"rules": [("Write", ["mask"])]}, "desiredState.workloads.workload_test." + "controlInterfaceAccess.denyRules"), + ("add_config", {"alias": "alias_test", "name": "config_test"}, + "desiredState.workloads.workload_test.configs") ]) def test_mask_generation(function_name: str, data: dict, mask: str): """ diff --git a/tests/workload/test_workload_builder.py b/tests/workload/test_workload_builder.py index c77f294..2e32351 100644 --- a/tests/workload/test_workload_builder.py +++ b/tests/workload/test_workload_builder.py @@ -135,8 +135,10 @@ def test_add_config( Args: builder (WorkloadBuilder): The WorkloadBuilder fixture. """ - with pytest.raises(NotImplementedError, match="not implemented yet"): - builder.add_config(alias="alias_test", name="config_test") + assert len(builder.configs) == 0 + + assert builder.add_config("alias_Test", "config1") == builder + assert builder.configs == {"alias_Test": "config1"} def test_build( diff --git a/tests/workload_state/test_workload_state_collection.py b/tests/workload_state/test_workload_state_collection.py index 5c00a97..22d6047 100644 --- a/tests/workload_state/test_workload_state_collection.py +++ b/tests/workload_state/test_workload_state_collection.py @@ -18,7 +18,8 @@ class in the ankaios_sdk. """ from ankaios_sdk import WorkloadStateCollection, WorkloadState, \ - WorkloadExecutionState, WorkloadInstanceName + WorkloadExecutionState, WorkloadInstanceName, WorkloadStateEnum, \ + WorkloadSubStateEnum from ankaios_sdk._protos import _ank_base @@ -96,4 +97,18 @@ def test_from_proto(): workload_state_collection = WorkloadStateCollection() workload_state_collection._from_proto(ank_workload_state) assert len(workload_state_collection._workload_states) == 1 - assert len(workload_state_collection.get_as_list()) == 1 + workload_states = workload_state_collection.get_as_list() + assert len(workload_states) == 1 + + assert workload_states[0].workload_instance_name.agent_name == \ + "agent_Test" + assert workload_states[0].workload_instance_name.workload_name == \ + "workload_Test" + assert workload_states[0].workload_instance_name.workload_id == \ + "1234" + assert workload_states[0].execution_state.state == \ + WorkloadStateEnum.PENDING + assert workload_states[0].execution_state.substate == \ + WorkloadSubStateEnum.PENDING_WAITING_TO_START + assert workload_states[0].execution_state.info == \ + "Dummy information" From 98e894bb1aa0b54119f5f44fcbfeaef9b05712e6 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 21 Oct 2024 14:40:25 +0300 Subject: [PATCH 41/72] Fix findings --- ankaios_sdk/_components/complete_state.py | 7 ++----- ankaios_sdk/ankaios.py | 7 ++++--- tests/test_complete_state.py | 7 ++++--- tests/workload_state/test_workload_state.py | 10 +++++++++- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 7d7b2df..91ca9c3 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -76,15 +76,12 @@ class CompleteState: """ A class to represent the complete state. """ - def __init__(self, api_version: str = SUPPORTED_API_VERSION) -> None: + def __init__(self) -> None: """ Initializes an empty CompleteState instance with the given API version. - - Args: - api_version (str): The API version to set for the complete state. """ self._complete_state = _ank_base.CompleteState() - self._set_api_version(api_version) + self._set_api_version(SUPPORTED_API_VERSION) self._workloads: list[Workload] = [] self._workload_state_collection = WorkloadStateCollection() self._configs = {} diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index e81dbd7..cba06b0 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -225,6 +225,8 @@ def _read_from_control_interface(self) -> None: AnkaiosConnectionException: If an error occurs while reading the fifo. """ + # pylint: disable=invalid-name + MOST_SIGNIFICANT_BIT_MASK = 0b10000000 try: # pylint: disable=consider-using-with input_fifo = open( @@ -239,9 +241,8 @@ def _read_from_control_interface(self) -> None: if not next_byte: # pragma: no cover break varint_buffer += next_byte - # Stop if the most significant bit is 0 - # (indicating the last byte of the varint) - if next_byte[0] & 0b10000000 == 0: + # Check if we reached the last byte + if next_byte[0] & MOST_SIGNIFICANT_BIT_MASK == 0: break # Decode the varint and receive the proto msg length msg_len, _ = _DecodeVarint(varint_buffer, 0) diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index e6fc31b..77f8b55 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -17,6 +17,7 @@ """ from ankaios_sdk import CompleteState, WorkloadStateCollection +from ankaios_sdk._components.complete_state import SUPPORTED_API_VERSION from ankaios_sdk._protos import _ank_base from tests.workload.test_workload import generate_test_workload @@ -39,8 +40,8 @@ def test_general_functionality(): """ Test general functionality of the CompleteState class. """ - complete_state = CompleteState(api_version="v0.1") - assert complete_state.get_api_version() == "v0.1" + complete_state = CompleteState() + assert complete_state.get_api_version() == SUPPORTED_API_VERSION complete_state._set_api_version("v0.2") assert complete_state.get_api_version() == "v0.2" assert str(complete_state) == "desiredState {\n apiVersion: \"v0.2\"\n}\n" @@ -199,7 +200,7 @@ def test_proto(): """ Test converting the CompleteState instance to and from a protobuf message. """ - complete_state = CompleteState(api_version="v0.1") + complete_state = CompleteState() wl_nginx = generate_test_workload("nginx_test") config = generate_test_config() diff --git a/tests/workload_state/test_workload_state.py b/tests/workload_state/test_workload_state.py index 3760af9..7ffa89e 100644 --- a/tests/workload_state/test_workload_state.py +++ b/tests/workload_state/test_workload_state.py @@ -17,7 +17,7 @@ class in the ankaios_sdk. """ -from ankaios_sdk import WorkloadState +from ankaios_sdk import WorkloadState, WorkloadStateEnum, WorkloadSubStateEnum from ankaios_sdk._protos import _ank_base @@ -36,4 +36,12 @@ def test_creation(): ) assert workload_state is not None assert workload_state.execution_state is not None + assert workload_state.execution_state.state == WorkloadStateEnum.PENDING + assert workload_state.execution_state.substate == \ + WorkloadSubStateEnum.PENDING_WAITING_TO_START + assert workload_state.execution_state.info == "Dummy information" assert workload_state.workload_instance_name is not None + assert workload_state.workload_instance_name.agent_name == "agent_Test" + assert workload_state.workload_instance_name.workload_name == \ + "workload_Test" + assert workload_state.workload_instance_name.workload_id == "1234" From b9dbd3792e4141f66f20e65da5c3acac59e16e6d Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 22 Oct 2024 09:55:23 +0300 Subject: [PATCH 42/72] Update README.md Co-authored-by: Holger Dormann --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d71719e..bc34cf4 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ with Ankaios() as ankaios: if ret is not None: workload_instance_name = ret["added_workloads"][0] - # Wait until the workload raches the running state + # Wait until the workload reaches the running state ret = ankaios.wait_for_workload_to_reach_state( workload_instance_name, state=WorkloadStateEnum.RUNNING, From f872cc27ae2e9b6720e1b48dea1313e2f1e6b14e Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 25 Oct 2024 08:31:29 +0300 Subject: [PATCH 43/72] Fix findings --- README.md | 12 ++-- ankaios_sdk/_components/complete_state.py | 35 ++++++----- ankaios_sdk/_components/manifest.py | 21 +------ ankaios_sdk/_components/workload.py | 12 ++-- ankaios_sdk/ankaios.py | 76 +++++++++++++---------- ankaios_sdk/utils.py | 20 ++++++ tests/test_ankaios.py | 48 +++++++++++++- tests/test_complete_state.py | 39 +++--------- tests/test_manifest.py | 18 ++---- tests/workload/test_workload.py | 31 ++++----- 10 files changed, 173 insertions(+), 139 deletions(-) create mode 100644 ankaios_sdk/utils.py diff --git a/README.md b/README.md index bc34cf4..a3f4bfc 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,12 @@ with Ankaios() as ankaios: # Check if the workload is scheduled and get the WorkloadInstanceName if ret is not None: workload_instance_name = ret["added_workloads"][0] - + + # Request the workload state based on the workload instance name + ret = ankaios.get_workload_state_for_instance_name(workload_instance_name) + if ret is not None: + print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.info}") + # Wait until the workload reaches the running state ret = ankaios.wait_for_workload_to_reach_state( workload_instance_name, @@ -73,11 +78,6 @@ with Ankaios() as ankaios: if ret: print("Workload reached the RUNNING state.") - # Request the workload state based on the workload instance name - ret = ankaios.get_workload_state_for_instance_name(workload_instance_name) - if ret is not None: - print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.info}") - # Request the state of the system, filtered with the agent name complete_state = ankaios.get_state( timeout=5, diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 91ca9c3..8b3fdc4 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -59,6 +59,11 @@ .. code-block:: python workload_states = complete_state.get_workload_states() + +- Create a CompleteState instance from a Manifest: + .. code-block:: python + + complete_state = CompleteState.from_manifest(manifest) """ __all__ = ["CompleteState"] @@ -67,9 +72,8 @@ from .._protos import _ank_base from .workload import Workload from .workload_state import WorkloadStateCollection - - -SUPPORTED_API_VERSION = "v0.1" +from .manifest import Manifest +from ..utils import SUPPORTED_API_VERSION class CompleteState: @@ -189,27 +193,30 @@ def get_configs(self) -> dict: """ return self._configs - def _from_dict(self, dict_state: dict) -> None: + @staticmethod + def from_manifest(manifest: Manifest) -> 'CompleteState': """ - Converts a dictionary to a CompleteState object. + Creates a CompleteState instance from a Manifest. Args: - dict_state (dict): The dictionary representing the complete state. + manifest (Manifest): The manifest to create the + complete state from. """ - self._complete_state = _ank_base.CompleteState() - self._set_api_version( - dict_state.get("apiVersion", self.get_api_version()) + state = CompleteState() + state._complete_state = _ank_base.CompleteState() + dict_state = manifest._manifest + state._set_api_version( + dict_state.get("apiVersion", state.get_api_version()) ) - self._workloads = [] - if dict_state.get("workloads") is None: - return + state._workloads = [] for workload_name, workload_dict in \ dict_state.get("workloads").items(): - self._workloads.append( + state._workloads.append( Workload._from_dict(workload_name, workload_dict) ) if dict_state.get("configs") is not None: - self._configs = dict_state.get("configs") + state._configs = dict_state.get("configs") + return state def _to_proto(self) -> _ank_base.CompleteState: """ diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index 4867596..cf6f144 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -39,16 +39,11 @@ .. code-block:: python manifest = Manifest.from_dict({"apiVersion": "1.0", "workloads": {}}) - -- Generate a CompleteState instance from the manifest: - .. code-block:: python - - complete_state = manifest.generate_complete_state() """ import yaml from ..exceptions import InvalidManifestException -from .complete_state import CompleteState +from ..utils import WORKLOADS_PREFIX class Manifest(): @@ -156,17 +151,5 @@ def _calculate_masks(self) -> list[str]: Returns: list[str]: A list of masks for the workloads. """ - return [f"desiredState.workloads.{key}" + return [f"{WORKLOADS_PREFIX}.{key}" for key in self._manifest["workloads"].keys()] - - def generate_complete_state(self) -> CompleteState: - """ - Generates a CompleteState instance from the manifest. - - Returns: - CompleteState: An instance of the CompleteState class - populated with the manifest data. - """ - complete_state = CompleteState() - complete_state._from_dict(self._manifest) - return complete_state diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index ae6e0f2..5e7cd7b 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -73,6 +73,7 @@ from .._protos import _ank_base from ..exceptions import WorkloadFieldException, WorkloadBuilderException +from ..utils import WORKLOADS_PREFIX # pylint: disable=too-many-public-methods @@ -95,7 +96,7 @@ def __init__(self, name: str) -> None: """ self._workload = _ank_base.Workload() self.name = name - self._main_mask = f"desiredState.workloads.{self.name}" + self._main_mask = f"{WORKLOADS_PREFIX}.{self.name}" self.masks = [self._main_mask] def __str__(self) -> str: @@ -254,8 +255,7 @@ def update_tags(self, tags: list) -> None: Args: tags (list): A list of tuples containing tag keys and values. """ - while len(self._workload.tags.tags) > 0: - self._workload.tags.tags.pop() + del self._workload.tags.tags[:] for key, value in tags: tag = _ank_base.Tag(key=key, value=value) self._workload.tags.tags.append(tag) @@ -345,8 +345,7 @@ def update_allow_rules(self, rules: list[tuple[str, list[str]]]) -> None: Raises: WorkloadFieldException: If an invalid operation is provided """ - while len(self._workload.controlInterfaceAccess.allowRules) > 0: - self._workload.controlInterfaceAccess.allowRules.pop() + del self._workload.controlInterfaceAccess.allowRules[:] for operation, filter_masks in rules: self._workload.controlInterfaceAccess.allowRules.append( self._generate_access_right_rule(operation, filter_masks) @@ -377,8 +376,7 @@ def update_deny_rules(self, rules: list[tuple[str, list[str]]]) -> None: Raises: WorkloadFieldException: If an invalid operation is provided """ - while len(self._workload.controlInterfaceAccess.denyRules) > 0: - self._workload.controlInterfaceAccess.denyRules.pop() + del self._workload.controlInterfaceAccess.denyRules[:] for operation, filter_masks in rules: self._workload.controlInterfaceAccess.denyRules.append( self._generate_access_right_rule(operation, filter_masks) diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index cba06b0..dcd4d97 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -130,6 +130,7 @@ ResponseEvent, WorkloadStateCollection, Manifest, \ WorkloadInstanceName, WorkloadStateEnum, \ WorkloadExecutionState +from .utils import WORKLOADS_PREFIX class AnkaiosLogLevel(Enum): @@ -402,7 +403,7 @@ def apply_manifest(self, manifest: Manifest) -> dict: the manifest. """ request = Request(request_type="update_state") - request.set_complete_state(manifest.generate_complete_state()) + request.set_complete_state(CompleteState.from_manifest(manifest)) request.set_masks(manifest._calculate_masks()) # Send request @@ -418,13 +419,15 @@ def apply_manifest(self, manifest: Manifest) -> dict: self.logger.error("Error while trying to apply manifest: %s", content) raise AnkaiosException(f"Received error: {content}") - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content + if content_type == "update_state_success": + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content + raise AnkaiosException("Received unexpected content type.") def delete_manifest(self, manifest: Manifest) -> dict: """ @@ -458,13 +461,15 @@ def delete_manifest(self, manifest: Manifest) -> dict: self.logger.error("Error while trying to delete manifest: %s", content) raise AnkaiosException(f"Received error: {content}") - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content + if content_type == "update_state_success": + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content + raise AnkaiosException("Received unexpected content type.") def run_workload(self, workload: Workload) -> dict: """ @@ -501,13 +506,15 @@ def run_workload(self, workload: Workload) -> dict: self.logger.error("Error while trying to run workload: %s", content) raise AnkaiosException(f"Received error: {content}") - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content + if content_type == "update_state_success": + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content + raise AnkaiosException("Received unexpected content type.") def delete_workload(self, workload_name: str) -> dict: """ @@ -525,7 +532,7 @@ def delete_workload(self, workload_name: str) -> dict: """ request = Request(request_type="update_state") request.set_complete_state(CompleteState()) - request.add_mask(f"desiredState.workloads.{workload_name}") + request.add_mask(f"{WORKLOADS_PREFIX}.{workload_name}") try: response = self._send_request(request) @@ -539,13 +546,15 @@ def delete_workload(self, workload_name: str) -> dict: self.logger.error("Error while trying to delete workload: %s", content) raise AnkaiosException(f"Received error: {content}") - self.logger.info( - "Update successfull: %s added workloads, " - + "%s deleted workloads.", - len(content["added_workloads"]), - len(content["deleted_workloads"]) - ) - return content + if content_type == "update_state_success": + self.logger.info( + "Update successfull: %s added workloads, " + + "%s deleted workloads.", + len(content["added_workloads"]), + len(content["deleted_workloads"]) + ) + return content + raise AnkaiosException("Received unexpected content type.") def get_workload_with_instance_name( self, instance_name: WorkloadInstanceName, @@ -564,7 +573,7 @@ def get_workload_with_instance_name( Workload: The workload object. """ return self.get_state( - timeout, [f"desiredState.workloads.{str(instance_name)}"] + timeout, [f"{WORKLOADS_PREFIX}.{str(instance_name)}"] ).get_workloads()[0] def set_configs(self, configs: dict) -> bool: @@ -669,8 +678,9 @@ def get_state(self, timeout: float = DEFAULT_TIMEOUT, self.logger.error("Error while trying to get the state: %s", content) raise AnkaiosException(f"Received error: {content}") - - return content + if content_type == "complete_state": + return content + raise AnkaiosException("Received unexpected content type.") def get_agents( self, timeout: float = DEFAULT_TIMEOUT diff --git a/ankaios_sdk/utils.py b/ankaios_sdk/utils.py new file mode 100644 index 0000000..975c1ed --- /dev/null +++ b/ankaios_sdk/utils.py @@ -0,0 +1,20 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This script provides general functionality and constants for the ankaios_sdk. +""" + +SUPPORTED_API_VERSION = "v0.1" +WORKLOADS_PREFIX = "desiredState.workloads" diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 72007a6..3b61787 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -23,6 +23,7 @@ from ankaios_sdk import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, \ Manifest, CompleteState, WorkloadInstanceName, WorkloadStateCollection, \ WorkloadStateEnum, AnkaiosConnectionException, AnkaiosException +from ankaios_sdk.utils import WORKLOADS_PREFIX from tests.workload.test_workload import generate_test_workload from tests.test_request import generate_test_request from tests.response.test_response import MESSAGE_BUFFER_ERROR, \ @@ -233,6 +234,15 @@ def test_apply_manifest(): mock_send_request.assert_called_once() ankaios.logger.error.assert_called() + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.apply_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + def test_delete_manifest(): """ @@ -267,6 +277,15 @@ def test_delete_manifest(): mock_send_request.assert_called_once() ankaios.logger.error.assert_called() + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.delete_manifest(manifest) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + def test_run_workload(): """ @@ -301,6 +320,15 @@ def test_run_workload(): mock_send_request.assert_called_once() ankaios.logger.error.assert_called() + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.run_workload(workload) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + def test_delete_workload(): """ @@ -334,6 +362,15 @@ def test_delete_workload(): mock_send_request.assert_called_once() ankaios.logger.error.assert_called() + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.delete_workload("nginx") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + def test_get_workload_with_instance_name(): """ @@ -358,7 +395,7 @@ def test_get_workload_with_instance_name(): assert ret == workload mock_get_state.assert_called_once_with( Ankaios.DEFAULT_TIMEOUT, - ["desiredState.workloads.nginx.1234.agent_Test"] + [f"{WORKLOADS_PREFIX}.nginx.1234.agent_Test"] ) mock_state_get_workloads.assert_called_once() @@ -419,6 +456,15 @@ def test_get_state(): mock_send_request.assert_called_once() ankaios.logger.error.assert_called() + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + with pytest.raises(AnkaiosException): + ankaios.get_state() + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + def test_get_agents(): """ diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index 77f8b55..86b336e 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -16,10 +16,11 @@ This module contains unit tests for the Manifest class in the ankaios_sdk. """ -from ankaios_sdk import CompleteState, WorkloadStateCollection +from ankaios_sdk import CompleteState, WorkloadStateCollection, Manifest from ankaios_sdk._components.complete_state import SUPPORTED_API_VERSION from ankaios_sdk._protos import _ank_base from tests.workload.test_workload import generate_test_workload +from tests.test_manifest import MANIFEST_DICT def generate_test_config(): @@ -157,44 +158,22 @@ def test_get_configs(): assert complete_state.get_configs() == generate_test_config() -def test_from_dict(): +def test_from_manifest(): """ - Test the from_dict method of the CompleteState class. + Test the from_manifest method of the CompleteState class. """ - complete_state = CompleteState() - complete_state._from_dict({ - "apiVersion": "v0.1", - "workloads": { - "nginx": { - "runtime": "podman", - "restartPolicy": "NEVER", - "agent": "agent_A", - "configs": { - "ports": "test_ports" - }, - "runtimeConfig": "config", - } - }, - 'configs': { - "test_ports": { - "port": "8081" - } - } - }) + manifest = Manifest(MANIFEST_DICT) + complete_state = CompleteState.from_manifest(manifest) assert complete_state.get_api_version() == "v0.1" - assert len(complete_state.get_workloads()) == 1 + workloads = complete_state.get_workloads() + assert len(workloads) == 1 + assert workloads[0].name == "nginx_test" assert complete_state.get_configs() == { "test_ports": { "port": "8081" } } - complete_state._from_dict({ - "apiVersion": "v0.2", - }) - assert complete_state.get_api_version() == "v0.2" - assert len(complete_state.get_workloads()) == 0 - def test_proto(): """ diff --git a/tests/test_manifest.py b/tests/test_manifest.py index a1e8c45..0579ab3 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -18,7 +18,8 @@ from unittest.mock import patch, mock_open import pytest -from ankaios_sdk import Manifest, CompleteState, InvalidManifestException +from ankaios_sdk import Manifest, InvalidManifestException +from ankaios_sdk.utils import WORKLOADS_PREFIX MANIFEST_CONTENT = """apiVersion: v0.1 @@ -140,17 +141,6 @@ def test_calculate_masks(): manifest = Manifest(manifest_dict) assert len(manifest._calculate_masks()) == 2 assert manifest._calculate_masks() == [ - "desiredState.workloads.nginx_test", - "desiredState.workloads.nginx_test_other" + f"{WORKLOADS_PREFIX}.nginx_test", + f"{WORKLOADS_PREFIX}.nginx_test_other" ] - - -def test_generate_complete_state(): - """ - Test the CompleteState instance generation from a Manifest instance. - """ - with patch("ankaios_sdk.CompleteState._from_dict") as mock_complete_state: - manifest = Manifest(MANIFEST_DICT) - complete_state = manifest.generate_complete_state() - mock_complete_state.assert_called_once_with(manifest._manifest) - assert isinstance(complete_state, CompleteState) diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index f2ccc59..ec062d1 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -27,6 +27,7 @@ import pytest from ankaios_sdk import Workload, WorkloadBuilder, WorkloadFieldException from ankaios_sdk._protos import _ank_base +from ankaios_sdk.utils import WORKLOADS_PREFIX def generate_test_workload(workload_name: str = "workload_test") -> Workload: @@ -46,7 +47,7 @@ def generate_test_workload(workload_name: str = "workload_test") -> Workload: .add_tag("key1", "value1") \ .add_tag("key2", "value2") \ .add_allow_rule("Write", - ["desiredState.workloads.another_workload"]) \ + [f"{WORKLOADS_PREFIX}.another_workload"]) \ .add_deny_rule("Read", ["workloadStates.agent_Test.another_workload"]) \ .add_config("alias_test", "config1") \ @@ -85,7 +86,7 @@ def test_update_fields( Args: workload (Workload): The Workload fixture. """ - assert workload.masks == ["desiredState.workloads.workload_test"] + assert workload.masks == [f"{WORKLOADS_PREFIX}.workload_test"] workload.update_workload_name("new_workload_test") assert workload.name == "new_workload_test" @@ -175,7 +176,7 @@ def test_rules(workload: Workload): # pylint: disable=redefined-outer-name with pytest.raises(WorkloadFieldException): workload.update_deny_rules([("Invalid", ["mask"])]) - allow_rules.append(("Write", ["desiredState.workloads.another_workload"])) + allow_rules.append(("Write", [f"{WORKLOADS_PREFIX}.another_workload"])) deny_rules.append(("Read", ["workloadStates.agent_Test.another_workload"])) workload.update_allow_rules(allow_rules) @@ -263,7 +264,7 @@ def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name "allowRules": [{ "type": "StateRule", "operation": "Write", - "filterMask": ["desiredState.workloads.another_workload"] + "filterMask": [f"{WORKLOADS_PREFIX}.another_workload"] }], "denyRules": [{ "type": "StateRule", @@ -283,30 +284,30 @@ def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name @pytest.mark.parametrize("function_name, data, mask", [ ("update_workload_name", {"name": "workload_test"}, - "desiredState.workloads.workload_test"), + f"{WORKLOADS_PREFIX}.workload_test"), ("update_agent_name", {"agent_name": "agent_Test"}, - "desiredState.workloads.workload_test.agent"), + f"{WORKLOADS_PREFIX}.workload_test.agent"), ("update_runtime", {"runtime": "runtime_test"}, - "desiredState.workloads.workload_test.runtime"), + f"{WORKLOADS_PREFIX}.workload_test.runtime"), ("update_restart_policy", {"policy": "NEVER"}, - "desiredState.workloads.workload_test.restartPolicy"), + f"{WORKLOADS_PREFIX}.workload_test.restartPolicy"), ("update_runtime_config", {"config": "config_test"}, - "desiredState.workloads.workload_test.runtimeConfig"), + f"{WORKLOADS_PREFIX}.workload_test.runtimeConfig"), ("update_dependencies", {"dependencies": {"workload_test_other": "ADD_COND_RUNNING"}}, - "desiredState.workloads.workload_test.dependencies"), + f"{WORKLOADS_PREFIX}.workload_test.dependencies"), ("add_tag", {"key": "key1", "value": "value1"}, - "desiredState.workloads.workload_test.tags.key1"), + f"{WORKLOADS_PREFIX}.workload_test.tags.key1"), ("update_tags", {"tags": [("key1", "value1"), ("key2", "value")]}, - "desiredState.workloads.workload_test.tags"), + f"{WORKLOADS_PREFIX}.workload_test.tags"), ("update_allow_rules", {"rules": [("Write", ["mask"])]}, - "desiredState.workloads.workload_test." + f"{WORKLOADS_PREFIX}.workload_test." + "controlInterfaceAccess.allowRules"), ("update_deny_rules", {"rules": [("Write", ["mask"])]}, - "desiredState.workloads.workload_test." + f"{WORKLOADS_PREFIX}.workload_test." + "controlInterfaceAccess.denyRules"), ("add_config", {"alias": "alias_test", "name": "config_test"}, - "desiredState.workloads.workload_test.configs") + f"{WORKLOADS_PREFIX}.workload_test.configs") ]) def test_mask_generation(function_name: str, data: dict, mask: str): """ From c915448dbd3e6e18af3da797f98d64bf6112a304 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 25 Oct 2024 14:11:07 +0300 Subject: [PATCH 44/72] Move connect to init of ankaios --- ankaios_sdk/_components/response.py | 8 +- ankaios_sdk/_protos/control_api.proto | 19 ++- ankaios_sdk/ankaios.py | 220 ++++++++++++++------------ ankaios_sdk/exceptions.py | 7 +- ankaios_sdk/utils.py | 8 + docs/source/index.rst | 2 +- tests/response/test_response.py | 14 +- tests/test_ankaios.py | 210 ++++++++++++++++-------- 8 files changed, 317 insertions(+), 171 deletions(-) diff --git a/ankaios_sdk/_components/response.py b/ankaios_sdk/_components/response.py index 78924d2..3fbe6e5 100644 --- a/ankaios_sdk/_components/response.py +++ b/ankaios_sdk/_components/response.py @@ -46,7 +46,7 @@ from typing import Union from threading import Event from .._protos import _control_api -from ..exceptions import ResponseException +from ..exceptions import ResponseException, ConnectionClosedException from .complete_state import CompleteState from .workload_state import WorkloadInstanceName @@ -90,7 +90,11 @@ def _parse_response(self) -> None: from_ankaios.ParseFromString(self.buffer) except Exception as e: raise ResponseException(f"Parsing error: '{e}'") from e - self._response = from_ankaios.response + if from_ankaios.HasField("response"): + self._response = from_ankaios.response + else: + raise ConnectionClosedException( + from_ankaios.connectionClosed.reason) def _from_proto(self) -> None: """ diff --git a/ankaios_sdk/_protos/control_api.proto b/ankaios_sdk/_protos/control_api.proto index 8fd93e9..1b52264 100644 --- a/ankaios_sdk/_protos/control_api.proto +++ b/ankaios_sdk/_protos/control_api.proto @@ -34,15 +34,32 @@ import "ank_base.proto"; */ message ToAnkaios { oneof ToAnkaiosEnum { - ank_base.Request request = 3; + Hello hello = 1; /// The fist message sent when a connection is established. The message is needed to make sure the connected components are compatible. + ank_base.Request request = 3; /// A request to Ankaios } } +/** +* This message is the first one that needs to be sent when a new connection to the Ankaios cluster is established. Without this message being sent all further request are rejected. +*/ +message Hello { + string protocolVersion = 2; /// The protocol version used by the calling component. +} + /** * Messages from the Ankaios server to e.g. the Ankaios agent. */ message FromAnkaios { oneof FromAnkaiosEnum { ank_base.Response response = 3; /// A message containing a response to a previous request. + ConnectionClosed connectionClosed = 5; /// A message sent by Ankaios to inform a workload that the connection to Anakios was closed. } } + +/** +* This message informs the user of the Control Interface that the connection was closed by Ankaios. +* No more messages will be processed by Ankaios after this message is sent. +*/ +message ConnectionClosed { + string reason = 1; /// A string containing the reason for closing the connection. +} diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index dcd4d97..8832a87 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -34,41 +34,32 @@ - Create an Ankaios object and connect to the control interface: .. code-block:: python - with Ankaios() as ankaios: - pass + ankaios = Ankaios() - Apply a manifest: .. code-block:: python ret = ankaios.apply_manifest(manifest) - if ret is not None: - print("Manifest applied successfully.") - print(ret["added_workloads"]) - print(ret["deleted_workloads"]) + print(ret["added_workloads"]) + print(ret["deleted_workloads"]) - Delete a manifest: .. code-block:: python ret = ankaios.delete_manifest(manifest) - if ret is not None: - print("Manifest deleted successfully.") - print(ret["deleted_workloads"]) + print(ret["deleted_workloads"]) - Run a workload: .. code-block:: python ret = ankaios.run_workload(workload) - if ret is not None: - print("Workload started successfully.") - print(ret["added_workloads"]) + print(ret["added_workloads"]) - Delete a workload: .. code-block:: python ret = ankaios.delete_workload(workload_name) - if ret is not None: - print("Workload deleted successfully.") - print(ret["deleted_workloads"]) + print(ret["deleted_workloads"]) - Get a workload: .. code-block:: python @@ -99,8 +90,7 @@ .. code-block:: python ret = ankaios.get_execution_state_for_instance_name(instance_name) - if ret is not None: - print(f"State: {ret.state}, substate: {ret.substate}") + print(f"State: {ret.state}, substate: {ret.substate}") - Wait for a workload to reach a state: .. code-block:: python @@ -116,6 +106,7 @@ __all__ = ["Ankaios", "AnkaiosLogLevel"] import logging +import os import time from typing import Union from enum import Enum @@ -125,12 +116,12 @@ from ._protos import _control_api from .exceptions import AnkaiosConnectionException, AnkaiosException, \ - ResponseException + ResponseException, ConnectionClosedException from ._components import Workload, CompleteState, Request, Response, \ ResponseEvent, WorkloadStateCollection, Manifest, \ WorkloadInstanceName, WorkloadStateEnum, \ WorkloadExecutionState -from .utils import WORKLOADS_PREFIX +from .utils import WORKLOADS_PREFIX, ANKAIOS_VERSION class AnkaiosLogLevel(Enum): @@ -165,45 +156,27 @@ class Ankaios: "(float): The default timeout, if not manually provided." def __init__(self) -> None: - """Initialize the Ankaios object.""" + """ + Initialize the Ankaios object. The logger will be created and + the connection to the control interface will be established. + """ self.logger = None self.path = self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH self._read_thread = None self._connected = False + self._output_file = None self._responses_lock = threading.Lock() self._responses: dict[str, ResponseEvent] = {} self._create_logger() - - def __enter__(self) -> "Ankaios": - """ - Connect to the control interface. - - Returns: - Ankaios: The Ankaios object. - """ self._connect() - return self - def __exit__(self, exc_type, exc_value, traceback) -> None: + def __del__(self) -> None: """ - Disconnect from the control interface. - - Args: - exc_type (type): The exception type. - exc_value (Exception): The exception instance. - traceback (traceback): The traceback object. - - Raises: - AnkaiosConnectionException: If an exception occurred. + Disconnect from the control interface when the object is deleted. """ self._disconnect() - if exc_type is not None: # pragma: no cover - self.logger.error("An exception occurred: %s, %s, %s", - exc_type, exc_value, traceback) - raise AnkaiosConnectionException( - f"An exception occurred: {exc_type}, {exc_value}, {traceback}") def _create_logger(self) -> None: """Create a logger with custom format and default log level.""" @@ -215,6 +188,56 @@ def _create_logger(self) -> None: self.logger.addHandler(handler) self.set_logger_level(AnkaiosLogLevel.INFO) + def _connect(self) -> None: + """ + Connect to the control interface by starting to read + from the input fifo and opening the output fifo. + + Raises: + AnkaiosConnectionException: If an error occurred. + """ + if self._connected: + raise AnkaiosConnectionException("Already connected.") + if not os.path.exists( + "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\input"): + raise AnkaiosConnectionException( + "Control interface input fifo does not exist." + ) + if not os.path.exists( + "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output"): + raise AnkaiosConnectionException( + "Control interface output fifo does not exist." + ) + + self._read_thread = threading.Thread( + target=self._read_from_control_interface + ) + self._read_thread.start() + + # pylint: disable=consider-using-with + self._output_file = open( + f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output", "ab" + ) + + self._connected = True + self._send_initial_hello() + + def _disconnect(self) -> None: + """ + Disconnect from the control interface by stopping to read + from the input fifo. + + Raises: + AnkaiosConnectionException: If already disconnected. + """ + self._connected = False + if self._read_thread is not None: + self._read_thread.join() + self._read_thread = None + if self._output_file is not None: + self._output_file.close() + self._output_file = None + def _read_from_control_interface(self) -> None: """ Reads from the control interface input fifo and saves the response. @@ -270,11 +293,62 @@ def _read_from_control_interface(self) -> None: else: self._responses[request_id] = ResponseEvent(response) self._responses[request_id].set() + except ConnectionClosedException as e: # pragma: no cover + self.logger.error("Connection closed: %s", e) + self._disconnect() except Exception as e: # pylint: disable=broad-exception-caught self.logger.error("Error while reading fifo file: %s", e) finally: input_fifo.close() + def _write_to_pipe(self, to_ankaios: _control_api.ToAnkaios) -> None: + """ + Writes the ToAnkaios proto message to the control + interface output fifo. + + Args: + to_ankaios (_control_api.ToAnkaios): The ToAnkaios proto message. + + Raises: + AnkaiosConnectionException: If not connected. + """ + if not self._connected: + raise AnkaiosConnectionException( + "Could not write to pipe, not connected.") + + # Adds the byte length of the proto msg + self._output_file.write(_VarintBytes(to_ankaios.ByteSize())) + # Adds the proto msg itself + self._output_file.write(to_ankaios.SerializeToString()) + self._output_file.flush() + + def _write_request(self, request: Request) -> None: + """ + Writes the request into the control interface output fifo. + + Args: + request (Request): The request object to be written. + """ + request_to_ankaios = _control_api.ToAnkaios( + request=request._to_proto() + ) + self._write_to_pipe(request_to_ankaios) + + def _send_initial_hello(self) -> None: + """ + Send an initial hello message to the control interface with + the version in order to check it. + + Raises: + AnkaiosConnectionException: If an error occurred. + """ + initial_hello = _control_api.ToAnkaios( + hello=_control_api.Hello( + protocolVersion=str(ANKAIOS_VERSION) + ) + ) + self._write_to_pipe(initial_hello) + def _get_response_by_id(self, request_id: str, timeout: float = DEFAULT_TIMEOUT) -> Response: """ @@ -293,8 +367,7 @@ def _get_response_by_id(self, request_id: str, is not started. """ if not self._connected: - raise AnkaiosConnectionException( - "Reading from the control interface is not started.") + raise AnkaiosConnectionException("Not connected.") with self._responses_lock: if request_id in self._responses: @@ -303,24 +376,6 @@ def _get_response_by_id(self, request_id: str, return self._responses[request_id].wait_for_response(timeout) - def _write_to_pipe(self, request: Request) -> None: - """ - Writes the request into the control interface output fifo. - - Args: - request (Request): The request object to be written. - """ - with open(f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", - "ab") as f: - request_to_ankaios = _control_api.ToAnkaios( - request=request._to_proto() - ) - # Adds the byte length of the proto msg - f.write(_VarintBytes(request_to_ankaios.ByteSize())) - # Adds the proto msg itself - f.write(request_to_ankaios.SerializeToString()) - f.flush() - def _send_request(self, request: Request, timeout: float = DEFAULT_TIMEOUT) -> Response: """ @@ -335,13 +390,9 @@ def _send_request(self, request: Request, Response: The response object. Raises: - AnkaiosConnectionException: If not connected. + TimeoutError: If the request timed out. """ - if not self._connected: - raise AnkaiosConnectionException( - "Cannot request if not connected." - ) - self._write_to_pipe(request) + self._write_request(request) try: response = self._get_response_by_id(request.get_id(), timeout) @@ -358,35 +409,6 @@ def set_logger_level(self, level: AnkaiosLogLevel) -> None: """ self.logger.setLevel(level.value) - def _connect(self) -> None: - """ - Connect to the control interface by starting to read - from the input fifo. - - Raises: - AnkaiosConnectionException: If already connected. - """ - if self._connected: - raise AnkaiosConnectionException("Already connected.") - self._connected = True - self._read_thread = threading.Thread( - target=self._read_from_control_interface - ) - self._read_thread.start() - - def _disconnect(self) -> None: - """ - Disconnect from the control interface by stopping to read - from the input fifo. - - Raises: - AnkaiosConnectionException: If already disconnected. - """ - if not self._connected: - raise AnkaiosConnectionException("Already disconnected.") - self._connected = False - self._read_thread.join() - def apply_manifest(self, manifest: Manifest) -> dict: """ Send a request to apply a manifest. diff --git a/ankaios_sdk/exceptions.py b/ankaios_sdk/exceptions.py index a3b76be..0e6fd03 100644 --- a/ankaios_sdk/exceptions.py +++ b/ankaios_sdk/exceptions.py @@ -29,7 +29,8 @@ import inspect __all__ = ['WorkloadFieldException', 'WorkloadBuilderException', - 'InvalidManifestException', 'RequestException', 'ResponseException', + 'InvalidManifestException', 'ConnectionClosedException', + 'RequestException', 'ResponseException', 'AnkaiosConnectionException', 'AnkaiosException'] @@ -53,6 +54,10 @@ class InvalidManifestException(AnkaiosBaseException): """Raised when the manifest file is invalid.""" +class ConnectionClosedException(AnkaiosBaseException): + """Raised when the connection is closed.""" + + class RequestException(AnkaiosBaseException): """Raised when the request is invalid.""" diff --git a/ankaios_sdk/utils.py b/ankaios_sdk/utils.py index 975c1ed..eaed794 100644 --- a/ankaios_sdk/utils.py +++ b/ankaios_sdk/utils.py @@ -16,5 +16,13 @@ This script provides general functionality and constants for the ankaios_sdk. """ +import os +import configparser + + SUPPORTED_API_VERSION = "v0.1" WORKLOADS_PREFIX = "desiredState.workloads" + +_config = configparser.ConfigParser() +_config.read(os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')) +ANKAIOS_VERSION = _config['metadata']['ankaios_version'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 0df0280..44e9112 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -33,7 +33,7 @@ contributing code_of_conduct - License + License Indices and tables ================== diff --git a/tests/response/test_response.py b/tests/response/test_response.py index 87034a3..079eb9e 100644 --- a/tests/response/test_response.py +++ b/tests/response/test_response.py @@ -18,7 +18,8 @@ import pytest from google.protobuf.internal.encoder import _VarintBytes -from ankaios_sdk import Response, CompleteState, ResponseException +from ankaios_sdk import Response, CompleteState, ResponseException, \ + ConnectionClosedException from ankaios_sdk._protos import _ank_base, _control_api @@ -65,6 +66,12 @@ ) ).SerializeToString() +MESSAGE_BUFFER_CONNECTION_CLOSED = _control_api.FromAnkaios( + connectionClosed=_control_api.ConnectionClosed( + reason="Connection closed reason", + ) +).SerializeToString() + def test_initialisation(): """ @@ -100,6 +107,11 @@ def test_initialisation(): with pytest.raises(ResponseException, match="Invalid response type"): response = Response(MESSAGE_BUFFER_INVALID_RESPONSE) + # Test connection closed + with pytest.raises(ConnectionClosedException, + match="Connection closed reason"): + response = Response(MESSAGE_BUFFER_CONNECTION_CLOSED) + def test_getters(): """ diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 3b61787..3201e8b 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -18,25 +18,40 @@ from io import StringIO import logging +import threading from unittest.mock import patch, mock_open, MagicMock import pytest from ankaios_sdk import Ankaios, AnkaiosLogLevel, Response, ResponseEvent, \ Manifest, CompleteState, WorkloadInstanceName, WorkloadStateCollection, \ WorkloadStateEnum, AnkaiosConnectionException, AnkaiosException -from ankaios_sdk.utils import WORKLOADS_PREFIX +from ankaios_sdk.utils import WORKLOADS_PREFIX, ANKAIOS_VERSION +from ankaios_sdk._protos import _control_api from tests.workload.test_workload import generate_test_workload from tests.test_request import generate_test_request from tests.response.test_response import MESSAGE_BUFFER_ERROR, \ - MESSAGE_BUFFER_COMPLETE_STATE, MESSAGE_BUFFER_UPDATE_SUCCESS, \ - MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH + MESSAGE_BUFFER_COMPLETE_STATE, MESSAGE_UPDATE_SUCCESS, \ + MESSAGE_BUFFER_UPDATE_SUCCESS, MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH from tests.test_manifest import MANIFEST_DICT +def generate_test_ankaios() -> Ankaios: + """ + Helper function to generate an Ankaios instance without connecting to the + control interface. + + Returns: + Ankaios: The Ankaios instance. + """ + with patch("ankaios_sdk.Ankaios._connect"): + ankaios = Ankaios() + return ankaios + + def test_logger(): """ Test the logger functionality of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() assert ankaios.logger.level == AnkaiosLogLevel.INFO.value ankaios.set_logger_level(AnkaiosLogLevel.ERROR) assert ankaios.logger.level == AnkaiosLogLevel.ERROR.value @@ -55,35 +70,60 @@ def test_connection(): """ Test the connect / disconnect functionality of the Ankaios class. """ - ankaios = Ankaios() - assert not ankaios._connected + # Already connected + with pytest.raises(AnkaiosConnectionException, + match="Already connected."): + ankaios = generate_test_ankaios() + ankaios._connected = True + ankaios._connect() - with patch("threading.Thread") as mock_thread: + # Test input pipe does not exist + with patch("os.path.exists") as mock_exists, \ + pytest.raises(AnkaiosConnectionException, + match="Control interface input fifo"): + mock_exists.side_effect = lambda path: \ + path != "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\input" + ankaios = Ankaios() + + # Test output pipe does not exist + with patch("os.path.exists") as mock_exists, \ + pytest.raises(AnkaiosConnectionException, + match="Control interface output fifo"): + mock_exists.side_effect = lambda path: \ + path != "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output" + ankaios = Ankaios() + + # Test success + with patch("os.path.exists") as mock_exists, \ + patch("threading.Thread") as mock_thread, \ + patch("builtins.open") as mock_open_file, \ + patch("ankaios_sdk.Ankaios._send_initial_hello") \ + as mock_initial_hello: + mock_exists.return_value = True mock_thread_instance = MagicMock() mock_thread.return_value = mock_thread_instance + output_file_mock = MagicMock() + mock_open_file.return_value = output_file_mock - ankaios._connect() + # Build ankaios and connect + ankaios = Ankaios() mock_thread.assert_called_once_with( target=ankaios._read_from_control_interface ) mock_thread_instance.start.assert_called_once() + mock_open_file.assert_called_once_with( + f"{ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output", "ab" + ) + assert ankaios._read_thread is not None + assert ankaios._output_file == output_file_mock + mock_initial_hello.assert_called_once() assert ankaios._connected - with pytest.raises(AnkaiosConnectionException, - match="Already connected."): - ankaios._connect() - + # Disconnect ankaios._disconnect() - mock_thread_instance.join.assert_called_once() - assert not ankaios._connected - with pytest.raises(AnkaiosConnectionException, - match="Already disconnected."): - ankaios._disconnect() - - with Ankaios() as ank: - assert ank._connected - assert not ank._connected + mock_thread_instance.join.assert_called_once() + output_file_mock.close.assert_called_once() def test_read_from_control_interface(): @@ -99,13 +139,18 @@ def test_read_from_control_interface(): mock_file_handle.read.side_effect = \ [bytes([b]) for b in input_file_content] - ankaios = Ankaios() + ankaios = generate_test_ankaios() - # will call _read_from_control_interface - ankaios._connect() + # Start thread (similar to _connect) + ankaios._connected = True + ankaios._read_thread = threading.Thread( + target=ankaios._read_from_control_interface + ) + ankaios._read_thread.start() - # will stop the thread after reading the message - ankaios._disconnect() + # Stop thread (similar to _disconnect) + ankaios._connected = False + ankaios._read_thread.join() mock_file.assert_called_once_with( f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") @@ -118,14 +163,19 @@ def test_read_from_control_interface(): mock_file_handle.read.side_effect = \ [bytes([b]) for b in input_file_content] - ankaios = Ankaios() + ankaios = generate_test_ankaios() ankaios._responses["1234"] = ResponseEvent() - # will call _read_from_control_interface - ankaios._connect() + # Start thread (similar to _connect) + ankaios._connected = True + ankaios._read_thread = threading.Thread( + target=ankaios._read_from_control_interface + ) + ankaios._read_thread.start() - # will stop the thread after reading the message - ankaios._disconnect() + # Stop thread (similar to _disconnect) + ankaios._connected = False + ankaios._read_thread.join() mock_file.assert_called_once_with( f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") @@ -133,14 +183,59 @@ def test_read_from_control_interface(): assert ankaios._responses["1234"].is_set() +def test_write_to_pipe(): + """ + Test the _write_to_pipe method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + with pytest.raises(AnkaiosConnectionException, + match="Could not write to pipe, not connected."): + ankaios._write_to_pipe(None) + + ankaios._connected = True + output_file = MagicMock() + ankaios._output_file = output_file + + ankaios._write_to_pipe(MESSAGE_UPDATE_SUCCESS) + + output_file.write.assert_called() + output_file.flush.assert_called_once() + + +def test_write_request(): + """ + Test the _write_request method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + with patch("ankaios_sdk.Ankaios._write_to_pipe") as mock_write: + request = generate_test_request() + ankaios._write_request(request) + mock_write.assert_called_once() + + +def test_send_initial_hello(): + """ + Test the _send_initial_hello method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + with patch("ankaios_sdk.Ankaios._write_to_pipe") as mock_write: + initial_hello = _control_api.ToAnkaios( + hello=_control_api.Hello( + protocolVersion=str(ANKAIOS_VERSION) + ) + ) + ankaios._send_initial_hello() + mock_write.assert_called_once_with(initial_hello) + + def test_get_reponse_by_id(): """ Test the get_response_by_id method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() with pytest.raises( AnkaiosConnectionException, - match="Reading from the control interface is not started." + match="Not connected." ): ankaios._get_response_by_id("1234") ankaios._connected = True @@ -158,32 +253,15 @@ def test_get_reponse_by_id(): assert not list(ankaios._responses.keys()) -def test_write_to_pipe(): - """ - Test the _write_to_pipe method of the Ankaios class. - """ - with patch("builtins.open", mock_open()) as mock_file: - ankaios = Ankaios() - ankaios._write_to_pipe(generate_test_request()) - - mock_file.assert_called_once_with( - f"{ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab") - mock_file().write.assert_called() - mock_file().flush.assert_called_once() - - def test_send_request(): """ Test the _send_request method of the Ankaios class. """ - ankaios = Ankaios() - with pytest.raises(AnkaiosConnectionException, - match="Cannot request if not connected."): - ankaios._send_request(None) + ankaios = generate_test_ankaios() ankaios._connected = True request = generate_test_request() - with patch("ankaios_sdk.Ankaios._write_to_pipe") as mock_write, \ + with patch("ankaios_sdk.Ankaios._write_request") as mock_write, \ patch("ankaios_sdk.Ankaios._get_response_by_id") \ as mock_get_response: ankaios._send_request(request) @@ -192,7 +270,7 @@ def test_send_request(): request.get_id(), Ankaios.DEFAULT_TIMEOUT ) - with patch("ankaios_sdk.Ankaios._write_to_pipe") as mock_write, \ + with patch("ankaios_sdk.Ankaios._write_request") as mock_write, \ patch("ankaios_sdk.Ankaios._get_response_by_id") \ as mock_get_response: mock_get_response.side_effect = TimeoutError() @@ -205,7 +283,7 @@ def test_apply_manifest(): """ Test the apply manifest method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() ankaios.logger = MagicMock() manifest = Manifest(MANIFEST_DICT) @@ -248,7 +326,7 @@ def test_delete_manifest(): """ Test the delete manifest method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() ankaios.logger = MagicMock() manifest = Manifest(MANIFEST_DICT) @@ -291,7 +369,7 @@ def test_run_workload(): """ Test the run workload method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() ankaios.logger = MagicMock() workload = generate_test_workload() @@ -334,7 +412,7 @@ def test_delete_workload(): """ Test the delete workload method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() ankaios.logger = MagicMock() # Test success @@ -376,7 +454,7 @@ def test_get_workload_with_instance_name(): """ Test the get workload with instance name of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() workload_instance_name = WorkloadInstanceName( agent_name="agent_Test", workload_name="nginx", @@ -404,7 +482,7 @@ def test_configs(): """ Test the configs methods of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() with pytest.raises(NotImplementedError, match="not implemented yet"): ankaios.set_configs(configs={'name': 'config'}) @@ -429,7 +507,7 @@ def test_get_state(): """ Test the get state method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() ankaios.logger = MagicMock() # Test success @@ -470,7 +548,7 @@ def test_get_agents(): """ Test the get agents method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_agents") \ @@ -485,7 +563,7 @@ def test_get_workload_states(): """ Test the get workload states method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ @@ -500,7 +578,7 @@ def test_get_workload_state_for_instance_name(): """ Test the get workload state for instance name method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() ankaios.logger = MagicMock() workload_instance_name = WorkloadInstanceName( agent_name="agent_Test", @@ -539,7 +617,7 @@ def test_get_workload_states_on_agent(): """ Test the get workload states on agent method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ @@ -556,7 +634,7 @@ def test_get_workload_states_for_name(): """ Test the get workload states for workload name method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ patch("ankaios_sdk.CompleteState.get_workload_states") \ @@ -576,7 +654,7 @@ def test_wait_for_workload_to_reach_state(): """ Test the wait for workload to reach state method of the Ankaios class. """ - ankaios = Ankaios() + ankaios = generate_test_ankaios() instance_name = WorkloadInstanceName( agent_name="agent_Test", workload_name="workload_Test", From e565d93a183170ddbb407dff5b65cd5d9fbc7148 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 25 Oct 2024 15:38:44 +0300 Subject: [PATCH 45/72] Fix findings --- README.md | 2 +- ankaios_sdk/_components/complete_state.py | 52 +++++------ ankaios_sdk/_components/manifest.py | 14 +-- ankaios_sdk/_components/workload_state.py | 88 +++++++++++++------ ankaios_sdk/ankaios.py | 9 +- ankaios_sdk/utils.py | 1 + tests/test_ankaios.py | 23 ++--- tests/test_complete_state.py | 6 +- tests/test_manifest.py | 7 +- tests/test_request.py | 2 +- tests/workload_state/test_workload_state.py | 13 ++- .../test_workload_state_collection.py | 11 +-- 12 files changed, 138 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index a3f4bfc..5b374a1 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ with Ankaios() as ankaios: .build() # Run the workload - ret = ankaios.run_workload(workload) + ret = ankaios.apply_workload(workload) # Check if the workload is scheduled and get the WorkloadInstanceName if ret is not None: diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 8b3fdc4..82c84a5 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -38,7 +38,7 @@ - Add a workload to the complete state: .. code-block:: python - complete_state.set_workload(workload) + complete_state.add_workload(workload) - Get a workload from the complete state: .. code-block:: python @@ -117,7 +117,7 @@ def get_api_version(self) -> str: """ return str(self._complete_state.desiredState.apiVersion) - def set_workload(self, workload: Workload) -> None: + def add_workload(self, workload: Workload) -> None: """ Adds a workload to the complete state. @@ -125,6 +125,8 @@ def set_workload(self, workload: Workload) -> None: workload (Workload): The workload to add. """ self._workloads.append(workload) + self._complete_state.desiredState.workloads.\ + workloads[workload.name].CopyFrom(workload._to_proto()) def get_workload(self, workload_name: str) -> Workload: """ @@ -182,7 +184,26 @@ def set_configs(self, configs: dict) -> None: Args: configs (dict): The configurations to set in the complete state. """ + def _to_config_item(item: Union[str, list, dict] + ) -> _ank_base.ConfigItem: + config_item = _ank_base.ConfigItem() + if isinstance(item, str): + config_item.String = item + elif isinstance(item, list): + for value in [_to_config_item(value) for value in item]: + config_item.array.values.append(value) + elif isinstance(item, dict): + for key, value in item.items(): + config_item.object.fields[key]. \ + CopyFrom(_to_config_item(value)) + return config_item + self._configs = configs + self._complete_state.desiredState.configs.configs.clear() + for key, value in self._configs.items(): + self._complete_state.desiredState.configs.configs[key].CopyFrom( + _to_config_item(value) + ) def get_configs(self) -> dict: """ @@ -220,37 +241,12 @@ def from_manifest(manifest: Manifest) -> 'CompleteState': def _to_proto(self) -> _ank_base.CompleteState: """ - Converts the CompleteState object to a proto message. + Returns the CompleteState as a proto message. Returns: _ank_base.CompleteState: The protobuf message representing the complete state. """ - def _to_config_item(item: Union[str, list, dict] - ) -> _ank_base.ConfigItem: - config_item = _ank_base.ConfigItem() - if isinstance(item, str): - config_item.String = item - elif isinstance(item, list): - for value in [_to_config_item(value) for value in item]: - config_item.array.values.append(value) - elif isinstance(item, dict): - for key, value in item.items(): - config_item.object.fields[key]. \ - CopyFrom(_to_config_item(value)) - return config_item - - if len(self._complete_state.desiredState.workloads.workloads) != 0: - self._complete_state.desiredState.workloads.workloads.clear() - for workload in self._workloads: - self._complete_state.desiredState.workloads.\ - workloads[workload.name].CopyFrom(workload._to_proto()) - if len(self._complete_state.desiredState.configs.configs) != 0: - self._complete_state.desiredState.configs.configs.clear() - for key, value in self._configs.items(): - self._complete_state.desiredState.configs.configs[key].CopyFrom( - _to_config_item(value) - ) return self._complete_state def _from_proto(self, proto: _ank_base.CompleteState) -> None: diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index cf6f144..a594b4b 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -43,7 +43,7 @@ import yaml from ..exceptions import InvalidManifestException -from ..utils import WORKLOADS_PREFIX +from ..utils import WORKLOADS_PREFIX, CONFIGS_PREFIX class Manifest(): @@ -146,10 +146,14 @@ def check(self) -> None: def _calculate_masks(self) -> list[str]: """ - Calculates the masks for the workloads in the manifest. + Calculates the masks for the manifest. This includes + the names of the workloads and of the configs. Returns: - list[str]: A list of masks for the workloads. + list[str]: A list of masks. """ - return [f"{WORKLOADS_PREFIX}.{key}" - for key in self._manifest["workloads"].keys()] + masks = [f"{WORKLOADS_PREFIX}.{key}" + for key in self._manifest["workloads"].keys()] + masks.extend([f"{CONFIGS_PREFIX}.{key}" + for key in self._manifest["configs"].keys()]) + return masks diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index 0f7b7dd..a85c6a3 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -63,7 +63,7 @@ "WorkloadInstanceName", "WorkloadExecutionState", "WorkloadStateEnum", "WorkloadSubStateEnum"] -from typing import Optional, TypeAlias +from typing import Optional, TypeAlias, Union from enum import Enum from .._protos import _ank_base @@ -231,6 +231,15 @@ def __init__(self, state: _ank_base.ExecutionState) -> None: self._interpret_state(state) + def __str__(self) -> str: + """ + Returns the string representation of the workload execution state. + + Returns: + str: The string representation of the workload execution state. + """ + return f"{self.state.name} ({self.substate.name}): {self.info}" + def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: """ Interprets the execution state and sets the state, substate, @@ -327,8 +336,11 @@ class WorkloadState: workload_instance_name (WorkloadInstanceName): The name of the workload instance. """ - def __init__(self, agent_name: str, workload_name: str, - workload_id: str, state: _ank_base.ExecutionState) -> None: + def __init__(self, agent_name: str, + workload_name: str, + workload_id: str, + state: Union[WorkloadExecutionState, _ank_base.ExecutionState] + ) -> None: """ Initializes a WorkloadState instance. @@ -336,13 +348,25 @@ def __init__(self, agent_name: str, workload_name: str, agent_name (str): The name of the agent. workload_name (str): The name of the workload. workload_id (str): The ID of the workload. - state (_ank_base.ExecutionState): The execution state to interpret. + state (WorkloadExecutionState): The execution state. """ - self.execution_state = WorkloadExecutionState(state) + if isinstance(state, _ank_base.ExecutionState): + self.execution_state = WorkloadExecutionState(state) + else: + self.execution_state = state self.workload_instance_name = WorkloadInstanceName( agent_name, workload_name, workload_id ) + def __str__(self) -> str: + """ + Returns the string representation of the workload state. + + Returns: + str: The string representation of the workload state. + """ + return f"{self.workload_instance_name}: {self.execution_state}" + class WorkloadStateCollection: """ @@ -357,7 +381,7 @@ def __init__(self) -> None: """ Initializes a WorkloadStateCollection instance. """ - self._workload_states: list[WorkloadState] = [] + self._workload_states: dict = {} def add_workload_state(self, state: WorkloadState) -> None: """ @@ -366,7 +390,17 @@ def add_workload_state(self, state: WorkloadState) -> None: Args: state (WorkloadState): The workload state to add. """ - self._workload_states.append(state) + agent_name = state.workload_instance_name.agent_name + workload_name = state.workload_instance_name.workload_name + workload_id = state.workload_instance_name.workload_id + if agent_name not in self._workload_states: + self._workload_states[agent_name] = \ + self.ExecutionsStatesOfWorkload() + if workload_name not in self._workload_states[agent_name]: + self._workload_states[agent_name][workload_name] = \ + self.ExecutionsStatesForId() + self._workload_states[agent_name][workload_name][workload_id] = \ + state.execution_state def get_as_dict(self) -> WorkloadStatesMap: """ @@ -375,22 +409,7 @@ def get_as_dict(self) -> WorkloadStatesMap: Returns: WorkloadStatesMap: A dict of workload states. """ - return_dict = self.WorkloadStatesMap() - for state in self._workload_states: - - agent_name = state.workload_instance_name.agent_name - if agent_name not in return_dict: - return_dict[agent_name] = self.ExecutionsStatesOfWorkload() - - workload_name = state.workload_instance_name.workload_name - if workload_name not in return_dict[agent_name]: - return_dict[agent_name][workload_name] = \ - self.ExecutionsStatesForId() - - workload_id = state.workload_instance_name.workload_id - return_dict[agent_name][workload_name][workload_id] = \ - state.execution_state - return return_dict + return self._workload_states def get_as_list(self) -> list[WorkloadState]: """ @@ -399,7 +418,14 @@ def get_as_list(self) -> list[WorkloadState]: Returns: list[WorkloadState]: A list of workload states. """ - return self._workload_states + workload_states = [] + for agent_name, workloads in self._workload_states.items(): + for workload_name, workload_ids in workloads.items(): + for workload_id, exec_state in workload_ids.items(): + workload_states.append(WorkloadState( + agent_name, workload_name, workload_id, exec_state + )) + return workload_states def get_for_instance_name(self, instance_name: WorkloadInstanceName ) -> Optional[WorkloadState]: @@ -414,10 +440,16 @@ def get_for_instance_name(self, instance_name: WorkloadInstanceName WorkloadState: The workload state for the given instance name. None: If no workload state was found. """ - for state in self._workload_states: - if state.workload_instance_name == instance_name: - return state - return None + try: + return WorkloadState( + instance_name.agent_name, + instance_name.workload_name, + instance_name.workload_id, + self._workload_states[instance_name.agent_name] + [instance_name.workload_name][instance_name.workload_id] + ) + except KeyError: + return None def _from_proto(self, state: _ank_base.WorkloadStatesMap) -> None: """ diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 8832a87..e4a790f 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -52,7 +52,7 @@ - Run a workload: .. code-block:: python - ret = ankaios.run_workload(workload) + ret = ankaios.apply_workload(workload) print(ret["added_workloads"]) - Delete a workload: @@ -493,7 +493,7 @@ def delete_manifest(self, manifest: Manifest) -> dict: return content raise AnkaiosException("Received unexpected content type.") - def run_workload(self, workload: Workload) -> dict: + def apply_workload(self, workload: Workload) -> dict: """ Send a request to run a workload. @@ -508,7 +508,7 @@ def run_workload(self, workload: Workload) -> dict: AnkaiosException: If an error occurred while running the workload. """ complete_state = CompleteState() - complete_state.set_workload(workload) + complete_state.add_workload(workload) # Create the request request = Request(request_type="update_state") @@ -806,7 +806,8 @@ def get_workload_states_for_name(self, workload_name: str, workload_states = state.get_workload_states().get_as_list() workload_states_for_name = WorkloadStateCollection() for workload_state in workload_states: - if workload_state.name == workload_name: + if workload_state.workload_instance_name.workload_name == \ + workload_name: workload_states_for_name.add_workload_state(workload_state) return workload_states_for_name diff --git a/ankaios_sdk/utils.py b/ankaios_sdk/utils.py index eaed794..d0fcfa1 100644 --- a/ankaios_sdk/utils.py +++ b/ankaios_sdk/utils.py @@ -22,6 +22,7 @@ SUPPORTED_API_VERSION = "v0.1" WORKLOADS_PREFIX = "desiredState.workloads" +CONFIGS_PREFIX = "desiredState.configs" _config = configparser.ConfigParser() _config.read(os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')) diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 3201e8b..cc7481b 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -32,6 +32,8 @@ MESSAGE_BUFFER_COMPLETE_STATE, MESSAGE_UPDATE_SUCCESS, \ MESSAGE_BUFFER_UPDATE_SUCCESS, MESSAGE_BUFFER_UPDATE_SUCCESS_LENGTH from tests.test_manifest import MANIFEST_DICT +from tests.workload_state.test_workload_state import \ + generate_test_workload_state def generate_test_ankaios() -> Ankaios: @@ -365,9 +367,9 @@ def test_delete_manifest(): ankaios.logger.error.assert_called() -def test_run_workload(): +def test_apply_workload(): """ - Test the run workload method of the Ankaios class. + Test the apply workload method of the Ankaios class. """ ankaios = generate_test_ankaios() ankaios.logger = MagicMock() @@ -377,7 +379,7 @@ def test_run_workload(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = \ Response(MESSAGE_BUFFER_UPDATE_SUCCESS) - ret = ankaios.run_workload(workload) + ret = ankaios.apply_workload(workload) assert isinstance(ret, dict) mock_send_request.assert_called_once() ankaios.logger.info.assert_called() @@ -386,7 +388,7 @@ def test_run_workload(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) with pytest.raises(AnkaiosException): - ankaios.run_workload(workload) + ankaios.apply_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -394,7 +396,7 @@ def test_run_workload(): with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: mock_send_request.side_effect = TimeoutError() with pytest.raises(TimeoutError): - ankaios.run_workload(workload) + ankaios.apply_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -403,7 +405,7 @@ def test_run_workload(): mock_send_request.return_value = \ Response(MESSAGE_BUFFER_COMPLETE_STATE) with pytest.raises(AnkaiosException): - ankaios.run_workload(workload) + ankaios.apply_workload(workload) mock_send_request.assert_called_once() ankaios.logger.error.assert_called() @@ -641,13 +643,14 @@ def test_get_workload_states_for_name(): as mock_state_get_workload_states: mock_get_state.return_value = CompleteState() wl_state_collection = WorkloadStateCollection() - wl_state = MagicMock() - wl_state.name = "nginx" + wl_state = generate_test_workload_state() wl_state_collection.add_workload_state(wl_state) mock_state_get_workload_states.return_value = wl_state_collection - ret = ankaios.get_workload_states_for_name("nginx") + ret = ankaios.get_workload_states_for_name("workload_Test") assert isinstance(ret, WorkloadStateCollection) - assert wl_state in ret.get_as_list() + wl_list = ret.get_as_list() + assert len(wl_list) == 1 + assert str(wl_list[0]) == str(wl_state) def test_wait_for_workload_to_reach_state(): diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index 86b336e..097011e 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -57,9 +57,11 @@ def test_workload_functionality(): assert len(complete_state.get_workloads()) == 0 wl_nginx = generate_test_workload("nginx_test") - complete_state.set_workload(wl_nginx) + complete_state.add_workload(wl_nginx) assert len(complete_state.get_workloads()) == 1 assert complete_state.get_workload("nginx_test") == wl_nginx + assert complete_state._complete_state.desiredState.workloads\ + .workloads["nginx_test"] == wl_nginx._to_proto() assert complete_state.get_workload("invalid") is None @@ -183,7 +185,7 @@ def test_proto(): wl_nginx = generate_test_workload("nginx_test") config = generate_test_config() - complete_state.set_workload(wl_nginx) + complete_state.add_workload(wl_nginx) complete_state.set_configs(config) new_complete_state = CompleteState() diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 0579ab3..cbe2f5d 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -19,7 +19,7 @@ from unittest.mock import patch, mock_open import pytest from ankaios_sdk import Manifest, InvalidManifestException -from ankaios_sdk.utils import WORKLOADS_PREFIX +from ankaios_sdk.utils import WORKLOADS_PREFIX, CONFIGS_PREFIX MANIFEST_CONTENT = """apiVersion: v0.1 @@ -139,8 +139,9 @@ def test_calculate_masks(): 'runtimeConfig': 'image: image/test' } manifest = Manifest(manifest_dict) - assert len(manifest._calculate_masks()) == 2 + assert len(manifest._calculate_masks()) == 3 assert manifest._calculate_masks() == [ f"{WORKLOADS_PREFIX}.nginx_test", - f"{WORKLOADS_PREFIX}.nginx_test_other" + f"{WORKLOADS_PREFIX}.nginx_test_other", + f"{CONFIGS_PREFIX}.test_ports" ] diff --git a/tests/test_request.py b/tests/test_request.py index 47b248e..878f187 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -31,7 +31,7 @@ def generate_test_request(request_type: str = "update_state") -> Request: if request_type == "update_state": request = Request("update_state") complete_state = CompleteState() - complete_state.set_workload(generate_test_workload()) + complete_state.add_workload(generate_test_workload()) request.set_complete_state(complete_state) return request return Request("get_state") diff --git a/tests/workload_state/test_workload_state.py b/tests/workload_state/test_workload_state.py index 7ffa89e..84c66df 100644 --- a/tests/workload_state/test_workload_state.py +++ b/tests/workload_state/test_workload_state.py @@ -21,11 +21,11 @@ class in the ankaios_sdk. from ankaios_sdk._protos import _ank_base -def test_creation(): +def generate_test_workload_state(): """ - Test the creation of a WorkloadState instance. + Generate a test WorkloadState instance. """ - workload_state = WorkloadState( + return WorkloadState( agent_name="agent_Test", workload_name="workload_Test", workload_id="1234", @@ -34,6 +34,13 @@ def test_creation(): pending=_ank_base.PENDING_WAITING_TO_START ) ) + + +def test_creation(): + """ + Test the creation of a WorkloadState instance. + """ + workload_state = generate_test_workload_state() assert workload_state is not None assert workload_state.execution_state is not None assert workload_state.execution_state.state == WorkloadStateEnum.PENDING diff --git a/tests/workload_state/test_workload_state_collection.py b/tests/workload_state/test_workload_state_collection.py index 22d6047..631587a 100644 --- a/tests/workload_state/test_workload_state_collection.py +++ b/tests/workload_state/test_workload_state_collection.py @@ -48,12 +48,13 @@ def test_get(): # Test get_as_list workload_state_collection.add_workload_state(workload_state) assert len(workload_state_collection._workload_states) == 1 - assert workload_state_collection.get_as_list() == [workload_state] + assert str(workload_state_collection.get_as_list()[0]) == \ + str(workload_state) # Test get_as_dict workload_states_dict = workload_state_collection.get_as_dict() assert len(workload_states_dict) == 1 - assert "agent_Test" in workload_states_dict.keys() + assert "agent_Test" in workload_states_dict assert len(workload_states_dict["agent_Test"]) == 1 assert "workload_Test" in workload_states_dict["agent_Test"].keys() assert len(workload_states_dict["agent_Test"]["workload_Test"]) == 1 @@ -69,9 +70,9 @@ def test_get(): workload_name="workload_Test", workload_id="1234" ) - assert workload_state_collection.get_for_instance_name( - workload_instance_name - ) == workload_state + ret = workload_state_collection.get_for_instance_name( + workload_instance_name) + assert str(ret) == str(workload_state) workload_instance_name.workload_id = "5678" assert workload_state_collection.get_for_instance_name( workload_instance_name From 6942c344f8e83aa34701e9ae4b27a7c0610aaf97 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 25 Oct 2024 16:36:37 +0300 Subject: [PATCH 46/72] Add getting started doc page --- ankaios_sdk/exceptions.py | 1 + docs/source/getting_started.rst | 171 ++++++++++++++++++++++++++++++++ docs/source/index.rst | 2 + docs/source/security.rst | 1 + 4 files changed, 175 insertions(+) create mode 100644 docs/source/getting_started.rst create mode 100644 docs/source/security.rst diff --git a/ankaios_sdk/exceptions.py b/ankaios_sdk/exceptions.py index 0e6fd03..ccd9b1c 100644 --- a/ankaios_sdk/exceptions.py +++ b/ankaios_sdk/exceptions.py @@ -20,6 +20,7 @@ - WorkloadFieldException: Raised when the workload field is invalid. - WorkloadBuilderException: Raised when the workload builder is invalid. - InvalidManifestException: Raised when the manifest file is invalid. +- ConnectionClosedException: Raised when the connection is closed. - RequestException: Raised when the request is invalid. - ResponseException: Raised when the response is invalid. - AnkaiosConnectionException: Raised when an operation on the connection fails. diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst new file mode 100644 index 0000000..b0d2c08 --- /dev/null +++ b/docs/source/getting_started.rst @@ -0,0 +1,171 @@ +Getting started +=============== + +For installation of the Ankaios SDK, see the `Installation section `_. + +Once the SDK is installed, you can start using it by importing the module and creating a client object. + +.. code-block:: python + + from ankaios import Ankaios + + ankaios = Ankaios() + +The initialization of the Ankaios object will automatically connect to the fifo pipes of the control interface. Once this is done, +the communication with the Ankaios ecosystem can be started. + +**Apply a manifest** +-------------------- + +Considering we have a manifest file with a workload called ``nginx_test`` and a config called ``test_ports``. The manifest file is as follows: + +.. code-block:: yaml + :caption: my_manifest.yaml + + apiVersion: v0.1 + workloads: + nginx_test: + runtime: podman + restartPolicy: NEVER + agent: agent_A + configs: + ports: test_ports + runtimeConfig: | + image: image/test + configs: + test_ports: + port: \"8081\" + +The manifest can now be applied using the following code: + +.. code-block:: python + + from ankaios import Ankaios, Manifest + + # Create an Ankaios object + ankaios = Ankaios() + + # Load the manifest from the file + manifest = Manifest.from_file('my_manifest.yaml') + + # Apply the manifest and get the result + ret = ankaios.apply_manifest(manifest) + + # Get the workload instance name + wl_instance_name = ret["added_workloads"][0] + + # Print the instance name + print(wl_instance_name) + +IF the operation is succesfull, the result will contain a list with the added workloads that contains the workload instance name of our own. +The workload instance name contains the name of the workload, the agent it is running on and an unique identifier. + +**Update a workload** +--------------------- + +Considering we have the above workload running, we can update certain parameters of the workload. For this example, we will update the `restartPolicy`. To be able to pin-point +the exact workload we want to modify, we must know the workload instance name. This can be obtained as a result when starting the workload (either using `apply_manifest` or other methods), +deleting or modifying a one.. In case we don't have the workload instance name, we can take all the workloads that have the same name as the one we are looking for (or the agent). +For simplisity, we will consider the workload instance name is known. + +.. code-block:: python + + from ankaios import Ankaios + + # Create an Ankaios object + ankaios = Ankaios() + + # Considering we have the workload instance name + wl_instance_name = WorkloadInstanceName(...) + + # Get the workload base don the instance name + workload = ankaios.get_workload_with_instance_name(wl_instance_name) + + # Update the restart policy + ret = workload.update_restart_policy("ALWAYS") + + # Unpack the result + added_workloads = ret["added_workloads"] + deleted_workloads = ret["deleted_workloads"] + +Depending on the updated parameter, the workload can be restarted or not. If this is the case, the `deleted_workloads` will contain the old instance name and +the `added_workloads` will contain the new one. + +**Get the state of a workload** +------------------------------- + +Having a workload running in the Ankaios system, we can retrieve the state of the workload. The state has two fields, a primary state and a substate (See `Workload States `_). +Using the workload instance name, we can get the state of our specific workload. + +.. code-block:: python + + from ankaios import Ankaios + + # Create an Ankaios object + ankaios = Ankaios() + + # Considering we have the workload instance name + wl_instance_name = WorkloadInstanceName(...) + + # Get the workload state based on the instance name + execution_state = ankaios.get_execution_state_for_instance_name(wl_instance_name) + + # Output the state + print(execution_state.state) + print(execution_state.substate) + print(execution_state.info) + +If the workload instance name is not known, the state can be retrieved using the workload name or the agent name. This will return a `WorkloadStateCollection `_ +that contains all the workload states that match. + +**Get the complete state** +-------------------------- + +The complete state of the Ankaios system can be retrieved using the following code: + +.. code-block:: python + + from ankaios import Ankaios + + # Create an Ankaios object + ankaios = Ankaios() + + # Get the complete state + complete_state = ankaios.get_state() + + # Output the state + print(complete_state) + +The complete state contains information regarding the workloads running in the ANkaios cluster, configurations and agents. The state can be filtered using filter masks +(See `get_state `_). + +**Delete a workload** +--------------------- + +To delete a workload, there are multiple methods. We can either use the same manifest that we used to start it and call `delete_manifest` with it or we can +delete the workload based on it's name. In this example, we will delete the workload using the manifest. Considering the same manifest as before (`my_manifest.yaml `_): + +.. code-block:: python + + from ankaios import Ankaios, Manifest + + # Create an Ankaios object + ankaios = Ankaios() + + # Load the manifest from the file + manifest = Manifest.from_file('my_manifest.yaml') + + # Delete the manifest (this will delete the workload contained in the manifest) + ret = ankaios.delete_manifest(manifest) + + # Get the workload instance name + wl_instance_name = ret["deleted_workloads"][0] + + # Print the instance name of the deleted workload + print(wl_instance_name) + +Notes +----- + +* Exceptions might be raised during the usage of the sdk. For this, please see the `Exceptions section `_. +* For any issue or feature request, please see the `Issues section `_. diff --git a/docs/source/index.rst b/docs/source/index.rst index 44e9112..9616189 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,7 @@ :maxdepth: 2 :caption: Contents: + getting_started ankaios complete_state workload @@ -34,6 +35,7 @@ contributing code_of_conduct License + security Indices and tables ================== diff --git a/docs/source/security.rst b/docs/source/security.rst new file mode 100644 index 0000000..a555fe8 --- /dev/null +++ b/docs/source/security.rst @@ -0,0 +1 @@ +.. mdinclude:: ../../SECURITY.md \ No newline at end of file From 99599a6fbfd58053cf3a2bd052e30006440e39f2 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Fri, 25 Oct 2024 20:28:35 +0300 Subject: [PATCH 47/72] Fix minor issues about the docs --- ankaios_sdk/_components/response.py | 7 +++---- ankaios_sdk/ankaios.py | 3 +-- docs/source/getting_started.rst | 24 ++++++++++++------------ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/ankaios_sdk/_components/response.py b/ankaios_sdk/_components/response.py index 3fbe6e5..4b37226 100644 --- a/ankaios_sdk/_components/response.py +++ b/ankaios_sdk/_components/response.py @@ -161,12 +161,11 @@ def check_request_id(self, request_id: str) -> bool: def get_content(self) -> tuple[str, Union[str, CompleteState, dict]]: """ - Gets the content of the response. + Gets the content of the response. It can be either a string (if error), + a CompleteState instance, or a dictionary (if update state success). Returns: - tuple[str, str]: in case of an error response. - tuple[str, CompleteState]: in case of a complete state response. - tuple[str, dict]: in case of an update state success response. + tuple[str, any]: the content type and the content. """ return (self.content_type, self.content) diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index e4a790f..b3d90bf 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -650,8 +650,7 @@ def delete_all_configs(self) -> bool: Delete all the configs. Returns: - bool: True if the configs were deleted successfully, - False otherwise. + bool: if the configs were deleted successfully. """ raise NotImplementedError("delete_all_configs is not implemented yet.") diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index b0d2c08..3e9387c 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -3,7 +3,7 @@ Getting started For installation of the Ankaios SDK, see the `Installation section `_. -Once the SDK is installed, you can start using it by importing the module and creating a client object. +Once the SDK is installed, you can start using it by importing the module and creating an ankaios object. .. code-block:: python @@ -57,16 +57,16 @@ The manifest can now be applied using the following code: # Print the instance name print(wl_instance_name) -IF the operation is succesfull, the result will contain a list with the added workloads that contains the workload instance name of our own. +If the operation is succesfull, the result will contain a list with the added workloads that contains the workload instance name of our own. The workload instance name contains the name of the workload, the agent it is running on and an unique identifier. **Update a workload** --------------------- -Considering we have the above workload running, we can update certain parameters of the workload. For this example, we will update the `restartPolicy`. To be able to pin-point +Considering we have the above workload running, we can update certain parameters of the workload. For this example, we will update the `restartPolicy`. To be able to pinpoint the exact workload we want to modify, we must know the workload instance name. This can be obtained as a result when starting the workload (either using `apply_manifest` or other methods), -deleting or modifying a one.. In case we don't have the workload instance name, we can take all the workloads that have the same name as the one we are looking for (or the agent). -For simplisity, we will consider the workload instance name is known. +deleting or modifying one. In case we don't have the workload instance name, we can take all the workloads that have the same name as the one we are looking for (or the agent). +For simplicity, we will consider that the workload instance name is known. .. code-block:: python @@ -115,13 +115,13 @@ Using the workload instance name, we can get the state of our specific workload. print(execution_state.substate) print(execution_state.info) -If the workload instance name is not known, the state can be retrieved using the workload name or the agent name. This will return a `WorkloadStateCollection `_ -that contains all the workload states that match. +If the workload instance name is not known, the state can be retrieved using the workload name or the agent name. This will return a +`WorkloadStateCollection `_ that contains all the workload states that match. **Get the complete state** -------------------------- -The complete state of the Ankaios system can be retrieved using the following code: +The complete state of the Ankaios system can be retrieved using the `get_state` method of the `Ankaios` class: .. code-block:: python @@ -136,14 +136,14 @@ The complete state of the Ankaios system can be retrieved using the following co # Output the state print(complete_state) -The complete state contains information regarding the workloads running in the ANkaios cluster, configurations and agents. The state can be filtered using filter masks +The complete state contains information regarding the workloads running in the Ankaios cluster, configurations and agents. The state can be filtered using filter masks (See `get_state `_). **Delete a workload** --------------------- To delete a workload, there are multiple methods. We can either use the same manifest that we used to start it and call `delete_manifest` with it or we can -delete the workload based on it's name. In this example, we will delete the workload using the manifest. Considering the same manifest as before (`my_manifest.yaml `_): +delete the workload based on its name. In this example, we will delete the workload using the manifest. Considering the same manifest as before (`my_manifest.yaml `_): .. code-block:: python @@ -167,5 +167,5 @@ delete the workload based on it's name. In this example, we will delete the work Notes ----- -* Exceptions might be raised during the usage of the sdk. For this, please see the `Exceptions section `_. -* For any issue or feature request, please see the `Issues section `_. +* Exceptions might be raised during the usage of the sdk. For this, please consult the `Exceptions section `_ for a complete list. +* For any issue or feature request, please see the `Contributing section `_. From 90a1ec2071ccc0f6b7e24f0479a9453fb3e0524b Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 28 Oct 2024 13:47:11 +0200 Subject: [PATCH 48/72] Fix findings --- .gitignore | 4 +- README.md | 2 +- ankaios_sdk/_components/workload_state.py | 9 +- ankaios_sdk/ankaios.py | 81 ++++++++------ docs/source/getting_started.rst | 100 +++++++----------- tests/test_ankaios.py | 61 ++++++----- .../test_workload_execution_state.py | 2 +- tests/workload_state/test_workload_state.py | 3 +- .../test_workload_state_collection.py | 2 +- 9 files changed, 128 insertions(+), 136 deletions(-) diff --git a/.gitignore b/.gitignore index cf49b4f..50fe3a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +.eggs/ # Reports directory reports/ @@ -11,7 +12,8 @@ reports/ .pytest_cache/ # Build directory -ankaios_sdk.egg-info +*.egg-info +*.egg build/ dist/ diff --git a/README.md b/README.md index 5b374a1..6265054 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ with Ankaios() as ankaios: # Request the workload state based on the workload instance name ret = ankaios.get_workload_state_for_instance_name(workload_instance_name) if ret is not None: - print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.info}") + print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.additional_info}") # Wait until the workload reaches the running state ret = ankaios.wait_for_workload_to_reach_state( diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index a85c6a3..5aedace 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -56,7 +56,7 @@ workload_name = workload_state.workload_instance_name.workload_name state = workload_state.execution_state.state substate = workload_state.execution_state.substate - info = workload_state.execution_state.info + info = workload_state.execution_state.additional_info """ __all__ = ["WorkloadStateCollection", "WorkloadState", @@ -227,7 +227,7 @@ def __init__(self, state: _ank_base.ExecutionState) -> None: """ self.state: WorkloadStateEnum = None self.substate: WorkloadSubStateEnum = None - self.info: str = None + self.additional_info: str = None self._interpret_state(state) @@ -238,7 +238,8 @@ def __str__(self) -> str: Returns: str: The string representation of the workload execution state. """ - return f"{self.state.name} ({self.substate.name}): {self.info}" + return f"{self.state.name} ({self.substate.name}):" \ + + f"{self.additional_info}" def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: """ @@ -252,7 +253,7 @@ def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: Raises: ValueError: If the execution state is invalid. """ - self.info = str(exec_state.additionalInfo) + self.additional_info = str(exec_state.additionalInfo) field = exec_state.WhichOneof("ExecutionStateEnum") if field is None: diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index b3d90bf..9bccb7a 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -55,16 +55,16 @@ ret = ankaios.apply_workload(workload) print(ret["added_workloads"]) -- Delete a workload: +- Get a workload: .. code-block:: python - ret = ankaios.delete_workload(workload_name) - print(ret["deleted_workloads"]) + workload = ankaios.get_workload(workload_name) -- Get a workload: +- Delete a workload: .. code-block:: python - workload = ankaios.get_workload(workload_name) + ret = ankaios.delete_workload(workload_name) + print(ret["deleted_workloads"]) - Get the state: .. code-block:: python @@ -81,10 +81,15 @@ workload_states = ankaios.get_workload_states() -- Get the workload states: +- Get the workload states for workloads with a specific name: .. code-block:: python - workload_states = ankaios.get_workload_states() + workload_states = ankaios.get_workload_states_for_name(workload_name) + +- Get the workload states for a specific agent: + .. code-block:: python + + workload_states = ankaios.get_workload_states_on_agent(agent_name) - Get the workload execution state for instance name: .. code-block:: python @@ -209,16 +214,23 @@ def _connect(self) -> None: "Control interface output fifo does not exist." ) + # pylint: disable=consider-using-with + try: + self._output_file = open( + f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output", "ab" + ) + except Exception as e: + self.logger.error("Error while opening output fifo: %s", e) + self._disconnect() + raise AnkaiosConnectionException( + "Error while opening output fifo." + ) from e + self._read_thread = threading.Thread( target=self._read_from_control_interface ) self._read_thread.start() - # pylint: disable=consider-using-with - self._output_file = open( - f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output", "ab" - ) - self._connected = True self._send_initial_hello() @@ -360,7 +372,7 @@ def _get_response_by_id(self, request_id: str, in seconds. Returns: - AnkaiosConnectionException: The response object. + Response: The response object. Raises: AnkaiosConnectionException: If reading from the control interface @@ -391,6 +403,7 @@ def _send_request(self, request: Request, Raises: TimeoutError: If the request timed out. + AnkaiosConnectionException: If not connected. """ self._write_request(request) @@ -538,6 +551,24 @@ def apply_workload(self, workload: Workload) -> dict: return content raise AnkaiosException("Received unexpected content type.") + def get_workload(self, workload_name: str, + timeout: float = DEFAULT_TIMEOUT) -> Workload: + """ + Get the workload with the provided name from the + requested complete state. + + Args: + workload_name (str): The name of the workload. + timeout (float): The maximum time to wait for the response, + in seconds. + + Returns: + Workload: The workload object. + """ + return self.get_state( + timeout, [f"{WORKLOADS_PREFIX}.{workload_name}"] + ).get_workloads()[0] + def delete_workload(self, workload_name: str) -> dict: """ Send a request to delete a workload. @@ -578,26 +609,6 @@ def delete_workload(self, workload_name: str) -> dict: return content raise AnkaiosException("Received unexpected content type.") - def get_workload_with_instance_name( - self, instance_name: WorkloadInstanceName, - timeout: float = DEFAULT_TIMEOUT - ) -> Workload: - """ - Get the workload from the requested complete state, filtered - with the provided instance name. - - Args: - instance_name (instance_name): The instance name of the workload. - timeout (float): The maximum time to wait for the response, - in seconds. - - Returns: - Workload: The workload object. - """ - return self.get_state( - timeout, [f"{WORKLOADS_PREFIX}.{str(instance_name)}"] - ).get_workloads()[0] - def set_configs(self, configs: dict) -> bool: """ Set the configs. The names will be the keys of the dictionary. @@ -633,7 +644,7 @@ def get_configs(self) -> dict: """ raise NotImplementedError("get_configs is not implemented yet.") - def get_config(self, name: str) -> Union[dict, list, str]: + def get_config(self, name: str) -> dict: """ Get the config with the provided name. @@ -641,7 +652,7 @@ def get_config(self, name: str) -> Union[dict, list, str]: name (str): The name of the config. Returns: - Union[dict, list, str]: The config. + dict: The config in a dict format. """ raise NotImplementedError("get_config is not implemented yet.") diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 3e9387c..b76e026 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -3,7 +3,7 @@ Getting started For installation of the Ankaios SDK, see the `Installation section `_. -Once the SDK is installed, you can start using it by importing the module and creating an ankaios object. +Once the SDK is installed, you can start using it by importing the module and creating an Ankaios object. .. code-block:: python @@ -12,12 +12,12 @@ Once the SDK is installed, you can start using it by importing the module and cr ankaios = Ankaios() The initialization of the Ankaios object will automatically connect to the fifo pipes of the control interface. Once this is done, -the communication with the Ankaios ecosystem can be started. +the communication with the Ankaios cluster can be started. **Apply a manifest** -------------------- -Considering we have a manifest file with a workload called ``nginx_test`` and a config called ``test_ports``. The manifest file is as follows: +Considering we have the following manifest file with a workload called ``nginx_test`` and a config called ``test_ports``: .. code-block:: yaml :caption: my_manifest.yaml @@ -28,13 +28,8 @@ Considering we have a manifest file with a workload called ``nginx_test`` and a runtime: podman restartPolicy: NEVER agent: agent_A - configs: - ports: test_ports runtimeConfig: | image: image/test - configs: - test_ports: - port: \"8081\" The manifest can now be applied using the following code: @@ -57,71 +52,22 @@ The manifest can now be applied using the following code: # Print the instance name print(wl_instance_name) -If the operation is succesfull, the result will contain a list with the added workloads that contains the workload instance name of our own. -The workload instance name contains the name of the workload, the agent it is running on and an unique identifier. - -**Update a workload** ---------------------- - -Considering we have the above workload running, we can update certain parameters of the workload. For this example, we will update the `restartPolicy`. To be able to pinpoint -the exact workload we want to modify, we must know the workload instance name. This can be obtained as a result when starting the workload (either using `apply_manifest` or other methods), -deleting or modifying one. In case we don't have the workload instance name, we can take all the workloads that have the same name as the one we are looking for (or the agent). -For simplicity, we will consider that the workload instance name is known. - -.. code-block:: python - - from ankaios import Ankaios - - # Create an Ankaios object - ankaios = Ankaios() - - # Considering we have the workload instance name - wl_instance_name = WorkloadInstanceName(...) - - # Get the workload base don the instance name - workload = ankaios.get_workload_with_instance_name(wl_instance_name) - - # Update the restart policy - ret = workload.update_restart_policy("ALWAYS") - - # Unpack the result - added_workloads = ret["added_workloads"] - deleted_workloads = ret["deleted_workloads"] - -Depending on the updated parameter, the workload can be restarted or not. If this is the case, the `deleted_workloads` will contain the old instance name and -the `added_workloads` will contain the new one. - -**Get the state of a workload** -------------------------------- - -Having a workload running in the Ankaios system, we can retrieve the state of the workload. The state has two fields, a primary state and a substate (See `Workload States `_). -Using the workload instance name, we can get the state of our specific workload. - -.. code-block:: python - - from ankaios import Ankaios - - # Create an Ankaios object - ankaios = Ankaios() - - # Considering we have the workload instance name - wl_instance_name = WorkloadInstanceName(...) - # Get the workload state based on the instance name execution_state = ankaios.get_execution_state_for_instance_name(wl_instance_name) # Output the state print(execution_state.state) print(execution_state.substate) - print(execution_state.info) + print(execution_state.additional_info) -If the workload instance name is not known, the state can be retrieved using the workload name or the agent name. This will return a -`WorkloadStateCollection `_ that contains all the workload states that match. +If the operation is successful, the result will contain a list with the added workloads that contains the workload instance name of the newly added workload. +The workload instance name contains the name of the workload, the agent it is running on and a unique identifier. Using it, we can request the current execution state of +the workload. The state has 3 elements: the primary state, the substate and additional information (See `Workload States `_). **Get the complete state** -------------------------- -The complete state of the Ankaios system can be retrieved using the `get_state` method of the `Ankaios` class: +The complete state of the Ankaios system can be retrieved using the ``get_state`` method of the ``Ankaios`` class: .. code-block:: python @@ -139,10 +85,36 @@ The complete state of the Ankaios system can be retrieved using the `get_state` The complete state contains information regarding the workloads running in the Ankaios cluster, configurations and agents. The state can be filtered using filter masks (See `get_state `_). +**Update a workload** +--------------------- + +Considering we have the above workload running, we can now modify it. For this example we will update the ``restartPolicy``. To be able to pinpoint +the exact workload we want to modify, we must know only it's name. + +.. code-block:: python + + from ankaios import Ankaios + + # Create an Ankaios object + ankaios = Ankaios() + + # Get the workload based on the name + workload = ankaios.get_workload("nginx") + + # Update the restart policy + ret = workload.update_restart_policy("ALWAYS") + + # Unpack the result + added_workloads = ret["added_workloads"] + deleted_workloads = ret["deleted_workloads"] + +Depending on the updated parameter, the workload can be restarted or not. If this is the case, the ``deleted_workloads`` will contain the old instance name and +the ``added_workloads`` will contain the new one. + **Delete a workload** --------------------- -To delete a workload, there are multiple methods. We can either use the same manifest that we used to start it and call `delete_manifest` with it or we can +There are multiple methods to delete a workload: we can either use the same manifest that we used to start it and call ``delete_manifest`` or we can delete the workload based on its name. In this example, we will delete the workload using the manifest. Considering the same manifest as before (`my_manifest.yaml `_): .. code-block:: python @@ -167,5 +139,5 @@ delete the workload based on its name. In this example, we will delete the workl Notes ----- -* Exceptions might be raised during the usage of the sdk. For this, please consult the `Exceptions section `_ for a complete list. +* Exceptions might be raised during the usage of the SDK. Please consult the `Exceptions section `_ for a complete list. * For any issue or feature request, please see the `Contributing section `_. diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index cc7481b..460d234 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -95,6 +95,15 @@ def test_connection(): path != "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output" ankaios = Ankaios() + # Test output pipe error + with patch("os.path.exists") as mock_exists, \ + patch("builtins.open") as mock_open_file, \ + pytest.raises(AnkaiosConnectionException, + match="Error while opening output fifo"): + mock_exists.return_value = True + mock_open_file.side_effect = OSError + ankaios = Ankaios() + # Test success with patch("os.path.exists") as mock_exists, \ patch("threading.Thread") as mock_thread, \ @@ -410,6 +419,30 @@ def test_apply_workload(): ankaios.logger.error.assert_called() +def test_get_workload(): + """ + Test the get workload of the Ankaios class. + """ + ankaios = generate_test_ankaios() + workload_name = "nginx" + workload = generate_test_workload( + workload_name=workload_name + ) + + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_workloads") \ + as mock_state_get_workloads: + mock_get_state.return_value = CompleteState() + mock_state_get_workloads.return_value = [workload] + ret = ankaios.get_workload(workload_name) + assert ret == workload + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, + [f"{WORKLOADS_PREFIX}.nginx"] + ) + mock_state_get_workloads.assert_called_once() + + def test_delete_workload(): """ Test the delete workload method of the Ankaios class. @@ -452,34 +485,6 @@ def test_delete_workload(): ankaios.logger.error.assert_called() -def test_get_workload_with_instance_name(): - """ - Test the get workload with instance name of the Ankaios class. - """ - ankaios = generate_test_ankaios() - workload_instance_name = WorkloadInstanceName( - agent_name="agent_Test", - workload_name="nginx", - workload_id="1234" - ) - workload = generate_test_workload( - workload_name="nginx" - ) - - with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ - patch("ankaios_sdk.CompleteState.get_workloads") \ - as mock_state_get_workloads: - mock_get_state.return_value = CompleteState() - mock_state_get_workloads.return_value = [workload] - ret = ankaios.get_workload_with_instance_name(workload_instance_name) - assert ret == workload - mock_get_state.assert_called_once_with( - Ankaios.DEFAULT_TIMEOUT, - [f"{WORKLOADS_PREFIX}.nginx.1234.agent_Test"] - ) - mock_state_get_workloads.assert_called_once() - - def test_configs(): """ Test the configs methods of the Ankaios class. diff --git a/tests/workload_state/test_workload_execution_state.py b/tests/workload_state/test_workload_execution_state.py index 5e5a12b..86f61a8 100644 --- a/tests/workload_state/test_workload_execution_state.py +++ b/tests/workload_state/test_workload_execution_state.py @@ -38,7 +38,7 @@ def test_interpret_state(): assert workload_state.state == WorkloadStateEnum.PENDING assert workload_state.substate == \ WorkloadSubStateEnum.PENDING_WAITING_TO_START - assert workload_state.info == "Dummy information" + assert workload_state.additional_info == "Dummy information" def test_interpret_state_error(): diff --git a/tests/workload_state/test_workload_state.py b/tests/workload_state/test_workload_state.py index 84c66df..28f282a 100644 --- a/tests/workload_state/test_workload_state.py +++ b/tests/workload_state/test_workload_state.py @@ -46,7 +46,8 @@ def test_creation(): assert workload_state.execution_state.state == WorkloadStateEnum.PENDING assert workload_state.execution_state.substate == \ WorkloadSubStateEnum.PENDING_WAITING_TO_START - assert workload_state.execution_state.info == "Dummy information" + assert workload_state.execution_state.additional_info == \ + "Dummy information" assert workload_state.workload_instance_name is not None assert workload_state.workload_instance_name.agent_name == "agent_Test" assert workload_state.workload_instance_name.workload_name == \ diff --git a/tests/workload_state/test_workload_state_collection.py b/tests/workload_state/test_workload_state_collection.py index 631587a..5b096c8 100644 --- a/tests/workload_state/test_workload_state_collection.py +++ b/tests/workload_state/test_workload_state_collection.py @@ -111,5 +111,5 @@ def test_from_proto(): WorkloadStateEnum.PENDING assert workload_states[0].execution_state.substate == \ WorkloadSubStateEnum.PENDING_WAITING_TO_START - assert workload_states[0].execution_state.info == \ + assert workload_states[0].execution_state.additional_info == \ "Dummy information" From e4965c619f950d9f40073278a57ec106ac4de0f7 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 28 Oct 2024 15:04:37 +0200 Subject: [PATCH 49/72] Fix Getting started doc --- docs/source/getting_started.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index b76e026..062beff 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -17,19 +17,19 @@ the communication with the Ankaios cluster can be started. **Apply a manifest** -------------------- -Considering we have the following manifest file with a workload called ``nginx_test`` and a config called ``test_ports``: +Considering we have the following manifest file with a workload called ``nginx``: .. code-block:: yaml :caption: my_manifest.yaml apiVersion: v0.1 workloads: - nginx_test: + nginx: runtime: podman restartPolicy: NEVER agent: agent_A runtimeConfig: | - image: image/test + image: image/nginx The manifest can now be applied using the following code: From 90c61f20aafc42e934c274134177bbb0c9adf493 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 28 Oct 2024 15:57:37 +0200 Subject: [PATCH 50/72] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6265054..39465b0 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,12 @@ cd ank-sdk-python pip install -e . # If you plan on contributing or running tests locally -pip install -e .[dev] +pip install -e ".[dev]" ``` +> [!Note] +Depending on your Linux distribution, it could be that you need to create and activate a [virtual environment](https://docs.python.org/3/library/venv.html) to run the pip commands. + ## Usage After installation, you can use the Ankaios SDK to configure and run workloads and request From 13fb48e1b5473d61d571d6ce04b40243b38a935c Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 28 Oct 2024 16:18:29 +0200 Subject: [PATCH 51/72] Github actions improvements --- .github/workflows/build.yml | 60 ++++++++--------------------------- .github/workflows/publish.yml | 6 ---- .github/workflows/release.yml | 6 ---- README.md | 4 +-- setup.py | 8 +++-- 5 files changed, 22 insertions(+), 62 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1647ecc..046a757 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,34 +12,32 @@ on: jobs: setup: - runs-on: ubuntu-latest - outputs: - cache-key: ${{ steps.cache-deps.outputs.cache-hit }} + name: Setup ${{ matrix.python }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python: ['3.10', '3.11', '3.12', '3.13'] + os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up Python + - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v5 with: - python-version: '3.10' - - - name: Cache dependencies - id: cache-deps - uses: actions/cache@v4 - with: - path: | - ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - restore-keys: | - python-deps-${{ runner.os }}- + python-version: ${{ matrix.python }} - name: Install dependencies - if: steps.cache-deps.outputs.cache-hit != 'true' run: | python3 -m pip install --upgrade pip pip install .[dev] + - name: Print packages + run: python3 -m pip list + + - name: Test installation + run: python3 -c "from ankaios_sdk import Ankaios" + unit_test: needs: setup runs-on: ubuntu-latest @@ -52,12 +50,6 @@ jobs: with: python-version: '3.10' - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - name: Install dependencies run: | python3 -m pip install --upgrade pip @@ -85,12 +77,6 @@ jobs: with: python-version: '3.10' - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - name: Install dependencies run: | python3 -m pip install --upgrade pip @@ -118,12 +104,6 @@ jobs: with: python-version: '3.10' - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - name: Install dependencies run: | python3 -m pip install --upgrade pip @@ -151,12 +131,6 @@ jobs: with: python-version: '3.10' - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - name: Install dependencies run: | python3 -m pip install --upgrade pip @@ -184,12 +158,6 @@ jobs: with: python-version: '3.10' - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - name: Install dependencies run: | python3 -m pip install --upgrade pip diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a2d1f32..6b426f8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,12 +21,6 @@ jobs: with: python-version: '3.10' - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - name: Install dependencies run: | python3 -m pip install --upgrade pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ebff575..6db237f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,12 +30,6 @@ jobs: with: python-version: '3.10' - - name: Restore cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: python-deps-${{ runner.os }}-${{ hashFiles('setup.py', 'setup.cfg') }} - - name: Install dependencies run: | python3 -m pip install --upgrade pip diff --git a/README.md b/README.md index 39465b0..dd47182 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ pip install -e . pip install -e ".[dev]" ``` -> [!Note] -Depending on your Linux distribution, it could be that you need to create and activate a [virtual environment](https://docs.python.org/3/library/venv.html) to run the pip commands. +> **Note:** +> Depending on your Linux distribution, it could be that you need to create and activate a [virtual environment](https://docs.python.org/3/library/venv.html) to run the pip commands. ## Usage diff --git a/setup.py b/setup.py index 1a7ee9a..ae85cff 100644 --- a/setup.py +++ b/setup.py @@ -87,14 +87,18 @@ def generate_protos(): long_description=open('README.md').read(), long_description_content_type="text/markdown", url="https://eclipse-ankaios.github.io/ankaios/latest/", - python_requires='>=3.6', + python_requires='>=3.10', package_dir={'': '.'}, packages=find_packages(where="."), include_package_data=True, classifiers=[ - "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], project_urls={ "Documentation": "https://eclipse-ankaios.github.io/ankaios/latest/", From e85334822069e8e3d6222b0937fbb4f8964afe55 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 08:11:38 +0200 Subject: [PATCH 52/72] Make code compatible with Python 3.9 --- .github/workflows/build.yml | 2 +- ankaios_sdk/_components/workload_state.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 046a757..60c3ca2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python: ['3.10', '3.11', '3.12', '3.13'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] steps: - name: Checkout code diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index 5aedace..b1bcefe 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -63,7 +63,7 @@ "WorkloadInstanceName", "WorkloadExecutionState", "WorkloadStateEnum", "WorkloadSubStateEnum"] -from typing import Optional, TypeAlias, Union +from typing import Optional, Union from enum import Enum from .._protos import _ank_base @@ -374,9 +374,9 @@ class WorkloadStateCollection: A class that represents a collection of workload states and provides methods to manipulate them. """ - ExecutionsStatesForId: TypeAlias = dict[str, WorkloadExecutionState] - ExecutionsStatesOfWorkload: TypeAlias = dict[str, ExecutionsStatesForId] - WorkloadStatesMap: TypeAlias = dict[str, ExecutionsStatesOfWorkload] + ExecutionsStatesForId = dict[str, WorkloadExecutionState] + ExecutionsStatesOfWorkload = dict[str, ExecutionsStatesForId] + WorkloadStatesMap = dict[str, ExecutionsStatesOfWorkload] def __init__(self) -> None: """ From 02722be460004d60c79337ab315ec44001767d73 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 08:12:33 +0200 Subject: [PATCH 53/72] Small fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ae85cff..09365e9 100644 --- a/setup.py +++ b/setup.py @@ -87,7 +87,7 @@ def generate_protos(): long_description=open('README.md').read(), long_description_content_type="text/markdown", url="https://eclipse-ankaios.github.io/ankaios/latest/", - python_requires='>=3.10', + python_requires='>=3.9', package_dir={'': '.'}, packages=find_packages(where="."), include_package_data=True, From 1ff7c37601cf2dcb9ee2509003b3465914f712f6 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 08:14:10 +0200 Subject: [PATCH 54/72] Small fix --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 09365e9..8d7ff76 100644 --- a/setup.py +++ b/setup.py @@ -95,6 +95,7 @@ def generate_protos(): "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From 66bbc68aff66a0d1faf290d4d2241cb63c3a7fc0 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 09:10:58 +0200 Subject: [PATCH 55/72] Fix ReadMe --- README.md | 94 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index dd47182..071e24e 100644 --- a/README.md +++ b/README.md @@ -49,52 +49,54 @@ Example: ```python from ankaios_sdk import Workload, Ankaios, WorkloadStateEnum, WorkloadSubStateEnum -# Connect to control interface -with Ankaios() as ankaios: - # Create a new workload - workload = Workload.builder() \ - .workload_name("dynamic_nginx") \ - .agent_name("agent_A") \ - .runtime("podman") \ - .restart_policy("NEVER") \ - .runtime_config("image: docker.io/library/nginx\ncommandOptions: [\"-p\", \"8080:80\"]") \ - .build() - - # Run the workload - ret = ankaios.apply_workload(workload) - - # Check if the workload is scheduled and get the WorkloadInstanceName - if ret is not None: - workload_instance_name = ret["added_workloads"][0] - - # Request the workload state based on the workload instance name - ret = ankaios.get_workload_state_for_instance_name(workload_instance_name) - if ret is not None: - print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.additional_info}") - - # Wait until the workload reaches the running state - ret = ankaios.wait_for_workload_to_reach_state( - workload_instance_name, - state=WorkloadStateEnum.RUNNING, - timeout=5 - ) - if ret: - print("Workload reached the RUNNING state.") - - # Request the state of the system, filtered with the agent name - complete_state = ankaios.get_state( - timeout=5, - field_mask=["workloadStates.agent_A"]) - - # Get the workload states present in the complete_state - workload_states_dict = complete_state.get_workload_states().get_as_dict() - - # Print the states of the workloads: - for workload_name in workload_states_dict["agent_A"]: - for workload_id in workload_states_dict["agent_A"][workload_name]: - print(f"Workload {workload_name} with id {workload_id} has the state " - + str(workload_states_dict["agent_A"] \ - [workload_name][workload_id].state)) +# Create a new Ankaios object. +# The connection to the control interface is automatically done at this step. +ankaios = Ankaios() + +# Create a new workload +workload = Workload.builder() \ + .workload_name("dynamic_nginx") \ + .agent_name("agent_A") \ + .runtime("podman") \ + .restart_policy("NEVER") \ + .runtime_config("image: docker.io/library/nginx\ncommandOptions: [\"-p\", \"8080:80\"]") \ + .build() + +# Run the workload +ret = ankaios.apply_workload(workload) + +# Check if the workload is scheduled and get the WorkloadInstanceName +if ret is not None: + workload_instance_name = ret["added_workloads"][0] + +# Request the workload state based on the workload instance name +ret = ankaios.get_workload_state_for_instance_name(workload_instance_name) +if ret is not None: + print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.additional_info}") + +# Wait until the workload reaches the running state +ret = ankaios.wait_for_workload_to_reach_state( + workload_instance_name, + state=WorkloadStateEnum.RUNNING, + timeout=5 + ) +if ret: + print("Workload reached the RUNNING state.") + +# Request the state of the system, filtered with the agent name +complete_state = ankaios.get_state( + timeout=5, + field_mask=["workloadStates.agent_A"]) + +# Get the workload states present in the complete_state +workload_states_dict = complete_state.get_workload_states().get_as_dict() + +# Print the states of the workloads: +for workload_name in workload_states_dict["agent_A"]: + for workload_id in workload_states_dict["agent_A"][workload_name]: + print(f"Workload {workload_name} with id {workload_id} has the state " + + str(workload_states_dict["agent_A"] \ + [workload_name][workload_id].state)) ``` ## Contributing From 6c28519f108685d9dce0d517e5efd40896eaf823 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 10:30:48 +0200 Subject: [PATCH 56/72] Fix reading of Ankaios version --- ankaios_sdk/utils.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ankaios_sdk/utils.py b/ankaios_sdk/utils.py index d0fcfa1..e95d414 100644 --- a/ankaios_sdk/utils.py +++ b/ankaios_sdk/utils.py @@ -16,14 +16,8 @@ This script provides general functionality and constants for the ankaios_sdk. """ -import os -import configparser - SUPPORTED_API_VERSION = "v0.1" +ANKAIOS_VERSION = "0.5.0" WORKLOADS_PREFIX = "desiredState.workloads" CONFIGS_PREFIX = "desiredState.configs" - -_config = configparser.ConfigParser() -_config.read(os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')) -ANKAIOS_VERSION = _config['metadata']['ankaios_version'] From 472b7016e2e774d3bf0b50c7bec08b247cb77dd6 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 11:07:29 +0200 Subject: [PATCH 57/72] Fix connection problem --- ankaios_sdk/ankaios.py | 6 +++--- tests/test_ankaios.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 9bccb7a..7c1383c 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -204,12 +204,12 @@ def _connect(self) -> None: if self._connected: raise AnkaiosConnectionException("Already connected.") if not os.path.exists( - "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\input"): + f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input"): raise AnkaiosConnectionException( "Control interface input fifo does not exist." ) if not os.path.exists( - "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output"): + f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output"): raise AnkaiosConnectionException( "Control interface output fifo does not exist." ) @@ -217,7 +217,7 @@ def _connect(self) -> None: # pylint: disable=consider-using-with try: self._output_file = open( - f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output", "ab" + f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output", "ab" ) except Exception as e: self.logger.error("Error while opening output fifo: %s", e) diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 460d234..aa208c6 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -84,7 +84,7 @@ def test_connection(): pytest.raises(AnkaiosConnectionException, match="Control interface input fifo"): mock_exists.side_effect = lambda path: \ - path != "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\input" + path != "/run/ankaios/control_interface/input" ankaios = Ankaios() # Test output pipe does not exist @@ -92,7 +92,7 @@ def test_connection(): pytest.raises(AnkaiosConnectionException, match="Control interface output fifo"): mock_exists.side_effect = lambda path: \ - path != "f{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output" + path != "/run/ankaios/control_interface/output" ankaios = Ankaios() # Test output pipe error @@ -123,7 +123,7 @@ def test_connection(): ) mock_thread_instance.start.assert_called_once() mock_open_file.assert_called_once_with( - f"{ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}\\output", "ab" + "/run/ankaios/control_interface/output", "ab" ) assert ankaios._read_thread is not None assert ankaios._output_file == output_file_mock @@ -164,7 +164,7 @@ def test_read_from_control_interface(): ankaios._read_thread.join() mock_file.assert_called_once_with( - f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") + "/run/ankaios/control_interface/input", "rb") assert "1234" in list(ankaios._responses) assert ankaios._responses["1234"].is_set() @@ -189,7 +189,7 @@ def test_read_from_control_interface(): ankaios._read_thread.join() mock_file.assert_called_once_with( - f"{Ankaios.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") + "/run/ankaios/control_interface/input", "rb") assert "1234" in list(ankaios._responses) assert ankaios._responses["1234"].is_set() From a33d82d9b716b35bf7db72c3bb4623dc302ad030 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 11:38:13 +0200 Subject: [PATCH 58/72] Small fix --- README.md | 4 ++-- tests/test_ankaios.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 071e24e..440302f 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ ret = ankaios.apply_workload(workload) if ret is not None: workload_instance_name = ret["added_workloads"][0] -# Request the workload state based on the workload instance name -ret = ankaios.get_workload_state_for_instance_name(workload_instance_name) +# Request the execution state based on the workload instance name +ret = ankaios.get_execution_state_for_instance_name(workload_instance_name) if ret is not None: print(f"State: {ret.state}, substate: {ret.substate}, info: {ret.additional_info}") diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index aa208c6..2921ddb 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -581,9 +581,9 @@ def test_get_workload_states(): mock_state_get_workload_states.assert_called_once() -def test_get_workload_state_for_instance_name(): +def test_get_execution_state_for_instance_name(): """ - Test the get workload state for instance name method of the Ankaios class. + Test the get execution state for instance name method of the Ankaios class. """ ankaios = generate_test_ankaios() ankaios.logger = MagicMock() From ffe10628b522dc66b8a84a75a7c5be6ae31e065f Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 11:54:44 +0200 Subject: [PATCH 59/72] Add docs README --- docs/README.md | 25 +++++++++++++++++++++++++ generate_docs.sh | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3fce9b6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,25 @@ +## Documentation + +The documentation can be automatically generated using the script `generate_docs.sh`. This will create a virtual environment, install the +necesarry dependencies, generate the documentation and then delete that temporary environment. + +All the steps can be done manually as well. The documentation dependencies can be installed by running the `pip install` with the `docs` extra, and the +documentation can be handled by the Makefile: + +```sh +pip install -e ".[docs]" + +# To install both the dev and docs dependencies +# pip install -e ".[dev, docs]" + +cd docs + +# This will generate the documentation +make html + +# This will open the documentation on localhost:8001 +make open + +# This will delete the build of the documentation +make clean +``` diff --git a/generate_docs.sh b/generate_docs.sh index f9843c3..9d078d1 100755 --- a/generate_docs.sh +++ b/generate_docs.sh @@ -3,7 +3,7 @@ python3 -m venv docs_env source docs_env/bin/activate -pip install .[docs] +pip install ".[docs]" cd docs From 57eba9c8437d15a18bc948856b5814dfe6908348 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 14:04:32 +0200 Subject: [PATCH 60/72] Small fix for ReadMe --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 440302f..b0bcff4 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ if ret: # Request the state of the system, filtered with the agent name complete_state = ankaios.get_state( timeout=5, - field_mask=["workloadStates.agent_A"]) + field_masks=["workloadStates.agent_A"]) # Get the workload states present in the complete_state workload_states_dict = complete_state.get_workload_states().get_as_dict() From 71da4c810f8889f3316b67c93d272774d15bd8c2 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 14:08:02 +0200 Subject: [PATCH 61/72] Fix getting started examples --- docs/source/getting_started.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 062beff..38cd74c 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -7,7 +7,7 @@ Once the SDK is installed, you can start using it by importing the module and cr .. code-block:: python - from ankaios import Ankaios + from ankaios_sdk import Ankaios ankaios = Ankaios() @@ -35,7 +35,7 @@ The manifest can now be applied using the following code: .. code-block:: python - from ankaios import Ankaios, Manifest + from ankaios_sdk import Ankaios, Manifest # Create an Ankaios object ankaios = Ankaios() @@ -71,7 +71,7 @@ The complete state of the Ankaios system can be retrieved using the ``get_state` .. code-block:: python - from ankaios import Ankaios + from ankaios_sdk import Ankaios # Create an Ankaios object ankaios = Ankaios() @@ -93,7 +93,7 @@ the exact workload we want to modify, we must know only it's name. .. code-block:: python - from ankaios import Ankaios + from ankaios_sdk import Ankaios # Create an Ankaios object ankaios = Ankaios() @@ -119,7 +119,7 @@ delete the workload based on its name. In this example, we will delete the workl .. code-block:: python - from ankaios import Ankaios, Manifest + from ankaios_sdk import Ankaios, Manifest # Create an Ankaios object ankaios = Ankaios() From db0e8f792c24c0b78e00294912259c494ea13d17 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 15:59:58 +0200 Subject: [PATCH 62/72] Add more logs to the sdk --- ankaios_sdk/_components/request.py | 5 +++ ankaios_sdk/_components/response.py | 12 +++++++ ankaios_sdk/_components/workload.py | 9 ++++- ankaios_sdk/ankaios.py | 52 ++++++++++++----------------- ankaios_sdk/utils.py | 35 +++++++++++++++++++ tests/test_ankaios.py | 3 ++ tests/test_utils.py | 35 +++++++++++++++++++ 7 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 tests/test_utils.py diff --git a/ankaios_sdk/_components/request.py b/ankaios_sdk/_components/request.py index 7c77084..9509f48 100644 --- a/ankaios_sdk/_components/request.py +++ b/ankaios_sdk/_components/request.py @@ -53,6 +53,7 @@ import uuid from .._protos import _ank_base from ..exceptions import RequestException +from ..utils import get_logger from .complete_state import CompleteState @@ -74,10 +75,14 @@ def __init__(self, request_type: str) -> None: self._request = _ank_base.Request() self._request.requestId = str(uuid.uuid4()) self._request_type = request_type + self.logger = get_logger() if request_type not in ["update_state", "get_state"]: + self.logger.error("Invalid request type.") raise RequestException("Invalid request type. Supported values: " + "'update_state', 'get_state'.") + self.logger.debug("Created request for %s with id %s", + request_type, self._request.requestId) def __str__(self) -> str: """ diff --git a/ankaios_sdk/_components/response.py b/ankaios_sdk/_components/response.py index 4b37226..e8103a0 100644 --- a/ankaios_sdk/_components/response.py +++ b/ankaios_sdk/_components/response.py @@ -47,10 +47,14 @@ from threading import Event from .._protos import _control_api from ..exceptions import ResponseException, ConnectionClosedException +from ..utils import get_logger from .complete_state import CompleteState from .workload_state import WorkloadInstanceName +logger = get_logger() + + class Response: """ Represents a response received from the Ankaios system. @@ -89,10 +93,16 @@ def _parse_response(self) -> None: # Deserialize the received proto msg from_ankaios.ParseFromString(self.buffer) except Exception as e: + logger.error( + "Error parsing the received message: %s", e + ) raise ResponseException(f"Parsing error: '{e}'") from e if from_ankaios.HasField("response"): self._response = from_ankaios.response else: + logger.error( + "Connection closed by the server." + ) raise ConnectionClosedException( from_ankaios.connectionClosed.reason) @@ -220,5 +230,7 @@ def wait_for_response(self, timeout: int) -> Response: specified timeout. """ if not self.wait(timeout): + logger.debug("Timeout while waiting for the response with id %s", + self._response.get_request_id()) raise TimeoutError("Timeout while waiting for the response.") return self.get_response() diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index 5e7cd7b..4fed940 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -73,7 +73,7 @@ from .._protos import _ank_base from ..exceptions import WorkloadFieldException, WorkloadBuilderException -from ..utils import WORKLOADS_PREFIX +from ..utils import get_logger, WORKLOADS_PREFIX # pylint: disable=too-many-public-methods @@ -98,6 +98,7 @@ def __init__(self, name: str) -> None: self.name = name self._main_mask = f"{WORKLOADS_PREFIX}.{self.name}" self.masks = [self._main_mask] + self.logger = get_logger() def __str__(self) -> str: """ @@ -180,6 +181,8 @@ def update_restart_policy(self, policy: str) -> None: WorkloadFieldException: If an invalid restart policy is provided. """ if policy not in _ank_base.RestartPolicy.keys(): + self.logger.error( + "Invalid restart policy provided.") raise WorkloadFieldException( "restart policy", policy, _ank_base.RestartPolicy.keys() ) @@ -215,6 +218,8 @@ def update_dependencies(self, dependencies: dict[str, str]) -> None: self._workload.dependencies.dependencies.clear() for workload_name, condition in dependencies.items(): if condition not in _ank_base.AddCondition.keys(): + self.logger.error( + "Invalid dependency condition provided.") raise WorkloadFieldException( "dependency condition", condition, _ank_base.AddCondition.keys() @@ -288,6 +293,8 @@ def _generate_access_right_rule(self, "ReadWrite": _ank_base.ReadWriteEnum.RW_READ_WRITE, } if operation not in enum_mapper: + self.logger.error( + "Invalid rule operation provided.") raise WorkloadFieldException( "rule operation", operation, enum_mapper.keys() ) diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 7c1383c..9123899 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -110,11 +110,9 @@ __all__ = ["Ankaios", "AnkaiosLogLevel"] -import logging import os import time from typing import Union -from enum import Enum import threading from google.protobuf.internal.encoder import _VarintBytes from google.protobuf.internal.decoder import _DecodeVarint @@ -126,21 +124,8 @@ ResponseEvent, WorkloadStateCollection, Manifest, \ WorkloadInstanceName, WorkloadStateEnum, \ WorkloadExecutionState -from .utils import WORKLOADS_PREFIX, ANKAIOS_VERSION - - -class AnkaiosLogLevel(Enum): - """ Ankaios log levels. """ - FATAL = logging.FATAL - "(int): Fatal log level." - ERROR = logging.ERROR - "(int): Error log level." - WARN = logging.WARN - "(int): Warning log level." - INFO = logging.INFO - "(int): Info log level." - DEBUG = logging.DEBUG - "(int): Debug log level." +from .utils import AnkaiosLogLevel, get_logger, \ + WORKLOADS_PREFIX, ANKAIOS_VERSION # pylint: disable=too-many-public-methods @@ -160,12 +145,13 @@ class Ankaios: DEFAULT_TIMEOUT = 5.0 "(float): The default timeout, if not manually provided." - def __init__(self) -> None: + def __init__(self, + log_level: AnkaiosLogLevel = AnkaiosLogLevel.INFO + ) -> None: """ Initialize the Ankaios object. The logger will be created and the connection to the control interface will be established. """ - self.logger = None self.path = self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH self._read_thread = None @@ -174,7 +160,8 @@ def __init__(self) -> None: self._responses_lock = threading.Lock() self._responses: dict[str, ResponseEvent] = {} - self._create_logger() + self.logger = get_logger() + self.set_logger_level(log_level) self._connect() def __del__(self) -> None: @@ -183,16 +170,6 @@ def __del__(self) -> None: """ self._disconnect() - def _create_logger(self) -> None: - """Create a logger with custom format and default log level.""" - formatter = logging.Formatter('%(asctime)s %(message)s', - datefmt="%FT%TZ") - self.logger = logging.getLogger("Ankaios logger") - handler = logging.StreamHandler() - handler.setFormatter(formatter) - self.logger.addHandler(handler) - self.set_logger_level(AnkaiosLogLevel.INFO) - def _connect(self) -> None: """ Connect to the control interface by starting to read @@ -208,11 +185,13 @@ def _connect(self) -> None: raise AnkaiosConnectionException( "Control interface input fifo does not exist." ) + self.logger.debug("Found input pipe.") if not os.path.exists( f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output"): raise AnkaiosConnectionException( "Control interface output fifo does not exist." ) + self.logger.debug("Found output pipe.") # pylint: disable=consider-using-with try: @@ -232,6 +211,7 @@ def _connect(self) -> None: self._read_thread.start() self._connected = True + self.logger.info("Connected to the control interface.") self._send_initial_hello() def _disconnect(self) -> None: @@ -268,6 +248,8 @@ def _read_from_control_interface(self) -> None: input_fifo = open( f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/input", "rb") + self.logger.info("Started reading from the input pipe.") + while self._connected: # Buffer for reading in the byte size of the proto msg varint_buffer = bytearray() @@ -299,10 +281,14 @@ def _read_from_control_interface(self) -> None: continue request_id = response.get_request_id() + self.logger.debug("Received a response with the id %s", + request_id) with self._responses_lock: if request_id in self._responses: + self.logger.debug("Response expected.") self._responses[request_id].set_response(response) else: + self.logger.debug("Response saved for later.") self._responses[request_id] = ResponseEvent(response) self._responses[request_id].set() except ConnectionClosedException as e: # pragma: no cover @@ -334,6 +320,8 @@ def _write_to_pipe(self, to_ankaios: _control_api.ToAnkaios) -> None: self._output_file.write(to_ankaios.SerializeToString()) self._output_file.flush() + self.logger.debug("Wrote a message to the pipe.") + def _write_request(self, request: Request) -> None: """ Writes the request into the control interface output fifo. @@ -360,6 +348,8 @@ def _send_initial_hello(self) -> None: ) ) self._write_to_pipe(initial_hello) + self.logger.debug("Sent initial hello message with the version %s", + ANKAIOS_VERSION) def _get_response_by_id(self, request_id: str, timeout: float = DEFAULT_TIMEOUT) -> Response: @@ -383,9 +373,11 @@ def _get_response_by_id(self, request_id: str, with self._responses_lock: if request_id in self._responses: + self.logger.debug("Found response.") return self._responses.pop(request_id).get_response() self._responses[request_id] = ResponseEvent() + self.logger.debug("Waiting for response.") return self._responses[request_id].wait_for_response(timeout) def _send_request(self, request: Request, diff --git a/ankaios_sdk/utils.py b/ankaios_sdk/utils.py index e95d414..0e5c365 100644 --- a/ankaios_sdk/utils.py +++ b/ankaios_sdk/utils.py @@ -16,8 +16,43 @@ This script provides general functionality and constants for the ankaios_sdk. """ +import logging +from enum import Enum + SUPPORTED_API_VERSION = "v0.1" ANKAIOS_VERSION = "0.5.0" WORKLOADS_PREFIX = "desiredState.workloads" CONFIGS_PREFIX = "desiredState.configs" + + +class AnkaiosLogLevel(Enum): + """ Ankaios log levels. """ + ERROR = logging.ERROR + "(int): Error log level." + WARN = logging.WARN + "(int): Warning log level." + INFO = logging.INFO + "(int): Info log level." + DEBUG = logging.DEBUG + "(int): Debug log level." + + +def get_logger(name="Ankaios logger"): + """ + Returns a configured logger with a custom format. + + Args: + name (str): The name of the logger. + """ + logger = logging.getLogger(name) + + if not logger.handlers: + formatter = logging.Formatter( + '%(asctime)s %(message)s', datefmt="%FT%TZ" + ) + handler = logging.StreamHandler() + handler.setFormatter(formatter) + logger.addHandler(handler) + + return logger diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 2921ddb..a4a9931 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -17,6 +17,7 @@ """ from io import StringIO +import time import logging import threading from unittest.mock import patch, mock_open, MagicMock @@ -158,6 +159,7 @@ def test_read_from_control_interface(): target=ankaios._read_from_control_interface ) ankaios._read_thread.start() + time.sleep(0.01) # Stop thread (similar to _disconnect) ankaios._connected = False @@ -183,6 +185,7 @@ def test_read_from_control_interface(): target=ankaios._read_from_control_interface ) ankaios._read_thread.start() + time.sleep(0.01) # Stop thread (similar to _disconnect) ankaios._connected = False diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..7051679 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024 Elektrobit Automotive GmbH +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This module contains unit tests for the utils methods. +""" + +import logging +from ankaios_sdk.utils import get_logger + + +def test_get_logger(): + """ + Test the get_logger method. + """ + logger = get_logger("test_logger") + assert logger is not None + assert isinstance(logger, logging.Logger) + assert logger.name == "test_logger" + assert len(logger.handlers) == 1 + + # Creating another with the same name should not add more handlers + logger = get_logger("test_logger") + assert len(logger.handlers) == 1 From 8732df401d75603ff9dbedb0f348adf4a560ed05 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 16:05:17 +0200 Subject: [PATCH 63/72] Small fix regarding logs --- ankaios_sdk/_components/response.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ankaios_sdk/_components/response.py b/ankaios_sdk/_components/response.py index e8103a0..bf0bc73 100644 --- a/ankaios_sdk/_components/response.py +++ b/ankaios_sdk/_components/response.py @@ -230,7 +230,5 @@ def wait_for_response(self, timeout: int) -> Response: specified timeout. """ if not self.wait(timeout): - logger.debug("Timeout while waiting for the response with id %s", - self._response.get_request_id()) raise TimeoutError("Timeout while waiting for the response.") return self.get_response() From ba950cf0af26dad876241cfffffb887d026c94c0 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 17:59:41 +0200 Subject: [PATCH 64/72] Fix reading thread bug --- ankaios_sdk/ankaios.py | 34 ++++++++++++++++------------------ ankaios_sdk/utils.py | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 9123899..948ed67 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -208,10 +208,8 @@ def _connect(self) -> None: self._read_thread = threading.Thread( target=self._read_from_control_interface ) - self._read_thread.start() - self._connected = True - self.logger.info("Connected to the control interface.") + self._read_thread.start() self._send_initial_hello() def _disconnect(self) -> None: @@ -285,10 +283,12 @@ def _read_from_control_interface(self) -> None: request_id) with self._responses_lock: if request_id in self._responses: - self.logger.debug("Response expected.") + self.logger.debug( + "Setting response for existing request.") self._responses[request_id].set_response(response) else: - self.logger.debug("Response saved for later.") + self.logger.debug( + "Adding early response.") self._responses[request_id] = ResponseEvent(response) self._responses[request_id].set() except ConnectionClosedException as e: # pragma: no cover @@ -320,8 +320,6 @@ def _write_to_pipe(self, to_ankaios: _control_api.ToAnkaios) -> None: self._output_file.write(to_ankaios.SerializeToString()) self._output_file.flush() - self.logger.debug("Wrote a message to the pipe.") - def _write_request(self, request: Request) -> None: """ Writes the request into the control interface output fifo. @@ -373,11 +371,11 @@ def _get_response_by_id(self, request_id: str, with self._responses_lock: if request_id in self._responses: - self.logger.debug("Found response.") + self.logger.debug("Immediate response available.") return self._responses.pop(request_id).get_response() self._responses[request_id] = ResponseEvent() - self.logger.debug("Waiting for response.") + self.logger.debug("Waiting on response.") return self._responses[request_id].wait_for_response(timeout) def _send_request(self, request: Request, @@ -448,7 +446,7 @@ def apply_manifest(self, manifest: Manifest) -> dict: raise AnkaiosException(f"Received error: {content}") if content_type == "update_state_success": self.logger.info( - "Update successfull: %s added workloads, " + "Update successful: %s added workloads, " + "%s deleted workloads.", len(content["added_workloads"]), len(content["deleted_workloads"]) @@ -490,7 +488,7 @@ def delete_manifest(self, manifest: Manifest) -> dict: raise AnkaiosException(f"Received error: {content}") if content_type == "update_state_success": self.logger.info( - "Update successfull: %s added workloads, " + "Update successful: %s added workloads, " + "%s deleted workloads.", len(content["added_workloads"]), len(content["deleted_workloads"]) @@ -535,7 +533,7 @@ def apply_workload(self, workload: Workload) -> dict: raise AnkaiosException(f"Received error: {content}") if content_type == "update_state_success": self.logger.info( - "Update successfull: %s added workloads, " + "Update successful: %s added workloads, " + "%s deleted workloads.", len(content["added_workloads"]), len(content["deleted_workloads"]) @@ -593,7 +591,7 @@ def delete_workload(self, workload_name: str) -> dict: raise AnkaiosException(f"Received error: {content}") if content_type == "update_state_success": self.logger.info( - "Update successfull: %s added workloads, " + "Update successful: %s added workloads, " + "%s deleted workloads.", len(content["added_workloads"]), len(content["deleted_workloads"]) @@ -609,7 +607,7 @@ def set_configs(self, configs: dict) -> bool: configs (dict): The configs dictionary. Returns: - bool: True if the configs were set successfully, False otherwise. + bool: True if the configs were set successfuly, False otherwise. """ raise NotImplementedError("set_configs is not implemented yet.") @@ -623,7 +621,7 @@ def set_config(self, name: str, config: Union[dict, list, str]) -> bool: config (Union[dict, list, str]): The config dictionary. Returns: - bool: True if the config was set successfully, False otherwise. + bool: True if the config was set successfuly, False otherwise. """ raise NotImplementedError("set_config is not implemented yet.") @@ -653,7 +651,7 @@ def delete_all_configs(self) -> bool: Delete all the configs. Returns: - bool: if the configs were deleted successfully. + bool: if the configs were deleted successfuly. """ raise NotImplementedError("delete_all_configs is not implemented yet.") @@ -665,7 +663,7 @@ def delete_config(self, name: str) -> bool: name (str): The name of the config. Returns: - bool: True if the config was deleted successfully, False otherwise. + bool: True if the config was deleted successfuly, False otherwise. """ raise NotImplementedError("delete_config is not implemented yet.") @@ -756,7 +754,7 @@ def get_execution_state_for_instance_name( Raises: AnkaiosException: If the workload state was not - retrieved successfully. + retrieved successfuly. """ state = self.get_state(timeout, [instance_name.get_filter_mask()]) workload_states = state.get_workload_states().get_as_list() diff --git a/ankaios_sdk/utils.py b/ankaios_sdk/utils.py index 0e5c365..01d561b 100644 --- a/ankaios_sdk/utils.py +++ b/ankaios_sdk/utils.py @@ -49,7 +49,7 @@ def get_logger(name="Ankaios logger"): if not logger.handlers: formatter = logging.Formatter( - '%(asctime)s %(message)s', datefmt="%FT%TZ" + '%(asctime)s %(message)s', datefmt="[%F %T]" ) handler = logging.StreamHandler() handler.setFormatter(formatter) From 58e2b242a200163ccb43056f9b88e8029160a0af Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 29 Oct 2024 18:18:51 +0200 Subject: [PATCH 65/72] Fix config in manifest --- ankaios_sdk/_components/manifest.py | 5 +++-- ankaios_sdk/ankaios.py | 2 -- tests/test_ankaios.py | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index a594b4b..4e1ff44 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -154,6 +154,7 @@ def _calculate_masks(self) -> list[str]: """ masks = [f"{WORKLOADS_PREFIX}.{key}" for key in self._manifest["workloads"].keys()] - masks.extend([f"{CONFIGS_PREFIX}.{key}" - for key in self._manifest["configs"].keys()]) + if "configs" in self._manifest.keys(): + masks.extend([f"{CONFIGS_PREFIX}.{key}" + for key in self._manifest["configs"].keys()]) return masks diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 948ed67..ea4bd98 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -185,13 +185,11 @@ def _connect(self) -> None: raise AnkaiosConnectionException( "Control interface input fifo does not exist." ) - self.logger.debug("Found input pipe.") if not os.path.exists( f"{self.ANKAIOS_CONTROL_INTERFACE_BASE_PATH}/output"): raise AnkaiosConnectionException( "Control interface output fifo does not exist." ) - self.logger.debug("Found output pipe.") # pylint: disable=consider-using-with try: diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index a4a9931..25bd00a 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -154,10 +154,10 @@ def test_read_from_control_interface(): ankaios = generate_test_ankaios() # Start thread (similar to _connect) - ankaios._connected = True ankaios._read_thread = threading.Thread( target=ankaios._read_from_control_interface ) + ankaios._connected = True ankaios._read_thread.start() time.sleep(0.01) @@ -180,10 +180,10 @@ def test_read_from_control_interface(): ankaios._responses["1234"] = ResponseEvent() # Start thread (similar to _connect) - ankaios._connected = True ankaios._read_thread = threading.Thread( target=ankaios._read_from_control_interface ) + ankaios._connected = True ankaios._read_thread.start() time.sleep(0.01) From 72892a8b21b64cb4c7bd02666dfe1b61100bb666 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 30 Oct 2024 11:10:27 +0200 Subject: [PATCH 66/72] Fix CompleteState._to_proto() --- ankaios_sdk/_components/complete_state.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 82c84a5..74dfc1b 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -247,6 +247,9 @@ def _to_proto(self) -> _ank_base.CompleteState: _ank_base.CompleteState: The protobuf message representing the complete state. """ + for workload in self._workloads: + self._complete_state.desiredState.workloads.\ + workloads[workload.name].CopyFrom(workload._to_proto()) return self._complete_state def _from_proto(self, proto: _ank_base.CompleteState) -> None: From f00dc5e9244d94db9c7ea929e18f8ca588b752ae Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Wed, 30 Oct 2024 21:58:59 +0200 Subject: [PATCH 67/72] Added to_dict for complete state --- ankaios_sdk/_components/complete_state.py | 30 +++ ankaios_sdk/_components/workload.py | 50 ++++ ankaios_sdk/_components/workload_state.py | 13 + ankaios_sdk/ankaios.py | 6 - ankaios_sdk/utils.py | 33 ++- docs/source/index.rst | 1 + docs/source/utils.rst | 7 + tests/test_complete_state.py | 246 ++++++++++++------ tests/workload/test_workload.py | 71 ++++- .../test_workload_state_collection.py | 63 +++-- 10 files changed, 399 insertions(+), 121 deletions(-) create mode 100644 docs/source/utils.rst diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 74dfc1b..d8dfafe 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -239,6 +239,36 @@ def from_manifest(manifest: Manifest) -> 'CompleteState': state._configs = dict_state.get("configs") return state + def to_dict(self) -> dict: + """ + Returns the CompleteState as a dictionary + + Returns: + dict: The CompleteState as a dictionary. + """ + data = { + "desired_state": { + "api_version": self.get_api_version(), + "workloads": {}, + "configs": self._configs + }, + "workload_states": {}, + "agents": {} + } + for wl in self._workloads: + data["desired_state"]["workloads"][wl.name] = \ + wl.to_dict() + wl_states = self._workload_state_collection.get_as_dict() + for agent_name, exec_states in wl_states.items(): + data["workload_states"][agent_name] = {} + for workload_name, exec_states_id in exec_states.items(): + data["workload_states"][agent_name][workload_name] = {} + for workload_id, exec_state in exec_states_id.items(): + data["workload_states"][agent_name][workload_name][ + workload_id] = exec_state.to_dict() + data["agents"] = self.get_agents() + return data + def _to_proto(self) -> _ank_base.CompleteState: """ Returns the CompleteState as a proto message. diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index 4fed940..d732d09 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -437,6 +437,56 @@ def _add_mask(self, mask: str) -> None: if self._main_mask not in self.masks and mask not in self.masks: self.masks.append(mask) + def to_dict(self) -> dict: + """ + Convert the Workload object to a dictionary. + + Returns: + dict: The dictionary representation of the Workload object. + """ + workload_dict = {} + if self._workload.agent: + workload_dict["agent"] = self._workload.agent + if self._workload.runtime: + workload_dict["runtime"] = self._workload.runtime + if self._workload.runtimeConfig: + workload_dict["runtimeConfig"] = self._workload.runtimeConfig + workload_dict["restartPolicy"] = _ank_base.RestartPolicy.Name( + self._workload.restartPolicy + ) + workload_dict["dependencies"] = {} + if self._workload.dependencies: + for dep_key, dep_value in \ + self._workload.dependencies.dependencies.items(): + workload_dict["dependencies"][dep_key] = \ + _ank_base.AddCondition.Name(dep_value) + workload_dict["tags"] = [] + if self._workload.tags: + for tag in self._workload.tags.tags: + workload_dict["tags"].append( + {"key": tag.key, "value": tag.value} + ) + workload_dict["controlInterfaceAccess"] = {} + if self._workload.controlInterfaceAccess: + workload_dict["controlInterfaceAccess"]["allowRules"] = [] + for rule in self._workload.controlInterfaceAccess.allowRules: + operation, filter_masks = self._access_right_rule_to_str(rule) + workload_dict["controlInterfaceAccess"]["allowRules"].append({ + "type": "StateRule", + "operation": operation, + "filterMask": [str(mask) for mask in filter_masks]} + ) + workload_dict["controlInterfaceAccess"]["denyRules"] = [] + for rule in self._workload.controlInterfaceAccess.denyRules: + operation, filter_masks = self._access_right_rule_to_str(rule) + workload_dict["controlInterfaceAccess"]["denyRules"].append({ + "type": "StateRule", + "operation": operation, + "filterMask": [str(mask) for mask in filter_masks]} + ) + workload_dict["configs"] = {} + return workload_dict + # pylint: disable=too-many-branches @staticmethod def _from_dict(workload_name: str, dict_workload: dict) -> "Workload": diff --git a/ankaios_sdk/_components/workload_state.py b/ankaios_sdk/_components/workload_state.py index b1bcefe..0082bb5 100644 --- a/ankaios_sdk/_components/workload_state.py +++ b/ankaios_sdk/_components/workload_state.py @@ -264,6 +264,19 @@ def _interpret_state(self, exec_state: _ank_base.ExecutionState) -> None: self.state, getattr(exec_state, field) ) + def to_dict(self) -> dict: + """ + Returns the execution state as a dictionary. + + Returns: + dict: The execution state as a dictionary. + """ + return { + "state": str(self.state), + "substate": str(self.substate), + "additional_info": self.additional_info + } + # pylint: disable=too-few-public-methods class WorkloadInstanceName: diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index ea4bd98..467e1bf 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -22,12 +22,6 @@ - Ankaios: Handles the interaction with the Ankaios control interface. -Enums ------ - -- AnkaiosLogLevel: - Represents the log levels for the Ankaios class. - Usage ----- diff --git a/ankaios_sdk/utils.py b/ankaios_sdk/utils.py index 01d561b..1204715 100644 --- a/ankaios_sdk/utils.py +++ b/ankaios_sdk/utils.py @@ -14,10 +14,23 @@ """ This script provides general functionality and constants for the ankaios_sdk. + +Enums +----- + +- AnkaiosLogLevel: + Represents the log levels for the Ankaios class. + +Functions +--------- + +- get_logger: + Creates and returns the logger. """ import logging from enum import Enum +import threading SUPPORTED_API_VERSION = "v0.1" @@ -26,6 +39,10 @@ CONFIGS_PREFIX = "desiredState.configs" +# Used to sync across different threads when adding handlers +_logger_lock = threading.Lock() + + class AnkaiosLogLevel(Enum): """ Ankaios log levels. """ ERROR = logging.ERROR @@ -47,12 +64,14 @@ def get_logger(name="Ankaios logger"): """ logger = logging.getLogger(name) - if not logger.handlers: - formatter = logging.Formatter( - '%(asctime)s %(message)s', datefmt="[%F %T]" - ) - handler = logging.StreamHandler() - handler.setFormatter(formatter) - logger.addHandler(handler) + with _logger_lock: + if not any(isinstance(handler, logging.StreamHandler) + for handler in logger.handlers): + formatter = logging.Formatter( + '%(asctime)s %(message)s', datefmt="[%F %T]" + ) + handler = logging.StreamHandler() + handler.setFormatter(formatter) + logger.addHandler(handler) return logger diff --git a/docs/source/index.rst b/docs/source/index.rst index 9616189..4e08f0a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,6 +19,7 @@ manifest request response + utils exceptions .. toctree:: diff --git a/docs/source/utils.rst b/docs/source/utils.rst new file mode 100644 index 0000000..ff0e728 --- /dev/null +++ b/docs/source/utils.rst @@ -0,0 +1,7 @@ +Utils +===== + +.. automodule:: ankaios_sdk.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index 097011e..dce8ecf 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -16,25 +16,60 @@ This module contains unit tests for the Manifest class in the ankaios_sdk. """ +import json from ankaios_sdk import CompleteState, WorkloadStateCollection, Manifest from ankaios_sdk._components.complete_state import SUPPORTED_API_VERSION from ankaios_sdk._protos import _ank_base -from tests.workload.test_workload import generate_test_workload +from tests.workload.test_workload import generate_test_workload, WORKLOAD_PROTO +from tests.workload_state.test_workload_state_collection import \ + WORKLOAD_STATES_PROTO from tests.test_manifest import MANIFEST_DICT -def generate_test_config(): - """ - Generate a test configuration. - """ - return { - "config_1": "val_1", - "config_2": ["val_2", "val_3"], - "config_3": { - "key_1": "val_4", - "key_2": "val_5" - } +CONFIGS_PROTO = _ank_base.ConfigMap( + configs={ + "config_1": _ank_base.ConfigItem( + String="val_1" + ), + "config_2": _ank_base.ConfigItem( + array=_ank_base.ConfigArray( + values=[ + _ank_base.ConfigItem(String="val_2"), + _ank_base.ConfigItem(String="val_3") + ] + ) + ), + "config_3": _ank_base.ConfigItem( + object=_ank_base.ConfigObject( + fields={ + "key_1": _ank_base.ConfigItem(String="val_4"), + "key_2": _ank_base.ConfigItem(String="val_5") + } + ) + ) + } +) + + +AGENTS_PROTO = _ank_base.AgentMap( + agents={ + "agent_A": _ank_base.AgentAttributes( + cpu_usage=_ank_base.CpuUsage(cpu_usage=50), + free_memory=_ank_base.FreeMemory(free_memory=1024) + ) } +) + + +COMPLETE_PROTO = proto_msg = _ank_base.CompleteState( + desiredState=_ank_base.State( + apiVersion="v0.1", + workloads=WORKLOAD_PROTO, + configs=CONFIGS_PROTO + ), + workloadStates=WORKLOAD_STATES_PROTO, + agents=AGENTS_PROTO +) def test_general_functionality(): @@ -72,31 +107,9 @@ def test_workload_states(): setting and getting workload states. """ complete_state = CompleteState() - complete_state._from_proto(_ank_base.CompleteState( - workloadStates=_ank_base.WorkloadStatesMap(agentStateMap={ - "agent_A": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ - "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ - "1234": _ank_base.ExecutionState( - additionalInfo="Random info", - succeeded=_ank_base.SUCCEEDED_OK, - ) - }) - }), - "agent_B": _ank_base.ExecutionsStatesOfWorkload(wlNameStateMap={ - "nginx": _ank_base.ExecutionsStatesForId(idStateMap={ - "5678": _ank_base.ExecutionState( - additionalInfo="Random info", - pending=_ank_base.PENDING_WAITING_TO_START, - ) - }), - "dyn_nginx": _ank_base.ExecutionsStatesForId(idStateMap={ - "9012": _ank_base.ExecutionState( - additionalInfo="Random info", - stopping=_ank_base.STOPPING_WAITING_TO_STOP, - ) - }) - }) - }) + complete_state._from_proto( + _ank_base.CompleteState( + workloadStates=WORKLOAD_STATES_PROTO ) ) @@ -110,14 +123,11 @@ def test_get_agents(): Test the get_agents method of the CompleteState class. """ complete_state = CompleteState() - complete_state._from_proto(_ank_base.CompleteState( - agents=_ank_base.AgentMap(agents={ - "agent_A": _ank_base.AgentAttributes( - cpu_usage=_ank_base.CpuUsage(cpu_usage=50), - free_memory=_ank_base.FreeMemory(free_memory=1024) - ) - }) - )) + complete_state._from_proto( + _ank_base.CompleteState( + agents=AGENTS_PROTO + ) + ) agents = complete_state.get_agents() assert len(agents) == 1 assert "agent_A" in agents @@ -132,32 +142,20 @@ def test_get_configs(): complete_state = CompleteState() complete_state._from_proto(_ank_base.CompleteState( desiredState=_ank_base.State( - configs=_ank_base.ConfigMap( - configs={ - "config_1": _ank_base.ConfigItem( - String="val_1" - ), - "config_2": _ank_base.ConfigItem( - array=_ank_base.ConfigArray( - values=[ - _ank_base.ConfigItem(String="val_2"), - _ank_base.ConfigItem(String="val_3") - ] - ) - ), - "config_3": _ank_base.ConfigItem( - object=_ank_base.ConfigObject( - fields={ - "key_1": _ank_base.ConfigItem(String="val_4"), - "key_2": _ank_base.ConfigItem(String="val_5") - } - ) - ) - } - ) + configs=CONFIGS_PROTO ) )) - assert complete_state.get_configs() == generate_test_config() + configs = complete_state.get_configs() + assert configs == { + "config_1": "val_1", + "config_2": ["val_2", "val_3"], + "config_3": { + "key_1": "val_4", + "key_2": "val_5" + } + } + complete_state.set_configs(configs) + assert complete_state.get_configs() == configs def test_from_manifest(): @@ -177,18 +175,110 @@ def test_from_manifest(): } -def test_proto(): +def test_to_dict(): """ - Test converting the CompleteState instance to and from a protobuf message. + Test converting the CompleteState to a dictionary. """ complete_state = CompleteState() - wl_nginx = generate_test_workload("nginx_test") - config = generate_test_config() + complete_state._from_proto(COMPLETE_PROTO) - complete_state.add_workload(wl_nginx) - complete_state.set_configs(config) + complete_state_dict = complete_state.to_dict() + assert complete_state_dict == { + 'desired_state': { + 'api_version': 'v0.1', + 'workloads': { + 'dynamic_nginx': { + 'agent': 'agent_A', + 'runtime': 'podman', + 'runtimeConfig': 'image: control_interface_prod:0.1\\n', + 'dependencies': { + 'nginx': 'ADD_COND_RUNNING' + }, + 'restartPolicy': 'ALWAYS', + 'tags': [ + { + 'key': 'owner', + 'value': 'Ankaios team' + } + ], + 'controlInterfaceAccess': { + 'allowRules': [ + { + 'type': 'StateRule', + 'operation': 'Write', + 'filterMask': [ + 'desiredState.workloads.dynamic_nginx' + ] + } + ], + 'denyRules': [ + { + 'type': 'StateRule', + 'operation': 'Read', + 'filterMask': [ + 'desiredState.workloads.dynamic_nginx' + ] + }] + }, + 'configs': {} + } + }, + 'configs': { + 'config_1': 'val_1', + 'config_2': [ + 'val_2', 'val_3' + ], + 'config_3': { + 'key_1': 'val_4', + 'key_2': 'val_5' + } + } + }, + 'workload_states': { + 'agent_B': { + 'nginx': { + '5678': { + 'state': 'PENDING', + 'substate': 'PENDING_WAITING_TO_START', + 'additional_info': 'Random info' + } + }, + 'dyn_nginx': { + '9012': { + 'state': 'STOPPING', + 'substate': 'STOPPING_WAITING_TO_STOP', + 'additional_info': 'Random info' + } + } + }, + 'agent_A': { + 'nginx': { + '1234': { + 'state': 'SUCCEEDED', + 'substate': 'SUCCEEDED_OK', + 'additional_info': 'Random info' + } + } + } + }, + 'agents': { + 'agent_A': { + 'cpu_usage': 50, + 'free_memory': 1024 + } + } + } - new_complete_state = CompleteState() - new_complete_state._from_proto(complete_state._to_proto()) + # Test that it can be converted to json + json.dumps(complete_state_dict) + + +def test_proto(): + """ + Test converting the CompleteState instance to and from a protobuf message. + """ + complete_state = CompleteState() + complete_state._from_proto(COMPLETE_PROTO) + new_proto = complete_state._to_proto() - assert str(complete_state) == str(new_complete_state) + assert new_proto == COMPLETE_PROTO diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index ec062d1..6d888c6 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -30,6 +30,60 @@ from ankaios_sdk.utils import WORKLOADS_PREFIX +WORKLOAD_PROTO = _ank_base.WorkloadMap( + workloads={ + "dynamic_nginx": _ank_base.Workload( + agent="agent_A", + runtime="podman", + runtimeConfig=r"image: control_interface_prod:0.1\n", + restartPolicy=_ank_base.ALWAYS, + tags=_ank_base.Tags( + tags=[ + _ank_base.Tag( + key="owner", + value="Ankaios team" + ) + ] + ), + dependencies=_ank_base.Dependencies( + dependencies={ + "nginx": _ank_base.ADD_COND_RUNNING + } + ), + controlInterfaceAccess=_ank_base.ControlInterfaceAccess( + allowRules=[ + _ank_base.AccessRightsRule( + stateRule=_ank_base.StateRule( + operation=_ank_base.RW_WRITE, + filterMasks=[ + "desiredState.workloads.dynamic_nginx" + ] + ) + ) + ], + denyRules=[ + _ank_base.AccessRightsRule( + stateRule=_ank_base.StateRule( + operation=_ank_base.RW_READ, + filterMasks=[ + "desiredState.workloads.dynamic_nginx" + ] + ) + ) + ] + ), + configs=_ank_base.ConfigMappings( + configs={ + "str": "config_1", + "array": "config_2", + "dict": "config_3", + } + ) + ) + } +) + + def generate_test_workload(workload_name: str = "workload_test") -> Workload: """ Helper function to generate a Workload instance with some default values. @@ -226,20 +280,13 @@ def test_to_proto(workload: Workload): # pylint: disable=redefined-outer-name ]) -def test_from_proto( - workload: Workload - ): # pylint: disable=redefined-outer-name +def test__proto(): """ - Test converting theprotobuf message to a Workload instance. - - Args: - workload (Workload): The Workload fixture. + Test converting the workload to and from a proto. """ - proto = workload._to_proto() - new_workload = Workload("workload_test") - new_workload._from_proto(proto) - assert new_workload is not None - assert str(workload) == str(new_workload) + workload_new = Workload("workload_test") + workload_new._from_proto(WORKLOAD_PROTO) + assert workload_new._to_proto() == WORKLOAD_PROTO def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name diff --git a/tests/workload_state/test_workload_state_collection.py b/tests/workload_state/test_workload_state_collection.py index 5b096c8..1290aab 100644 --- a/tests/workload_state/test_workload_state_collection.py +++ b/tests/workload_state/test_workload_state_collection.py @@ -23,6 +23,44 @@ class in the ankaios_sdk. from ankaios_sdk._protos import _ank_base +WORKLOAD_STATES_PROTO = _ank_base.WorkloadStatesMap( + agentStateMap={ + "agent_A": _ank_base.ExecutionsStatesOfWorkload( + wlNameStateMap={ + "nginx": _ank_base.ExecutionsStatesForId( + idStateMap={ + "1234": _ank_base.ExecutionState( + additionalInfo="Random info", + succeeded=_ank_base.SUCCEEDED_OK, + ) + } + ) + } + ), + "agent_B": _ank_base.ExecutionsStatesOfWorkload( + wlNameStateMap={ + "nginx": _ank_base.ExecutionsStatesForId( + idStateMap={ + "5678": _ank_base.ExecutionState( + additionalInfo="Random info", + pending=_ank_base.PENDING_WAITING_TO_START, + ) + } + ), + "dyn_nginx": _ank_base.ExecutionsStatesForId( + idStateMap={ + "9012": _ank_base.ExecutionState( + additionalInfo="Random info", + stopping=_ank_base.STOPPING_WAITING_TO_STOP, + ) + } + ) + } + ) + } +) + + def test_get(): """ Test the basic functionality of the WorkloadStateCollection @@ -84,32 +122,21 @@ def test_from_proto(): Test the _from_proto method of the WorkloadStateCollection class, ensuring it correctly populates the collection from a proto message. """ - ank_workload_state = _ank_base.WorkloadStatesMap( - agentStateMap={"agent_Test": _ank_base.ExecutionsStatesOfWorkload( - wlNameStateMap={"workload_Test": _ank_base.ExecutionsStatesForId( - idStateMap={"1234": _ank_base.ExecutionState( - additionalInfo="Dummy information", - pending=_ank_base.PENDING_WAITING_TO_START - )} - )} - )} - ) - workload_state_collection = WorkloadStateCollection() - workload_state_collection._from_proto(ank_workload_state) - assert len(workload_state_collection._workload_states) == 1 + workload_state_collection._from_proto(WORKLOAD_STATES_PROTO) + assert len(workload_state_collection._workload_states) == 2 workload_states = workload_state_collection.get_as_list() - assert len(workload_states) == 1 + assert len(workload_states) == 3 assert workload_states[0].workload_instance_name.agent_name == \ - "agent_Test" + "agent_B" assert workload_states[0].workload_instance_name.workload_name == \ - "workload_Test" + "nginx" assert workload_states[0].workload_instance_name.workload_id == \ - "1234" + "5678" assert workload_states[0].execution_state.state == \ WorkloadStateEnum.PENDING assert workload_states[0].execution_state.substate == \ WorkloadSubStateEnum.PENDING_WAITING_TO_START assert workload_states[0].execution_state.additional_info == \ - "Dummy information" + "Random info" From 049f5740447422926ac87e74c2d09c6b8dcbe7ee Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Thu, 31 Oct 2024 08:55:37 +0200 Subject: [PATCH 68/72] Fix request empty masks --- ankaios_sdk/_components/request.py | 6 +++++- tests/test_request.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ankaios_sdk/_components/request.py b/ankaios_sdk/_components/request.py index 9509f48..4f7ec53 100644 --- a/ankaios_sdk/_components/request.py +++ b/ankaios_sdk/_components/request.py @@ -77,7 +77,11 @@ def __init__(self, request_type: str) -> None: self._request_type = request_type self.logger = get_logger() - if request_type not in ["update_state", "get_state"]: + if request_type == "update_state": + self._request.updateStateRequest.updateMask[:] = [] + elif request_type == "get_state": + self._request.completeStateRequest.fieldMask[:] = [] + else: self.logger.error("Invalid request type.") raise RequestException("Invalid request type. Supported values: " + "'update_state', 'get_state'.") diff --git a/tests/test_request.py b/tests/test_request.py index 878f187..d400bb3 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -46,7 +46,8 @@ def test_general_functionality(): request = Request("update_state") assert request.get_id() is not None - assert str(request) == f"requestId: \"{request.get_id()}\"\n" + assert str(request) == f"requestId: \"{request.get_id()}\"\n" \ + + "updateStateRequest {\n}\n" def test_update_state(): From e4776e10c00aef7eac5a9847c1c5f3b6b97e145a Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 4 Nov 2024 10:51:26 +0200 Subject: [PATCH 69/72] Implement configurations --- ankaios_sdk/_components/complete_state.py | 7 +- ankaios_sdk/_components/workload.py | 3 +- ankaios_sdk/ankaios.py | 165 ++++++++++++++---- tests/test_ankaios.py | 198 ++++++++++++++++++++-- tests/test_complete_state.py | 6 +- tests/workload/test_workload.py | 45 +---- 6 files changed, 338 insertions(+), 86 deletions(-) diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index d8dfafe..2526af9 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -236,7 +236,7 @@ def from_manifest(manifest: Manifest) -> 'CompleteState': Workload._from_dict(workload_name, workload_dict) ) if dict_state.get("configs") is not None: - state._configs = dict_state.get("configs") + state.set_configs(dict_state.get("configs")) return state def to_dict(self) -> dict: @@ -312,7 +312,8 @@ def _from_config_item(item: _ank_base.ConfigItem self._workload_state_collection._from_proto( self._complete_state.workloadStates ) - self._configs = {} + configs = {} for key, value in self._complete_state.desiredState. \ configs.configs.items(): - self._configs[key] = _from_config_item(value) + configs[key] = _from_config_item(value) + self.set_configs(configs) diff --git a/ankaios_sdk/_components/workload.py b/ankaios_sdk/_components/workload.py index d732d09..1260e67 100644 --- a/ankaios_sdk/_components/workload.py +++ b/ankaios_sdk/_components/workload.py @@ -399,7 +399,6 @@ def add_config(self, alias: str, name: str) -> None: name (str): The name of the configuration. """ self._workload.configs.configs[alias] = name - # Currently the mask is for all configs, not for individual aliases self._add_mask(f"{self._main_mask}.configs") def get_configs(self) -> dict[str, str]: @@ -485,6 +484,8 @@ def to_dict(self) -> dict: "filterMask": [str(mask) for mask in filter_masks]} ) workload_dict["configs"] = {} + for alias, name in self._workload.configs.configs.items(): + workload_dict["configs"][alias] = name return workload_dict # pylint: disable=too-many-branches diff --git a/ankaios_sdk/ankaios.py b/ankaios_sdk/ankaios.py index 467e1bf..d13f43d 100644 --- a/ankaios_sdk/ankaios.py +++ b/ankaios_sdk/ankaios.py @@ -119,7 +119,7 @@ WorkloadInstanceName, WorkloadStateEnum, \ WorkloadExecutionState from .utils import AnkaiosLogLevel, get_logger, \ - WORKLOADS_PREFIX, ANKAIOS_VERSION + WORKLOADS_PREFIX, ANKAIOS_VERSION, CONFIGS_PREFIX # pylint: disable=too-many-public-methods @@ -404,12 +404,14 @@ def set_logger_level(self, level: AnkaiosLogLevel) -> None: """ self.logger.setLevel(level.value) - def apply_manifest(self, manifest: Manifest) -> dict: + def apply_manifest(self, manifest: Manifest, + timeout: float = DEFAULT_TIMEOUT) -> dict: """ Send a request to apply a manifest. Args: manifest (Manifest): The manifest object to be applied. + timeout (float): The maximum time to wait for the response. Returns: dict: a dict with the added and deleted workloads. @@ -425,7 +427,7 @@ def apply_manifest(self, manifest: Manifest) -> dict: # Send request try: - response = self._send_request(request) + response = self._send_request(request, timeout) except TimeoutError as e: self.logger.error("%s", e) raise e @@ -446,12 +448,14 @@ def apply_manifest(self, manifest: Manifest) -> dict: return content raise AnkaiosException("Received unexpected content type.") - def delete_manifest(self, manifest: Manifest) -> dict: + def delete_manifest(self, manifest: Manifest, + timeout: float = DEFAULT_TIMEOUT) -> dict: """ Send a request to delete a manifest. Args: manifest (Manifest): The manifest object to be deleted. + timeout (float): The maximum time to wait for the response. Returns: dict: a dict with the added and deleted workloads. @@ -467,7 +471,7 @@ def delete_manifest(self, manifest: Manifest) -> dict: # Send request try: - response = self._send_request(request) + response = self._send_request(request, timeout) except TimeoutError as e: self.logger.error("%s", e) raise e @@ -488,12 +492,14 @@ def delete_manifest(self, manifest: Manifest) -> dict: return content raise AnkaiosException("Received unexpected content type.") - def apply_workload(self, workload: Workload) -> dict: + def apply_workload(self, workload: Workload, + timeout: float = DEFAULT_TIMEOUT) -> dict: """ Send a request to run a workload. Args: workload (Workload): The workload object to be run. + timeout (float): The maximum time to wait for the response. Returns: dict: a dict with the added and deleted workloads. @@ -512,7 +518,7 @@ def apply_workload(self, workload: Workload) -> dict: # Send request try: - response = self._send_request(request) + response = self._send_request(request, timeout) except TimeoutError as e: self.logger.error("%s", e) raise e @@ -551,12 +557,14 @@ def get_workload(self, workload_name: str, timeout, [f"{WORKLOADS_PREFIX}.{workload_name}"] ).get_workloads()[0] - def delete_workload(self, workload_name: str) -> dict: + def delete_workload(self, workload_name: str, + timeout: float = DEFAULT_TIMEOUT) -> dict: """ Send a request to delete a workload. Args: workload_name (str): The name of the workload to be deleted. + timeout (float): The maximum time to wait for the response. Returns: dict: a dict with the added and deleted workloads. @@ -570,7 +578,7 @@ def delete_workload(self, workload_name: str) -> dict: request.add_mask(f"{WORKLOADS_PREFIX}.{workload_name}") try: - response = self._send_request(request) + response = self._send_request(request, timeout) except TimeoutError as e: self.logger.error("%s", e) raise e @@ -591,42 +599,95 @@ def delete_workload(self, workload_name: str) -> dict: return content raise AnkaiosException("Received unexpected content type.") - def set_configs(self, configs: dict) -> bool: + def update_configs(self, configs: dict, + timeout: float = DEFAULT_TIMEOUT): """ - Set the configs. The names will be the keys of the dictionary. + Update the configs. The names will be the keys of the dictionary. Args: configs (dict): The configs dictionary. + timeout (float): The maximum time to wait for the response. - Returns: - bool: True if the configs were set successfuly, False otherwise. + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred. """ - raise NotImplementedError("set_configs is not implemented yet.") + complete_state = CompleteState() + complete_state.set_configs(configs) + + request = Request(request_type="update_state") + request.set_complete_state(complete_state) + request.add_mask(CONFIGS_PREFIX) - def set_config(self, name: str, config: Union[dict, list, str]) -> bool: + try: + response = self._send_request(request, timeout) + except TimeoutError as e: + self.logger.error("%s", e) + raise e + + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error("Error while trying to set the configs: %s", + content) + raise AnkaiosException(f"Received error: {content}") + if content_type == "update_state_success": + self.logger.info("Update successful") + return + raise AnkaiosException("Received unexpected content type.") + + def add_config(self, name: str, config: Union[dict, list, str], + timeout: float = DEFAULT_TIMEOUT): """ - Set the config with the provided name. + Adds the config with the provided name. If the config exists, it will be replaced. Args: name (str): The name of the config. config (Union[dict, list, str]): The config dictionary. + timeout (float): The maximum time to wait for the response. - Returns: - bool: True if the config was set successfuly, False otherwise. + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred. """ - raise NotImplementedError("set_config is not implemented yet.") + complete_state = CompleteState() + complete_state.set_configs({name: config}) + + request = Request(request_type="update_state") + request.set_complete_state(complete_state) + request.add_mask(f"{CONFIGS_PREFIX}.{name}") + + try: + response = self._send_request(request, timeout) + except TimeoutError as e: + self.logger.error("%s", e) + raise e + + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error("Error while trying to add the config: %s", + content) + raise AnkaiosException(f"Received error: {content}") + if content_type == "update_state_success": + self.logger.info("Update successful") + return + raise AnkaiosException("Received unexpected content type.") - def get_configs(self) -> dict: + def get_configs(self, + timeout: float = DEFAULT_TIMEOUT) -> dict: """ Get the configs. The keys will be the names. Returns: dict: The configs dictionary. """ - raise NotImplementedError("get_configs is not implemented yet.") + return self.get_state( + timeout, field_masks=[CONFIGS_PREFIX]).get_configs() - def get_config(self, name: str) -> dict: + def get_config(self, name: str, + timeout: float = DEFAULT_TIMEOUT) -> dict: """ Get the config with the provided name. @@ -636,28 +697,70 @@ def get_config(self, name: str) -> dict: Returns: dict: The config in a dict format. """ - raise NotImplementedError("get_config is not implemented yet.") + return self.get_state( + timeout, field_masks=[f"{CONFIGS_PREFIX}.{name}"]).get_configs() - def delete_all_configs(self) -> bool: + def delete_all_configs(self, timeout: float = DEFAULT_TIMEOUT): """ Delete all the configs. - Returns: - bool: if the configs were deleted successfuly. + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred. """ - raise NotImplementedError("delete_all_configs is not implemented yet.") + request = Request(request_type="update_state") + request.set_complete_state(CompleteState()) + request.add_mask(CONFIGS_PREFIX) + + try: + response = self._send_request(request, timeout) + except TimeoutError as e: + self.logger.error("%s", e) + raise e - def delete_config(self, name: str) -> bool: + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error("Error while trying to delete all configs: %s", + content) + raise AnkaiosException(f"Received error: {content}") + if content_type == "update_state_success": + self.logger.info("Update successful") + return + raise AnkaiosException("Received unexpected content type.") + + def delete_config(self, name: str, timeout: float = DEFAULT_TIMEOUT): """ Delete the config. Args: name (str): The name of the config. + timeout (float): The maximum time to wait for the response. - Returns: - bool: True if the config was deleted successfuly, False otherwise. + Raises: + TimeoutError: If the request timed out. + AnkaiosException: If an error occurred. """ - raise NotImplementedError("delete_config is not implemented yet.") + request = Request(request_type="update_state") + request.set_complete_state(CompleteState()) + request.add_mask(f"{CONFIGS_PREFIX}.{name}") + + try: + response = self._send_request(request, timeout) + except TimeoutError as e: + self.logger.error("%s", e) + raise e + + # Interpret response + (content_type, content) = response.get_content() + if content_type == "error": + self.logger.error("Error while trying to delete all configs: %s", + content) + raise AnkaiosException(f"Received error: {content}") + if content_type == "update_state_success": + self.logger.info("Update successful") + return + raise AnkaiosException("Received unexpected content type.") def get_state(self, timeout: float = DEFAULT_TIMEOUT, field_masks: list[str] = None) -> CompleteState: diff --git a/tests/test_ankaios.py b/tests/test_ankaios.py index 25bd00a..cb80bf0 100644 --- a/tests/test_ankaios.py +++ b/tests/test_ankaios.py @@ -488,29 +488,203 @@ def test_delete_workload(): ankaios.logger.error.assert_called() -def test_configs(): +def test_update_configs(): """ - Test the configs methods of the Ankaios class. + Test the update configs method of the Ankaios class. """ ankaios = generate_test_ankaios() + ankaios.logger = MagicMock() + + # Test success + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios.update_configs({"name": "config"}) + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() + + # Test error + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + with pytest.raises(AnkaiosException): + ankaios.update_configs({"name": "config"}) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + with pytest.raises(TimeoutError): + ankaios.update_configs({"name": "config"}) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() - with pytest.raises(NotImplementedError, match="not implemented yet"): - ankaios.set_configs(configs={'name': 'config'}) + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.update_configs({"name": "config"}) + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() - with pytest.raises(NotImplementedError, match="not implemented yet"): - ankaios.set_config(name="config_test", config={'config_test': 'value'}) - with pytest.raises(NotImplementedError, match="not implemented yet"): +def test_add_config(): + """ + Test the add config method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + ankaios.logger = MagicMock() + + # Test success + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios.add_config("name", "config") + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() + + # Test error + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + with pytest.raises(AnkaiosException): + ankaios.add_config("name", "config") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + with pytest.raises(TimeoutError): + ankaios.add_config("name", "config") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.add_config("name", "config") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + +def test_get_configs(): + """ + Test the get configs method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_configs") \ + as mock_state_get_configs: + mock_get_state.return_value = CompleteState() ankaios.get_configs() + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, field_masks=['desiredState.configs'] + ) + mock_state_get_configs.assert_called_once() + + +def test_get_config(): + """ + Test the get config method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + + with patch("ankaios_sdk.Ankaios.get_state") as mock_get_state, \ + patch("ankaios_sdk.CompleteState.get_configs") \ + as mock_state_get_configs: + mock_get_state.return_value = CompleteState() + ankaios.get_config("config_name") + mock_get_state.assert_called_once_with( + Ankaios.DEFAULT_TIMEOUT, + field_masks=['desiredState.configs.config_name'] + ) + mock_state_get_configs.assert_called_once() - with pytest.raises(NotImplementedError, match="not implemented yet"): - ankaios.get_config(name="config_test") - with pytest.raises(NotImplementedError, match="not implemented yet"): +def test_delete_all_configs(): + """ + Test the delete all configs method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + ankaios.logger = MagicMock() + + # Test success + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) ankaios.delete_all_configs() + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() - with pytest.raises(NotImplementedError, match="not implemented yet"): - ankaios.delete_config(name="config_test") + # Test error + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + with pytest.raises(AnkaiosException): + ankaios.delete_all_configs() + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + with pytest.raises(TimeoutError): + ankaios.delete_all_configs() + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.delete_all_configs() + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + +def test_delete_config(): + """ + Test the delete config method of the Ankaios class. + """ + ankaios = generate_test_ankaios() + ankaios.logger = MagicMock() + + # Test success + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_UPDATE_SUCCESS) + ankaios.delete_config("config_name") + mock_send_request.assert_called_once() + ankaios.logger.info.assert_called() + + # Test error + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = Response(MESSAGE_BUFFER_ERROR) + with pytest.raises(AnkaiosException): + ankaios.delete_config("config_name") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test timeout + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.side_effect = TimeoutError() + with pytest.raises(TimeoutError): + ankaios.delete_config("config_name") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() + + # Test invalid content type + with patch("ankaios_sdk.Ankaios._send_request") as mock_send_request: + mock_send_request.return_value = \ + Response(MESSAGE_BUFFER_COMPLETE_STATE) + with pytest.raises(AnkaiosException): + ankaios.delete_config("config_name") + mock_send_request.assert_called_once() + ankaios.logger.error.assert_called() def test_get_state(): diff --git a/tests/test_complete_state.py b/tests/test_complete_state.py index dce8ecf..931db5f 100644 --- a/tests/test_complete_state.py +++ b/tests/test_complete_state.py @@ -220,7 +220,11 @@ def test_to_dict(): ] }] }, - 'configs': {} + 'configs': { + 'array': 'config_2', + 'dict': 'config_3', + 'str': 'config_1' + } } }, 'configs': { diff --git a/tests/workload/test_workload.py b/tests/workload/test_workload.py index 6d888c6..76377cd 100644 --- a/tests/workload/test_workload.py +++ b/tests/workload/test_workload.py @@ -280,7 +280,7 @@ def test_to_proto(workload: Workload): # pylint: disable=redefined-outer-name ]) -def test__proto(): +def test_proto(): """ Test converting the workload to and from a proto. """ @@ -289,44 +289,13 @@ def test__proto(): assert workload_new._to_proto() == WORKLOAD_PROTO -def test_from_dict(workload: Workload): # pylint: disable=redefined-outer-name - """ - Test creating a Workload instance from a dictionary. - - Args: - workload (Workload): The Workload fixture. - """ - workload_dict = { - "name": "workload_test", - "agent": "agent_Test", - "runtime": "runtime_test", - "restartPolicy": "NEVER", - "runtimeConfig": "config_test", - "dependencies": {"workload_test_other": "ADD_COND_RUNNING"}, - "tags": [ - {"key": "key1", "value": "value1"}, - {"key": "key2", "value": "value2"}, - ], - "controlInterfaceAccess": { - "allowRules": [{ - "type": "StateRule", - "operation": "Write", - "filterMask": [f"{WORKLOADS_PREFIX}.another_workload"] - }], - "denyRules": [{ - "type": "StateRule", - "operation": "Read", - "filterMask": ["workloadStates.agent_Test.another_workload"] - }] - }, - "configs": { - "alias_test": "config1" - } - } +def test_from_to_dict(): + """Test converting a Workload instance to and from a dictionary.""" + workload_new = generate_test_workload() - new_workload = Workload._from_dict("workload_test", workload_dict) - assert new_workload is not None - assert str(workload) == str(new_workload) + workload_other = Workload._from_dict( + workload_new.name, workload_new.to_dict()) + assert str(workload_new) == str(workload_other) @pytest.mark.parametrize("function_name, data, mask", [ From c6a666baa7efb943f4e1010be999ec267d0416ff Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 4 Nov 2024 11:56:48 +0200 Subject: [PATCH 70/72] Fix manifest check --- ankaios_sdk/_components/manifest.py | 2 -- tests/test_manifest.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index 4e1ff44..f6fd6dc 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -126,8 +126,6 @@ def check(self) -> None: """ if "apiVersion" not in self._manifest.keys(): raise InvalidManifestException("apiVersion is missing.") - if "workloads" not in self._manifest.keys(): - raise InvalidManifestException("workloads is missing.") wl_allowed_keys = ["runtime", "agent", "restartPolicy", "runtimeConfig", "dependencies", "tags", "controlInterfaceAccess", "configs"] diff --git a/tests/test_manifest.py b/tests/test_manifest.py index cbe2f5d..5fb65d1 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -111,10 +111,6 @@ def test_check(): match="apiVersion is missing."): _ = Manifest({}) - with pytest.raises(InvalidManifestException, - match="workloads is missing."): - _ = Manifest({'apiVersion': 'v0.1'}) - with pytest.raises(InvalidManifestException, match="Mandatory key"): _ = Manifest({'apiVersion': 'v0.1', 'workloads': From c0ad5911dbdf20a3fff6f7f422f2b9134049f879 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Mon, 4 Nov 2024 12:27:47 +0200 Subject: [PATCH 71/72] Fix manifest with configs --- ankaios_sdk/_components/complete_state.py | 11 ++++--- ankaios_sdk/_components/manifest.py | 38 +++++++++++++---------- tests/test_manifest.py | 11 +++++++ 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 2526af9..191ea81 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -230,11 +230,12 @@ def from_manifest(manifest: Manifest) -> 'CompleteState': dict_state.get("apiVersion", state.get_api_version()) ) state._workloads = [] - for workload_name, workload_dict in \ - dict_state.get("workloads").items(): - state._workloads.append( - Workload._from_dict(workload_name, workload_dict) - ) + if dict_state.get("workloads") is not None: + for workload_name, workload_dict in \ + dict_state.get("workloads").items(): + state._workloads.append( + Workload._from_dict(workload_name, workload_dict) + ) if dict_state.get("configs") is not None: state.set_configs(dict_state.get("configs")) return state diff --git a/ankaios_sdk/_components/manifest.py b/ankaios_sdk/_components/manifest.py index f6fd6dc..78ed1c2 100644 --- a/ankaios_sdk/_components/manifest.py +++ b/ankaios_sdk/_components/manifest.py @@ -126,21 +126,23 @@ def check(self) -> None: """ if "apiVersion" not in self._manifest.keys(): raise InvalidManifestException("apiVersion is missing.") - wl_allowed_keys = ["runtime", "agent", "restartPolicy", - "runtimeConfig", "dependencies", "tags", - "controlInterfaceAccess", "configs"] - wl_mandatory_keys = ["runtime", "runtimeConfig", "agent"] - for wl_name in self._manifest["workloads"]: - # Check allowed keys - for key in self._manifest["workloads"][wl_name].keys(): - if key not in wl_allowed_keys: - raise InvalidManifestException( - f"Invalid key in workload {wl_name}: {key}") - # Check mandatory keys - for key in wl_mandatory_keys: - if key not in self._manifest["workloads"][wl_name].keys(): - raise InvalidManifestException( - f"Mandatory key {key} missing in workload {wl_name}") + if "workloads" in self._manifest.keys(): + wl_allowed_keys = ["runtime", "agent", "restartPolicy", + "runtimeConfig", "dependencies", "tags", + "controlInterfaceAccess", "configs"] + wl_mandatory_keys = ["runtime", "runtimeConfig", "agent"] + for wl_name in self._manifest["workloads"]: + # Check allowed keys + for key in self._manifest["workloads"][wl_name].keys(): + if key not in wl_allowed_keys: + raise InvalidManifestException( + f"Invalid key in workload {wl_name}: {key}") + # Check mandatory keys + for key in wl_mandatory_keys: + if key not in self._manifest["workloads"][wl_name].keys(): + raise InvalidManifestException( + f"Mandatory key {key} " + f"missing in workload {wl_name}") def _calculate_masks(self) -> list[str]: """ @@ -150,8 +152,10 @@ def _calculate_masks(self) -> list[str]: Returns: list[str]: A list of masks. """ - masks = [f"{WORKLOADS_PREFIX}.{key}" - for key in self._manifest["workloads"].keys()] + masks = [] + if "workloads" in self._manifest.keys(): + masks.extend([f"{WORKLOADS_PREFIX}.{key}" + for key in self._manifest["workloads"].keys()]) if "configs" in self._manifest.keys(): masks.extend([f"{CONFIGS_PREFIX}.{key}" for key in self._manifest["configs"].keys()]) diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 5fb65d1..fe1e2a2 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -141,3 +141,14 @@ def test_calculate_masks(): f"{WORKLOADS_PREFIX}.nginx_test_other", f"{CONFIGS_PREFIX}.test_ports" ] + + +def test_manifest_only_configs(): + """ + Test the manifest with only configs. + """ + manifest_dict = MANIFEST_DICT.copy() + manifest_dict.pop("workloads") + manifest = Manifest(manifest_dict) + assert len(manifest._calculate_masks()) == 1 + assert manifest._calculate_masks() == [f"{CONFIGS_PREFIX}.test_ports"] From fd1feeac7db1eb9f476465564d73dbfef18aded2 Mon Sep 17 00:00:00 2001 From: Tomuta Gabriel Date: Tue, 5 Nov 2024 13:04:59 +0200 Subject: [PATCH 72/72] Fix findings --- README.md | 2 +- ankaios_sdk/_components/complete_state.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b0bcff4..64b475c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ workload = Workload.builder() \ # Run the workload ret = ankaios.apply_workload(workload) -# Check if the workload is scheduled and get the WorkloadInstanceName +# Get the WorkloadInstanceName to check later if the workload is running if ret is not None: workload_instance_name = ret["added_workloads"][0] diff --git a/ankaios_sdk/_components/complete_state.py b/ankaios_sdk/_components/complete_state.py index 191ea81..c1523b1 100644 --- a/ankaios_sdk/_components/complete_state.py +++ b/ankaios_sdk/_components/complete_state.py @@ -233,7 +233,7 @@ def from_manifest(manifest: Manifest) -> 'CompleteState': if dict_state.get("workloads") is not None: for workload_name, workload_dict in \ dict_state.get("workloads").items(): - state._workloads.append( + state.add_workload( Workload._from_dict(workload_name, workload_dict) ) if dict_state.get("configs") is not None: @@ -278,9 +278,6 @@ def _to_proto(self) -> _ank_base.CompleteState: _ank_base.CompleteState: The protobuf message representing the complete state. """ - for workload in self._workloads: - self._complete_state.desiredState.workloads.\ - workloads[workload.name].CopyFrom(workload._to_proto()) return self._complete_state def _from_proto(self, proto: _ank_base.CompleteState) -> None: