Skip to content

Commit 3d16f88

Browse files
Merge pull request #79 from Dynatrace-James-Kitson/tweak-settings
settings changes to align with API and rest of project
2 parents 68b8acd + 67fd8cf commit 3d16f88

10 files changed

+299
-133
lines changed

dynatrace/environment_v2/settings.py

Lines changed: 139 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
from typing import Optional, Dict, Any
1+
from typing import Optional, Dict, Any, List, Union
2+
from datetime import datetime
23

34
from dynatrace.dynatrace_object import DynatraceObject
45
from dynatrace.http_client import HttpClient
56
from dynatrace.pagination import PaginatedList
7+
from dynatrace.utils import int64_to_datetime
68

79

810
class SettingService:
911
ENDPOINT = "/api/v2/settings/objects"
1012

1113
def __init__(self, http_client: HttpClient):
1214
self.__http_client = http_client
13-
14-
def list_objects(self,schema_id: Optional[str] = None,
15-
scope: Optional[str] = None,external_ids: Optional[str] = None,
16-
fields: Optional[str] = None,
17-
filter:Optional[str] = None, sort:Optional[str] = None, page_size:Optional[str] = None) -> PaginatedList["DynatraceObject"]:
15+
16+
def list_objects(
17+
self,
18+
schema_id: Optional[str] = None,
19+
scope: Optional[str] = None,
20+
external_ids: Optional[str] = None,
21+
fields: Optional[str] = None,
22+
filter: Optional[str] = None,
23+
sort: Optional[str] = None,
24+
page_size: Optional[str] = None,
25+
) -> PaginatedList["SettingsObject"]:
1826
"""Lists settings
1927
2028
:return: a list of settings with details
@@ -28,64 +36,151 @@ def list_objects(self,schema_id: Optional[str] = None,
2836
"sort": sort,
2937
"pageSize": page_size,
3038
}
31-
return PaginatedList(Settings, self.__http_client, target_url=self.ENDPOINT, list_item="items", target_params=params)
32-
33-
def create_object(self,external_id,object_id,schema_id,schema_version,scope, value,validate_only):
34-
"""Creates a new settings object
39+
return PaginatedList(
40+
SettingsObject,
41+
self.__http_client,
42+
target_url=self.ENDPOINT,
43+
list_item="items",
44+
target_params=params,
45+
)
3546

36-
:param external_id: External identifier for the object being created
37-
:param object_id: The ID of the settings object that should be replaced. Only applicable if an external identifier
38-
:param object_id: the ID of the object
39-
:param schema_id: The schema on which the object is based
40-
:param schema_version: The version of the schema on which the object is based.
41-
:param scope The scope that the object targets. For more details, please see Dynatrace Documentation.
42-
:param value The value of the setting.
43-
:return: a Settings object
44-
"""
45-
params = {
46-
"validate_only": validate_only,
47-
}
48-
body =[ {
49-
"externalId" : external_id,
50-
"objectId": object_id,
51-
"schemaId": schema_id,
52-
"schemaVersion": schema_version,
53-
"scope": scope,
54-
"value" : value
47+
def create_object(
48+
self,
49+
validate_only: Optional[bool] = False,
50+
body: Union[
51+
Optional[List["SettingsObjectCreate"]], Optional["SettingsObjectCreate"]
52+
] = [],
53+
):
54+
"""
55+
Creates a new settings object or validates the provided settigns object
56+
57+
:param validate_only: If true, the request runs only validation of the submitted settings objects, without saving them
58+
:param body: The JSON body of the request. Contains the settings objects
59+
"""
60+
query_params = {"validateOnly": validate_only}
5561

56-
}]
57-
58-
response = self.__http_client.make_request(self.ENDPOINT,params=body, method="POST",query_params=params).json()
62+
if isinstance(body, SettingsObjectCreate):
63+
body = [body]
64+
65+
body = [o.json() for o in body]
66+
67+
response = self.__http_client.make_request(
68+
self.ENDPOINT, params=body, method="POST", query_params=query_params
69+
).json()
5970
return response
60-
61-
71+
6272
def get_object(self, object_id: str):
6373
"""Gets parameters of specified settings object
6474
6575
:param object_id: the ID of the object
6676
:return: a Settings object
6777
"""
68-
response = self.__http_client.make_request(f"{self.ENDPOINT}/{object_id}").json()
69-
return Settings(raw_element=response)
78+
response = self.__http_client.make_request(
79+
f"{self.ENDPOINT}/{object_id}"
80+
).json()
81+
return SettingsObject(raw_element=response)
7082

71-
def update_object(self, object_id: str, value):
83+
def update_object(
84+
self, object_id: str, value: Optional["SettingsObjectCreate"] = None
85+
):
7286
"""Updates an existing settings object
73-
74-
:param object_id: the ID of the object
7587
88+
:param object_id: the ID of the object
89+
:param value: the JSON body of the request. Contains updated parameters of the settings object.
7690
"""
77-
return self.__http_client.make_request(path=f"{self.ENDPOINT}/{object_id}", params=value, method="PUT")
91+
return self.__http_client.make_request(
92+
f"{self.ENDPOINT}/{object_id}", params=value.json(), method="PUT"
93+
)
7894

79-
def delete_object(self, object_id: str):
95+
def delete_object(self, object_id: str, update_token: Optional[str] = None):
8096
"""Deletes the specified object
8197
8298
:param object_id: the ID of the object
99+
:param update_token: The update token of the object. You can use it to detect simultaneous modifications by different users
83100
:return: HTTP response
84101
"""
85-
return self.__http_client.make_request(path=f"{self.ENDPOINT}/{object_id}", method="DELETE")
102+
query_params = {"updateToken": update_token}
103+
return self.__http_client.make_request(
104+
f"{self.ENDPOINT}/{object_id}",
105+
method="DELETE",
106+
query_params=query_params,
107+
).json()
108+
109+
110+
class ModificationInfo(DynatraceObject):
111+
def _create_from_raw_data(self, raw_element: Dict[str, Any]):
112+
self.deleteable: bool = raw_element.get("deleteable")
113+
self.first: bool = raw_element.get("first")
114+
self.modifiable: bool = raw_element.get("modifiable")
115+
self.modifiable_paths: List[str] = raw_element.get("modifiablePaths", [])
116+
self.movable: bool = raw_element.get("movable")
117+
self.non_modifiable_paths: List[str] = raw_element.get("nonModifiablePaths", [])
118+
86119

87-
class Settings(DynatraceObject):
120+
class SettingsObject(DynatraceObject):
88121
def _create_from_raw_data(self, raw_element: Dict[str, Any]):
89122
# Mandatory
90123
self.objectId: str = raw_element["objectId"]
91-
self.value: str = raw_element["value"]
124+
self.value: dict = raw_element["value"]
125+
# Optional
126+
self.author: str = raw_element.get("author")
127+
self.created: datetime = (
128+
int64_to_datetime(int(raw_element.get("created")))
129+
if raw_element.get("created")
130+
else None
131+
)
132+
self.created_by: str = raw_element.get("createdBy")
133+
self.external_id: str = raw_element.get("externalId")
134+
self.modification_info: ModificationInfo = (
135+
ModificationInfo(
136+
self._http_client, self._headers, raw_element.get("modificationInfo")
137+
)
138+
if raw_element.get("modificationInfo")
139+
else None
140+
)
141+
self.modified: datetime = (
142+
int64_to_datetime(int(raw_element.get("modified")))
143+
if raw_element.get("modified")
144+
else None
145+
)
146+
self.modified_by: str = raw_element.get("modifiedBy")
147+
self.schema_id: str = raw_element.get("schemaId")
148+
self.schema_version: str = raw_element.get("schemaVersion")
149+
self.scope: str = raw_element.get("scope")
150+
self.search_summary: str = raw_element.get("searchSummary")
151+
self.summary: str = raw_element.get("summary")
152+
self.update_token: str = raw_element.get("updateToken")
153+
154+
155+
class SettingsObjectCreate:
156+
def __init__(
157+
self,
158+
schema_id: str,
159+
value: dict,
160+
scope: str,
161+
external_id: Optional[str] = None,
162+
insert_after: Optional[str] = None,
163+
object_id: Optional[str] = None,
164+
schema_version: Optional[str] = None,
165+
):
166+
self.schema_id = schema_id
167+
self.value = value
168+
self.scope = scope
169+
self.external_id = external_id
170+
self.insert_after = insert_after
171+
self.object_id = object_id
172+
self.schema_version = schema_version
173+
174+
def json(self) -> dict:
175+
body = {"schemaId": self.schema_id, "value": self.value, "scope": self.scope}
176+
177+
if self.external_id:
178+
body["externalId"] = self.external_id
179+
if self.insert_after:
180+
body["insertAfter"] = self.insert_after
181+
if self.object_id:
182+
body["objectId"] = self.object_id
183+
if self.schema_version:
184+
body["schemaVersion"] = self.schema_version
185+
186+
return body

test/environment_v2/test_settings.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,59 @@
11
from datetime import datetime
22

3-
import dynatrace.environment_v2.settings as st
3+
from dynatrace.environment_v2.settings import SettingsObject, SettingsObjectCreate
44
from dynatrace import Dynatrace
55
from dynatrace.pagination import PaginatedList
6-
payload = {"additionalInformation": [],
7-
"contactDetails":[{"email": 'unittest@contoso.com', "integrationType": "EMAIL"}],
8-
"description": 'unittest',
9-
"identifier":'unittest',
10-
"links": [],
11-
"name": 'unittest',
12-
"responsibilities": {"development": False,
13-
"infrastructure": False,
14-
"lineOfBusiness": True,
15-
"operations": False,
16-
"security": False},
17-
"supplementaryIdentifiers": [] }
6+
7+
settings_dict = {
8+
"enabled": True,
9+
"summary": "DT API TEST 22",
10+
"queryDefinition": {
11+
"type": "METRIC_KEY",
12+
"metricKey": "netapp.ontap.node.fru.state",
13+
"aggregation": "AVG",
14+
"entityFilter": {
15+
"dimensionKey": "dt.entity.netapp_ontap:fru",
16+
"conditions": [],
17+
},
18+
"dimensionFilter": [],
19+
},
20+
"modelProperties": {
21+
"type": "STATIC_THRESHOLD",
22+
"threshold": 100.0,
23+
"alertOnNoData": False,
24+
"alertCondition": "BELOW",
25+
"violatingSamples": 3,
26+
"samples": 5,
27+
"dealertingSamples": 5,
28+
},
29+
"eventTemplate": {
30+
"title": "OnTap {dims:type} {dims:fru_id} is in Error State",
31+
"description": "OnTap field replaceable unit (FRU) {dims:type} with id {dims:fru_id} on node {dims:node} in cluster {dims:cluster} is in an error state.\n",
32+
"eventType": "RESOURCE",
33+
"davisMerge": True,
34+
"metadata": [],
35+
},
36+
"eventEntityDimensionKey": "dt.entity.netapp_ontap:fru",
37+
}
38+
settings_object = SettingsObjectCreate("builtin:anomaly-detection.metric-events", settings_dict, "environment")
39+
test_object_id = "vu9U3hXa3q0AAAABACdidWlsdGluOmFub21hbHktZGV0ZWN0aW9uLm1ldHJpYy1ldmVudHMABnRlbmFudAAGdGVuYW50ACRiYmYzZWNhNy0zMmZmLTM2ZTEtOTFiOS05Y2QxZjE3OTc0YjC-71TeFdrerQ"
40+
1841
def test_list_objects(dt: Dynatrace):
19-
settings = dt.settings.list_objects(schema_id="builtin:ownership.teams")
42+
settings = dt.settings.list_objects(schema_id="builtin:anomaly-detection.metric-events")
2043
assert isinstance(settings, PaginatedList)
21-
assert len(list(settings)) == 1
22-
assert all(isinstance(s, st.Settings) for s in settings)
44+
assert len(list(settings)) == 2
45+
assert all(isinstance(s, SettingsObject) for s in settings)
2346

2447
def test_get_object(dt: Dynatrace):
25-
setting = dt.settings.get_object(object_id="vu9U3hXa3q0AAAABABdidWlsdGluOm93bmVyc2hpcC50ZWFtcwAGdGVuYW50AAZ0ZW5hbnQAJGVjN2UyNTdhLWM5MTktM2YzMC05NWFiLTliMzNkMmQwZGRkY77vVN4V2t6t")
26-
assert isinstance(setting, st.Settings)
48+
setting = dt.settings.get_object(object_id=test_object_id)
49+
assert isinstance(setting, SettingsObject)
50+
assert setting.schema_version == "1.0.16"
2751

2852
def test_post_object(dt: Dynatrace):
29-
30-
response = dt.settings.create_object(external_id='unittest',object_id='unittest',schema_id="builtin:ownership.teams",schema_version="1.0.6",scope="environment", value=payload,validate_only=False)
53+
response = dt.settings.create_object(body=settings_object)
3154
assert response[0].get("code") == 200
32-
assert response[0].get("code") is not None
3355

3456
def test_put_object(dt: Dynatrace):
35-
payload["identifier"] = "unittestupdate"
36-
response = dt.settings.update_object("unittest",payload)
37-
print(response)
57+
response = dt.settings.update_object(test_object_id, settings_object)
58+
print(response)
59+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"items": [
3+
{
4+
"objectId": "vu9U3hXa3q0AAAABACdidWlsdGluOmFub21hbHktZGV0ZWN0aW9uLm1ldHJpYy1ldmVudHMABnRlbmFudAAGdGVuYW50ACQ4OWNhMmY2Ny0wY2Q3LTM0MzAtYmU5Ny1kOTg4YTRmMWRiYWa-71TeFdrerQ",
5+
"value": {
6+
"enabled": true,
7+
"summary": "OnTap Node Field Replaceable Unit in Error State",
8+
"queryDefinition": {
9+
"type": "METRIC_KEY",
10+
"metricKey": "netapp.ontap.node.fru.state",
11+
"aggregation": "AVG",
12+
"entityFilter": {
13+
"dimensionKey": "dt.entity.netapp_ontap:fru",
14+
"conditions": []
15+
},
16+
"dimensionFilter": []
17+
},
18+
"modelProperties": {
19+
"type": "STATIC_THRESHOLD",
20+
"threshold": 100.0,
21+
"alertOnNoData": false,
22+
"alertCondition": "BELOW",
23+
"violatingSamples": 3,
24+
"samples": 5,
25+
"dealertingSamples": 5
26+
},
27+
"eventTemplate": {
28+
"title": "OnTap {dims:type} {dims:fru_id} is in Error State",
29+
"description": "OnTap field replaceable unit (FRU) {dims:type} with id {dims:fru_id} on node {dims:node} in cluster {dims:cluster} is in an error state.\n",
30+
"eventType": "RESOURCE",
31+
"davisMerge": true,
32+
"metadata": []
33+
},
34+
"eventEntityDimensionKey": "dt.entity.netapp_ontap:fru"
35+
}
36+
},
37+
{
38+
"objectId": "vu9U3hXa3q0AAAABACdidWlsdGluOmFub21hbHktZGV0ZWN0aW9uLm1ldHJpYy1ldmVudHMABnRlbmFudAAGdGVuYW50ACQ5MjRiNjdiYS00OGQ1LTM0NDctODg4Zi05NzUzMzg5NzcxMze-71TeFdrerQ",
39+
"value": {
40+
"enabled": false,
41+
"summary": "High outbound bandwidth utilization",
42+
"queryDefinition": {
43+
"type": "METRIC_SELECTOR",
44+
"metricSelector": "func:com.dynatrace.extension.snmp-generic-cisco-device.if.out.bandwidth"
45+
},
46+
"modelProperties": {
47+
"type": "STATIC_THRESHOLD",
48+
"threshold": 90.0,
49+
"alertOnNoData": false,
50+
"alertCondition": "ABOVE",
51+
"violatingSamples": 3,
52+
"samples": 5,
53+
"dealertingSamples": 5
54+
},
55+
"eventTemplate": {
56+
"title": "High outbound bandwidth utilization",
57+
"description": "The {metricname} value of {severity} was {alert_condition} your custom threshold of {threshold}.",
58+
"eventType": "CUSTOM_ALERT",
59+
"davisMerge": false,
60+
"metadata": []
61+
},
62+
"eventEntityDimensionKey": "dt.entity.snmp:com_dynatrace_extension_snmp_generic_cisco_network_interface",
63+
"legacyId": "E|649d5713-380b-6751-a64f-6904a2c4cd2f"
64+
}
65+
}
66+
],
67+
"totalCount": 25,
68+
"pageSize": 100
69+
}

0 commit comments

Comments
 (0)