Skip to content

Commit c57f3fa

Browse files
[Ready to merge] Onboarding BYOR feature in AQUA (#1179)
2 parents ddb3094 + df69212 commit c57f3fa

File tree

8 files changed

+112
-5
lines changed

8 files changed

+112
-5
lines changed

ads/aqua/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(self) -> None:
6464
set_auth("resource_principal")
6565
self._auth = default_signer({"service_endpoint": OCI_ODSC_SERVICE_ENDPOINT})
6666
self.ds_client = oc.OCIClientFactory(**self._auth).data_science
67+
self.compute_client = oc.OCIClientFactory(**default_signer()).compute
6768
self.logging_client = oc.OCIClientFactory(**default_signer()).logging_management
6869
self.identity_client = oc.OCIClientFactory(**default_signer()).identity
6970
self.region = extract_region(self._auth)

ads/aqua/extension/ui_handler.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from ads.aqua.extension.utils import validate_function_parameters
1616
from ads.aqua.model.entities import ImportModelDetails
1717
from ads.aqua.ui import AquaUIApp
18-
from ads.config import COMPARTMENT_OCID
18+
from ads.config import COMPARTMENT_OCID, IS_BYOR_ENABLED
1919

2020

2121
@dataclass
@@ -82,6 +82,10 @@ def get(self, id=""):
8282
return self.is_bucket_versioned()
8383
elif paths.startswith("aqua/containers"):
8484
return self.list_containers()
85+
elif paths.startswith("aqua/capacityreservations/enabled"):
86+
return self.is_capacity_reservations_enabled()
87+
elif paths.startswith("aqua/capacityreservations"):
88+
return self.list_capacity_reservations()
8589
else:
8690
raise HTTPError(400, f"The request {self.request.path} is invalid.")
8791

@@ -103,6 +107,19 @@ def list_log_groups(self, **kwargs):
103107
AquaUIApp().list_log_groups(compartment_id=compartment_id, **kwargs)
104108
)
105109

110+
def is_capacity_reservations_enabled(self):
111+
"""Checks if the tenant is whitelisted for BYOR (Bring your own reservation) feature."""
112+
return self.finish({"status": str(IS_BYOR_ENABLED).strip().lower() == "true"})
113+
114+
def list_capacity_reservations(self, **kwargs):
115+
"""Lists users compute reservations in a specified compartment."""
116+
compartment_id = self.get_argument("compartment_id", default=COMPARTMENT_OCID)
117+
return self.finish(
118+
AquaUIApp().list_capacity_reservations(
119+
compartment_id=compartment_id, **kwargs
120+
)
121+
)
122+
106123
def list_logs(self, log_group_id: str, **kwargs):
107124
"""Lists the specified log group's log objects."""
108125
return self.finish(AquaUIApp().list_logs(log_group_id=log_group_id, **kwargs))
@@ -279,4 +296,5 @@ def post(self, *args, **kwargs):
279296
("bucket/versioning/?([^/]*)", AquaUIHandler),
280297
("containers/?([^/]*)", AquaUIHandler),
281298
("cli/?([^/]*)", AquaCLIHandler),
299+
("capacityreservations/?([^/]*)", AquaUIHandler),
282300
]

ads/aqua/ui.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ def list_logs(self, **kwargs) -> str:
9090
res = self.logging_client.list_logs(log_group_id=log_group_id, **kwargs).data
9191
return sanitize_response(oci_client=self.logging_client, response=res)
9292

93+
@telemetry(entry_point="plugin=ui&action=list_capacity_reservations", name="aqua")
94+
def list_capacity_reservations(self, **kwargs) -> list:
95+
"""
96+
Lists users compute reservations in a specified compartment
97+
98+
Returns
99+
-------
100+
json representation of `oci.core.models.ComputeCapacityReservationSummary`.
101+
102+
"""
103+
compartment_id = kwargs.pop("compartment_id", COMPARTMENT_OCID)
104+
logger.info(f"Loading Capacity reservations from compartment: {compartment_id}")
105+
106+
reservations = self.compute_client.list_compute_capacity_reservations(
107+
compartment_id=compartment_id, **kwargs
108+
)
109+
return sanitize_response(
110+
oci_client=self.compute_client, response=reservations.data
111+
)
112+
93113
@telemetry(entry_point="plugin=ui&action=list_compartments", name="aqua")
94114
def list_compartments(self) -> str:
95115
"""Lists the compartments in a tenancy specified by TENANCY_OCID env variable. This is a pass through the OCI list_compartments

ads/common/oci_client.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env python
2-
# -*- coding: utf-8; -*-
32

43
# Copyright (c) 2021, 2024 Oracle and/or its affiliates.
54
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
@@ -9,20 +8,20 @@
98
import oci.artifacts
109
from oci.ai_language import AIServiceLanguageClient
1110
from oci.artifacts import ArtifactsClient
11+
from oci.core import ComputeClient, VirtualNetworkClient
1212
from oci.data_catalog import DataCatalogClient
1313
from oci.data_flow import DataFlowClient
1414
from oci.data_labeling_service import DataLabelingManagementClient
1515
from oci.data_labeling_service_dataplane import DataLabelingClient
1616
from oci.data_science import DataScienceClient
1717
from oci.identity import IdentityClient
18+
from oci.limits import LimitsClient
19+
from oci.logging import LoggingManagementClient
1820
from oci.marketplace import MarketplaceClient
1921
from oci.object_storage import ObjectStorageClient
2022
from oci.resource_search import ResourceSearchClient
2123
from oci.secrets import SecretsClient
2224
from oci.vault import VaultsClient
23-
from oci.logging import LoggingManagementClient
24-
from oci.core import VirtualNetworkClient
25-
from oci.limits import LimitsClient
2625

2726
logger = logging.getLogger(__name__)
2827

@@ -69,6 +68,7 @@ def _client_impl(client):
6968
"secret": SecretsClient,
7069
"vault": VaultsClient,
7170
"identity": IdentityClient,
71+
"compute": ComputeClient,
7272
"ai_language": AIServiceLanguageClient,
7373
"data_labeling_dp": DataLabelingClient,
7474
"data_labeling_cp": DataLabelingManagementClient,
@@ -114,6 +114,10 @@ def create_client(self, client_name):
114114
def object_storage(self):
115115
return self.create_client("object_storage")
116116

117+
@property
118+
def compute(self):
119+
return self.create_client("compute")
120+
117121
@property
118122
def identity(self):
119123
return self.create_client("identity")

ads/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
OCI_IDENTITY_SERVICE_ENDPOINT = os.environ.get("OCI_IDENTITY_SERVICE_ENDPOINT")
1515
NB_SESSION_COMPARTMENT_OCID = os.environ.get("NB_SESSION_COMPARTMENT_OCID")
1616
PROJECT_OCID = os.environ.get("PROJECT_OCID") or os.environ.get("PIPELINE_PROJECT_OCID")
17+
IS_BYOR_ENABLED = os.environ.get("ALLOWLISTED_FOR_BYOR", False)
1718
NB_SESSION_OCID = os.environ.get("NB_SESSION_OCID")
1819
USER_OCID = os.environ.get("USER_OCID")
1920
OCI_RESOURCE_PRINCIPAL_VERSION = os.environ.get("OCI_RESOURCE_PRINCIPAL_VERSION")
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[
2+
{
3+
"availability_domain": "<AD>",
4+
"compartment_id": "ocid1.compartment.oc1..<OCID>",
5+
"defined_tags": {},
6+
"display_name": "test-reservation",
7+
"freeform_tags": {},
8+
"id": "ocid1.capacityreservation.oc1.iad...<OCID>",
9+
"is_default_reservation": false,
10+
"lifecycle_state": "ACTIVE",
11+
"reserved_instance_count": 0,
12+
"time_created": "2025-05-14T07:17:53.795000Z",
13+
"used_instance_count": 0
14+
}
15+
]

tests/unitary/with_extras/aqua/test_ui.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,46 @@ def tearDownClass(cls):
9797
reload(ads.aqua)
9898
reload(ads.aqua.ui)
9999

100+
def test_list_capacity_reservations(self):
101+
capacity_reservations_list = os.path.join(
102+
self.curr_dir, "test_data/ui/capacity_reservations_list.json"
103+
)
104+
with open(capacity_reservations_list, "r") as _file:
105+
capacity_reservations = json.load(_file)
106+
107+
self.app.compute_client.list_compute_capacity_reservations = MagicMock(
108+
return_value=oci.response.Response(
109+
status=200,
110+
request=MagicMock(),
111+
headers=MagicMock(),
112+
data=[
113+
oci.core.models.ComputeCapacityReservationSummary(
114+
**capacity_reservation
115+
)
116+
for capacity_reservation in capacity_reservations
117+
],
118+
)
119+
)
120+
results = self.app.list_capacity_reservations()
121+
expected_attributes = {
122+
"id",
123+
"compartmentId",
124+
"displayName",
125+
"definedTags",
126+
"freeformTags",
127+
"lifecycleState",
128+
"availabilityDomain",
129+
"reservedInstanceCount",
130+
"usedInstanceCount",
131+
"isDefaultReservation",
132+
"timeCreated",
133+
}
134+
for result in results:
135+
self.assertTrue(
136+
expected_attributes.issuperset(set(result)), "Attributes mismatch"
137+
)
138+
assert len(results) == len(capacity_reservations)
139+
100140
def test_list_log_groups(self):
101141
"""Test to lists all log groups for the specified compartment or tenancy"""
102142
log_groups_list = os.path.join(

tests/unitary/with_extras/aqua/test_ui_handler.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ def tearDownClass(cls):
5050
reload(ads.aqua)
5151
reload(ads.aqua.extension.ui_handler)
5252

53+
@patch("ads.aqua.ui.AquaUIApp.list_capacity_reservations")
54+
def test_list_capacity_reservations(self, mock_list_reservations):
55+
self.ui_handler.request.path = "aqua/capacityreservations"
56+
self.ui_handler.get(id="")
57+
mock_list_reservations.assert_called_with(
58+
compartment_id=TestDataset.USER_COMPARTMENT_ID
59+
)
60+
5361
@patch("ads.aqua.ui.AquaUIApp.list_log_groups")
5462
def test_list_log_groups(self, mock_list_log_groups):
5563
"""Test get method to fetch log groups"""

0 commit comments

Comments
 (0)