-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathnd_inv.py
357 lines (304 loc) · 9.9 KB
/
nd_inv.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
#!/usr/bin/env python
import psycopg2
import psycopg2.extras
import yaml
import re
from jinja2 import Template
import json
import argparse
import sys
import os
class ScriptConfiguration:
def __init__(self, filename):
self.inputs = {}
self.outputs = {}
self.type_sw = {
'psql': psql,
'AnsibleJSON': AnsibleJSON,
'AnsibleINI': AnsibleINI,
}
yaml_file = open(filename, 'r')
try:
yaml_obj = yaml.safe_load(yaml_file)
except yaml.YAMLError as err:
print(err)
exit()
# Validate
if 'Input' not in yaml_obj:
raise ValueError("Invalid YAML format - missing data")
if 'Output' not in yaml_obj:
raise ValueError("Invalid YAML format - missing data")
# Load inputs
for name, attrs in yaml_obj['Input'].items():
t = attrs['type']
if "use" in attrs:
i = self.type_sw[t](attrs, use=yaml_obj['Input'][attrs["use"]])
else:
i = self.type_sw[t](attrs)
self.inputs[name] = i
for name, attrs in yaml_obj['Output'].items():
t = attrs['type']
i = self.type_sw[t](attrs)
self.outputs[name] = i
# Only honor the passed input
def filter_outputs(self, output):
self.outputs = {
output: self.outputs[output]
}
def generate(self):
# Init inputs
for name, i in self.inputs.items():
data = i.get()
data = i.transform(data)
grouped = i.group(data)
for name, o in self.outputs.items():
if "host_vars" in i.config:
host_vars = i.vars(data)
o.add_host_vars(host_vars)
o.add_grouped_data(grouped)
# Write outputs
for name, o in self.outputs.items():
o.out()
# Base class for Output types
class Output(object):
def __init__(self, config):
struct = [
'type'
]
self.transforms = []
for k in struct:
if k not in config:
raise ValueError("Invalid YAML - missing {0} in struct".format(k))
self.config = config
def get(self):
return
def out(self):
if "file" in self.config:
f = open(self.config["file"], "w")
f.write(self.get())
else:
print(self.get())
# Ansible formatted JSON output method
class AnsibleJSON(Output):
def __init__(self, config):
super(AnsibleJSON, self).__init__(config)
self.formatted = {}
self.hostvars = {}
# Add grouped data
def add_grouped_data(self, grouped):
for group, hosts in grouped.items():
struct = {
'hosts': hosts,
}
self.formatted[group] = struct
def add_host_vars(self, vars):
self.hostvars = vars
def dump(self):
# Required to prevent --host being called for every host
print(json.dumps(self.formatted, indent=4, sort_keys=True, separators=(',', ': ')))
def prints(self):
self.formatted['_meta'] = {
'hostvars': self.hostvars
}
print(json.dumps(self.formatted))
def get(self):
self.formatted['_meta'] = {
'hostvars': self.hostvars
}
return json.dumps(self.formatted)
class AnsibleINI(Output):
"""
AnsibleINI Format
Example:
[ group ]
host1
host2
"""
def __init__(self, config):
super(AnsibleINI, self).__init__(config)
self.formatted = {}
self.hostvars = {}
# Add grouped data
def add_grouped_data(self, grouped):
hosts_with_vars = []
for group, hosts in grouped.items():
for host in hosts:
if host in self.hostvars:
str = self.vars_to_string(self.hostvars[host])
str = "{} {}".format(host, str)
hosts_with_vars.append(str)
else:
hosts_with_vars.append(host)
l = "\n".join(hosts_with_vars)
self.formatted[group] = l
def vars_to_string(self, vars):
pairs = []
for k, v in vars.items():
s = "{}={}".format(k, v)
pairs.append(s)
return " ".join(pairs)
def dump(self):
lines =[]
for group, hosts in self.formatted.items():
lines.append("[{0}]".format(group))
lines.append("{0}".format(hosts))
# Newline
lines.append("")
return lines
def add_host_vars(self, vars):
self.hostvars = vars
def prints(self):
for line in self.dump():
print(line)
def get(self):
s = ''
for line in self.dump():
s = s + line + "\n"
return s
# Base Class for Inputs
class Input(object):
def __init__(self, config, use=None):
"""
Base Input class
:param config: Dictionary of configuration for this input type
:param use: Dictionary of configuration to inherit
"""
struct = [
'group_field',
'host_field'
]
self.transforms = []
self.config = config
if use:
self.use(use)
for k in struct:
if k not in config:
raise ValueError("Invalid YAML - missing {0} in struct".format(k))
if 'transform' in self.config:
for transform in self.config['transform']:
t = Transform(transform)
self.transforms.append(t)
def use(self, config):
"""
If a "use" statement is configured, inherit settings that aren't configured from it
:var config (DICT): Dictionary of configuration for this input
:return: None
"""
for k, v in config.items():
if k not in self.config:
self.config[k] = v
def transform(self, data):
newdata = data
for t in self.transforms:
newdata = t.do(newdata)
return newdata
def group(self, data):
grouped = data.group(self.config['group_field'], self.config['host_field'])
return grouped
def vars(self, data):
vars = {}
for row in data.get_rows():
v = {}
for var_def in self.config['host_vars']:
col = var_def['column']
vard = var_def['var']
if col in row:
v[vard] = row[col]
vars[row[self.config['host_field']]] = v
return vars
# Postgres Input method
class psql(Input):
def __init__(self, config, use=None):
super(psql, self).__init__(config, use)
self.dsn = "dbname='{0}' user='{1}' host='{2}' password='{3}'".format(
self.config['dbname'], self.config['user'], self.config['host'], self.config['password']
)
self.query = self.config['select']
def get(self):
conn = psycopg2.connect(self.dsn)
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
cur.execute(self.query)
data = Data()
for row in cur:
# simplify the sql data
s = {}
for k, v in row.items():
s[k] = v
data.add_row(s)
return data
# Transform class, transforms data within a dict
class Transform:
def __init__(self, config):
struct = [
'field',
'regex',
'out',
]
for k in struct:
if k not in config:
raise ValueError("Invalid YAML - missing {0} in struct".format(k))
self.field = config['field']
self.regex = config['regex']
self.out = config['out']
def do(self, data):
newdata = Data()
for row in data.get_rows():
newrow = row
if self.field not in row:
raise ValueError("Invalid data in transform - field {0} does not exist".format(self.field))
v = re.match(self.regex, row[self.field])
# Note: transform filters the data such that if the regex does not match, that row is omitted
if v:
v = v.group(1)
newrow[self.out] = v
newdata.add_row(newrow)
return newdata
# Data represents the underlying input
# It is comprised of a list of dictionaries
class Data:
def __init__(self):
self.rows = []
# Host variarbles
self.vars = {}
def add_row(self, dict):
self.rows.append(dict)
def get_rows(self):
return self.rows
def group(self, group_field, host_field):
grouped = {}
for row in self.get_rows():
group = self.sub_variables(row, group_field)
host = row[host_field]
if group not in grouped:
grouped[group] = []
grouped[group].append(host)
return grouped
def sub_variables(self, row, string):
template = Template(string)
result = template.render(row)
return result
# Parse commandline arguments
parser = argparse.ArgumentParser()
parser.add_argument('--list', action="store_true", dest="list", help="List output")
parser.add_argument('--host', action="store_true", dest="host", help="Host vars")
parser.add_argument('--output', action="store", dest="output", help="Only run a specific output.")
args = parser.parse_args()
# Dump json
rundir = sys.path[0]
# Config file is one directory back usually, in the root of the ansible directory
scf = os.path.join(rundir, "..", "inv.yml")
# If it isn't then we use the current directory.
if not os.path.exists(scf):
scf = os.path.join("inv.yml")
if args.list:
c = ScriptConfiguration(scf)
c.generate()
elif args.output:
c = ScriptConfiguration(scf)
c.filter_outputs(args.output)
c.generate()
# dump nothing
elif args.host:
print({})
else:
raise ValueError("Missing arguments!")