|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8; -*- |
| 3 | + |
| 4 | +# Copyright (c) 2023 Oracle and/or its affiliates. |
| 5 | +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ |
| 6 | +import copy |
| 7 | +from unittest.mock import MagicMock, patch |
| 8 | +import oci |
| 9 | +import unittest |
| 10 | +import pytest |
| 11 | + |
| 12 | +from ads.jobs.ads_job import Job |
| 13 | +from ads.jobs.builders.infrastructure import DSCFileStorage, DataScienceJob |
| 14 | +from ads.jobs.builders.runtimes.python_runtime import PythonRuntime |
| 15 | + |
| 16 | +try: |
| 17 | + from oci.data_science.models import FileStorageMountConfigurationDetails |
| 18 | +except (ImportError, AttributeError) as e: |
| 19 | + raise unittest.SkipTest( |
| 20 | + "Support for mounting file systems to OCI Job is not available. Skipping the Job tests." |
| 21 | + ) |
| 22 | + |
| 23 | +dsc_job_payload = oci.data_science.models.Job( |
| 24 | + compartment_id="test_compartment_id", |
| 25 | + created_by="test_created_by", |
| 26 | + description="test_description", |
| 27 | + display_name="test_display_name", |
| 28 | + freeform_tags={"test_key": "test_value"}, |
| 29 | + id="test_id", |
| 30 | + job_configuration_details=oci.data_science.models.DefaultJobConfigurationDetails( |
| 31 | + **{ |
| 32 | + "command_line_arguments": [], |
| 33 | + "environment_variables": {"key": "value"}, |
| 34 | + "job_type": "DEFAULT", |
| 35 | + "maximum_runtime_in_minutes": 10, |
| 36 | + } |
| 37 | + ), |
| 38 | + job_log_configuration_details=oci.data_science.models.JobLogConfigurationDetails( |
| 39 | + **{ |
| 40 | + "enable_auto_log_creation": False, |
| 41 | + "enable_logging": True, |
| 42 | + "log_group_id": "test_log_group_id", |
| 43 | + "log_id": "test_log_id", |
| 44 | + }, |
| 45 | + ), |
| 46 | + job_storage_mount_configuration_details_list=[ |
| 47 | + { |
| 48 | + "destinationDirectoryName": "test_destination_directory_name_from_dsc", |
| 49 | + "exportId": "export_id_from_dsc", |
| 50 | + "mountTargetId": "mount_target_id_from_dsc", |
| 51 | + "storageType": "FILE_STORAGE", |
| 52 | + }, |
| 53 | + { |
| 54 | + "destinationDirectoryName": "test_destination_directory_name_from_dsc", |
| 55 | + "exportId": "export_id_from_dsc", |
| 56 | + "mountTargetId": "mount_target_id_from_dsc", |
| 57 | + "storageType": "FILE_STORAGE", |
| 58 | + }, |
| 59 | + ], |
| 60 | + lifecycle_details="ACTIVE", |
| 61 | + lifecycle_state="STATE", |
| 62 | + project_id="test_project_id", |
| 63 | +) |
| 64 | + |
| 65 | +job = ( |
| 66 | + Job(name="My Job") |
| 67 | + .with_infrastructure( |
| 68 | + DataScienceJob() |
| 69 | + .with_subnet_id("ocid1.subnet.oc1.iad.xxxx") |
| 70 | + .with_shape_name("VM.Standard.E3.Flex") |
| 71 | + .with_shape_config_details(memory_in_gbs=16, ocpus=1) |
| 72 | + .with_block_storage_size(50) |
| 73 | + .with_storage_mount( |
| 74 | + DSCFileStorage( |
| 75 | + destination_directory_name="test_mount_one", |
| 76 | + mount_target="test_mount_target_one", |
| 77 | + export_path="test_export_path_one", |
| 78 | + ), |
| 79 | + { |
| 80 | + "destination_directory_name": "test_mount_two", |
| 81 | + "mount_target": "test_mount_target_two", |
| 82 | + "export_path": "test_export_path_two", |
| 83 | + "storage_type": "FILE_STORAGE", |
| 84 | + }, |
| 85 | + ) |
| 86 | + ) |
| 87 | + .with_runtime( |
| 88 | + PythonRuntime() |
| 89 | + .with_service_conda("pytorch110_p38_cpu_v1") |
| 90 | + .with_source("custom_script.py") |
| 91 | + .with_environment_variable(NAME="Welcome to OCI Data Science.") |
| 92 | + ) |
| 93 | +) |
| 94 | + |
| 95 | +job_yaml_string = """ |
| 96 | +kind: job |
| 97 | +spec: |
| 98 | + infrastructure: |
| 99 | + kind: infrastructure |
| 100 | + spec: |
| 101 | + blockStorageSize: 50 |
| 102 | + jobType: DEFAULT |
| 103 | + shapeConfigDetails: |
| 104 | + memoryInGBs: 16 |
| 105 | + ocpus: 1 |
| 106 | + shapeName: VM.Standard.E3.Flex |
| 107 | + storageMount: |
| 108 | + - destinationDirectoryName: test_mount_one |
| 109 | + mountTarget: test_mount_target_one |
| 110 | + exportPath: test_export_path_one |
| 111 | + storageType: FILE_STORAGE |
| 112 | + - destinationDirectoryName: test_mount_two |
| 113 | + mountTarget: test_mount_target_two |
| 114 | + exportPath: test_export_path_two |
| 115 | + storageType: FILE_STORAGE |
| 116 | + subnetId: ocid1.subnet.oc1.iad.xxxx |
| 117 | + type: dataScienceJob |
| 118 | + name: My Job |
| 119 | + runtime: |
| 120 | + kind: runtime |
| 121 | + spec: |
| 122 | + conda: |
| 123 | + slug: pytorch110_p38_cpu_v1 |
| 124 | + type: service |
| 125 | + env: |
| 126 | + - name: NAME |
| 127 | + value: Welcome to OCI Data Science. |
| 128 | + scriptPathURI: custom_script.py |
| 129 | + type: python |
| 130 | +""" |
| 131 | + |
| 132 | + |
| 133 | +class TestDataScienceJobMountFileSystem(unittest.TestCase): |
| 134 | + def test_data_science_job_initialize(self): |
| 135 | + assert isinstance(job.infrastructure.storage_mount, list) |
| 136 | + dsc_file_storage_one = job.infrastructure.storage_mount[0] |
| 137 | + assert isinstance(dsc_file_storage_one, DSCFileStorage) |
| 138 | + assert dsc_file_storage_one.storage_type == "FILE_STORAGE" |
| 139 | + assert dsc_file_storage_one.destination_directory_name == "test_mount_one" |
| 140 | + assert dsc_file_storage_one.mount_target == "test_mount_target_one" |
| 141 | + assert dsc_file_storage_one.export_path == "test_export_path_one" |
| 142 | + |
| 143 | + dsc_file_storage_two = job.infrastructure.storage_mount[1] |
| 144 | + assert isinstance(dsc_file_storage_two, DSCFileStorage) |
| 145 | + assert dsc_file_storage_two.storage_type == "FILE_STORAGE" |
| 146 | + assert dsc_file_storage_two.destination_directory_name == "test_mount_two" |
| 147 | + assert dsc_file_storage_two.mount_target == "test_mount_target_two" |
| 148 | + assert dsc_file_storage_two.export_path == "test_export_path_two" |
| 149 | + |
| 150 | + def test_data_science_job_from_yaml(self): |
| 151 | + job_from_yaml = Job.from_yaml(job_yaml_string) |
| 152 | + |
| 153 | + assert isinstance(job_from_yaml.infrastructure.storage_mount, list) |
| 154 | + dsc_file_storage_one = job_from_yaml.infrastructure.storage_mount[0] |
| 155 | + assert isinstance(dsc_file_storage_one, DSCFileStorage) |
| 156 | + assert dsc_file_storage_one.storage_type == "FILE_STORAGE" |
| 157 | + assert dsc_file_storage_one.destination_directory_name == "test_mount_one" |
| 158 | + assert dsc_file_storage_one.mount_target == "test_mount_target_one" |
| 159 | + assert dsc_file_storage_one.export_path == "test_export_path_one" |
| 160 | + |
| 161 | + dsc_file_storage_two = job.infrastructure.storage_mount[1] |
| 162 | + assert isinstance(dsc_file_storage_two, DSCFileStorage) |
| 163 | + assert dsc_file_storage_two.storage_type == "FILE_STORAGE" |
| 164 | + assert dsc_file_storage_two.destination_directory_name == "test_mount_two" |
| 165 | + assert dsc_file_storage_two.mount_target == "test_mount_target_two" |
| 166 | + assert dsc_file_storage_two.export_path == "test_export_path_two" |
| 167 | + |
| 168 | + def test_data_science_job_to_dict(self): |
| 169 | + assert job.to_dict() == { |
| 170 | + "kind": "job", |
| 171 | + "spec": { |
| 172 | + "name": "My Job", |
| 173 | + "runtime": { |
| 174 | + "kind": "runtime", |
| 175 | + "type": "python", |
| 176 | + "spec": { |
| 177 | + "conda": {"type": "service", "slug": "pytorch110_p38_cpu_v1"}, |
| 178 | + "scriptPathURI": "custom_script.py", |
| 179 | + "env": [ |
| 180 | + {"name": "NAME", "value": "Welcome to OCI Data Science."} |
| 181 | + ], |
| 182 | + }, |
| 183 | + }, |
| 184 | + "infrastructure": { |
| 185 | + "kind": "infrastructure", |
| 186 | + "type": "dataScienceJob", |
| 187 | + "spec": { |
| 188 | + "jobType": "DEFAULT", |
| 189 | + "subnetId": "ocid1.subnet.oc1.iad.xxxx", |
| 190 | + "shapeName": "VM.Standard.E3.Flex", |
| 191 | + "shapeConfigDetails": {"ocpus": 1, "memoryInGBs": 16}, |
| 192 | + "blockStorageSize": 50, |
| 193 | + "storageMount": [ |
| 194 | + { |
| 195 | + "destinationDirectoryName": "test_mount_one", |
| 196 | + "mountTarget": "test_mount_target_one", |
| 197 | + "exportPath": "test_export_path_one", |
| 198 | + "storageType": "FILE_STORAGE", |
| 199 | + }, |
| 200 | + { |
| 201 | + "destinationDirectoryName": "test_mount_two", |
| 202 | + "mountTarget": "test_mount_target_two", |
| 203 | + "exportPath": "test_export_path_two", |
| 204 | + "storageType": "FILE_STORAGE", |
| 205 | + }, |
| 206 | + ], |
| 207 | + }, |
| 208 | + }, |
| 209 | + }, |
| 210 | + } |
| 211 | + |
| 212 | + def test_mount_file_system_failed(self): |
| 213 | + with pytest.raises( |
| 214 | + ValueError, |
| 215 | + match="Either parameter `export_path` or `export_id` must be provided to mount file system.", |
| 216 | + ): |
| 217 | + DSCFileStorage( |
| 218 | + destination_directory_name="test_mount", |
| 219 | + mount_target_id="ocid1.mounttarget.oc1.iad.xxxx", |
| 220 | + ) |
| 221 | + |
| 222 | + with pytest.raises( |
| 223 | + ValueError, |
| 224 | + match="Either parameter `mount_target` or `mount_target_id` must be provided to mount file system.", |
| 225 | + ): |
| 226 | + DSCFileStorage( |
| 227 | + destination_directory_name="test_mount", |
| 228 | + export_id="ocid1.export.oc1.iad.xxxx", |
| 229 | + ) |
| 230 | + |
| 231 | + with pytest.raises( |
| 232 | + ValueError, |
| 233 | + match="Parameter `destination_directory_name` must be provided to mount file system.", |
| 234 | + ): |
| 235 | + DSCFileStorage( |
| 236 | + mount_target_id="ocid1.mounttarget.oc1.iad.xxxx", |
| 237 | + export_id="ocid1.export.oc1.iad.xxxx", |
| 238 | + ) |
| 239 | + |
| 240 | + job_copy = copy.deepcopy(job) |
| 241 | + dsc_file_storage = DSCFileStorage( |
| 242 | + destination_directory_name="test_mount", |
| 243 | + mount_target="test_mount_target", |
| 244 | + export_id="ocid1.export.oc1.iad.xxxx", |
| 245 | + ) |
| 246 | + storage_mount_list = [dsc_file_storage] * 6 |
| 247 | + with pytest.raises( |
| 248 | + ValueError, |
| 249 | + match="A maximum number of 5 file systems are allowed to be mounted at this time for a job.", |
| 250 | + ): |
| 251 | + job_copy.infrastructure.with_storage_mount(*storage_mount_list) |
| 252 | + |
| 253 | + job_copy = copy.deepcopy(job) |
| 254 | + with pytest.raises( |
| 255 | + ValueError, |
| 256 | + match="Parameter `storage_mount` should be a list of either DSCFileSystem instances or dictionaries.", |
| 257 | + ): |
| 258 | + job_copy.infrastructure.with_storage_mount(dsc_file_storage, [1, 2, 3]) |
| 259 | + |
| 260 | + job_copy = copy.deepcopy(job) |
| 261 | + with pytest.raises( |
| 262 | + ValueError, |
| 263 | + match="Parameter `storage_type` must be provided for each file system to be mounted.", |
| 264 | + ): |
| 265 | + job_copy.infrastructure.with_storage_mount( |
| 266 | + dsc_file_storage, |
| 267 | + { |
| 268 | + "destination_directory_name": "test_mount", |
| 269 | + "mount_target_id": "ocid1.mounttarget.oc1.iad.xxxx", |
| 270 | + "export_id": "ocid1.export.oc1.iad.xxxx", |
| 271 | + }, |
| 272 | + ) |
| 273 | + |
| 274 | + job_copy = copy.deepcopy(job) |
| 275 | + wrong_type = "WRONG_TYPE" |
| 276 | + wrong_file_system_dict = { |
| 277 | + "destination_directory_name": "test_mount", |
| 278 | + "mount_target_id": "ocid1.mounttarget.oc1.iad.xxxx", |
| 279 | + "export_id": "ocid1.export.oc1.iad.xxxx", |
| 280 | + "storage_type": wrong_type, |
| 281 | + } |
| 282 | + with pytest.raises( |
| 283 | + ValueError, match=f"Storage type {wrong_type} is not supprted." |
| 284 | + ): |
| 285 | + job_copy.infrastructure.with_storage_mount( |
| 286 | + dsc_file_storage, wrong_file_system_dict |
| 287 | + ) |
| 288 | + |
| 289 | + @patch.object(oci.file_storage.FileStorageClient, "get_export") |
| 290 | + @patch.object(oci.file_storage.FileStorageClient, "get_mount_target") |
| 291 | + def test_update_storage_mount_from_dsc_model( |
| 292 | + self, mock_get_mount_target, mock_get_export |
| 293 | + ): |
| 294 | + mount_target_mock = MagicMock() |
| 295 | + mount_target_mock.data = MagicMock() |
| 296 | + mount_target_mock.data.display_name = "mount_target_from_dsc" |
| 297 | + mock_get_mount_target.return_value = mount_target_mock |
| 298 | + |
| 299 | + export_mock = MagicMock() |
| 300 | + export_mock.data = MagicMock() |
| 301 | + export_mock.data.path = "export_path_from_dsc" |
| 302 | + mock_get_export.return_value = export_mock |
| 303 | + job_copy = copy.deepcopy(job) |
| 304 | + infrastructure = job_copy.infrastructure |
| 305 | + infrastructure._update_from_dsc_model(dsc_job_payload) |
| 306 | + |
| 307 | + assert len(infrastructure.storage_mount) == 2 |
| 308 | + assert isinstance(infrastructure.storage_mount[0], DSCFileStorage) |
| 309 | + assert isinstance(infrastructure.storage_mount[1], DSCFileStorage) |
| 310 | + assert infrastructure.storage_mount[0].to_dict() == { |
| 311 | + "destinationDirectoryName": "test_destination_directory_name_from_dsc", |
| 312 | + "exportId": "export_id_from_dsc", |
| 313 | + "exportPath": "export_path_from_dsc", |
| 314 | + "mountTarget": "mount_target_from_dsc", |
| 315 | + "mountTargetId": "mount_target_id_from_dsc", |
| 316 | + "storageType": "FILE_STORAGE", |
| 317 | + } |
| 318 | + assert infrastructure.storage_mount[1].to_dict() == { |
| 319 | + "destinationDirectoryName": "test_destination_directory_name_from_dsc", |
| 320 | + "exportId": "export_id_from_dsc", |
| 321 | + "exportPath": "export_path_from_dsc", |
| 322 | + "mountTarget": "mount_target_from_dsc", |
| 323 | + "mountTargetId": "mount_target_id_from_dsc", |
| 324 | + "storageType": "FILE_STORAGE", |
| 325 | + } |
| 326 | + |
| 327 | + @patch.object(oci.file_storage.FileStorageClient, "list_exports") |
| 328 | + @patch.object(oci.file_storage.FileStorageClient, "list_mount_targets") |
| 329 | + @patch.object(oci.identity.IdentityClient, "list_availability_domains") |
| 330 | + def test_update_job_infra( |
| 331 | + self, mock_list_availability_domains, mock_list_mount_targets, mock_list_exports |
| 332 | + ): |
| 333 | + job_copy = copy.deepcopy(job) |
| 334 | + dsc_job_payload_copy = copy.deepcopy(dsc_job_payload) |
| 335 | + |
| 336 | + list_availability_domains_mock = MagicMock() |
| 337 | + list_availability_domains_mock.data = [ |
| 338 | + oci.identity.models.availability_domain.AvailabilityDomain( |
| 339 | + compartment_id=job_copy.infrastructure.compartment_id, |
| 340 | + name="NNFR:US-ASHBURN-AD-1", |
| 341 | + id="test_id_one", |
| 342 | + ) |
| 343 | + ] |
| 344 | + |
| 345 | + mock_list_availability_domains.return_value = list_availability_domains_mock |
| 346 | + |
| 347 | + list_mount_targets_mock = MagicMock() |
| 348 | + list_mount_targets_mock.data = [ |
| 349 | + oci.file_storage.models.mount_target_summary.MountTargetSummary( |
| 350 | + **{ |
| 351 | + "availability_domain": "NNFR:US-ASHBURN-AD-1", |
| 352 | + "compartment_id": job_copy.infrastructure.compartment_id, |
| 353 | + "display_name": "test_mount_target_one", |
| 354 | + "id": "test_mount_target_id_one", |
| 355 | + } |
| 356 | + ), |
| 357 | + ] |
| 358 | + mock_list_mount_targets.return_value = list_mount_targets_mock |
| 359 | + |
| 360 | + list_exports_mock = MagicMock() |
| 361 | + list_exports_mock.data = [ |
| 362 | + oci.file_storage.models.export.Export( |
| 363 | + **{ |
| 364 | + "id": "test_export_id_one", |
| 365 | + "path": "test_export_path_one", |
| 366 | + } |
| 367 | + ), |
| 368 | + oci.file_storage.models.export.Export( |
| 369 | + **{ |
| 370 | + "id": "test_export_id_two", |
| 371 | + "path": "test_export_path_two", |
| 372 | + } |
| 373 | + ), |
| 374 | + ] |
| 375 | + mock_list_exports.return_value = list_exports_mock |
| 376 | + |
| 377 | + dsc_job_payload_copy.job_storage_mount_configuration_details_list = [] |
| 378 | + infrastructure = job_copy.infrastructure |
| 379 | + with pytest.raises( |
| 380 | + ValueError, |
| 381 | + match="No `mount_target` with value test_mount_target_two found under compartment test_compartment_id.", |
| 382 | + ): |
| 383 | + infrastructure._update_job_infra(dsc_job_payload_copy) |
| 384 | + |
| 385 | + assert ( |
| 386 | + len(dsc_job_payload_copy.job_storage_mount_configuration_details_list) |
| 387 | + == 1 |
| 388 | + ) |
| 389 | + assert dsc_job_payload_copy.job_storage_mount_configuration_details_list[ |
| 390 | + 0 |
| 391 | + ] == { |
| 392 | + "destinationDirectoryName": "test_destination_directory_name_from_dsc", |
| 393 | + "exportId": "test_export_id_one", |
| 394 | + "mountTargetId": "test_mount_target_id_one", |
| 395 | + "storageType": "FILE_STORAGE", |
| 396 | + } |
0 commit comments