diff --git a/docs/dictionary.txt b/docs/dictionary.txt index 76adebcb..4a822588 100644 --- a/docs/dictionary.txt +++ b/docs/dictionary.txt @@ -35,4 +35,6 @@ Kubernetes yaml absolutized moveit -replan \ No newline at end of file +replan +effector +mnt diff --git a/docs/libraries.rst b/docs/libraries.rst index 16bda4c7..3be0bc3d 100644 --- a/docs/libraries.rst +++ b/docs/libraries.rst @@ -10,6 +10,8 @@ Beside ``osc.standard`` provided by OpenSCENARIO 2 (which we divide into ``osc.s * - Name - Description + * - ``osc.docker`` + - Docker Library (provided with :repo_link:`libs/scenario_execution_docker`) * - ``osc.gazebo`` - Gazebo Library (provided with :repo_link:`libs/scenario_execution_gazebo`) * - ``osc.helpers`` @@ -32,6 +34,163 @@ Beside ``osc.standard`` provided by OpenSCENARIO 2 (which we divide into ``osc.s Additional features can be implemented by defining your own library. +Docker +------ + +The library contains actions to interact with `Docker `_. Import it with ``import osc.docker``. It's provided by the package :repo_link:`libs/scenario_execution_docker`. + +``docker_run()`` +^^^^^^^^^^^^^^^^ + +Runs a Docker container + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``image`` + - ``string`` + - + - The image to run + * - ``command`` + - ``string`` + - + - The command to run in the container + * - ``container_name`` + - ``string`` + - + - The name for this container + * - ``detach`` + - ``bool`` + - false + - Whether to run container in the background + * - ``environment`` + - ``list of string`` + - + - Environment variables to set inside the container, i.e., a list of strings in the format ["SOMEVARIABLE=xxx"]. + * - ``network`` + - ``string`` + - + - Name of the network this container will be connected to at creation time + * - ``privileged`` + - ``bool`` + - false + - Give extended privileges to this container + * - ``remove`` + - ``bool`` + - true + - Remove the container when it as finished running + * - ``stream`` + - ``bool`` + - true + - If true and detach is false, return a log generator instead of a string. Ignored if detach is true. + * - ``volumes`` + - ``list of string`` + - + - A list of strings which each one of its elements specifies a mount volume: ['/home/user1/:/mount/vol2','/home/user2/:/mount/vol1'] + +``docker_exec()`` +^^^^^^^^^^^^^^^^^ + +Runs a command inside a given Docker container + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``container`` + - ``string`` + - + - The name or id of the container to run the command in + * - ``container`` + - ``string`` + - + - The name or id of the container to run the command in + * - ``command`` + - ``string`` + - + - The command to run inside the container + * - ``environment`` + - ``list of string`` + - + - Environment variables to set inside the container, i.e., a list of strings in the format ["SOMEVARIABLE=xxx"]. + * - ``privileged`` + - ``bool`` + - false + - Give extended privileges to this container + * - ``user`` + - ``string`` + - root + - User to execute command as + * - ``workdir`` + - ``string`` + - + - Path to working directory for this exec session + +``docker_copy()`` +^^^^^^^^^^^^^^^^^ + +Copy a file or folder from the container. +Note that this actions potentially blocks other action calls if the copied content is large. +In case large files or folders need to be copied, consider mounting a volume to the container instead of this action. + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``container`` + - ``string`` + - + - The name or id of the container to run the command in + * - ``file_path`` + - ``string`` + - + - Path to the file or folder inside the container to retrieve + +``docker_put()`` +^^^^^^^^^^^^^^^^^ + +Copy a file or folder from the local system into a running container. +Note that this actions potentially blocks other action calls if the copied content is large. +In case large files or folders need to be copied, consider mounting a volume to the container instead of this action. + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``container`` + - ``string`` + - + - The name or id of the container to put the file or folder into + * - ``source_path`` + - ``string`` + - + - Path to the file or folder in the local system to copy + * - ``target_path`` + - ``string`` + - + - Target path inside the container to put the file or folder + Gazebo ------ @@ -1604,4 +1763,4 @@ Capture the screen content within a video. * - ``frame_rate`` - ``float`` - ``25.0`` - - Frame-rate of the resulting video \ No newline at end of file + - Frame-rate of the resulting video diff --git a/libs/scenario_execution_docker/MANIFEST.in b/libs/scenario_execution_docker/MANIFEST.in new file mode 100644 index 00000000..15f85d4a --- /dev/null +++ b/libs/scenario_execution_docker/MANIFEST.in @@ -0,0 +1 @@ +include scenario_execution_docker/lib_osc/*.osc diff --git a/libs/scenario_execution_docker/README.md b/libs/scenario_execution_docker/README.md new file mode 100644 index 00000000..432b872d --- /dev/null +++ b/libs/scenario_execution_docker/README.md @@ -0,0 +1,8 @@ +# Scenario Execution Library for Docker interaction + +The `scenario_execution_docker` package provides actions to interact with docker. + +It provides the following scenario execution library: + +- `docker.osc`: Actions to interact with docker + diff --git a/libs/scenario_execution_docker/package.xml b/libs/scenario_execution_docker/package.xml new file mode 100644 index 00000000..92bafcc9 --- /dev/null +++ b/libs/scenario_execution_docker/package.xml @@ -0,0 +1,23 @@ + + + + scenario_execution_docker + 1.2.0 + Scenario Execution library for docker interactions + Intel Labs + Intel Labs + Apache-2.0 + + scenario_execution + + python3-docker + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/libs/scenario_execution_docker/resource/scenario_execution_docker b/libs/scenario_execution_docker/resource/scenario_execution_docker new file mode 100644 index 00000000..e69de29b diff --git a/libs/scenario_execution_docker/scenario_execution_docker/__init__.py b/libs/scenario_execution_docker/scenario_execution_docker/__init__.py new file mode 100644 index 00000000..3ba13780 --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/libs/scenario_execution_docker/scenario_execution_docker/actions/__init__.py b/libs/scenario_execution_docker/scenario_execution_docker/actions/__init__.py new file mode 100644 index 00000000..3ba13780 --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/actions/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_copy.py b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_copy.py new file mode 100644 index 00000000..11b2c4d5 --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_copy.py @@ -0,0 +1,99 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +from enum import Enum + +import os +import docker +import tempfile +import tarfile +import py_trees +from scenario_execution.actions.base_action import BaseAction, ActionError + + +class CopyStatus(Enum): + IDLE = 1 + FOUND_CONTAINER = 2 + COPYING = 3 + DONE = 4 + + +class DockerCopy(BaseAction): + """ + Copy a file or folder from a running container + """ + + def __init__(self, container: str, file_path: str): + super().__init__() + self.container = container + self.file_path = file_path + + self.container_object = None + self.current_state = CopyStatus.IDLE + self.output_dir = None + self.client = None + self.result_data = None + + def setup(self, **kwargs): + # create docker client + self.client = docker.from_env() + + # check output_dir + if "output_dir" not in kwargs: + raise ActionError("output_dir not defined.", action=self) + + if kwargs['output_dir']: + if not os.path.exists(kwargs['output_dir']): + raise ActionError(f"Specified destination dir '{kwargs['output_dir']}' does not exist", action=self) + self.output_dir = kwargs['output_dir'] + + def update(self) -> py_trees.common.Status: + if self.current_state == CopyStatus.IDLE: + try: + self.container_object = self.client.containers.get(self.container) + self.current_state = CopyStatus.FOUND_CONTAINER + except docker.errors.APIError as e: + self.feedback_message = f"Docker container {self.container} not yet running {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.RUNNING + + if self.current_state == CopyStatus.FOUND_CONTAINER: + try: + self.result_data, _ = self.container_object.get_archive( + path=self.file_path) + self.current_state = CopyStatus.COPYING + self.feedback_message = f"Copying data from path {self.file_path} in container {self.container} to {self.output_dir}" # pylint: disable= attribute-defined-outside-init + except docker.errors.APIError as e: + self.feedback_message = f"Copying of data from path {self.file_path} failed: {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.FAILURE + + if self.current_state == CopyStatus.COPYING: + output_tar = tempfile.NamedTemporaryFile(suffix=".tar") + try: + with open(output_tar.name, 'wb') as f: + for chunk in self.result_data: + f.write(chunk) + with tarfile.open(output_tar.name, 'r') as tar: + tar.extractall(self.output_dir) + self.current_state = CopyStatus.DONE + except tarfile.ReadError as e: + self.feedback_message = f"Copying of data from path {self.file_path} failed: {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.FAILURE + + if self.current_state == CopyStatus.DONE: + self.feedback_message = f"Finished copying of data from path {self.file_path} to {self.output_dir}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.SUCCESS + + return py_trees.common.Status.RUNNING diff --git a/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_exec.py b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_exec.py new file mode 100644 index 00000000..9d9e257c --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_exec.py @@ -0,0 +1,105 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +from enum import Enum + +import docker +import py_trees +from scenario_execution.actions.base_action import BaseAction + + +class ExecutionStatus(Enum): + IDLE = 1 + FOUND_CONTAINER = 2 + EXECUTING = 3 + DONE = 4 + + +class DockerExec(BaseAction): + """ + Run a command inside a container + """ + + def __init__(self, container: str, command: str, + environment: list, privileged: bool, + user: str, workdir: str): + super().__init__() + self.container = container + self.command = command + self.environment = environment + self.privileged = privileged + self.user = user + self.workdir = workdir + + self.client = None + self.container_object = None + self.execution_instance = None + self.execution_output = None + self.current_state = ExecutionStatus.IDLE + + def setup(self, **kwargs): + # create docker client + self.client = docker.from_env() + + def update(self) -> py_trees.common.Status: + if self.current_state == ExecutionStatus.IDLE: + try: + self.container_object = self.client.containers.get(self.container) + self.current_state = ExecutionStatus.FOUND_CONTAINER + except docker.errors.APIError as e: + self.feedback_message = f"Docker container {self.container} not yet running {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.RUNNING + + if self.current_state == ExecutionStatus.FOUND_CONTAINER: + try: + self.execution_instance = self.client.api.exec_create( + self.container_object.id, + self.command, + environment=self.environment, + privileged=self.privileged, + user=self.user, + workdir=self.workdir) + + self.execution_output = self.client.api.exec_start( + self.execution_instance['Id'], + tty=False, + stream=True + ) + self.current_state = ExecutionStatus.EXECUTING + self.feedback_message = f"Executing '{self.command}' in container {self.container}" # pylint: disable= attribute-defined-outside-init + except docker.errors.APIError as e: + self.feedback_message = f"Docker exec of command '{self.command}' failed: {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.FAILURE + + if self.current_state == ExecutionStatus.EXECUTING: + try: + log = next(self.execution_output) + self.feedback_message = f"Executing '{self.command}' in container {self.container} with output: {log.decode()}" # pylint: disable= attribute-defined-outside-init + except StopIteration: + self.current_state = ExecutionStatus.DONE + + if self.current_state == ExecutionStatus.DONE: + exit_metadata = self.client.api.exec_inspect(self.execution_instance['Id']) + assert not exit_metadata['Running'] + exit_code = exit_metadata['ExitCode'] + if exit_code == 0: + self.feedback_message = f"Finished execution of '{self.command}' in container {self.container}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.SUCCESS + else: + self.feedback_message = f"Execution of '{self.command}' in container {self.container} failed" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.FAILURE + + return py_trees.common.Status.RUNNING diff --git a/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_put.py b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_put.py new file mode 100644 index 00000000..8ea96dd9 --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_put.py @@ -0,0 +1,95 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +from enum import Enum + +import os +import docker +import tempfile +import tarfile +import py_trees +from scenario_execution.actions.base_action import BaseAction, ActionError + + +class CopyStatus(Enum): + IDLE = 1 + FOUND_CONTAINER = 2 + COPYING = 3 + DONE = 4 + + +class DockerPut(BaseAction): + """ + Copy a file or folder from the local filesystem into a running container + """ + + def __init__(self, container: str, source_path: str, target_path: str): + super().__init__() + self.container = container + self.source_path = source_path + self.target_path = target_path + + self.container_object = None + self.current_state = CopyStatus.IDLE + self.client = None + self.tar = None + + def setup(self, **kwargs): + # create docker client + self.client = docker.from_env() + + # check if source path exists + if not os.path.exists(self.source_path): + raise ActionError(f"The given source path {self.source_path} does not exist", action=self) + + def update(self) -> py_trees.common.Status: + if self.current_state == CopyStatus.IDLE: + try: + self.container_object = self.client.containers.get(self.container) + self.current_state = CopyStatus.FOUND_CONTAINER + except docker.errors.APIError as e: + self.feedback_message = f"Docker container {self.container} not yet running {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.RUNNING + + if self.current_state == CopyStatus.FOUND_CONTAINER: + self.tar = tempfile.NamedTemporaryFile(suffix=".tar") + try: + with tarfile.open(self.tar.name, 'w:') as tar: + tar.add( + self.source_path, + arcname=os.path.basename(self.source_path)) + self.current_state = CopyStatus.COPYING + except tarfile.ReadError as e: + self.feedback_message = f"Compressing data to a tar file from path {self.source_path} failed: {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.FAILURE + + if self.current_state == CopyStatus.COPYING: + success = self.container_object.put_archive( + path=self.target_path, + data=self.tar + ) + if success: + self.current_state = CopyStatus.DONE + self.feedback_message = f"Copying data from path {self.source_path} to {self.target_path} inside container {self.container}" # pylint: disable= attribute-defined-outside-init + else: + self.feedback_message = f"Copying data from path {self.source_path} to {self.target_path} inside container {self.container} failed: {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.FAILURE + + if self.current_state == CopyStatus.DONE: + self.feedback_message = f"Finished copying data from path {self.source_path} to {self.target_path} inside container {self.container}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.SUCCESS + + return py_trees.common.Status.RUNNING diff --git a/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_run.py b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_run.py new file mode 100644 index 00000000..9350c780 --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/actions/docker_run.py @@ -0,0 +1,115 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import os +from enum import Enum + +import docker +import py_trees +from scenario_execution.actions.base_action import BaseAction, ActionError + + +class ContainerStatus(Enum): + IDLE = 1 + RUNNING = 2 + DONE = 3 + + +class DockerRun(BaseAction): + """ + Run a container + """ + + def __init__(self, image: str, command: str, container_name: str, + detach: bool, environment: list, network: str, + privileged: bool, remove: bool, stream: bool, volumes: list): + super().__init__() + self.image = image + self.command = command + self.container_name = container_name + self.detach = detach + self.environment = environment + self.network = network + self.privileged = privileged + self.remove = remove + self.stream = stream + self.volumes = volumes + + self.client = None + self.container = None + self.current_state = ContainerStatus.IDLE + + def setup(self, **kwargs): + # create docker client + self.client = docker.from_env() + # check docker image + filterred_images = self.client.images.list(filters={'reference': self.image}) + if len(filterred_images) == 0: + raise ActionError(f"Required docker image '{self.image}' does not exist.", action=self) + + def update(self) -> py_trees.common.Status: + if self.current_state == ContainerStatus.IDLE: + try: + self.container = self.client.containers.run( + self.image, + command=self.command, + detach=self.detach, + environment=self.environment, + name=self.container_name, + network=self.network, + privileged=self.privileged, + stream=self.stream, + remove=self.remove, + user=os.getuid(), + group_add=[os.getgid()], + volumes=self.volumes) + except docker.errors.APIError as e: + self.feedback_message = f"Docker run failed: {e}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.FAILURE + self.current_state = ContainerStatus.RUNNING + self.feedback_message = f"Running docker container {self.image}" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.RUNNING + + if self.current_state == ContainerStatus.RUNNING: + if self.stream and not self.detach: + try: + log = next(self.container) + self.feedback_message = f"Running container {self.image} with output: {log.decode()}" # pylint: disable= attribute-defined-outside-init + except StopIteration: + self.current_state = ContainerStatus.DONE + self.feedback_message = f"Docker container {self.image} finished cleanly" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.SUCCESS + elif self.detach: + self.container.reload() + if self.container: + res = self.container.status + self.feedback_message = f"Running container {self.image} with status {res}" # pylint: disable= attribute-defined-outside-init + if res in ["removing", "exited"]: + self.current_state = ContainerStatus.DONE + self.feedback_message = f"Docker container {self.image} finished cleanly" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.SUCCESS + + if self.current_state == ContainerStatus.DONE: + self.feedback_message = f"Docker container {self.image} finished cleanly" # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.SUCCESS + + return py_trees.common.Status.RUNNING + + def shutdown(self): + if self.container is None: + return + + self.container.stop(timeout=0) diff --git a/libs/scenario_execution_docker/scenario_execution_docker/get_osc_library.py b/libs/scenario_execution_docker/scenario_execution_docker/get_osc_library.py new file mode 100644 index 00000000..959aa95e --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/get_osc_library.py @@ -0,0 +1,19 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + + +def get_osc_library(): + return 'scenario_execution_docker', 'docker.osc' diff --git a/libs/scenario_execution_docker/scenario_execution_docker/lib_osc/docker.osc b/libs/scenario_execution_docker/scenario_execution_docker/lib_osc/docker.osc new file mode 100644 index 00000000..85ac1007 --- /dev/null +++ b/libs/scenario_execution_docker/scenario_execution_docker/lib_osc/docker.osc @@ -0,0 +1,32 @@ +action docker_run: + # run a docker container + image: string # the image to run + command: string # the command to run in the container + container_name: string # the name for this container + detach: bool = false # Run container in the background + environment: list of string # Environment variables to set inside the container, i.e., a list of strings in the format ["SOMEVARIABLE=xxx"]. + network: string # Name of the network this container will be connected to at creation time + privileged: bool = false # Give extended privileges to this container + remove: bool = true # Remove the container when it as finished running + stream: bool = true # If true and detach is false, return a log generator instead of a string. Ignored if detach is true. + volumes: list of string # A list of strings which each one of its elements specifies a mount volume: ['/home/user1/:/mnt/vol2','/home/user2:/mnt/vol1'] + +action docker_exec: + # Run a command inside a given container + container: string # the name or id of the container to run the command in + command: string # the command to run inside the container + environment: list of string # Environment variables to set inside the container, i.e., a list of strings in the format ["SOMEVARIABLE=xxx"]. + privileged: bool = false # Give extended privileges to this container + user: string = 'root' # User to execute command as. Default: root + workdir: string # Path to working directory for this exec session + +action docker_copy: + # Copy a file or folder from the container + container: string # the name or id of the container to get the file or folder from + file_path: string # Path to the file or folder to retrieve + +action docker_put: + # Copy a file or folder from the local system into a running container + container: string # the name or id of the container to put the file or folder into + source_path: string # Path to the file or folder in the local system to copy + target_path: string # Target path inside the container to put the file or folder diff --git a/libs/scenario_execution_docker/scenarios/test_docker_copy.osc b/libs/scenario_execution_docker/scenarios/test_docker_copy.osc new file mode 100644 index 00000000..3fb0d7ff --- /dev/null +++ b/libs/scenario_execution_docker/scenarios/test_docker_copy.osc @@ -0,0 +1,13 @@ +import osc.docker +import osc.helpers + +scenario test_docker_copy: + timeout(25s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 20', detach: true, container_name: 'sleeping_beauty', remove: true) + serial: + docker_exec(container: 'sleeping_beauty', command: 'mkdir -p /tmp/test_dir/') + docker_exec(container: 'sleeping_beauty', command: 'touch /tmp/test_dir/test.txt') + docker_exec(container: 'sleeping_beauty', command: 'touch /tmp/test_dir/test_1.txt') + docker_copy(container: 'sleeping_beauty', file_path: '/tmp/test_dir/') + emit end \ No newline at end of file diff --git a/libs/scenario_execution_docker/scenarios/test_docker_exec.osc b/libs/scenario_execution_docker/scenarios/test_docker_exec.osc new file mode 100644 index 00000000..2386eecb --- /dev/null +++ b/libs/scenario_execution_docker/scenarios/test_docker_exec.osc @@ -0,0 +1,10 @@ +import osc.docker +import osc.helpers + +scenario test_docker_exec: + timeout(15s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 10', detach: true, container_name: 'sleeping_beauty', remove: true) + serial: + docker_exec(container: 'sleeping_beauty', command: 'echo hello world') + emit end \ No newline at end of file diff --git a/libs/scenario_execution_docker/scenarios/test_docker_put.osc b/libs/scenario_execution_docker/scenarios/test_docker_put.osc new file mode 100644 index 00000000..a4bc50ce --- /dev/null +++ b/libs/scenario_execution_docker/scenarios/test_docker_put.osc @@ -0,0 +1,12 @@ +import osc.docker +import osc.helpers + +# for this scenario to succeed, create a folder 'test_dir' in your /tmp folder with some dummy files in it +scenario test_docker_put: + timeout(10s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true, container_name: 'sleeping_beauty', remove: true) + serial: + docker_put(container: 'sleeping_beauty', source_path: '/tmp/test_dir/', target_path: '/tmp/') + docker_exec(container: 'sleeping_beauty', command: 'ls /tmp/ | grep test_dir') + emit end \ No newline at end of file diff --git a/libs/scenario_execution_docker/scenarios/test_docker_run.osc b/libs/scenario_execution_docker/scenarios/test_docker_run.osc new file mode 100644 index 00000000..1f10ac2e --- /dev/null +++ b/libs/scenario_execution_docker/scenarios/test_docker_run.osc @@ -0,0 +1,9 @@ +import osc.docker +import osc.helpers + +scenario test_docker_run: + timeout(15s) + do serial: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true, remove: true, container_name: 'sleeping_beauty') + docker_run(image: 'ubuntu', command: 'echo hello world') + emit end \ No newline at end of file diff --git a/libs/scenario_execution_docker/setup.cfg b/libs/scenario_execution_docker/setup.cfg new file mode 100644 index 00000000..66ac754b --- /dev/null +++ b/libs/scenario_execution_docker/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/scenario_execution_docker +[install] +install_scripts=$base/lib/scenario_execution_docker diff --git a/libs/scenario_execution_docker/setup.py b/libs/scenario_execution_docker/setup.py new file mode 100644 index 00000000..b46c4e63 --- /dev/null +++ b/libs/scenario_execution_docker/setup.py @@ -0,0 +1,51 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +""" Setup python package """ +from setuptools import find_namespace_packages, setup + +PACKAGE_NAME = 'scenario_execution_docker' + +setup( + name=PACKAGE_NAME, + version='1.2.0', + packages=find_namespace_packages(), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + PACKAGE_NAME]), + ('share/' + PACKAGE_NAME, ['package.xml']) + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='Intel Labs', + maintainer_email='scenario-execution@intel.com', + description='Scenario Execution library for Docker interactions', + license='Apache License 2.0', + tests_require=['pytest'], + include_package_data=True, + entry_points={ + 'scenario_execution.actions': [ + 'docker_run = scenario_execution_docker.actions.docker_run:DockerRun', + 'docker_exec = scenario_execution_docker.actions.docker_exec:DockerExec', + 'docker_copy = scenario_execution_docker.actions.docker_copy:DockerCopy', + 'docker_put = scenario_execution_docker.actions.docker_put:DockerPut', + ], + 'scenario_execution.osc_libraries': [ + 'docker = ' + 'scenario_execution_docker.get_osc_library:get_osc_library', + ] + }, +) diff --git a/test/scenario_execution_docker_test/README.md b/test/scenario_execution_docker_test/README.md new file mode 100644 index 00000000..024938d4 --- /dev/null +++ b/test/scenario_execution_docker_test/README.md @@ -0,0 +1,3 @@ +# Tests of Scenario Execution Library for Docker + +The `scenario_execution_docker_test` package tests functionality of `scenario_execution_docker`. diff --git a/test/scenario_execution_docker_test/package.xml b/test/scenario_execution_docker_test/package.xml new file mode 100644 index 00000000..e29c3e3a --- /dev/null +++ b/test/scenario_execution_docker_test/package.xml @@ -0,0 +1,23 @@ + + + + scenario_execution_docker_test + 1.2.0 + Tests for Scenario Execution library for Docker + Intel Labs + Intel Labs + Apache-2.0 + + scenario_execution_docker + scenario_execution_os + geometry_msgs + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/test/scenario_execution_docker_test/resource/scenario_execution_docker_test b/test/scenario_execution_docker_test/resource/scenario_execution_docker_test new file mode 100644 index 00000000..e69de29b diff --git a/test/scenario_execution_docker_test/setup.cfg b/test/scenario_execution_docker_test/setup.cfg new file mode 100644 index 00000000..84c2fa13 --- /dev/null +++ b/test/scenario_execution_docker_test/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/scenario_execution_docker_test +[install] +install_scripts=$base/lib/scenario_execution_docker_test diff --git a/test/scenario_execution_docker_test/setup.py b/test/scenario_execution_docker_test/setup.py new file mode 100644 index 00000000..967571ce --- /dev/null +++ b/test/scenario_execution_docker_test/setup.py @@ -0,0 +1,42 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +""" Setup python package """ +from glob import glob +import os +from setuptools import find_namespace_packages, setup + +PACKAGE_NAME = 'scenario_execution_docker_test' + +setup( + name=PACKAGE_NAME, + version='1.2.0', + packages=find_namespace_packages(), + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + PACKAGE_NAME]), + ('share/' + PACKAGE_NAME, ['package.xml']), + (os.path.join('share', PACKAGE_NAME, 'launch'), glob('launch/*launch.py')), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='Intel Labs', + maintainer_email='scenario-execution@intel.com', + description='Tests for Scenario Execution library for Docker', + license='Apache License 2.0', + tests_require=['pytest'], + entry_points={} +) diff --git a/test/scenario_execution_docker_test/test/test_docker_copy.py b/test/scenario_execution_docker_test/test/test_docker_copy.py new file mode 100644 index 00000000..e7d9b304 --- /dev/null +++ b/test/scenario_execution_docker_test/test/test_docker_copy.py @@ -0,0 +1,81 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import py_trees +import unittest +import tempfile +from scenario_execution import ScenarioExecution +from scenario_execution.model.osc2_parser import OpenScenario2Parser +from scenario_execution.model.model_to_py_tree import create_py_tree +from scenario_execution.utils.logging import Logger + +from antlr4.InputStream import InputStream + + +class TestDockerCopy(unittest.TestCase): + # pylint: disable=missing-function-docstring,missing-class-docstring + + def setUp(self) -> None: + self.parser = OpenScenario2Parser(Logger('test', False)) + self.tmp_dir = tempfile.TemporaryDirectory() + self.scenario_execution = ScenarioExecution(debug=False, log_model=False, live_tree=False, + scenario_file="test.osc", output_dir=self.tmp_dir.name) + self.tree = py_trees.composites.Sequence(name="", memory=True) + + def tearDown(self): + self.tmp_dir.cleanup() + + def parse(self, scenario_content): + parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content)) + model = self.parser.create_internal_model(parsed_tree, self.tree, "test.osc", False) + self.tree = create_py_tree(model, self.tree, self.parser.logger, False) + self.scenario_execution.tree = self.tree + self.scenario_execution.run() + + def test_success(self): + self.parse(""" +import osc.docker +import osc.helpers +import osc.os + +scenario test_success: + timeout(10) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true, container_name: 'sleeping_beauty_copy_success', remove: true) + serial: + docker_exec(container: 'sleeping_beauty_copy_success', command: 'mkdir -p /tmp/test_dir/') + docker_exec(container: 'sleeping_beauty_copy_success', command: 'touch /tmp/test_dir/test.txt') + docker_exec(container: 'sleeping_beauty_copy_success', command: 'touch /tmp/test_dir/test_1.txt') + docker_copy(container: 'sleeping_beauty_copy_success', file_path: '/tmp/test_dir/') + check_file_exists(file_name: '""" + self.tmp_dir.name + '/test_dir/test.txt' + """') + check_file_exists(file_name: '""" + self.tmp_dir.name + '/test_dir/test_1.txt' + """') + emit end +""") + self.assertTrue(self.scenario_execution.process_results()) + + def test_failure_missing_file(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test_fail: + timeout(10s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true, container_name: 'sleeping_beauty_copy_fail', remove: true) + serial: + docker_copy(container: 'sleeping_beauty_copy_fail', file_path: '/tmp/test_dir/') +""") + self.assertFalse(self.scenario_execution.process_results()) diff --git a/test/scenario_execution_docker_test/test/test_docker_exec.py b/test/scenario_execution_docker_test/test/test_docker_exec.py new file mode 100644 index 00000000..30675dc8 --- /dev/null +++ b/test/scenario_execution_docker_test/test/test_docker_exec.py @@ -0,0 +1,84 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import py_trees +import unittest +import tempfile +from scenario_execution import ScenarioExecution +from scenario_execution.model.osc2_parser import OpenScenario2Parser +from scenario_execution.model.model_to_py_tree import create_py_tree +from scenario_execution.utils.logging import Logger + +from antlr4.InputStream import InputStream + + +class TestDockerExec(unittest.TestCase): + # pylint: disable=missing-function-docstring,missing-class-docstring + + def setUp(self) -> None: + self.parser = OpenScenario2Parser(Logger('test', False)) + self.scenario_execution = ScenarioExecution(debug=False, log_model=False, live_tree=False, + scenario_file="test.osc", output_dir=None) + self.tree = py_trees.composites.Sequence(name="", memory=True) + self.tmp_file = tempfile.NamedTemporaryFile() + + def parse(self, scenario_content): + parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content)) + model = self.parser.create_internal_model(parsed_tree, self.tree, "test.osc", False) + self.tree = create_py_tree(model, self.tree, self.parser.logger, False) + self.scenario_execution.tree = self.tree + self.scenario_execution.run() + + def test_success(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test_success: + timeout(30s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 10', detach: true, container_name: 'sleeping_beauty_exec_success', remove: true) + serial: + docker_exec(container: 'sleeping_beauty_exec_success', command: 'echo hello world') + emit end +""") + self.assertTrue(self.scenario_execution.process_results()) + + def test_failure(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test_failure: + timeout(15s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 10', detach: true, container_name: 'sleeping_beauty_exec_fail', remove: true) + serial: + docker_exec(container: 'sleeping_beauty_exec_fail', command: 'ls UKNOWN_DIR') +""") + self.assertFalse(self.scenario_execution.process_results()) + + def test_failure_container_not_running(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test_failure_container_not_running: + timeout(3s) + do serial: + docker_exec(container: 'sleeping_beauty', command: 'echo hello world') +""") + self.assertFalse(self.scenario_execution.process_results()) diff --git a/test/scenario_execution_docker_test/test/test_docker_put.py b/test/scenario_execution_docker_test/test/test_docker_put.py new file mode 100644 index 00000000..93872120 --- /dev/null +++ b/test/scenario_execution_docker_test/test/test_docker_put.py @@ -0,0 +1,73 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import py_trees +import unittest +import tempfile +from scenario_execution import ScenarioExecution +from scenario_execution.model.osc2_parser import OpenScenario2Parser +from scenario_execution.model.model_to_py_tree import create_py_tree +from scenario_execution.utils.logging import Logger + +from antlr4.InputStream import InputStream + + +class TestDockerPut(unittest.TestCase): + # pylint: disable=missing-function-docstring,missing-class-docstring + + def setUp(self) -> None: + self.parser = OpenScenario2Parser(Logger('test', False)) + self.scenario_execution = ScenarioExecution(debug=False, log_model=False, live_tree=False, + scenario_file="test.osc", output_dir=None) + self.tree = py_trees.composites.Sequence(name="", memory=True) + self.tmp_file = tempfile.NamedTemporaryFile() + + def parse(self, scenario_content): + parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content)) + model = self.parser.create_internal_model(parsed_tree, self.tree, "test.osc", False) + self.tree = create_py_tree(model, self.tree, self.parser.logger, False) + self.scenario_execution.tree = self.tree + self.scenario_execution.run() + + def test_success(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test_success: + timeout(10s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true, container_name: 'sleeping_beauty_put', remove: true) + serial: + docker_put(container: 'sleeping_beauty_put', source_path: '""" + self.tmp_file.name + """', target_path: '/tmp/') + docker_exec(container: 'sleeping_beauty_put', command: 'test -f """ + self.tmp_file.name + """') + emit end +""") + self.assertTrue(self.scenario_execution.process_results()) + + def test_failure_missing_file(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test_failure_unknown_file: + timeout(10s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true, container_name: 'sleeping_beauty_put_fail', remove: true) + serial: + docker_put(container: 'sleeping_beauty_put_fail', source_path: 'UNKNOWN', target_path: '/tmp/') +""") + self.assertFalse(self.scenario_execution.process_results()) diff --git a/test/scenario_execution_docker_test/test/test_docker_run.py b/test/scenario_execution_docker_test/test/test_docker_run.py new file mode 100644 index 00000000..182818c3 --- /dev/null +++ b/test/scenario_execution_docker_test/test/test_docker_run.py @@ -0,0 +1,100 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import py_trees +import unittest +import tempfile +from scenario_execution import ScenarioExecution +from scenario_execution.model.osc2_parser import OpenScenario2Parser +from scenario_execution.model.model_to_py_tree import create_py_tree +from scenario_execution.utils.logging import Logger + +from antlr4.InputStream import InputStream + + +class TestDockerRun(unittest.TestCase): + # pylint: disable=missing-function-docstring,missing-class-docstring + + def setUp(self) -> None: + self.parser = OpenScenario2Parser(Logger('test', False)) + self.scenario_execution = ScenarioExecution(debug=False, log_model=False, live_tree=False, + scenario_file="test.osc", output_dir=None) + self.tree = py_trees.composites.Sequence(name="", memory=True) + self.tmp_dir = tempfile.TemporaryDirectory() + + def tearDown(self): + self.tmp_dir.cleanup() + + def parse(self, scenario_content): + parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content)) + model = self.parser.create_internal_model(parsed_tree, self.tree, "test.osc", False) + self.tree = create_py_tree(model, self.tree, self.parser.logger, False) + self.scenario_execution.tree = self.tree + self.scenario_execution.run() + + def test_success_stream(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test: + timeout(10s) + do serial: + docker_run(image: 'ubuntu', command: 'echo hello world') + emit end +""") + self.assertTrue(self.scenario_execution.process_results()) + + def test_success_detach(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test: + timeout(10s) + do serial: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true) + emit end +""") + self.assertTrue(self.scenario_execution.process_results()) + + def test_fail_unknown_command(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test: + timeout(10s) + do serial: + docker_run(image: 'ubuntu', command: 'UKNOWN', detach: true) + emit end +""") + self.assertFalse(self.scenario_execution.process_results()) + + def test_success_volume_mount(self): + self.parse(""" +import osc.docker +import osc.helpers + +scenario test: + timeout(10s) + do parallel: + docker_run(image: 'ubuntu', command: 'sleep 5', detach: true, volumes: ['""" + self.tmp_dir.name + """:/data'], container_name: 'sleeping_beauty_run_volume') + serial: + docker_exec(container: 'sleeping_beauty_run_volume', command: 'ls /data') + emit end +""") + self.assertTrue(self.scenario_execution.process_results())