Skip to content

Commit 3a32497

Browse files
committedOct 31, 2021
First commit, easy Dynatrace locations manager
1 parent 7081909 commit 3a32497

6 files changed

+237
-2
lines changed
 

‎README.md

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,51 @@
1-
# dynatrace-privatelocation-sync
2-
Ad-Hoc Python script that syncs Synthetic nodes to locations based on metadata
1+
<h4 align="center">
2+
<img alt="header pic" src="src/dynatrace_logo.png">
3+
</h4>
4+
5+
# Dynatrace Private Synthetic Locations Sync
6+
7+
8+
At the moment, Dynatrace does not support any sembience of linking a private synthetics agent to a 'location' at runtime - this is a huge problem in a majority of cases (including where you run these sort of agents in a scaled manner, each Private instance you scale out to must be manually registered to a location, which is both annoying and problematic at scale)
9+
10+
this script was made to attempt to combat that, and allows for Private Synthetics Locations to be automatically updated and definedbased on specific metadata that the node reports in the [**Synthetic Node API**](https://www.dynatrace.com/support/help/dynatrace-api/environment-api/synthetic/synthetic-nodes/get-node/) - Today this includes
11+
12+
- IP Block type (E.g. IP Prefix)
13+
- Synthetic Node Name
14+
15+
## Quickstart
16+
17+
By default, the script expects the following
18+
- Environment Variables `dynatracetoken` and `dynatracetenant` are set
19+
- Proper metadata provided in the `locations` folder exists
20+
21+
a easy quickstart would be to define these locally, and run the example script
22+
23+
```bash
24+
export dynatracetoken='mycooltoken'
25+
export dynatracetenant='isa2131'
26+
python3 locationsManager.py
27+
```
28+
29+
## Adding/Removing Definitions
30+
31+
Definitions of a Private Synthetic Location group are managed within the 'locations' folder of this repository - when parsing through this folder the script will
32+
- Fetch all files within the folder
33+
- Dynamically pull 'SyntheticData' configuration from each folder
34+
35+
Definitions can be grouped in whatever way you wish, ideally i'd recommend grouping them based on 'environment' (so eng, nonp, and prod respectively)
36+
37+
```yaml
38+
metadata:
39+
# Dictates whether we parse over the file
40+
Active: True
41+
Name: "Production Location Metadata"
42+
Type: ipBlock
43+
44+
syntheticData:
45+
# Address we look for
46+
- '172.16':
47+
# Custom prefix name (only used in logging)
48+
prefixName: 'Private 172 Address space'
49+
# This is the Synthetic Location ID as it appears in the API
50+
syntheticLocation: 'SYNTHETIC_LOCATION-AAAA'
51+
```

‎locations/example-locationblock.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
metadata:
2+
Active: True
3+
Name: "Production Location Metadata"
4+
Type: ipBlock
5+
6+
syntheticData:
7+
- '172.16':
8+
prefixName: 'Private 172 Address space'
9+
# This is the Synthetic Location ID as it appears in the API
10+
syntheticLocation: 'SYNTHETIC_LOCATION-AAAA'
11+
12+
- '192.168':
13+
prefixName: 'Private 192 Address space'
14+
# This is the Synthetic Location ID as it appears in the API
15+
syntheticLocation: 'SYNTHETIC_LOCATION-BBBB'

‎locationsManager.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import logging
2+
3+
# Initialize Logger
4+
logger = logging.getLogger('main')
5+
logger.setLevel(logging.INFO)
6+
logger.propagate = False
7+
8+
from manager import parser, dynatrace_utils
9+
10+
if __name__ == '__main__':
11+
args = parser.LocationArguments()
12+
location_management = dynatrace_utils.locationsManager(args)
13+
location_management.parse_metadata()

‎manager/dynatrace_utils.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import requests
2+
import logging
3+
import sys
4+
from collections import defaultdict
5+
6+
# Substantiate logger
7+
logger = logging.getLogger(__name__)
8+
logger.setLevel(logging.INFO)
9+
logger.addHandler(logging.StreamHandler(sys.stdout))
10+
11+
class locationsManager():
12+
"""
13+
locationsManager - Primary handler for Deployment of Locations API
14+
15+
"""
16+
17+
def __init__(self, args):
18+
self.dynatrace_tenant = args.dyantrace_tenant
19+
# NOTE: These are based on Dynatrace Cloud, change these URL formats if you are using Managed SaaS
20+
self.dynatraceURL = {
21+
'node': f"https://{self.dynatrace_tenant}.live.dynatrace.com/api/v1/synthetic/nodes",
22+
'location': f"https://{self.dynatrace_tenant}.live.dynatrace.com/api/v1/synthetic/nodes",
23+
'default': f"https://{self.dynatrace_tenant}.live.dynatrace.com"
24+
}
25+
self.dynatracecredentals = args.dynatracecredentials
26+
self.metadict = args.metadict
27+
28+
def parse_metadata(self):
29+
"""
30+
parse_metadata - this function fetches each Private Synthetic Location + Node and returns a list of nodes to update
31+
"""
32+
nodes_to_update = defaultdict(list)
33+
node_information = self.__fetch_node_block()
34+
for node in node_information['nodes']:
35+
logger.info(f"Finding IpBlock for Synthetic Location to {node['hostname']}")
36+
for ip_block in node['ips']:
37+
item = self.__parse_ipblock(ip_block)
38+
if item:
39+
if item.get('syntheticLocation'):
40+
logger.info(f"Node IP {ip_block} is in block itme {item['prefixName']}")
41+
nodes_to_update[item['syntheticLocation']].append(node['entityId'])
42+
# Once all the nodes are identified, return the list and patch all synthetic locatiosn
43+
self.__patch_synthetic_location(nodes_to_update)
44+
45+
def __parse_ipblock(self, ip_block):
46+
for block in self.metadict:
47+
# if we notice the block is part of the IP
48+
if '.'.join(ip_block.split('.'[0:2])) in block.keys():
49+
logger.debug(f"IpBlock is in {block['prefixName']} - Adding synthetic agent list to update")
50+
return(block)
51+
else:
52+
logger.debug(f"IpBlock is not in {block['prefixName']} - continuing")
53+
54+
def __fetch_node_block(self):
55+
# Static function that fetches all synthetic nodes, and returns them in dict format
56+
item = requests.get(
57+
url=self.dynatraceURL['node'],
58+
headers={ 'Authorization': f"Api-Token {self.dynatracecredentals['token']}", 'Content-Type': 'application/json'}
59+
)
60+
return item.json()
61+
62+
def __fetch_synthetic_location(self, location_name):
63+
item = requests.get(
64+
url=f"{self.dynatraceURL['location']}/{location_name}",
65+
headers={ 'Authorization': f"Api-Token {self.dynatracecredentals['token']}", 'Content-Type': 'application/json'}
66+
)
67+
return item.json()
68+
69+
def __patch_synthetic_location(self, nodes_to_update):
70+
# adds a PUT to synthetic nodes, returns them in a dict format
71+
for synthetic_location, nodes in nodes_to_update.items():
72+
# first, fetch the existing synthetic location ID to get the existing metadata to include in PUT request
73+
synthetic_location_data = self.__fetch_synthetic_location(synthetic_location)
74+
logger.info(f"Updating {synthetic_location} with nodes {nodes}")
75+
synthetic_location_data['nodes'] = nodes
76+
item = requests.put(
77+
url=f"{self.dynatraceURL['location']}/{synthetic_location}",
78+
headers={ 'Authorization': f"Api-Token {self.dynatracecredentals['token']}", "Content-Type": "application/json"},
79+
json=synthetic_location_data
80+
)
81+
item.raise_for_status()

‎manager/parser.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import os
2+
import logging
3+
import sys
4+
import yaml
5+
6+
# Fetch StreamHandler from root
7+
8+
logger = logging.getLogger(__name__)
9+
logger.setLevel(logging.INFO)
10+
logger.addHandler(logging.StreamHandler(sys.stdout))
11+
12+
def is_local():
13+
# Small function to determine if we're running locally
14+
if os.environ.get('local'):
15+
return True
16+
else:
17+
return False
18+
19+
class LocationArguments():
20+
""" Primary Location Argument Definitions
21+
22+
Parses all YAML files within a specific directory, along with local arguments, and returns a classful definition of all arguments
23+
24+
"""
25+
def __init__(self):
26+
self.is_local = is_local()
27+
self.__parse_dir_arguments()
28+
self.__parse_meta_config()
29+
self.dynatrace_credentials = {
30+
'token': os.environ.get('dynatracetoken', '')
31+
}
32+
self.dyantrace_tenant = os.environ.get('dynatracetenant', 'replaceme')
33+
34+
# Warn about any missing vars
35+
requiredVars = ['dynatracetoken', 'dynatracetenant']
36+
for var in requiredVars:
37+
if os.environ.get(var) is None:
38+
logger.warning(f"Environment variable {var} appears to be missing - attempting to use default value")
39+
40+
def __parse_dir_arguments(self):
41+
# Static DIR arguments, determines the location that we parse YAML files upon
42+
self.metapath = './locations'
43+
44+
def __parse_meta_config(self):
45+
"""
46+
__parse_meta_config - Generates a large 'meta' dict in response to parsing each of the metadata files
47+
48+
"""
49+
metadata_files = []
50+
try:
51+
for dirName, subdirList, fileList in os.walk(self.metapath):
52+
for file in fileList:
53+
if file.endswith('.yaml'):
54+
logger.info(f"Found MetaData File: {file}")
55+
metadata_files.append(file)
56+
except Exception as err:
57+
if self.is_local:
58+
logger.error(err)
59+
raise ValueError(f"Unable to parse directory {self.metapath} - raising error")
60+
else:
61+
logger.error(err)
62+
logger.warning(f"Unable to parse {self.metapath} - skipping...")
63+
64+
# Now that we have a generic listing of each metadata path, parse each of those files into a single config dict
65+
config = []
66+
for file in metadata_files:
67+
with open(f"{self.metapath}/{file}", 'r') as stream:
68+
try:
69+
localconfig = yaml.safe_load(stream)
70+
if localconfig['metadata']['Active']:
71+
config.extend(localconfig.pop('syntheticData'))
72+
else:
73+
logger.info(f"Locations file {file} is not marked as 'active' under metadata -> Active, ignoring")
74+
except yaml.YAMLError as err:
75+
logger.error(err)
76+
logger.warning(f"Unable to parse {self.metapath}/{file} - skipping...")
77+
self.metadict = config

‎src/dynatrace_logo.png

23.1 KB
Loading

0 commit comments

Comments
 (0)
Please sign in to comment.