|
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