Skip to content

Commit 565d2b5

Browse files
committed
Implement missing API endpoints
Add support for the following VinylDNS API endpoints: Health & Status: - ping, health, color, metrics_prometheus - get_status, update_status Zones: - get_zone_details (with adminGroupId/adminGroupName) - list_zone_backend_ids - list_zone_changes_failure - list_deleted_zones Record Sets: - get_record_set_count - list_record_set_change_history - list_record_set_changes_failure - Ownership transfer: request, approve, reject, cancel Groups: - get_group_change - list_group_valid_domains Users: - get_user - lock_user, unlock_user Also adds unit tests for all new endpoints.
1 parent ff9eeb2 commit 565d2b5

File tree

10 files changed

+921
-8
lines changed

10 files changed

+921
-8
lines changed

src/vinyldns/client.py

Lines changed: 291 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232

3333
from vinyldns.batch_change import BatchChange, ListBatchChangeSummaries, to_review_json
3434
from vinyldns.membership import Group, ListGroupsResponse, ListGroupChangesResponse, ListMembersResponse, \
35-
ListAdminsResponse
35+
ListAdminsResponse, GroupChange, UserInfo
3636
from vinyldns.serdes import to_json_string
37-
from vinyldns.zone import ListZonesResponse, ListZoneChangesResponse, Zone, ZoneChange
38-
from vinyldns.record import ListRecordSetsResponse, ListRecordSetChangesResponse, RecordSet, RecordSetChange
37+
from vinyldns.zone import ListZonesResponse, ListZoneChangesResponse, Zone, ZoneChange, ZoneDetails, \
38+
ZoneChangeFailuresResponse, DeletedZonesResponse
39+
from vinyldns.record import ListRecordSetsResponse, ListRecordSetChangesResponse, RecordSet, RecordSetChange, \
40+
RecordSetCount, RecordSetChangeFailuresResponse, OwnershipTransfer, OwnershipTransferStatus
41+
from vinyldns.status import SystemStatus
3942

4043
try:
4144
basestring
@@ -161,6 +164,24 @@ def __make_request(self, url, method=u'GET', headers=None, body_string=None, **k
161164

162165
return self.__check_response(response, method)
163166

167+
def __make_request_raw(self, url, method=u'GET', headers=None, body_string=None, **kwargs):
168+
169+
# remove retries arg if provided
170+
kwargs.pop(u'retries', None)
171+
172+
path = urlparse(url).path
173+
query = parse_qs(urlsplit(url).query)
174+
if query:
175+
query = dict((k, v if len(v) > 1 else v[0])
176+
for k, v in query.items())
177+
178+
signed_headers, signed_body = self.__build_vinyldns_request(method, path, body_string, query,
179+
with_headers=headers or {}, **kwargs)
180+
181+
response = self.session.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
182+
183+
return self.__check_response_raw(response, method)
184+
164185
def __check_response(self, response, method):
165186
status = response.status_code
166187
if status == 200 or status == 202:
@@ -183,6 +204,28 @@ def __check_response(self, response, method):
183204
else:
184205
raise ClientError(response.text)
185206

207+
def __check_response_raw(self, response, method):
208+
status = response.status_code
209+
if status == 200 or status == 202:
210+
return response.status_code, response.text
211+
elif status == 404:
212+
if method == 'GET':
213+
return 404, None
214+
else:
215+
raise NotFoundError(response.text)
216+
elif status == 400:
217+
raise BadRequestError(response.text)
218+
elif status == 401:
219+
raise UnauthorizedError(response.text)
220+
elif status == 403:
221+
raise ForbiddenError(response.text)
222+
elif status == 409:
223+
raise ConflictError(response.text)
224+
elif status == 422:
225+
raise UnprocessableError(response.text)
226+
else:
227+
raise ClientError(response.text)
228+
186229
def __build_vinyldns_request(self, method, path, body_data, params=None, **kwargs):
187230

188231
if isinstance(body_data, basestring):
@@ -379,6 +422,28 @@ def list_group_changes(self, group_id, start_from=None, max_items=None, **kwargs
379422

380423
return ListGroupChangesResponse.from_dict(data)
381424

425+
def get_group_change(self, group_change_id, **kwargs):
426+
"""
427+
Get a group change by ID.
428+
429+
:param group_change_id: the group change ID
430+
:return: the group change details
431+
"""
432+
url = urljoin(self.index_url, u'/groups/change/{0}'.format(group_change_id))
433+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
434+
435+
return GroupChange.from_dict(data) if data is not None else None
436+
437+
def list_group_valid_domains(self, **kwargs):
438+
"""
439+
List valid email domains for groups.
440+
441+
:return: list of valid domains
442+
"""
443+
url = urljoin(self.index_url, u'/groups/valid/domains')
444+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
445+
return data if data is not None else []
446+
382447
def connect_zone(self, zone, **kwargs):
383448
"""
384449
Create a new zone with the given name and email.
@@ -447,6 +512,71 @@ def get_zone_by_name(self, name, **kwargs):
447512
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
448513
return Zone.from_dict(data['zone']) if data is not None else None
449514

515+
def get_zone_details(self, zone_id, **kwargs):
516+
"""
517+
Get detailed zone info for the given zone id.
518+
519+
:param zone_id: the id of the zone to retrieve
520+
:return: the zone details, or will 404 if not found
521+
"""
522+
url = urljoin(self.index_url, u'/zones/{0}/details'.format(zone_id))
523+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
524+
525+
return ZoneDetails.from_dict(data['zone']) if data is not None else None
526+
527+
def list_zone_backend_ids(self, **kwargs):
528+
"""
529+
List configured backend IDs.
530+
"""
531+
url = urljoin(self.index_url, u'/zones/backendids')
532+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
533+
534+
if data is None:
535+
return []
536+
if isinstance(data, dict):
537+
return data.get('backendIds', [])
538+
return data
539+
540+
def list_zone_changes_failure(self, name_filter=None, start_from=None, max_items=None, **kwargs):
541+
"""
542+
List failed zone changes.
543+
"""
544+
args = []
545+
if name_filter:
546+
args.append(u'nameFilter={0}'.format(name_filter))
547+
if start_from:
548+
args.append(u'startFrom={0}'.format(start_from))
549+
if max_items is not None:
550+
args.append(u'maxItems={0}'.format(max_items))
551+
552+
url = urljoin(self.index_url, u'/metrics/health/zonechangesfailure')
553+
if args:
554+
url = url + u'?' + u'&'.join(args)
555+
556+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
557+
return ZoneChangeFailuresResponse.from_dict(data)
558+
559+
def list_deleted_zones(self, name_filter=None, start_from=None, max_items=None, ignore_access=None, **kwargs):
560+
"""
561+
List deleted zone changes.
562+
"""
563+
args = []
564+
if name_filter:
565+
args.append(u'nameFilter={0}'.format(name_filter))
566+
if start_from:
567+
args.append(u'startFrom={0}'.format(start_from))
568+
if max_items is not None:
569+
args.append(u'maxItems={0}'.format(max_items))
570+
if ignore_access is not None:
571+
args.append(u'ignoreAccess={0}'.format(ignore_access))
572+
573+
url = urljoin(self.index_url, u'/zones/deleted/changes')
574+
if args:
575+
url = url + u'?' + u'&'.join(args)
576+
577+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
578+
return DeletedZonesResponse.from_dict(data)
579+
450580
def list_zone_changes(self, zone_id, start_from=None, max_items=None, **kwargs):
451581
"""
452582
Get the zone changes for the given zone id.
@@ -564,6 +694,84 @@ def list_record_sets(self, zone_id, start_from=None, max_items=None, record_name
564694
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
565695
return ListRecordSetsResponse.from_dict(data)
566696

697+
def get_record_set_count(self, zone_id, **kwargs):
698+
"""
699+
Get record set count for a zone.
700+
"""
701+
url = urljoin(self.index_url, u'/zones/{0}/recordsetcount'.format(zone_id))
702+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
703+
return RecordSetCount.from_dict(data)
704+
705+
def list_record_set_change_history(self, zone_id, fqdn, record_type, start_from=None, max_items=None, **kwargs):
706+
"""
707+
Retrieve record set change history for a FQDN and type.
708+
"""
709+
args = [u'zoneId={0}'.format(zone_id), u'fqdn={0}'.format(fqdn), u'recordType={0}'.format(record_type)]
710+
if start_from:
711+
args.append(u'startFrom={0}'.format(start_from))
712+
if max_items is not None:
713+
args.append(u'maxItems={0}'.format(max_items))
714+
715+
url = urljoin(self.index_url, u'/recordsetchange/history') + u'?' + u'&'.join(args)
716+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
717+
return ListRecordSetChangesResponse.from_dict(data)
718+
719+
def list_record_set_changes_failure(self, zone_id, start_from=None, max_items=None, **kwargs):
720+
"""
721+
List failed record set changes for a zone.
722+
"""
723+
args = []
724+
if start_from:
725+
args.append(u'startFrom={0}'.format(start_from))
726+
if max_items is not None:
727+
args.append(u'maxItems={0}'.format(max_items))
728+
729+
url = urljoin(self.index_url, u'/metrics/health/zones/{0}/recordsetchangesfailure'.format(zone_id))
730+
if args:
731+
url = url + u'?' + u'&'.join(args)
732+
733+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
734+
return RecordSetChangeFailuresResponse.from_dict(data)
735+
736+
def request_record_set_ownership(self, record_set, requested_owner_group_id, **kwargs):
737+
"""
738+
Request record set ownership transfer.
739+
"""
740+
return self.__record_set_ownership_transfer(record_set, OwnershipTransferStatus.Requested,
741+
requested_owner_group_id, **kwargs)
742+
743+
def approve_record_set_ownership(self, record_set, requested_owner_group_id, **kwargs):
744+
"""
745+
Approve record set ownership transfer.
746+
"""
747+
return self.__record_set_ownership_transfer(record_set, OwnershipTransferStatus.ManuallyApproved,
748+
requested_owner_group_id, update_owner_group=True, **kwargs)
749+
750+
def reject_record_set_ownership(self, record_set, requested_owner_group_id, **kwargs):
751+
"""
752+
Reject record set ownership transfer.
753+
"""
754+
return self.__record_set_ownership_transfer(record_set, OwnershipTransferStatus.ManuallyRejected,
755+
requested_owner_group_id, **kwargs)
756+
757+
def cancel_record_set_ownership(self, record_set, requested_owner_group_id, **kwargs):
758+
"""
759+
Cancel record set ownership transfer.
760+
"""
761+
return self.__record_set_ownership_transfer(record_set, OwnershipTransferStatus.Cancelled,
762+
requested_owner_group_id, **kwargs)
763+
764+
def __record_set_ownership_transfer(self, record_set, status, requested_owner_group_id,
765+
update_owner_group=False, **kwargs):
766+
record_set.record_set_group_change = OwnershipTransfer(
767+
ownership_transfer_status=status,
768+
requested_owner_group_id=requested_owner_group_id
769+
)
770+
if update_owner_group:
771+
record_set.owner_group_id = requested_owner_group_id
772+
773+
return self.update_record_set(record_set, **kwargs)
774+
567775
def search_record_sets(self, start_from=None, max_items=None, record_name_filter=None,
568776
record_type_filter=None, record_owner_group_filter=None, name_sort=None, **kwargs):
569777
"""
@@ -745,3 +953,83 @@ def delete_zone_acl_rule(self, zone_id, acl_rule, **kwargs):
745953
to_json_string(acl_rule), **kwargs)
746954

747955
return ZoneChange.from_dict(data)
956+
957+
def ping(self, **kwargs):
958+
"""
959+
Simple health check.
960+
"""
961+
url = urljoin(self.index_url, u'/ping')
962+
response, data = self.__make_request_raw(url, u'GET', self.headers, **kwargs)
963+
return data
964+
965+
def health(self, **kwargs):
966+
"""
967+
Comprehensive health check.
968+
"""
969+
url = urljoin(self.index_url, u'/health')
970+
response, data = self.__make_request_raw(url, u'GET', self.headers, **kwargs)
971+
return data
972+
973+
def color(self, **kwargs):
974+
"""
975+
Blue/green deployment status.
976+
"""
977+
url = urljoin(self.index_url, u'/color')
978+
response, data = self.__make_request_raw(url, u'GET', self.headers, **kwargs)
979+
return data
980+
981+
def metrics_prometheus(self, names=None, **kwargs):
982+
"""
983+
Prometheus metrics export.
984+
"""
985+
args = []
986+
if names:
987+
for name in names:
988+
args.append(u'name={0}'.format(name))
989+
990+
url = urljoin(self.index_url, u'/metrics/prometheus')
991+
if args:
992+
url = url + u'?' + u'&'.join(args)
993+
994+
response, data = self.__make_request_raw(url, u'GET', self.headers, **kwargs)
995+
return data
996+
997+
def get_status(self, **kwargs):
998+
"""
999+
Get system processing status.
1000+
"""
1001+
url = urljoin(self.index_url, u'/status')
1002+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
1003+
return SystemStatus.from_dict(data)
1004+
1005+
def update_status(self, processing_disabled, **kwargs):
1006+
"""
1007+
Enable/disable processing (admin).
1008+
"""
1009+
url = urljoin(self.index_url, u'/status?processingDisabled={0}'.format(str(processing_disabled).lower()))
1010+
response, data = self.__make_request(url, u'POST', self.headers, **kwargs)
1011+
return SystemStatus.from_dict(data)
1012+
1013+
def get_user(self, user_id, **kwargs):
1014+
"""
1015+
Get user by ID.
1016+
"""
1017+
url = urljoin(self.index_url, u'/users/{0}'.format(user_id))
1018+
response, data = self.__make_request(url, u'GET', self.headers, **kwargs)
1019+
return UserInfo.from_dict(data) if data is not None else None
1020+
1021+
def lock_user(self, user_id, **kwargs):
1022+
"""
1023+
Lock a user (admin).
1024+
"""
1025+
url = urljoin(self.index_url, u'/users/{0}/lock'.format(user_id))
1026+
response, data = self.__make_request(url, u'PUT', self.headers, **kwargs)
1027+
return UserInfo.from_dict(data)
1028+
1029+
def unlock_user(self, user_id, **kwargs):
1030+
"""
1031+
Unlock a user (admin).
1032+
"""
1033+
url = urljoin(self.index_url, u'/users/{0}/unlock'.format(user_id))
1034+
response, data = self.__make_request(url, u'PUT', self.headers, **kwargs)
1035+
return UserInfo.from_dict(data)

0 commit comments

Comments
 (0)