Skip to content

Commit 45b4cdd

Browse files
nspadaccinoarpit4everJoshSEdwards
authored
Python Utility Scripts (#84)
* Added new utility scripts for common python client tasks * update docstrings * added check for old owner group to update_owner_group script * Update dependencies and Python compatibility to address security vulnerabilities (#82) * Update dependencies and Python compatibility Change pip install to pip3 install in bootstrap script for clarity. Remove future package with security vulnerabilities from setup.py dependencies. Update tox.ini to support Python 3.8 to 3.12 and remove older Python versions. Refactor client.py to eliminate usage of future library, replacing it with standard library methods. * Re-add python 3.7 environment to tox config * Re-add all python 3 versions to tox config --------- Co-authored-by: Arpit Shah <[email protected]> * added error handing for non existent groups and zones * updated documentation, updated conditonals and logging for update_record_owner.py * added additional checks for update owner group script * more logging * omit duplicate records --------- Co-authored-by: Arpit Shah <[email protected]> Co-authored-by: Josh Edwards <[email protected]>
1 parent 140d0b2 commit 45b4cdd

File tree

4 files changed

+857
-0
lines changed

4 files changed

+857
-0
lines changed

scripts/search_records_by_name.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import argparse
2+
import csv
3+
import logging
4+
import os
5+
import json
6+
import sys
7+
from typing import Any
8+
from datetime import datetime
9+
from vinyldns.client import VinylDNSClient
10+
11+
"""
12+
Fetches matching DNS records from VinylDNS using a record name filter.
13+
Outputs result in JSON format to stdout and writes to a timestamped CSV file.
14+
15+
Environment variables must be set for VinylDNS authentication:
16+
- VINYLDNS_HOST
17+
- VINYLDNS_ACCESS_KEY
18+
- VINYLDNS_SECRET_KEY
19+
20+
Usage:
21+
python search_records_by_name.py <record_name_filter>
22+
23+
Output example:
24+
fqdn,type,record_data
25+
test.example.com.,A,1.2.3.4
26+
4.3.2.1.in-addr.arpa.,PTR,test.example.com.
27+
"""
28+
29+
REQUIRED_ENV_VARS = ["VINYLDNS_HOST", "VINYLDNS_ACCESS_KEY", "VINYLDNS_SECRET_KEY"]
30+
31+
logging.basicConfig(
32+
level=logging.INFO,
33+
format="%(asctime)s [%(levelname)s] %(message)s",
34+
datefmt="%Y-%m-%d %H:%M:%S"
35+
)
36+
37+
38+
def safe_get_env_vars(env_vars: list[str]) -> dict[str, str]:
39+
"""
40+
Validate that all required environment variables exist and return their values.
41+
42+
Args:
43+
env_vars (list[str]): List of environment variable names (strings) to check and retrieve.
44+
45+
Returns:
46+
dict[str, str]: Dictionary mapping environment variable names to their string values.
47+
48+
Raises:
49+
EnvironmentError: If any of the required environment variables are missing.
50+
"""
51+
missing = [v for v in env_vars if v not in os.environ]
52+
if missing:
53+
raise EnvironmentError(f"Missing env vars: {missing}")
54+
return {v: os.environ[v] for v in env_vars}
55+
56+
57+
def format_record_data(record_type: str, record: Any) -> str:
58+
"""
59+
Format DNS record data into a human-readable string based on its type.
60+
61+
Args:
62+
record_type (str): The DNS record type (e.g., 'A', 'MX', 'TXT').
63+
record (Any): The record object containing fields relevant to the type.
64+
65+
Returns:
66+
str: A formatted string describing the record contents.
67+
Returns an informative message if the record type is unsupported.
68+
"""
69+
formatters = {
70+
"A": lambda r: r.address,
71+
"AAAA": lambda r: r.address,
72+
"PTR": lambda r: r.ptrdname,
73+
"CNAME": lambda r: r.cname,
74+
"NS": lambda r: r.nsdname,
75+
"TXT": lambda r: r.text,
76+
"SPF": lambda r: r.text,
77+
"SRV": lambda r: (
78+
f"priority: {r.priority}, weight: {r.weight}, "
79+
f"target: {r.target}, port: {r.port}"
80+
),
81+
"MX": lambda r: (
82+
f"preference: {r.preference}, exchange: {r.exchange}"
83+
),
84+
"SOA": lambda r: (
85+
f"mname: {r.mname}, rname: {r.rname}, serial: {r.serial}, "
86+
f"refresh: {r.refresh}, retry: {r.retry}, expire: {r.expire}, "
87+
f"minimum: {r.minimum}"
88+
),
89+
"SSHFP": lambda r: (
90+
f"algorithm: {r.algorithm}, type: {r.type}, fingerprint: {r.fingerprint}"
91+
)
92+
}
93+
if record_type not in formatters:
94+
logging.warning(f"Could not retrieve record data for unsupported record type: {record_type}")
95+
return f"could not retrieve record data for record type: {record_type}"
96+
97+
return formatters[record_type](record)
98+
99+
100+
def search_records(client: VinylDNSClient, record_name_filter: str) -> list[dict[str, str]]:
101+
"""
102+
Search VinylDNS for DNS records matching the given record name filter.
103+
104+
Args:
105+
client (VinylDNSClient): Initialized VinylDNS client instance.
106+
record_name_filter (str): Filter string to match record names/domains.
107+
108+
Returns:
109+
list[dict[str, str]]: List of dictionaries where each dictionary contains:
110+
- 'fqdn': Fully qualified domain name of the record set.
111+
- 'type': DNS record type as a string.
112+
- 'record_data': Formatted string with record-specific data.
113+
"""
114+
all_records = []
115+
seen_records = set()
116+
next_id = None
117+
118+
while True:
119+
response = client.search_record_sets(start_from=next_id, record_name_filter=record_name_filter)
120+
for record_set in response.record_sets:
121+
record_type = record_set.type
122+
for record in record_set.records:
123+
# check for duplicate records
124+
key = (record_set.fqdn, record_set.type, format_record_data(record_type, record))
125+
if key in seen_records:
126+
continue
127+
seen_records.add(key)
128+
record_info = {
129+
"fqdn": record_set.fqdn,
130+
"type": record_set.type,
131+
"record_data": format_record_data(record_type, record)
132+
}
133+
all_records.append(record_info)
134+
next_id = response.next_id
135+
if not next_id:
136+
break
137+
if not all_records:
138+
logging.error(f"No records found matching filter {record_name_filter}")
139+
140+
return all_records
141+
142+
143+
def write_records_to_csv(records: list[dict[str, str]], record_name_filter: str, output_dir: str = "output") -> str:
144+
"""
145+
Write a list of DNS record dictionaries to a timestamped CSV file.
146+
147+
Args:
148+
records (list[dict[str, str]]): List of record dictionaries to write to CSV.
149+
record_name_filter (str): Filter string used to name the CSV file safely.
150+
output_dir (str, optional): Directory to save the CSV file. Defaults to "output".
151+
152+
Returns:
153+
str: Full file path of the written CSV file.
154+
155+
Raises:
156+
OSError: If there is an error creating the directory or writing the CSV file,
157+
such as permission errors or disk IO problems.
158+
"""
159+
os.makedirs(output_dir, exist_ok=True)
160+
161+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
162+
safe_record_name_filter = record_name_filter.rstrip('.').replace('.', '_')
163+
filename = f"{safe_record_name_filter}_records_{timestamp}.csv"
164+
filepath = os.path.join(output_dir, filename)
165+
166+
fieldnames = ["fqdn", "type", "record_data"]
167+
try:
168+
with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
169+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
170+
writer.writeheader()
171+
for record in records:
172+
writer.writerow(record)
173+
logging.info(f"CSV file successfully written: {filepath}")
174+
except OSError as e:
175+
logging.error(f"Failed to write CSV file {filepath}: {e}")
176+
raise
177+
178+
return filepath
179+
180+
181+
def main() -> None:
182+
parser = argparse.ArgumentParser(
183+
description="Search VinylDNS records by name filter and output results."
184+
)
185+
parser.add_argument(
186+
"record_name_filter",
187+
type=str,
188+
help="Record name filter for DNS query"
189+
)
190+
args = parser.parse_args()
191+
192+
try:
193+
env_vars = safe_get_env_vars(REQUIRED_ENV_VARS)
194+
client = VinylDNSClient(
195+
env_vars["VINYLDNS_HOST"],
196+
env_vars["VINYLDNS_ACCESS_KEY"],
197+
env_vars["VINYLDNS_SECRET_KEY"],
198+
)
199+
200+
records = search_records(client, args.record_name_filter)
201+
202+
print(json.dumps(records, indent=2))
203+
204+
write_records_to_csv(records, args.record_name_filter)
205+
206+
except EnvironmentError as env_err:
207+
logging.error(f"Environment error: {env_err}")
208+
sys.exit(3)
209+
except Exception as e:
210+
logging.error(f"Unexpected error: {e}", exc_info=True)
211+
sys.exit(4)
212+
213+
214+
if __name__ == "__main__":
215+
main()

0 commit comments

Comments
 (0)