diff --git a/src/deadline_worker_agent/aws_credentials/aws_configs.py b/src/deadline_worker_agent/aws_credentials/aws_configs.py index a9f3cd5d..585ca554 100644 --- a/src/deadline_worker_agent/aws_credentials/aws_configs.py +++ b/src/deadline_worker_agent/aws_credentials/aws_configs.py @@ -3,7 +3,8 @@ from __future__ import annotations import stat - +import os +import subprocess import logging from abc import ABC, abstractmethod from configparser import ConfigParser @@ -28,8 +29,20 @@ def _run_cmd_as(*, user: PosixSessionUser, cmd: list[str]) -> None: def _setup_parent_dir(*, dir_path: Path, owner: SessionUser | None = None) -> None: if owner is None: - create_perms: int = stat.S_IRWXU - dir_path.mkdir(mode=create_perms, exist_ok=True) + if os.name == "posix": + create_perms: int = stat.S_IRWXU + dir_path.mkdir(mode=create_perms, exist_ok=True) + else: + dir_path.mkdir(exist_ok=True) + subprocess.run( + [ + "icacls", + dir_path, + "/inheritance:r", + "/grant", + ("{0}:(OI)(CI)(F)").format(os.getlogin()), + ] + ) else: assert isinstance(owner, PosixSessionUser) _run_cmd_as(user=owner, cmd=["mkdir", "-p", str(dir_path)]) diff --git a/src/deadline_worker_agent/aws_credentials/queue_boto3_session.py b/src/deadline_worker_agent/aws_credentials/queue_boto3_session.py index e80eb79d..7439b740 100644 --- a/src/deadline_worker_agent/aws_credentials/queue_boto3_session.py +++ b/src/deadline_worker_agent/aws_credentials/queue_boto3_session.py @@ -91,8 +91,6 @@ def __init__( interrupt_event: Event, worker_persistence_dir: Path, ) -> None: - if os.name != "posix": - raise NotImplementedError("Windows not supported.") super().__init__() self._deadline_client = deadline_client @@ -110,7 +108,11 @@ def __init__( self._credentials_filename = ( "aws_credentials" # note: .json extension added by JSONFileCache ) - self._credentials_process_script_path = self._credential_dir / "get_aws_credentials.sh" + + if os.name == "posix": + self._credentials_process_script_path = self._credential_dir / "get_aws_credentials.sh" + else: + self._credentials_process_script_path = self._credential_dir / "get_aws_credentials.cmd" self._aws_config = AWSConfig(self._os_user) self._aws_credentials = AWSCredentials(self._os_user) @@ -321,9 +323,14 @@ def _generate_credential_process_script(self) -> str: Generates the bash script which generates the credentials as JSON output on STDOUT. This script will be used by the installed credential process. """ - return ("#!/bin/bash\nset -eu\ncat {0}\n").format( - (self._credential_dir / self._credentials_filename).with_suffix(".json") - ) + if os.name == "posix": + return ("#!/bin/bash\nset -eu\ncat {0}\n").format( + (self._credential_dir / self._credentials_filename).with_suffix(".json") + ) + else: + return ("@echo off\ntype {0}\n").format( + (self._credential_dir / self._credentials_filename).with_suffix(".json") + ) def _uninstall_credential_process(self) -> None: """ diff --git a/src/deadline_worker_agent/aws_credentials/worker_boto3_session.py b/src/deadline_worker_agent/aws_credentials/worker_boto3_session.py index df452785..35f6b2f0 100644 --- a/src/deadline_worker_agent/aws_credentials/worker_boto3_session.py +++ b/src/deadline_worker_agent/aws_credentials/worker_boto3_session.py @@ -1,7 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. from __future__ import annotations -import os + import logging from typing import Any, cast @@ -34,8 +34,6 @@ def __init__( config: Configuration, worker_id: str, ) -> None: - if os.name != "posix": - raise NotImplementedError("Windows not supported.") super().__init__() self._bootstrap_session = bootstrap_session diff --git a/src/deadline_worker_agent/log_sync/cloudwatch.py b/src/deadline_worker_agent/log_sync/cloudwatch.py index c1499604..9a0babc3 100644 --- a/src/deadline_worker_agent/log_sync/cloudwatch.py +++ b/src/deadline_worker_agent/log_sync/cloudwatch.py @@ -151,6 +151,7 @@ def _validate_log_event_can_be_added(self, event: PartitionedCloudWatchLogEvent) raise CloudWatchLogEventRejectedException(batch_full=True, reason=None) now = datetime.now() + # datetime expects timestamp in seconds, convert log event timestamp which is in milliseconds log_event_time = datetime.fromtimestamp(event.log_event["timestamp"] / 1000) diff --git a/src/deadline_worker_agent/scheduler/scheduler.py b/src/deadline_worker_agent/scheduler/scheduler.py index 5a2b4761..91b5eaff 100644 --- a/src/deadline_worker_agent/scheduler/scheduler.py +++ b/src/deadline_worker_agent/scheduler/scheduler.py @@ -18,6 +18,7 @@ import logging import os import stat +import subprocess from openjd.sessions import ActionState, ActionStatus, SessionUser from openjd.sessions import LOG as OPENJD_SESSION_LOG @@ -636,7 +637,19 @@ def _create_new_sessions( if self._worker_logs_dir: queue_log_dir = self._queue_log_dir_path(queue_id=session_spec["queueId"]) try: - queue_log_dir.mkdir(mode=stat.S_IRWXU, exist_ok=True) + if os.name == "posix": + queue_log_dir.mkdir(mode=stat.S_IRWXU, exist_ok=True) + else: + queue_log_dir.mkdir(exist_ok=True) + subprocess.run( + [ + "icacls", + queue_log_dir.name, + "/inheritance:r", + "/grant", + ("{0}:(OI)(CI)(F)").format(os.getlogin()), + ] + ) except OSError: error_msg = ( f"Failed to create local session log directory on worker: {queue_log_dir}" diff --git a/src/deadline_worker_agent/sessions/job_entities/job_details.py b/src/deadline_worker_agent/sessions/job_entities/job_details.py index b0992c36..8c44a2bf 100644 --- a/src/deadline_worker_agent/sessions/job_entities/job_details.py +++ b/src/deadline_worker_agent/sessions/job_entities/job_details.py @@ -99,13 +99,13 @@ def jobs_runs_as_api_model_to_worker_agent( if not jobs_run_as_data: return None - if os.name == "posix": - jobs_run_as_posix = jobs_run_as_data.get("posix", {}) - user = jobs_run_as_posix.get("user", "") - group = jobs_run_as_posix.get("group", "") - if not (user and group): - return None + jobs_run_as_posix = jobs_run_as_data.get("posix", {}) + user = jobs_run_as_posix.get("user", "") + group = jobs_run_as_posix.get("group", "") + if not (user and group): + return None + if os.name == "posix": jobs_run_as = JobsRunAs( posix=PosixSessionUser(user=user, group=group), ) diff --git a/src/deadline_worker_agent/startup/config.py b/src/deadline_worker_agent/startup/config.py index 124e23c4..1501f27c 100644 --- a/src/deadline_worker_agent/startup/config.py +++ b/src/deadline_worker_agent/startup/config.py @@ -1,7 +1,9 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + from __future__ import annotations +import os import logging as _logging from dataclasses import dataclass from pathlib import Path @@ -119,7 +121,7 @@ def __init__( settings = WorkerSettings(**settings_kwargs) - if settings.posix_job_user is not None: + if os.name == "posix" and settings.posix_job_user is not None: user, group = self._get_user_and_group_from_posix_job_user(settings.posix_job_user) self.impersonation = ImpersonationOverrides( inactive=not settings.impersonation, diff --git a/src/deadline_worker_agent/startup/config_file.py b/src/deadline_worker_agent/startup/config_file.py index 3ca74a39..18c7bbe1 100644 --- a/src/deadline_worker_agent/startup/config_file.py +++ b/src/deadline_worker_agent/startup/config_file.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Any, Optional import sys +import os from pydantic import BaseModel, BaseSettings, Field @@ -20,6 +21,7 @@ DEFAULT_CONFIG_PATH: dict[str, Path] = { "darwin": Path("/etc/amazon/deadline/worker.toml"), "linux": Path("/etc/amazon/deadline/worker.toml"), + "win32": Path(os.path.expandvars(r"%LOCALAPPDATA%/Amazon/Deadline/Config/worker.toml")), } diff --git a/src/deadline_worker_agent/startup/entrypoint.py b/src/deadline_worker_agent/startup/entrypoint.py index 79335d9c..41570682 100644 --- a/src/deadline_worker_agent/startup/entrypoint.py +++ b/src/deadline_worker_agent/startup/entrypoint.py @@ -44,9 +44,12 @@ def detect_system_capabilities() -> Capabilities: "linux": "linux", "windows": "windows", } + platform_machine = platform.machine().lower() + python_machine_to_openjd_cpu_arch = {"x86_64": "x86_64", "amd64": "x86_64"} if openjd_os_family := python_system_to_openjd_os_family.get(platform_system): attributes[AttributeCapabilityName("attr.worker.os.family")] = [openjd_os_family] - attributes[AttributeCapabilityName("attr.worker.cpu.arch")] = [platform.machine()] + if openjd_cpu_arch := python_machine_to_openjd_cpu_arch.get(platform_machine): + attributes[AttributeCapabilityName("attr.worker.cpu.arch")] = [openjd_cpu_arch] amounts[AmountCapabilityName("amount.worker.vcpu")] = float(psutil.cpu_count()) amounts[AmountCapabilityName("amount.worker.memory")] = float(psutil.virtual_memory().total) / ( 1024.0**2 diff --git a/src/deadline_worker_agent/startup/settings.py b/src/deadline_worker_agent/startup/settings.py index 98b5087b..7561baf8 100644 --- a/src/deadline_worker_agent/startup/settings.py +++ b/src/deadline_worker_agent/startup/settings.py @@ -11,14 +11,20 @@ from .capabilities import Capabilities from .config_file import ConfigFile +import os + # Default path for the worker's logs. DEFAULT_POSIX_WORKER_LOGS_DIR = Path("/var/log/amazon/deadline") +DEFAULT_WINDOWS_WORKER_LOGS_DIR = Path(os.path.expandvars(r"%LOCALAPPDATA%/Amazon/Deadline/Logs")) # Default path for the worker persistence directory. # The persistence directory is expected to be located on a file-system that is local to the Worker # Node. The Worker's ID and credentials are persisted and these should not be accessible by other # Worker Nodes. DEFAULT_POSIX_WORKER_PERSISTENCE_DIR = Path("/var/lib/deadline") +DEFAULT_WINDOWS_WORKER_PERSISTENCE_DIR = Path( + os.path.expandvars(r"%LOCALAPPDATA%/Amazon/Deadline/Cache") +) class WorkerSettings(BaseSettings): @@ -80,8 +86,14 @@ class WorkerSettings(BaseSettings): capabilities: Capabilities = Field( default_factory=lambda: Capabilities(amounts={}, attributes={}) ) - worker_logs_dir: Path = DEFAULT_POSIX_WORKER_LOGS_DIR - worker_persistence_dir: Path = DEFAULT_POSIX_WORKER_PERSISTENCE_DIR + worker_logs_dir: Path = ( + DEFAULT_WINDOWS_WORKER_LOGS_DIR if os.name == "nt" else DEFAULT_POSIX_WORKER_LOGS_DIR + ) + worker_persistence_dir: Path = ( + DEFAULT_WINDOWS_WORKER_PERSISTENCE_DIR + if os.name == "nt" + else DEFAULT_POSIX_WORKER_PERSISTENCE_DIR + ) local_session_logs: bool = True class Config: diff --git a/src/deadline_worker_agent/worker.py b/src/deadline_worker_agent/worker.py index acfd3b32..ed1d65a1 100644 --- a/src/deadline_worker_agent/worker.py +++ b/src/deadline_worker_agent/worker.py @@ -4,6 +4,7 @@ import json import signal +import os import sys import traceback from concurrent.futures import Executor, Future, ThreadPoolExecutor, wait @@ -108,8 +109,10 @@ def __init__( signal.signal(signal.SIGTERM, self._signal_handler) signal.signal(signal.SIGINT, self._signal_handler) - # TODO: Remove this once WA is stable or put behind a debug flag - signal.signal(signal.SIGUSR1, self._output_thread_stacks) + + if os.name == "posix": + # TODO: Remove this once WA is stable or put behind a debug flag + signal.signal(signal.SIGUSR1, self._output_thread_stacks) def _signal_handler(self, signum: int, frame: FrameType | None = None) -> None: """ @@ -156,7 +159,7 @@ def id(self) -> str: @property def sessions(self) -> WorkerSessionCollection: - raise NotImplementedError("Worker.sessions property not implemeneted") + raise NotImplementedError("Worker.sessions property not implemented") def run(self) -> None: """Runs the main Worker loop for processing sessions.""" @@ -371,6 +374,7 @@ def _get_spot_instance_shutdown_action_timeout(self, *, imdsv2_token: str) -> ti ) return None logger.info(f"Spot {action} happening at {shutdown_time}") + print(f"Spot {action} happening at {shutdown_time}") # Spot gives the time in UTC with a trailing Z, but Python can't handle # the Z so we strip it shutdown_time = datetime.fromisoformat(shutdown_time[:-1]).astimezone(timezone.utc) diff --git a/test/unit/aws_credentials/test_aws_configs.py b/test/unit/aws_credentials/test_aws_configs.py index 2323fa55..36eda9f9 100644 --- a/test/unit/aws_credentials/test_aws_configs.py +++ b/test/unit/aws_credentials/test_aws_configs.py @@ -14,6 +14,7 @@ _setup_parent_dir, ) from openjd.sessions import PosixSessionUser, SessionUser +import os @pytest.fixture @@ -27,9 +28,17 @@ def mock_run_cmd_as() -> Generator[MagicMock, None, None]: yield mock_run_cmd_as -@pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"), None)) -def os_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: - return request.param +if os.name == "posix": + + @pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"), None)) + def os_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: + return request.param + +else: + + @pytest.fixture() + def os_user() -> Optional[SessionUser]: + return None class TestSetupParentDir: @@ -63,7 +72,6 @@ def test_creates_dir( mock_run_cmd_as.assert_any_call(user=os_user, cmd=["chmod", "770", str(dir_path)]) else: mkdir.assert_called_once_with( - mode=0o700, exist_ok=True, ) diff --git a/test/unit/aws_credentials/test_queue_boto3_session.py b/test/unit/aws_credentials/test_queue_boto3_session.py index 8748efad..b0a1de15 100644 --- a/test/unit/aws_credentials/test_queue_boto3_session.py +++ b/test/unit/aws_credentials/test_queue_boto3_session.py @@ -51,9 +51,17 @@ def deadline_client() -> MagicMock: return MagicMock() -@pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"), None)) -def os_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: - return request.param +if os.name == "posix": + + @pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"), None)) + def os_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: + return request.param + +else: + + @pytest.fixture() + def os_user() -> Optional[SessionUser]: + return None class TestInit: @@ -614,9 +622,14 @@ def test_success( session._install_credential_process() # THEN - credentials_process_script_path = ( - Path(tmpdir) / "queues" / queue_id / "get_aws_credentials.sh" - ) + if os.name == "posix": + credentials_process_script_path = ( + Path(tmpdir) / "queues" / queue_id / "get_aws_credentials.sh" + ) + else: + credentials_process_script_path = ( + Path(tmpdir) / "queues" / queue_id / "get_aws_credentials.cmd" + ) mock_os_open.assert_called_once_with( path=str(credentials_process_script_path), flags=os.O_WRONLY | os.O_CREAT | os.O_TRUNC, diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 92f6f601..9af67650 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -51,16 +51,34 @@ def logs_client() -> MagicMock: return MagicMock() -@pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"),)) -def posix_job_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: - return request.param +if os.name == "posix": + @pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"),)) + def posix_job_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: + return request.param -@pytest.fixture(params=(False,)) -def impersonation( - request: pytest.FixtureRequest, posix_job_user: Optional[SessionUser] -) -> ImpersonationOverrides: - return ImpersonationOverrides(inactive=request.param, posix_job_user=posix_job_user) +else: + + @pytest.fixture() + def os_user() -> Optional[SessionUser]: + return None + + +if os.name == "posix": + + @pytest.fixture(params=(False,)) + def impersonation( + request: pytest.FixtureRequest, posix_job_user: Optional[SessionUser] + ) -> ImpersonationOverrides: + return ImpersonationOverrides(inactive=request.param, posix_job_user=posix_job_user) + +else: + + @pytest.fixture(params=(True,)) + def impersonation( + request: pytest.FixtureRequest, + ) -> ImpersonationOverrides: + return ImpersonationOverrides(inactive=request.param) @pytest.fixture @@ -242,7 +260,7 @@ def jobs_run_as() -> JobsRunAs | None: """The OS user/group associated with the job's queue""" # TODO: windows support if os.name != "posix": - raise NotImplementedError(f"{os.name} is not supported") + return None return JobsRunAs(posix=PosixSessionUser(user="job-user", group="job-user")) diff --git a/test/unit/install/test_install.py b/test/unit/install/test_install.py index c1bd78b0..10f896f2 100644 --- a/test/unit/install/test_install.py +++ b/test/unit/install/test_install.py @@ -273,4 +273,4 @@ def test_unsupported_platform_raises(platform: str, capsys: pytest.CaptureFixtur assert raise_ctx.value.code == 1 capture = capsys.readouterr() - assert capture.out == f"ERROR: Unsupported platform {platform}{os.linesep}" + assert capture.out == f"ERROR: Unsupported platform {platform}\n" diff --git a/test/unit/log_sync/test_cloudwatch.py b/test/unit/log_sync/test_cloudwatch.py index 9740ed01..6290edb1 100644 --- a/test/unit/log_sync/test_cloudwatch.py +++ b/test/unit/log_sync/test_cloudwatch.py @@ -36,7 +36,7 @@ def mock_module_logger() -> Generator[MagicMock, None, None]: class TestCloudWatchLogEventBatch: @fixture(autouse=True) def now(self) -> datetime: - return datetime.fromtimestamp(123) + return datetime(2000, 1, 1) @fixture(autouse=True) def datetime_mock(self, now: datetime) -> Generator[MagicMock, None, None]: @@ -51,7 +51,7 @@ def datetime_mock(self, now: datetime) -> Generator[MagicMock, None, None]: @fixture def event(self, now: datetime) -> PartitionedCloudWatchLogEvent: return PartitionedCloudWatchLogEvent( - log_event=CloudWatchLogEvent(timestamp=int(now.timestamp()), message="abc"), + log_event=CloudWatchLogEvent(timestamp=int(now.timestamp() * 1000), message="abc"), size=len("abc".encode("utf-8")), ) @@ -168,10 +168,12 @@ def test_valid_log_event( datetime_mock: MagicMock, ): # GIVEN - now = datetime.fromtimestamp(1) + now = datetime(2000, 1, 1) datetime_mock.now.return_value = now event = PartitionedCloudWatchLogEvent( - log_event=CloudWatchLogEvent(message="abc", timestamp=int(now.timestamp())), + log_event=CloudWatchLogEvent( + message="abc", timestamp=(int(now.timestamp()) * 1000) + ), size=3, ) batch = CloudWatchLogEventBatch() diff --git a/test/unit/scheduler/test_scheduler.py b/test/unit/scheduler/test_scheduler.py index cd54898f..83a45940 100644 --- a/test/unit/scheduler/test_scheduler.py +++ b/test/unit/scheduler/test_scheduler.py @@ -11,6 +11,7 @@ from openjd.sessions import ActionState, ActionStatus from botocore.exceptions import ClientError import pytest +import os from deadline_worker_agent.api_models import ( AssignedSession, @@ -588,7 +589,10 @@ def test_local_logging( # THEN mock_queue_log_dir.assert_called_once_with(queue_id=queue_id) - queue_log_dir_path.mkdir.assert_called_once_with(mode=0o700, exist_ok=True) + if os.name == "posix": + queue_log_dir_path.mkdir.assert_called_once_with(mode=0o700, exist_ok=True) + else: + queue_log_dir_path.mkdir.assert_called_once_with(exist_ok=True) mock_queue_session_log_file_path.assert_called_once_with( session_id=session_id, queue_log_dir=queue_log_dir_path ) @@ -672,7 +676,10 @@ def test_local_logging_os_error( # THEN mock_queue_log_dir.assert_called_once_with(queue_id=queue_id) - queue_log_dir_path.mkdir.assert_called_once_with(mode=0o700, exist_ok=True) + if os.name == "posix": + queue_log_dir_path.mkdir.assert_called_once_with(mode=0o700, exist_ok=True) + else: + queue_log_dir_path.mkdir.assert_called_once_with(exist_ok=True) if mkdir_side_effect: mock_queue_session_log_file_path.assert_not_called() else: diff --git a/test/unit/scheduler/test_session_cleanup.py b/test/unit/scheduler/test_session_cleanup.py index b37da445..1e554e27 100644 --- a/test/unit/scheduler/test_session_cleanup.py +++ b/test/unit/scheduler/test_session_cleanup.py @@ -8,6 +8,7 @@ from openjd.sessions import SessionUser, PosixSessionUser import pytest +import os from deadline_worker_agent.scheduler.session_cleanup import ( SessionUserCleanupManager, @@ -20,6 +21,11 @@ def __init__(self, user: str): self.user = user +class WindowsSessionUser(SessionUser): + def __init__(self, user: str): + self.user = user + + class TestSessionUserCleanupManager: @pytest.fixture def manager(self) -> SessionUserCleanupManager: @@ -33,9 +39,17 @@ def user_session_map_lock_mock( with patch.object(manager, "_user_session_map_lock") as mock: yield mock - @pytest.fixture - def os_user(self) -> PosixSessionUser: - return PosixSessionUser(user="user", group="group") + if os.name == "posix": + + @pytest.fixture + def os_user(self) -> SessionUser: + return PosixSessionUser(user="user", group="group") + + else: + + @pytest.fixture + def os_user(self) -> SessionUser: + return WindowsSessionUser(user="user") @pytest.fixture def session(self, os_user: PosixSessionUser) -> MagicMock: @@ -45,11 +59,12 @@ def session(self, os_user: PosixSessionUser) -> MagicMock: return session_stub class TestRegister: + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_registers_session( self, manager: SessionUserCleanupManager, session: MagicMock, - os_user: PosixSessionUser, + os_user: SessionUser, user_session_map_lock_mock: MagicMock, ): # WHEN @@ -99,11 +114,12 @@ def test_register_raises_windows_not_supported( user_session_map_lock_mock.__exit__.assert_not_called() class TestDeregister: + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_deregisters_session( self, manager: SessionUserCleanupManager, session: MagicMock, - os_user: PosixSessionUser, + os_user: SessionUser, user_session_map_lock_mock: MagicMock, ): # GIVEN @@ -119,11 +135,12 @@ def test_deregisters_session( user_session_map_lock_mock.__enter__.assert_called_once() user_session_map_lock_mock.__exit__.assert_called_once() + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_deregister_skipped_no_user( self, manager: SessionUserCleanupManager, session: MagicMock, - os_user: PosixSessionUser, + os_user: SessionUser, user_session_map_lock_mock: MagicMock, ): # GIVEN @@ -166,9 +183,10 @@ def cleanup_session_user_processes_mock(self) -> Generator[MagicMock, None, None with patch.object(SessionUserCleanupManager, "cleanup_session_user_processes") as mock: yield mock + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_calls_cleanup_session_user_processes( self, - os_user: PosixSessionUser, + os_user: SessionUser, manager: SessionUserCleanupManager, cleanup_session_user_processes_mock: MagicMock, ): @@ -178,9 +196,10 @@ def test_calls_cleanup_session_user_processes( # THEN cleanup_session_user_processes_mock.assert_called_once_with(os_user) + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_skips_cleanup_when_configured_to( self, - os_user: PosixSessionUser, + os_user: SessionUser, cleanup_session_user_processes_mock: MagicMock, ): # GIVEN @@ -193,17 +212,28 @@ def test_skips_cleanup_when_configured_to( cleanup_session_user_processes_mock.assert_not_called() class TestCleanupSessionUserProcesses: - @pytest.fixture - def agent_user( - self, - os_user: PosixSessionUser, - ) -> PosixSessionUser: - return PosixSessionUser(user=f"agent_{os_user.user}", group=f"agent_{os_user.group}") + if os.name == "posix": + @pytest.fixture + def agent_user( + self, + os_user: PosixSessionUser, + ) -> PosixSessionUser: + return PosixSessionUser( + user=f"agent_{os_user.user}", group=f"agent_{os_user.group}" + ) + + else: + + @pytest.fixture + def agent_user(self) -> SessionUser: + return WindowsSessionUser(user="user") + + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") @pytest.fixture(autouse=True) def subprocess_check_output_mock( self, - agent_user: PosixSessionUser, + agent_user: SessionUser, ) -> Generator[MagicMock, None, None]: with patch.object( session_cleanup_mod.subprocess, @@ -212,14 +242,16 @@ def subprocess_check_output_mock( ) as mock: yield mock + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") @pytest.fixture(autouse=True) def subprocess_run_mock(self) -> Generator[MagicMock, None, None]: with patch.object(session_cleanup_mod.subprocess, "run") as mock: yield mock + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_cleans_up_processes( self, - os_user: PosixSessionUser, + os_user: SessionUser, subprocess_run_mock: MagicMock, caplog: pytest.LogCaptureFixture, ): @@ -258,6 +290,7 @@ def test_not_posix_user( assert str(raised_err.value) == "Windows not supported" subprocess_run_mock.assert_not_called() + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_no_processes_to_clean_up( self, os_user: PosixSessionUser, @@ -278,9 +311,10 @@ def test_no_processes_to_clean_up( in caplog.text ) + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_fails_to_clean_up_processes( self, - os_user: PosixSessionUser, + os_user: SessionUser, subprocess_run_mock: MagicMock, caplog: pytest.LogCaptureFixture, ): @@ -298,6 +332,7 @@ def test_fails_to_clean_up_processes( assert f"Failed to stop processes running as '{os_user.user}': {err}" in caplog.text assert raised_err.value is err + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_skips_if_session_user_is_agent_user( self, subprocess_run_mock: MagicMock, diff --git a/test/unit/sessions/test_job_entities.py b/test/unit/sessions/test_job_entities.py index 04b37297..83385e61 100644 --- a/test/unit/sessions/test_job_entities.py +++ b/test/unit/sessions/test_job_entities.py @@ -18,6 +18,7 @@ import pytest +import os from deadline_worker_agent.api_models import ( Attachments, @@ -175,6 +176,7 @@ def test_has_path_mapping_rules( assert job_details.path_mapping_rules not in (None, []) assert len(job_details.path_mapping_rules) == len(path_mapping_rules) + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_jobs_run_as(self) -> None: """Ensures that if we receive a jobs_run_as field in the response, that the created entity has a (Posix) SessionUser created with the diff --git a/test/unit/sessions/test_session.py b/test/unit/sessions/test_session.py index e270ce6f..1ba56a48 100644 --- a/test/unit/sessions/test_session.py +++ b/test/unit/sessions/test_session.py @@ -10,6 +10,8 @@ from unittest.mock import patch, MagicMock, ANY import pytest +import os + from openjd.model.v2023_09 import ( Action, Environment, @@ -50,10 +52,17 @@ ) import deadline_worker_agent.sessions.session as session_mod +if os.name == "posix": -@pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"),)) -def os_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: - return request.param + @pytest.fixture(params=(PosixSessionUser(user="some-user", group="some-group"),)) + def os_user(request: pytest.FixtureRequest) -> Optional[SessionUser]: + return request.param + +else: + + @pytest.fixture() + def os_user() -> Optional[SessionUser]: + return None @pytest.fixture @@ -485,6 +494,7 @@ def mock_asset_sync(self, session: Session) -> Generator[MagicMock, None, None]: # This overrides the asset_loading_method fixture in tests/unit/conftest.py which feeds into # the job_attachment_details fixture @pytest.mark.parametrize("asset_loading_method", [e.value for e in AssetLoadingMethod]) + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") def test_asset_loading_method( self, session: Session, diff --git a/test/unit/startup/test_config.py b/test/unit/startup/test_config.py index cd3a775d..78a76004 100644 --- a/test/unit/startup/test_config.py +++ b/test/unit/startup/test_config.py @@ -9,6 +9,7 @@ from typing import Any, Generator, List, Optional import logging import pytest +import os from openjd.sessions import PosixSessionUser @@ -264,8 +265,18 @@ def test_uses_parsed_cleanup_session_user_processes( @pytest.mark.parametrize( argnames="worker_logs_dir", argvalues=( - Path("/foo"), - Path("/bar"), + pytest.param( + Path("/foo"), marks=pytest.mark.skipif(os.name != "posix", reason="Not posix") + ), + pytest.param( + Path("/bar"), marks=pytest.mark.skipif(os.name != "posix", reason="Not posix") + ), + pytest.param( + Path("D:\\foo"), marks=pytest.mark.skipif(os.name != "nt", reason="Not windows") + ), + pytest.param( + Path("D:\\bar"), marks=pytest.mark.skipif(os.name != "nt", reason="Not windows") + ), ), ) def test_uses_worker_logs_dir( @@ -287,8 +298,18 @@ def test_uses_worker_logs_dir( @pytest.mark.parametrize( argnames="persistence_dir", argvalues=( - Path("/foo"), - Path("/bar"), + pytest.param( + Path("/foo"), marks=pytest.mark.skipif(os.name != "posix", reason="Not posix") + ), + pytest.param( + Path("/bar"), marks=pytest.mark.skipif(os.name != "posix", reason="Not posix") + ), + pytest.param( + Path("D:\\foo"), marks=pytest.mark.skipif(os.name != "nt", reason="Not windows") + ), + pytest.param( + Path("D:\\bar"), marks=pytest.mark.skipif(os.name != "nt", reason="Not windows") + ), ), ) def test_uses_worker_persistence_dir( @@ -596,6 +617,7 @@ def test_impersonation_passed_to_settings_initializer( else: assert "impersonation" not in call.kwargs + @pytest.mark.skipif(os.name != "posix", reason="Posix-only test.") @pytest.mark.parametrize( argnames="posix_job_user", argvalues=("user:group", None), @@ -774,8 +796,9 @@ def test_local_session_logs_passed_to_settings_initializer( argvalues=( pytest.param( "user:group", - PosixSessionUser(group="group", user="user"), + lambda: PosixSessionUser(group="group", user="user"), id="has-posix-job-user-setting", + marks=pytest.mark.skipif(os.name != "posix", reason="Posix-only test."), ), pytest.param( None, diff --git a/test/unit/startup/test_settings.py b/test/unit/startup/test_settings.py index 3bff5847..f7c9e8f0 100644 --- a/test/unit/startup/test_settings.py +++ b/test/unit/startup/test_settings.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, Mock, patch from typing import Any, Generator, NamedTuple, Type import pytest +import os from pathlib import Path from pydantic import ConstrainedStr @@ -105,14 +106,18 @@ class FieldTestCaseParams(NamedTuple): field_name="worker_logs_dir", expected_type=Path, expected_required=False, - expected_default=Path("/var/log/amazon/deadline"), + expected_default=Path("/var/log/amazon/deadline") + if os.name == "posix" + else Path(os.path.expandvars(r"%LOCALAPPDATA%/Amazon/Deadline/Logs")), expected_default_factory_return_value=None, ), FieldTestCaseParams( field_name="worker_persistence_dir", expected_type=Path, expected_required=False, - expected_default=Path("/var/lib/deadline"), + expected_default=Path("/var/lib/deadline") + if os.name == "posix" + else Path(os.path.expandvars(r"%LOCALAPPDATA%/Amazon/Deadline/Cache")), expected_default_factory_return_value=None, ), FieldTestCaseParams( diff --git a/test/unit/test_worker.py b/test/unit/test_worker.py index a9a43441..65d413c6 100644 --- a/test/unit/test_worker.py +++ b/test/unit/test_worker.py @@ -9,6 +9,7 @@ from pathlib import Path import pytest +import os from deadline_worker_agent import Worker from deadline_worker_agent.errors import ServiceShutdown @@ -452,7 +453,10 @@ def test_spot_shutdown_in_past( fake_token = "TOKEN_FAKE_VALUE" response_mock = MagicMock() timeout_delta = timedelta(seconds=-10) - timeout = datetime.utcnow() + timeout_delta + if os.name == "posix": + timeout = datetime.utcnow() + timeout_delta + else: + timeout = datetime.now() + timeout_delta # See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html # noqa: E501 response_mock.status_code = 200 response_mock.text = f'{{ "action": "terminate", "time": "{timeout.isoformat()}Z" }}'