|
1 | 1 | #!/usr/bin/env python3
|
2 |
| -import argparse |
3 |
| -from collections import defaultdict |
4 |
| -from faros_config import FarosConfig, PydanticEncoder |
5 |
| -import ipaddress |
6 |
| -import json |
7 |
| -import os |
8 | 2 | import sys
|
9 |
| -import pickle |
10 | 3 |
|
11 |
| -SSH_PRIVATE_KEY = '/data/id_rsa' |
12 |
| -IP_RESERVATIONS = '/data/ip_addresses' |
| 4 | +from faros_config.inventory.cli import main |
13 | 5 |
|
14 |
| - |
15 |
| -class InventoryGroup(object): |
16 |
| - |
17 |
| - def __init__(self, parent, name): |
18 |
| - self._parent = parent |
19 |
| - self._name = name |
20 |
| - |
21 |
| - def add_group(self, name, **groupvars): |
22 |
| - return(self._parent.add_group(name, self._name, **groupvars)) |
23 |
| - |
24 |
| - def add_host(self, name, hostname=None, **hostvars): |
25 |
| - return(self._parent.add_host(name, self._name, hostname, **hostvars)) |
26 |
| - |
27 |
| - def host(self, name): |
28 |
| - return self._parent.host(name) |
29 |
| - |
30 |
| - |
31 |
| -class Inventory(object): |
32 |
| - |
33 |
| - _modes = ['list', 'host', 'verify', 'none'] |
34 |
| - _data = {"_meta": {"hostvars": defaultdict(dict)}} |
35 |
| - |
36 |
| - def __init__(self, mode=0, host=None): |
37 |
| - if mode == 1: |
38 |
| - # host info requested |
39 |
| - # current, only list and none are implimented |
40 |
| - raise NotImplementedError() |
41 |
| - |
42 |
| - self._mode = mode |
43 |
| - self._host = host |
44 |
| - |
45 |
| - def host(self, name): |
46 |
| - return self._data['_meta']['hostvars'].get(name) |
47 |
| - |
48 |
| - def group(self, name): |
49 |
| - if name in self._data: |
50 |
| - return InventoryGroup(self, name) |
51 |
| - else: |
52 |
| - return None |
53 |
| - |
54 |
| - def add_group(self, name, parent=None, **groupvars): |
55 |
| - self._data[name] = {'hosts': [], 'vars': groupvars, 'children': []} |
56 |
| - |
57 |
| - if parent: |
58 |
| - if parent not in self._data: |
59 |
| - self.add_group(parent) |
60 |
| - self._data[parent]['children'].append(name) |
61 |
| - |
62 |
| - return InventoryGroup(self, name) |
63 |
| - |
64 |
| - def add_host(self, name, group=None, hostname=None, **hostvars): |
65 |
| - if not group: |
66 |
| - group = 'all' |
67 |
| - if group not in self._data: |
68 |
| - self.add_group(group) |
69 |
| - |
70 |
| - if hostname: |
71 |
| - hostvars.update({'ansible_host': hostname}) |
72 |
| - |
73 |
| - self._data[group]['hosts'].append(name) |
74 |
| - self._data['_meta']['hostvars'][name].update(hostvars) |
75 |
| - |
76 |
| - def to_json(self): |
77 |
| - return json.dumps(self._data, sort_keys=True, indent=4, |
78 |
| - separators=(',', ': '), cls=PydanticEncoder) |
79 |
| - |
80 |
| - |
81 |
| -class IPAddressManager(dict): |
82 |
| - |
83 |
| - def __init__(self, save_file, subnet): |
84 |
| - super().__init__() |
85 |
| - self._save_file = save_file |
86 |
| - |
87 |
| - # parse the subnet definition into a static and dynamic pool |
88 |
| - divided = subnet.subnets() |
89 |
| - self._static_pool = next(divided) |
90 |
| - self._dynamic_pool = next(divided) |
91 |
| - self._generator = self._static_pool.hosts() |
92 |
| - |
93 |
| - # calculate reverse dns zone |
94 |
| - classful_prefix = [32, 24, 16, 8, 0] |
95 |
| - classful = subnet |
96 |
| - while classful.prefixlen not in classful_prefix: |
97 |
| - classful = classful.supernet() |
98 |
| - host_octets = classful_prefix.index(classful.prefixlen) |
99 |
| - self._reverse_ptr_zone = \ |
100 |
| - '.'.join(classful.reverse_pointer.split('.')[host_octets:]) |
101 |
| - |
102 |
| - # load the last saved state |
103 |
| - try: |
104 |
| - restore = pickle.load(open(save_file, 'rb')) |
105 |
| - except: # noqa: E722 |
106 |
| - restore = {} |
107 |
| - self.update(restore) |
108 |
| - |
109 |
| - # reserve the first ip for the bastion |
110 |
| - _ = self['bastion'] |
111 |
| - |
112 |
| - def __getitem__(self, key): |
113 |
| - key = key.lower() |
114 |
| - try: |
115 |
| - return super().__getitem__(key) |
116 |
| - except KeyError: |
117 |
| - new_ip = self._next_ip() |
118 |
| - self[key] = new_ip |
119 |
| - return new_ip |
120 |
| - |
121 |
| - def __setitem__(self, key, value): |
122 |
| - return super().__setitem__(key.lower(), value) |
123 |
| - |
124 |
| - def _next_ip(self): |
125 |
| - used_ips = list(self.values()) |
126 |
| - loop = True |
127 |
| - |
128 |
| - while loop: |
129 |
| - new_ip = next(self._generator).exploded |
130 |
| - loop = new_ip in used_ips |
131 |
| - return new_ip |
132 |
| - |
133 |
| - def get(self, key, value=None): |
134 |
| - if value and value not in self.values(): |
135 |
| - self[key] = value |
136 |
| - return self[key] |
137 |
| - |
138 |
| - def save(self): |
139 |
| - with open(self._save_file, 'wb') as handle: |
140 |
| - pickle.dump(dict(self), handle) |
141 |
| - |
142 |
| - @property |
143 |
| - def static_pool(self): |
144 |
| - return str(self._static_pool) |
145 |
| - |
146 |
| - @property |
147 |
| - def dynamic_pool(self): |
148 |
| - return str(self._dynamic_pool) |
149 |
| - |
150 |
| - @property |
151 |
| - def reverse_ptr_zone(self): |
152 |
| - return str(self._reverse_ptr_zone) |
153 |
| - |
154 |
| - |
155 |
| -class Config(object): |
156 |
| - shim_var_keys = [ |
157 |
| - 'WAN_INT', |
158 |
| - 'BASTION_IP_ADDR', |
159 |
| - 'BASTION_INTERFACES', |
160 |
| - 'BASTION_HOST_NAME', |
161 |
| - 'BASTION_SSH_USER', |
162 |
| - 'CLUSTER_DOMAIN', |
163 |
| - 'CLUSTER_NAME', |
164 |
| - 'BOOT_DRIVE', |
165 |
| - ] |
166 |
| - |
167 |
| - def __init__(self, yaml_file): |
168 |
| - self.shim_vars = {} |
169 |
| - for var in self.shim_var_keys: |
170 |
| - self.shim_vars[var] = os.getenv(var) |
171 |
| - config = FarosConfig.from_yaml(yaml_file) |
172 |
| - self.network = config.network |
173 |
| - self.bastion = config.bastion |
174 |
| - self.cluster = config.cluster |
175 |
| - self.proxy = config.proxy |
176 |
| - |
177 |
| - |
178 |
| -def parse_args(): |
179 |
| - parser = argparse.ArgumentParser() |
180 |
| - parser.add_argument('--list', action='store_true') |
181 |
| - parser.add_argument('--verify', action='store_true') |
182 |
| - parser.add_argument('--host', action='store') |
183 |
| - args = parser.parse_args() |
184 |
| - return args |
185 |
| - |
186 |
| - |
187 |
| -def main(config, ipam, inv): |
188 |
| - # GATHER INFORMATION FOR EXTRA NODES |
189 |
| - for node in config.network.lan.dhcp.extra_reservations: |
190 |
| - addr = ipam.get(node.mac, str(node.ip)) |
191 |
| - node.ip = ipaddress.IPv4Address(addr) |
192 |
| - |
193 |
| - # CREATE INVENTORY |
194 |
| - inv.add_group( |
195 |
| - 'all', None, |
196 |
| - ansible_ssh_private_key_file=SSH_PRIVATE_KEY, |
197 |
| - cluster_name=config.shim_vars['CLUSTER_NAME'], |
198 |
| - cluster_domain=config.shim_vars['CLUSTER_DOMAIN'], |
199 |
| - admin_password=config.bastion.become_pass, |
200 |
| - pull_secret=json.loads(config.cluster.pull_secret), |
201 |
| - mgmt_provider=config.cluster.management.provider, |
202 |
| - mgmt_user=config.cluster.management.user, |
203 |
| - mgmt_password=config.cluster.management.password, |
204 |
| - install_disk=config.shim_vars['BOOT_DRIVE'], |
205 |
| - loadbalancer_vip=ipam['loadbalancer'], |
206 |
| - dynamic_ip_range=ipam.dynamic_pool, |
207 |
| - reverse_ptr_zone=ipam.reverse_ptr_zone, |
208 |
| - subnet=str(config.network.lan.subnet.network_address), |
209 |
| - subnet_mask=config.network.lan.subnet.prefixlen, |
210 |
| - wan_ip=config.shim_vars['BASTION_IP_ADDR'], |
211 |
| - extra_nodes=config.network.lan.dhcp.extra_reservations, |
212 |
| - ignored_macs=config.network.lan.dhcp.ignore_macs, |
213 |
| - dns_forwarders=config.network.lan.dns_forward_resolvers, |
214 |
| - proxy=config.proxy is not None, |
215 |
| - proxy_http=config.proxy.http if config.proxy is not None else '', |
216 |
| - proxy_https=config.proxy.https if config.proxy is not None else '', |
217 |
| - proxy_noproxy=config.proxy.noproxy if config.proxy is not None else [], |
218 |
| - proxy_ca=config.proxy.ca if config.proxy is not None else '' |
219 |
| - ) |
220 |
| - |
221 |
| - infra = inv.add_group('infra') |
222 |
| - router = infra.add_group( |
223 |
| - 'router', |
224 |
| - wan_interface=config.shim_vars['WAN_INT'], |
225 |
| - lan_interfaces=config.network.lan.interfaces, |
226 |
| - all_interfaces=config.shim_vars['BASTION_INTERFACES'].split(), |
227 |
| - allowed_services=config.network.port_forward |
228 |
| - ) |
229 |
| - # ROUTER INTERFACES |
230 |
| - router.add_host( |
231 |
| - 'wan', config.shim_vars['BASTION_IP_ADDR'], |
232 |
| - ansible_become_pass=config.bastion.become_pass, |
233 |
| - ansible_ssh_user=config.shim_vars['BASTION_SSH_USER'] |
234 |
| - ) |
235 |
| - router.add_host( |
236 |
| - 'lan', |
237 |
| - ipam['bastion'], |
238 |
| - ansible_become_pass=config.bastion.become_pass, |
239 |
| - ansible_ssh_user=config.shim_vars['BASTION_SSH_USER'] |
240 |
| - ) |
241 |
| - # DNS NODE |
242 |
| - router.add_host( |
243 |
| - 'dns', |
244 |
| - ipam['bastion'], |
245 |
| - ansible_become_pass=config.bastion.become_pass, |
246 |
| - ansible_ssh_user=config.shim_vars['BASTION_SSH_USER'] |
247 |
| - ) |
248 |
| - # DHCP NODE |
249 |
| - router.add_host( |
250 |
| - 'dhcp', |
251 |
| - ipam['bastion'], |
252 |
| - ansible_become_pass=config.bastion.become_pass, |
253 |
| - ansible_ssh_user=config.shim_vars['BASTION_SSH_USER'] |
254 |
| - ) |
255 |
| - # LOAD BALANCER NODE |
256 |
| - router.add_host( |
257 |
| - 'loadbalancer', |
258 |
| - ipam['loadbalancer'], |
259 |
| - ansible_become_pass=config.bastion.become_pass, |
260 |
| - ansible_ssh_user=config.shim_vars['BASTION_SSH_USER'] |
261 |
| - ) |
262 |
| - |
263 |
| - # BASTION NODE |
264 |
| - bastion = infra.add_group('bastion_hosts') |
265 |
| - bastion.add_host( |
266 |
| - config.shim_vars['BASTION_HOST_NAME'], |
267 |
| - ipam['bastion'], |
268 |
| - ansible_become_pass=config.bastion.become_pass, |
269 |
| - ansible_ssh_user=config.shim_vars['BASTION_SSH_USER'] |
270 |
| - ) |
271 |
| - |
272 |
| - # CLUSTER NODES |
273 |
| - cluster = inv.add_group('cluster') |
274 |
| - # BOOTSTRAP NODE |
275 |
| - ip = ipam['bootstrap'] |
276 |
| - cluster.add_host( |
277 |
| - 'bootstrap', ip, |
278 |
| - ansible_ssh_user='core', |
279 |
| - node_role='bootstrap' |
280 |
| - ) |
281 |
| - # CLUSTER CONTROL PLANE NODES |
282 |
| - cp = cluster.add_group('control_plane', node_role='master') |
283 |
| - for count, node in enumerate(config.cluster.nodes): |
284 |
| - ip = ipam[node.mac] |
285 |
| - mgmt_ip = ipam[node.mgmt_mac] |
286 |
| - cp.add_host( |
287 |
| - node.name, ip, |
288 |
| - mac_address=node.mac, |
289 |
| - mgmt_mac_address=node.mgmt_mac, |
290 |
| - mgmt_hostname=mgmt_ip, |
291 |
| - ansible_ssh_user='core', |
292 |
| - cp_node_id=count |
293 |
| - ) |
294 |
| - if node.install_drive is not None: |
295 |
| - cp.host(node['name'])['install_disk'] = node.install_drive |
296 |
| - |
297 |
| - # VIRTUAL NODES |
298 |
| - virt = inv.add_group( |
299 |
| - 'virtual', |
300 |
| - mgmt_provider='kvm', |
301 |
| - mgmt_hostname='bastion', |
302 |
| - install_disk='vda' |
303 |
| - ) |
304 |
| - virt.add_host('bootstrap') |
305 |
| - |
306 |
| - # MGMT INTERFACES |
307 |
| - mgmt = inv.add_group( |
308 |
| - 'management', |
309 |
| - ansible_ssh_user=config.cluster.management.user, |
310 |
| - ansible_ssh_pass=config.cluster.management.password |
311 |
| - ) |
312 |
| - for node in config.cluster.nodes: |
313 |
| - mgmt.add_host( |
314 |
| - node.name + '-mgmt', ipam[node.mgmt_mac], |
315 |
| - mac_address=node.mgmt_mac |
316 |
| - ) |
317 |
| - |
318 |
| - |
319 |
| -if __name__ == "__main__": |
320 |
| - # PARSE ARGUMENTS |
321 |
| - args = parse_args() |
322 |
| - if args.list: |
323 |
| - mode = 0 |
324 |
| - elif args.verify: |
325 |
| - mode = 2 |
326 |
| - else: |
327 |
| - mode = 1 |
328 |
| - |
329 |
| - # INTIALIZE CONFIG |
330 |
| - config = Config('/data/config.yml') |
331 |
| - |
332 |
| - # INTIALIZE IPAM |
333 |
| - ipam = IPAddressManager( |
334 |
| - IP_RESERVATIONS, |
335 |
| - config.network.lan.subnet |
336 |
| - ) |
337 |
| - |
338 |
| - # INITIALIZE INVENTORY |
339 |
| - inv = Inventory(mode, args.host) |
340 |
| - |
341 |
| - # CREATE INVENTORY |
342 |
| - try: |
343 |
| - main(config, ipam, inv) |
344 |
| - if mode == 0: |
345 |
| - print(inv.to_json()) |
346 |
| - |
347 |
| - except Exception as e: |
348 |
| - if mode == 2: |
349 |
| - sys.stderr.write(config.error) |
350 |
| - sys.exit(1) |
351 |
| - raise(e) |
352 |
| - |
353 |
| - # DONE |
354 |
| - ipam.save() |
355 |
| - sys.exit(0) |
| 6 | +main(sys.argv[1:]) |
0 commit comments