diff --git a/.gitignore b/.gitignore index 46b4574..bd406d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .pyo __pycache__ +*.egg-info *.swp data venv diff --git a/Dockerfile b/Dockerfile index 95d1e3d..81adceb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,28 +13,27 @@ RUN mkdir -p /deps/python /deps/ansible; \ COPY version.txt /version.txt COPY requirements.txt /deps/python_requirements.txt COPY requirements.yml /deps/ansible_requirements.yml -RUN microdnf update; \ +COPY home /root +COPY pre /pre + +RUN set -ex; \ + microdnf update; \ microdnf install python3 jq openssh-clients tar sshpass findutils telnet less ncurses; \ pip3 install --user -r /deps/python_requirements.txt; \ ansible-galaxy collection install -r /deps/ansible_requirements.yml; \ microdnf clean all; \ - rm -rf /var/cache/yum /tmp/* /root/.cache /usr/lib/python3.8/site-packages /usr/lib64/python3.8/__pycache__; - -# Install application -WORKDIR /app -COPY app /app -COPY data.skel /data.skel -COPY home /root - -# Initialize application -RUN rpm -i /app/tmp/ilorest-3.0.1-7.x86_64.rpm; \ - chmod -Rv g-rwx /root/.ssh; chmod -Rv o-rwx /root/.ssh; \ - rm -rf /app/tmp; \ + rpm -i /pre/ilorest-3.0.1-7.x86_64.rpm; \ + rm -rf /pre /var/cache/dnf /var/cache/yum /tmp/* /root/.cache /usr/lib/python3.8/site-packages /usr/lib64/python3.8/__pycache__; \ + chmod -Rv g-rwx,o-rwx /root/.ssh; \ cd /usr/local/bin; \ curl https://mirror.openshift.com/pub/openshift-v4/clients/ocp/latest/openshift-client-linux.tar.gz | tar xvzf -; \ curl https://raw.githubusercontent.com/project-faros/farosctl/master/bin/farosctl > farosctl; \ chmod 755 farosctl; +# Install application +COPY data.skel /data.skel +COPY app /app +WORKDIR /app + ENTRYPOINT ["/app/bin/entry.sh"] CMD ["/app/bin/run.sh"] - diff --git a/app/bin/entry.sh b/app/bin/entry.sh index a543dfb..365cf3b 100755 --- a/app/bin/entry.sh +++ b/app/bin/entry.sh @@ -4,9 +4,6 @@ if [ "$1" != "cat" ] && [ "$1" != "ls" ] && [ "$1" != "type" ]; then source /app/bin/shim-check.sh fi -if [ -e /data/config.sh ]; then - source /data/config.sh -fi if [ -e /data/proxy.sh ]; then source /data/proxy.sh fi diff --git a/app/bin/run.sh b/app/bin/run.sh index 5f4f4e1..f3dcb8b 100755 --- a/app/bin/run.sh +++ b/app/bin/run.sh @@ -1,8 +1,7 @@ #!/bin/bash # data directory initialization -if [ ! -e /data/config.sh ]; then - cp /data.skel/config.sh /data/config.sh +if [ ! -e /data/config.yml ]; then + cp /data.skel/config.yml /data/config.yml fi mkdir -p /data/ansible - diff --git a/app/cli/detect b/app/cli/detect new file mode 120000 index 0000000..1fa21ed --- /dev/null +++ b/app/cli/detect @@ -0,0 +1 @@ +.command \ No newline at end of file diff --git a/app/cli/shutdown b/app/cli/shutdown index c330b62..ce48d56 100755 --- a/app/cli/shutdown +++ b/app/cli/shutdown @@ -2,7 +2,6 @@ ## PREPARE ENVIRONMENT cd /app -source /data/config.sh ## EXECUTE ansible-playbook /app/playbooks/shutdown.yml || exit 1 diff --git a/app/cli/ssh b/app/cli/ssh index d35b8d5..3ef02c7 100755 --- a/app/cli/ssh +++ b/app/cli/ssh @@ -5,7 +5,6 @@ if [ $# -ne 1 ]; then exit 1 fi -source /data/config.sh cd /app HOST_DATA=$(ansible-inventory --host $1) diff --git a/app/cli/startup b/app/cli/startup index 6df9db9..6cd90f2 100755 --- a/app/cli/startup +++ b/app/cli/startup @@ -2,7 +2,6 @@ ## PREPARE ENVIRONMENT cd /app -source /data/config.sh ## EXECUTE ansible-playbook /app/playbooks/startup.yml || exit 1 diff --git a/app/cli/validate b/app/cli/validate new file mode 120000 index 0000000..1fa21ed --- /dev/null +++ b/app/cli/validate @@ -0,0 +1 @@ +.command \ No newline at end of file diff --git a/app/inventory.py b/app/inventory.py old mode 100755 new mode 100644 index 71c4477..be0fef5 --- a/app/inventory.py +++ b/app/inventory.py @@ -1,327 +1,6 @@ #!/usr/bin/env python3 -import argparse -from collections import defaultdict -import ipaddress -import json -import os import sys -import pickle -SSH_PRIVATE_KEY = '/data/id_rsa' -IP_RESERVATIONS = '/data/ip_addresses' +from faros_config.inventory.cli import main - -class InventoryGroup(object): - - def __init__(self, parent, name): - self._parent = parent - self._name = name - - def add_group(self, name, **groupvars): - return(self._parent.add_group(name, self._name, **groupvars)) - - def add_host(self, name, hostname=None, **hostvars): - return(self._parent.add_host(name, self._name, hostname, **hostvars)) - - def host(self, name): - return self._parent.host(name) - - -class Inventory(object): - - _modes = ['list', 'host', 'verify', 'none'] - _data = {"_meta": {"hostvars": defaultdict(dict)}} - - def __init__(self, mode=0, host=None): - if mode==1: - # host info requested - # current, only list and none are implimented - raise NotImplementedError() - - self._mode = mode - self._host = host - - def __del__(self): - if self._mode == 0: - print(self.to_json()) - - def host(self, name): - return self._data['_meta']['hostvars'].get(name) - - def group(self, name): - if name in self_data: - return InventoryGroup(self, name) - else: - return None - - def add_group(self, name, parent=None, **groupvars): - self._data[name] = {'hosts': [], 'vars': groupvars, 'children': []} - - if parent: - if parent not in self._data: - self.add_group(parent) - self._data[parent]['children'].append(name) - - return InventoryGroup(self, name) - - def add_host(self, name, group=None, hostname=None, **hostvars): - if not group: - group = 'all' - if group not in self._data: - self.add_group(group) - - if hostname: - hostvars.update({'ansible_host': hostname}) - - self._data[group]['hosts'].append(name) - self._data['_meta']['hostvars'][name].update(hostvars) - - def to_json(self): - return json.dumps(self._data, sort_keys=True, - indent=4, separators=(',', ': ')) - - -class IPAddressManager(dict): - - def __init__(self, save_file, subnet, subnet_mask): - super().__init__() - self._save_file = save_file - - # parse the subnet definition into a static and dynamic pool - subnet = ipaddress.ip_network(f'{subnet}/{subnet_mask}', strict=False) - divided = subnet.subnets() - self._static_pool = next(divided) - self._dynamic_pool = next(divided) - self._generator = self._static_pool.hosts() - - # calculate reverse dns zone - classful_prefix = [32, 24, 16, 8, 0] - classful = subnet - while classful.prefixlen not in classful_prefix: - classful = classful.supernet() - host_octets = classful_prefix.index(classful.prefixlen) - self._reverse_ptr_zone = \ - '.'.join(classful.reverse_pointer.split('.')[host_octets:]) - - # load the last saved state - try: - restore = pickle.load(open(save_file, 'rb')) - except: - restore = {} - self.update(restore) - - # reserve the first ip for the bastion - _ = self['bastion'] - - def __getitem__(self, key): - key = key.lower() - try: - return super().__getitem__(key) - except KeyError: - new_ip = self._next_ip() - self[key] = new_ip - return new_ip - - def __setitem__(self, key, value): - return super().__setitem__(key.lower(), value) - - def _next_ip(self): - used_ips = list(self.values()) - loop = True - - while loop: - new_ip = next(self._generator).exploded - loop = new_ip in used_ips - return new_ip - - def get(self, key, value=None): - if value and value not in self.values(): - self[key] = value - return self[key] - - def save(self): - with open(self._save_file, 'wb') as handle: - pickle.dump(dict(self), handle) - - @property - def static_pool(self): - return str(self._static_pool) - - @property - def dynamic_pool(self): - return str(self._dynamic_pool) - - @property - def reverse_ptr_zone(self): - return str(self._reverse_ptr_zone) - - -class Config(object): - _last_key = None - - def __getitem__(self, key): - return self.get(key) - - def get(self, key, default=None): - self._last_key = key - val = os.environ.get(key, default) - try: - return val.replace('\\n', '\n') - except AttributeError: - return val - - @property - def error(self): - return f'\n\033[31mThere was an error parsing the configuration\nPlease check the value for {self._last_key}.\033[0m\n\n' - - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument('--list', action = 'store_true') - parser.add_argument('--verify', action = 'store_true') - parser.add_argument('--host', action = 'store') - args = parser.parse_args() - return args - -def main(config, ipam, inv): - # GATHER INFORMATION FOR EXTRA NODES - extra_nodes = json.loads(config.get('EXTRA_NODES', '[]')) - for idx, item in enumerate(extra_nodes): - addr = ipam.get(item['mac'], item.get('ip')) - extra_nodes[idx].update({'ip': addr}) - - # CREATE INVENTORY - inv.add_group('all', None, - ansible_ssh_private_key_file=SSH_PRIVATE_KEY, - cluster_name=config['CLUSTER_NAME'], - cluster_domain=config['CLUSTER_DOMAIN'], - admin_password=config['ADMIN_PASSWORD'], - pull_secret=json.loads(config['PULL_SECRET']), - mgmt_provider=config['MGMT_PROVIDER'], - mgmt_user=config['MGMT_USER'], - mgmt_password=config['MGMT_PASSWORD'], - install_disk=config['BOOT_DRIVE'], - loadbalancer_vip=ipam['loadbalancer'], - dynamic_ip_range=ipam.dynamic_pool, - reverse_ptr_zone=ipam.reverse_ptr_zone, - subnet=config['SUBNET'], - subnet_mask=config['SUBNET_MASK'], - wan_ip=config['BASTION_IP_ADDR'], - extra_nodes=extra_nodes, - ignored_macs=config['IGNORE_MACS'], - dns_forwarders=[item['server'] for item in json.loads(config.get('DNS_FORWARDERS', '[]'))], - proxy=config['PROXY']=="True", - proxy_http=config.get('PROXY_HTTP', ''), - proxy_https=config.get('PROXY_HTTPS', ''), - proxy_noproxy=[item['dest'] for item in json.loads(config.get('PROXY_NOPROXY', '[]'))], - proxy_ca=config.get('PROXY_CA', '')) - - infra = inv.add_group('infra') - router = infra.add_group('router', - wan_interface=config['WAN_INT'], - lan_interfaces=json.loads(config['ROUTER_LAN_INT']), - all_interfaces=config['BASTION_INTERFACES'].split(), - allowed_services=json.loads(config['ALLOWED_SERVICES'])) - # ROUTER INTERFACES - router.add_host('wan', - config['BASTION_IP_ADDR'], - ansible_become_pass=config['ADMIN_PASSWORD'], - ansible_ssh_user=config['BASTION_SSH_USER']) - router.add_host('lan', - ipam['bastion'], - ansible_become_pass=config['ADMIN_PASSWORD'], - ansible_ssh_user=config['BASTION_SSH_USER']) - # DNS NODE - router.add_host('dns', - ipam['bastion'], - ansible_become_pass=config['ADMIN_PASSWORD'], - ansible_ssh_user=config['BASTION_SSH_USER']) - # DHCP NODE - router.add_host('dhcp', - ipam['bastion'], - ansible_become_pass=config['ADMIN_PASSWORD'], - ansible_ssh_user=config['BASTION_SSH_USER']) - # LOAD BALANCER NODE - router.add_host('loadbalancer', - ipam['loadbalancer'], - ansible_become_pass=config['ADMIN_PASSWORD'], - ansible_ssh_user=config['BASTION_SSH_USER']) - - # BASTION NODE - bastion = infra.add_group('bastion_hosts') - bastion.add_host(config['BASTION_HOST_NAME'], - ipam['bastion'], - ansible_become_pass=config['ADMIN_PASSWORD'], - ansible_ssh_user=config['BASTION_SSH_USER']) - - # CLUSTER NODES - cluster = inv.add_group('cluster') - # BOOTSTRAP NODE - ip = ipam['bootstrap'] - cluster.add_host('bootstrap', ip, - ansible_ssh_user='core', - node_role='bootstrap') - # CLUSTER CONTROL PLANE NODES - cp = cluster.add_group('control_plane', node_role='master') - node_defs = json.loads(config['CP_NODES']) - for count, node in enumerate(node_defs): - ip = ipam[node['mac']] - mgmt_ip = ipam[node['mgmt_mac']] - cp.add_host(node['name'], ip, - mac_address=node['mac'], - mgmt_mac_address=node['mgmt_mac'], - mgmt_hostname=mgmt_ip, - ansible_ssh_user='core', - cp_node_id=count) - if node.get('install_drive'): - cp.host(node['name'])['install_disk'] = node['install_drive'] - - # VIRTUAL NODES - virt = inv.add_group('virtual', - mgmt_provider='kvm', - mgmt_hostname='bastion', - install_disk='vda') - virt.add_host('bootstrap') - - # MGMT INTERFACES - mgmt = inv.add_group('management', - ansible_ssh_user=config['MGMT_USER'], - ansible_ssh_pass=config['MGMT_PASSWORD']) - for count, node in enumerate(node_defs): - mgmt.add_host(node['name'] + '-mgmt', ipam[node['mgmt_mac']], - mac_address=node['mgmt_mac']) - - -if __name__ == "__main__": - # PARSE ARGUMENTS - args = parse_args() - if args.list: - mode = 0 - elif args.verify: - mode = 2 - else: - mode = 1 - - # INITIALIZE CONFIG HANDLER - config = Config() - - # INTIALIZE IPAM - ipam = IPAddressManager( - IP_RESERVATIONS, - config['SUBNET'], config['SUBNET_MASK']) - - # INITIALIZE INVENTORY - inv = Inventory(mode, args.host) - - # CREATE INVENTORY - try: - main(config, ipam, inv) - except Exception as e: - if mode == 2: - sys.stderr.write(config.error) - sys.exit(1) - raise(e) - - # DONE - ipam.save() - sys.exit(0) +main(sys.argv[1:]) diff --git a/app/lib/python/conftui.py b/app/lib/python/conftui.py deleted file mode 100644 index 399f71d..0000000 --- a/app/lib/python/conftui.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python -import sys -import os -import json -from PyInquirer import prompt, Separator, default_style -from prompt_toolkit.shortcuts import Token, print_tokens - -STYLE = default_style - - -class Parameter(object): - disabled = False - _value_reprfun = str - - def __init__(self, name, prompt, disabled=False, default=None): - self._name = name - self._value = os.environ.get(self._name, default or '') - self._prompt = prompt - self.disabled = disabled - - @property - def name(self): - return self._name - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - - @property - def prompt(self): - return self._prompt - - def update(self): - question = [ - { - 'type': 'input', - 'name': 'newval', - 'message': self.prompt, - 'default': self.value, - } - ] - answer = prompt(question) - self.value = answer['newval'] - - def __repr__(self): - return '{}: {}'.format(self.prompt, self._value_reprfun(self.value)) - - def to_bash(self): - return "export {}={}".format(self.name, self.jsonify()) - - def jsonify(self): - try: - json.loads(self.value) - return "'" + self.value + "'" - except json.decoder.JSONDecodeError: - return json.dumps(self.value) - - -class PasswordParameter(Parameter): - - def __init__(self, name, prompt, disabled=False): - super().__init__(name, prompt, disabled) - - def _value_reprfun(self, password): - if not password: - return '' - return '*********' - - def update(self): - question = [ - { - 'type': 'password', - 'name': 'newval', - 'message': self.prompt - } - ] - answer = prompt(question) - self.value = answer['newval'] - -class ChoiceParameter(Parameter): - - def __init__(self, name, prompt, choices, value_reprfun=str, default=''): - self._name = name - self._value = os.environ.get(self._name, default) - self._prompt = prompt - self._choices = choices - self._value_reprfun = value_reprfun - - def update(self): - question = [ - { - 'type': 'list', - 'message': self._prompt, - 'name': 'choice', - 'default': self._value_reprfun(self._value), - 'choices': - [self._value_reprfun(item) for item in self._choices] - } - ] - answer = prompt(question) - self._value = answer['choice'] - - -class BooleanParameter(ChoiceParameter): - - def __init__(self, name, prompt, default="True"): - super().__init__(name, prompt, [True, False], default=default) - -class CheckParameter(Parameter): - - def __init__(self, name, prompt, choices): - self._name = name - self._value = json.loads(os.environ.get(self._name, '')) - self._prompt = prompt - self._choices = choices - self._choices = [{'name': f'{choice}', - 'checked': f'{choice}' in self._value} - for choice in choices] - - def update(self): - question = [ - { - 'type': 'checkbox', - 'message': self._prompt, - 'name': 'choice', - 'choices': self._choices - } - ] - answer = prompt(question) - self._value = answer['choice'] - - def to_bash(self): - return "export {}='{}'".format(self.name, json.dumps(self.value)) - - -class StaticParameter(Parameter): - - def __init__(self, name, prompt, value): - super().__init__(name, prompt, 'Static Value') - self._value = value - - -class ListDictParameter(Parameter): - - def __init__(self, name, prompt, keys, default=None): - self._name = name - self._value = json.loads(os.environ.get(self._name, default or '[]')) - self._prompt = prompt - self._primary_key = keys[0][0] - - # normalize keys - self._keys = [] - for key in keys: - if len(key) < 3: - self._keys += [(key[0], key[-1], "")] - continue - if len(key) == 3: - self._keys += [key] - - for val in self._value: - if not val.get(key[0]): - val[key[0]] = self._keys[-1][2] - - def _value_reprfun(self, value): - return '{} items'.format(len(self.value)) - - def print_status(self): - tokens = [] - tokens += [(Token.QuestionMark, '!'), - (Token.Question, f' Current {self._prompt}:\n')] - for entry in self._value: - for idx, key in enumerate(self._keys): - if idx == 0: - ptr = ' - ' - else: - ptr = ' ' - tokens += [(Token.Pointer, ptr), - (Token.Arboted, f'{key[1]}: {entry.get(key[0], key[2])}\n')] - print_tokens(tokens, style=STYLE) - sys.stdout.write('\n\n') - - def update(self): - done = False - - while not done: - self.print_status() - - question = [ - { - 'type': 'expand', - 'message': '{}: What would you like to do?'.format(self.prompt), - 'name': 'action', - 'default': 'a', - 'choices': [ - { - 'key': 'a', - 'name': 'Add Entry', - 'value': 'a' - }, - { - 'key': 'e', - 'name': 'Edit Entry', - 'value': 'e' - }, - { - 'key': 'r', - 'name': 'Remove Entry', - 'value': 'r' - }, - { - 'key': 'd', - 'name': 'Done', - 'value': 'd' - } - ] - } - ] - answer = prompt(question) - - if answer['action'] == 'd': - done = True - elif answer['action'] in 'er': - self._update_edit(answer['action']) - elif answer['action'] == 'a': - self._value.append(self._mkentry({})) - - def _update_edit(self, action_code): - if action_code == 'r': - action = 'remove' - else: - action = 'edit' - - question = [ - { - 'type': 'list', - 'name': 'index', - 'message': 'Which item would you like to {}?'.format(action), - 'choices': [{'name': item[self._primary_key], - 'value': index} for (index, item) in enumerate(self.value)] - } - ] - answer = prompt(question) - - if action_code == 'r': - self.value.pop(answer['index']) - else: - self.value[answer['index']] = self._mkentry(self.value[answer['index']]) - - def _mkentry(self, defaults): - questions = [ {'type': 'input', - 'message': item[1], - 'name': item[0], - 'default': defaults.get(item[0], item[2])} - for item in self._keys] - return prompt(questions) - - def to_bash(self): - return "export {}='{}'".format(self.name, json.dumps(self.value)) - - -class ParameterCollection(list): - - def __init__(self, name, prompt, values): - super().__init__(values) - self._name = name - self._prompt = prompt - - def to_choices(self): - out = [Separator(self._prompt)] - for item in self: - out += [{'name': repr(item), - 'description': str(item.value), - 'value': '{}|{}'.format(self._name, item.name)}] - if item.disabled: - out[-1].update({'disabled': item.disabled}) - return out - - def to_bash(self): - return ['# {}'.format(self._prompt.upper())] + [item.to_bash() for item in self] - - def get_param(self, pname): - - def filter_fun(val): - return val.name == pname - - return list(filter(filter_fun, self))[0] - - -class Configurator(object): - - def __init__(self, path, footer): - self._path = path - self._footer = footer - self.all = [] - - def _main_menu(self): - question = [ - { - 'type': 'checkbox', - 'message': 'Which items would you like to change?', - 'name': 'parameters', - 'choices': [val for section in self.all for val in section.to_choices()] - } - ] - return prompt(question) - - def _update_param(self, raw_param): - (collection, param_name) = raw_param.split('|') - try: - getattr(self, collection).get_param(param_name).update() - except AttributeError: - raise ValueError('{} not a valid collection'.format(collection)) - - def dump(self): - with open(self._path, 'w') as outfile: - _ = [outfile.write(line + '\n') for section in self.all for line in section.to_bash()] - outfile.write(self._footer) - - def configurate(self): - loop = True - - while loop: - to_update = self._main_menu() - print('') - - for parameter in to_update['parameters']: - self._update_param(parameter) - print('') - - loop = bool(to_update['parameters']) - - self.dump() diff --git a/app/playbooks/config.d/all b/app/playbooks/config.d/all deleted file mode 120000 index e89bbc8..0000000 --- a/app/playbooks/config.d/all +++ /dev/null @@ -1 +0,0 @@ -cluster \ No newline at end of file diff --git a/app/playbooks/config.d/cluster/config.py b/app/playbooks/config.d/cluster/config.py deleted file mode 100644 index f6e7fc6..0000000 --- a/app/playbooks/config.d/cluster/config.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -import sys -import os -from conftui import (Configurator, ParameterCollection, Parameter, - ListDictParameter, PasswordParameter, ChoiceParameter, - CheckParameter, StaticParameter) - -CONFIG_PATH = '/data/config.sh' -CONFIG_FOOTER = '' - -class ClusterConfigurator(Configurator): - - def __init__(self, path, footer, rtr_interfaces): - self._path = path - self._footer = footer - - self.router = ParameterCollection('router', 'Router Configuration', [ - CheckParameter('ROUTER_LAN_INT', 'LAN Interfaces', rtr_interfaces), - Parameter('SUBNET', 'Subnet'), - ChoiceParameter('SUBNET_MASK', 'Subnet Mask', ['20', '21', '22', '23', '24', '25', '26', '27']), - CheckParameter('ALLOWED_SERVICES', 'Permitted Ingress Traffic', ['SSH to Bastion', 'HTTPS to Cluster API', 'HTTP to Cluster Apps', 'HTTPS to Cluster Apps', 'HTTPS to Cockpit Panel', 'External to Internal Routing - DANGER']), - ListDictParameter('DNS_FORWARDERS', 'Upstream DNS Forwarders', - [('server', 'DNS Server')], - default='[{"server": "1.1.1.1"}]') - ]) - self.cluster = ParameterCollection('cluster', 'Cluster Configuration', [ - PasswordParameter('ADMIN_PASSWORD', 'Adminstrator Password'), - PasswordParameter('PULL_SECRET', 'Pull Secret') - ]) - self.architecture = ParameterCollection('architecture', 'Host Record Configuration', [ - StaticParameter('MGMT_PROVIDER', 'Machine Management Provider', 'ilo'), - Parameter('MGMT_USER', 'Machine Management User'), - PasswordParameter('MGMT_PASSWORD', 'Machine Management Password'), - ListDictParameter('CP_NODES', 'Control Plane Machines', - [('name', 'Node Name'), ('mac', 'MAC Address'), - ('mgmt_mac', 'Management MAC Address'), - ('install_drive', 'OS Install Drive', - os.environ.get('BOOT_DRIVE'))]) - ]) - self.extra = ParameterCollection('extra', 'Extra DNS/DHCP Records', [ - ListDictParameter('EXTRA_NODES', 'Static IP Reservations', - [('name', 'Node Name'), ('mac', 'MAC Address'), ('ip', 'Requested IP Address')]), - ListDictParameter('IGNORE_MACS', 'DHCP Ignored MAC Addresses', - [('name', 'Entry Name'), ('mac', 'MAC Address')]) - ]) - - self.all = [self.router, self.cluster, self.architecture, self.extra] - - -def main(): - rtr_interfaces = os.environ['BASTION_INTERFACES'].split() - return ClusterConfigurator( - CONFIG_PATH, - CONFIG_FOOTER, - rtr_interfaces).configurate() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/app/playbooks/config.d/cluster/config.py.bkp b/app/playbooks/config.d/cluster/config.py.bkp deleted file mode 100644 index ff89015..0000000 --- a/app/playbooks/config.d/cluster/config.py.bkp +++ /dev/null @@ -1,366 +0,0 @@ -#!/usr/bin/env python -import sys -import os -import json -from PyInquirer import prompt, Separator, default_style -from prompt_toolkit.shortcuts import Token, print_tokens - - -CONFIG_PATH = '/data/config.sh' -CONFIG_FOOTER = '' -STYLE = default_style - - - -class Parameter(object): - disabled = False - _value_reprfun = str - - def __init__(self, name, prompt, disabled=False, default=None): - self._name = name - self._value = os.environ.get(self._name, default or '') - self._prompt = prompt - self.disabled = disabled - - @property - def name(self): - return self._name - - @property - def value(self): - return self._value - - @value.setter - def value(self, value): - self._value = value - - @property - def prompt(self): - return self._prompt - - def update(self): - question = [ - { - 'type': 'input', - 'name': 'newval', - 'message': self.prompt, - 'default': self.value, - } - ] - answer = prompt(question) - self.value = answer['newval'] - - def __repr__(self): - return '{}: {}'.format(self.prompt, self._value_reprfun(self.value)) - - def to_bash(self): - return "export {}='{}'".format(self.name, self.value) - - -class PasswordParameter(Parameter): - - def __init__(self, name, prompt, disabled=False): - super().__init__(name, prompt, disabled) - - def _value_reprfun(self, password): - if not password: - return '' - return '*********' - - def update(self): - question = [ - { - 'type': 'password', - 'name': 'newval', - 'message': self.prompt - } - ] - answer = prompt(question) - self.value = answer['newval'] - -class ChoiceParameter(Parameter): - - def __init__(self, name, prompt, choices, value_reprfun=str): - self._name = name - self._value = os.environ.get(self._name, '') - self._prompt = prompt - self._choices = choices - self._value_reprfun = value_reprfun - - def update(self): - question = [ - { - 'type': 'list', - 'message': self._prompt, - 'name': 'choice', - 'default': self._value, - 'choices': self._choices - } - ] - answer = prompt(question) - self._value = answer['choice'] - - -class CheckParameter(Parameter): - - def __init__(self, name, prompt, choices): - self._name = name - self._value = json.loads(os.environ.get(self._name, '')) - self._prompt = prompt - self._choices = choices - self._choices = [{'name': f'{choice}', - 'checked': f'{choice}' in self._value} - for choice in choices] - - def update(self): - question = [ - { - 'type': 'checkbox', - 'message': self._prompt, - 'name': 'choice', - 'choices': self._choices - } - ] - answer = prompt(question) - self._value = answer['choice'] - - def to_bash(self): - return "export {}='{}'".format(self.name, json.dumps(self.value)) - - -class StaticParameter(Parameter): - - def __init__(self, name, prompt, value): - super().__init__(name, prompt, 'Static Value') - self._value = value - - -class ListDictParameter(Parameter): - - def __init__(self, name, prompt, keys, default=None): - self._name = name - self._value = json.loads(os.environ.get(self._name, default or '[]')) - self._prompt = prompt - self._primary_key = keys[0][0] - - # normalize keys - self._keys = [] - for key in keys: - if len(key) < 3: - self._keys += [(key[0], key[-1], "")] - continue - if len(key) == 3: - self._keys += [key] - - for val in self._value: - if not val.get(key[0]): - val[key[0]] = self._keys[-1][2] - - def _value_reprfun(self, value): - return '{} items'.format(len(self.value)) - - def print_status(self): - tokens = [] - tokens += [(Token.QuestionMark, '!'), - (Token.Question, f' Current {self._prompt}:\n')] - for entry in self._value: - for idx, key in enumerate(self._keys): - if idx == 0: - ptr = ' - ' - else: - ptr = ' ' - tokens += [(Token.Pointer, ptr), - (Token.Arboted, f'{key[1]}: {entry.get(key[0], key[2])}\n')] - print_tokens(tokens, style=STYLE) - sys.stdout.write('\n\n') - - def update(self): - done = False - - while not done: - self.print_status() - - question = [ - { - 'type': 'expand', - 'message': '{}: What would you like to do?'.format(self.prompt), - 'name': 'action', - 'default': 'a', - 'choices': [ - { - 'key': 'a', - 'name': 'Add Entry', - 'value': 'a' - }, - { - 'key': 'e', - 'name': 'Edit Entry', - 'value': 'e' - }, - { - 'key': 'r', - 'name': 'Remove Entry', - 'value': 'r' - }, - { - 'key': 'd', - 'name': 'Done', - 'value': 'd' - } - ] - } - ] - answer = prompt(question) - - if answer['action'] == 'd': - done = True - elif answer['action'] in 'er': - self._update_edit(answer['action']) - elif answer['action'] == 'a': - self._value.append(self._mkentry({})) - - def _update_edit(self, action_code): - if action_code == 'r': - action = 'remove' - else: - action = 'edit' - - question = [ - { - 'type': 'list', - 'name': 'index', - 'message': 'Which item would you like to {}?'.format(action), - 'choices': [{'name': item[self._primary_key], - 'value': index} for (index, item) in enumerate(self.value)] - } - ] - answer = prompt(question) - - if action_code == 'r': - self.value.pop(answer['index']) - else: - self.value[answer['index']] = self._mkentry(self.value[answer['index']]) - - def _mkentry(self, defaults): - questions = [ {'type': 'input', - 'message': item[1], - 'name': item[0], - 'default': defaults.get(item[0], item[2])} - for item in self._keys] - return prompt(questions) - - def to_bash(self): - return "export {}='{}'".format(self.name, json.dumps(self.value)) - - -class ParameterCollection(list): - - def __init__(self, name, prompt, values): - super().__init__(values) - self._name = name - self._prompt = prompt - - def to_choices(self): - out = [Separator(self._prompt)] - for item in self: - out += [{'name': repr(item), - 'description': str(item.value), - 'value': '{}|{}'.format(self._name, item.name)}] - if item.disabled: - out[-1].update({'disabled': item.disabled}) - return out - - def to_bash(self): - return ['# {}'.format(self._prompt.upper())] + [item.to_bash() for item in self] - - def get_param(self, pname): - - def filter_fun(val): - return val.name == pname - - return list(filter(filter_fun, self))[0] - - -class configurator(object): - - def __init__(self, path, footer, rtr_interfaces): - self._path = path - self._footer = footer - - self.router = ParameterCollection('router', 'Router Configuration', [ - CheckParameter('ROUTER_LAN_INT', 'LAN Interfaces', rtr_interfaces), - Parameter('SUBNET', 'Subnet'), - ChoiceParameter('SUBNET_MASK', 'Subnet Mask', ['20', '21', '22', '23', '24', '25', '26', '27']), - CheckParameter('ALLOWED_SERVICES', 'Permitted Ingress Traffic', ['SSH to Bastion', 'HTTPS to Cluster API', 'HTTP to Cluster Apps', 'HTTPS to Cluster Apps', 'HTTPS to Cockpit Panel', 'External to Internal Routing - DANGER']), - ListDictParameter('DNS_FORWARDERS', 'Upstream DNS Forwarders', - [('server', 'DNS Server')], - default='[{"server": "1.1.1.1"}]')]) - self.cluster = ParameterCollection('cluster', 'Cluster Configuration', [ - PasswordParameter('ADMIN_PASSWORD', 'Adminstrator Password'), - PasswordParameter('PULL_SECRET', 'Pull Secret')]) - self.architecture = ParameterCollection('architecture', 'Host Record Configuration', [ - StaticParameter('MGMT_PROVIDER', 'Machine Management Provider', 'ilo'), - Parameter('MGMT_USER', 'Machine Management User'), - PasswordParameter('MGMT_PASSWORD', 'Machine Management Password'), - ListDictParameter('CP_NODES', 'Control Plane Machines', - [('name', 'Node Name'), ('mac', 'MAC Address'), - ('mgmt_mac', 'Management MAC Address'), - ('install_drive', 'OS Install Drive', - os.environ.get('BOOT_DRIVE'))])]) - self.extra = ParameterCollection('extra', 'Extra DNS/DHCP Records', [ - ListDictParameter('EXTRA_NODES', 'Static IP Reservations', - [('name', 'Node Name'), ('mac', 'MAC Address'), ('ip', 'Requested IP Address')]), - ListDictParameter('IGNORE_MACS', 'DHCP Ignored MAC Addresses', - [('name', 'Entry Name'), ('mac', 'MAC Address')])]) - - self.all = [self.router, self.cluster, self.architecture, self.extra] - - def _main_menu(self): - question = [ - { - 'type': 'checkbox', - 'message': 'Which items would you like to change?', - 'name': 'parameters', - 'choices': [val for section in self.all for val in section.to_choices()] - } - ] - return prompt(question) - - def _update_param(self, raw_param): - (collection, param_name) = raw_param.split('|') - try: - getattr(self, collection).get_param(param_name).update() - except AttributeError: - raise ValueError('{} not a valid collection'.format(collection)) - - def dump(self): - with open(self._path, 'w') as outfile: - _ = [outfile.write(line + '\n') for section in self.all for line in section.to_bash()] - outfile.write(self._footer) - - def configurate(self): - loop = True - - while loop: - to_update = self._main_menu() - print('') - - for parameter in to_update['parameters']: - self._update_param(parameter) - print('') - - loop = bool(to_update['parameters']) - - self.dump() - - -def main(): - rtr_interfaces = os.environ['BASTION_INTERFACES'].split() - return configurator( - CONFIG_PATH, - CONFIG_FOOTER, - rtr_interfaces).configurate() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/app/playbooks/config.d/cluster/main.sh b/app/playbooks/config.d/cluster/main.sh deleted file mode 100755 index f6547bc..0000000 --- a/app/playbooks/config.d/cluster/main.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -cd /app -source /data/config.sh 2> /dev/null -python3 /app/playbooks/config.d/cluster/config.py -source /data/config.sh 2> /dev/null -python3 /app/inventory.py --verify > /dev/null diff --git a/app/playbooks/config.d/proxy/config.py b/app/playbooks/config.d/proxy/config.py deleted file mode 100644 index 1cd0286..0000000 --- a/app/playbooks/config.d/proxy/config.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -import sys -from conftui import (Configurator, ParameterCollection, Parameter, - ListDictParameter, PasswordParameter, - BooleanParameter) - -CONFIG_PATH = '/data/proxy.sh' -CONFIG_FOOTER = '' - -class ProxyConfigurator(Configurator): - - def __init__(self, path, footer): - self._path = path - self._footer = footer - - self.proxy = ParameterCollection('proxy', 'Proxy Configuration', [ - BooleanParameter('PROXY', 'Setup cluster proxy'), - Parameter('PROXY_HTTP', 'HTTP Proxy'), - Parameter('PROXY_HTTPS', 'HTTPS Proxy'), - ListDictParameter('PROXY_NOPROXY', 'No Proxy Destinations', - [('dest', 'Destination')]), - PasswordParameter('PROXY_CA', 'Additional CA Bundle') - ]) - - self.all = [self.proxy] - - -def main(): - return ProxyConfigurator( - CONFIG_PATH, - CONFIG_FOOTER).configurate() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/app/playbooks/config.d/proxy/main.sh b/app/playbooks/config.d/proxy/main.sh deleted file mode 100755 index 7326014..0000000 --- a/app/playbooks/config.d/proxy/main.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -cd /app -source /data/config.sh 2> /dev/null -source /data/proxy.sh 2> /dev/null -python3 /app/playbooks/config.d/proxy/config.py -source /data/config.sh 2> /dev/null -source /data/proxy.sh 2> /dev/null -python3 /app/inventory.py --verify > /dev/null diff --git a/app/playbooks/detect.d/drives/detect-drives.yml b/app/playbooks/detect.d/drives/detect-drives.yml new file mode 100644 index 0000000..37f17b5 --- /dev/null +++ b/app/playbooks/detect.d/drives/detect-drives.yml @@ -0,0 +1,31 @@ +- name: query cluster facts + hosts: localhost + become: no + gather_facts: no + + tasks: + - name: lookup cluster nodes + set_fact: + cluster_nodes: "{{ lookup('k8s', api_version='v1', kind='Node') | json_query('[*].metadata.name')}}" + + - name: query cluster node drives + shell: oc debug -n default node/{{ item }} -- chroot /host lsblk -J + loop: "{{ cluster_nodes }}" + register: cluster_drives + ignore_errors: yes + changed_when: no + + - name: save discovered drives + set_stats: + data: + drive_data: "{{ drive_data|from_yaml }}" + vars: + drive_data: | + {% for result in cluster_drives.results %} + {{ result.item }}: + {% for blockdevice in (result.stdout|from_json).blockdevices %} + {% if blockdevice.type == "disk" %} + - {{ blockdevice|to_json }} + {% endif %} + {% endfor %} + {% endfor %} diff --git a/app/playbooks/detect.d/drives/main.sh b/app/playbooks/detect.d/drives/main.sh new file mode 100644 index 0000000..b18b138 --- /dev/null +++ b/app/playbooks/detect.d/drives/main.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +ME=$(dirname $0) + +STATS_FILE=/tmp/drives.yml ansible-playbook $ME/detect-drives.yml "${@}" || exit $? +cp /tmp/drives.yml /data/drives.yml diff --git a/app/playbooks/validate.d/all b/app/playbooks/validate.d/all new file mode 120000 index 0000000..30fa1ce --- /dev/null +++ b/app/playbooks/validate.d/all @@ -0,0 +1 @@ +config \ No newline at end of file diff --git a/app/playbooks/validate.d/config/main.sh b/app/playbooks/validate.d/config/main.sh new file mode 100644 index 0000000..f6e3318 --- /dev/null +++ b/app/playbooks/validate.d/config/main.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +ME=$(dirname $0) + +ansible-playbook $ME/validate.yml $@ || exit 1 diff --git a/app/playbooks/validate.d/config/validate.yml b/app/playbooks/validate.d/config/validate.yml new file mode 100644 index 0000000..629f7b4 --- /dev/null +++ b/app/playbooks/validate.d/config/validate.yml @@ -0,0 +1,6 @@ +--- +- name: Validate configuration and inventory + hosts: control_plane,management,infra + tasks: + - ping: + - setup: diff --git a/data.skel/config.sh b/data.skel/config.sh deleted file mode 100644 index 7de94e5..0000000 --- a/data.skel/config.sh +++ /dev/null @@ -1,14 +0,0 @@ -# NETWORK ROUTER CONFIGURATION -export ROUTER_LAN_INT='[]' -export SUBNET=192.168.8.0 -export SUBNET_MASK=24 -export ALLOWED_SERVICES='["SSH to Bastion"]' -# CLUSTER CONFIGURATION -export ADMIN_PASSWORD='admin' -export PULL_SECRET='' -# CLUSTER HARDWARE -export MGMT_PROVIDER='ilo' -export MGMT_USER='Administrator' -export MGMT_PASSWORD='ilo-pass' -export BASTION_MGMT_MAC='ff:ff:ff:ff:ff:ff' -export CP_NODES='[{"name": "node-0", "mac": "ff:ff:ff:ff:ff:ff", "mgmt_mac": "ff:ff:ff:ff:ff:ff"}, {"name": "node-1", "mac": "ff:ff:ff:ff:ff:ff", "mgmt_mac": "ff:ff:ff:ff:ff:ff"}, {"name": "node-2", "mac": "ff:ff:ff:ff:ff:ff", "mgmt_mac": "ff:ff:ff:ff:ff:ff"}]' diff --git a/data.skel/config.yml b/data.skel/config.yml new file mode 100644 index 0000000..f1862c3 --- /dev/null +++ b/data.skel/config.yml @@ -0,0 +1,27 @@ +network: + port_forward: + - SSH to Bastion + lan: + subnet: 192.168.8.0/24 + interfaces: [] + dhcp: + ignore_macs: [] + extra_reservations: [] +bastion: + become_pass: admin +cluster: + pull_secret: '' + management: + provider: ilo + user: Administrator + password: ilo-pass + nodes: + - name: node-0 + mac: ff:ff:ff:ff:ff:ff + mgmt_mac: ff:ff:ff:ff:ff:ff + - name: node-1 + mac: ff:ff:ff:ff:ff:ff + mgmt_mac: ff:ff:ff:ff:ff:ff + - name: node-2 + mac: ff:ff:ff:ff:ff:ff + mgmt_mac: ff:ff:ff:ff:ff:ff diff --git a/home/.bashrc b/home/.bashrc index 64f769d..008c8a8 100644 --- a/home/.bashrc +++ b/home/.bashrc @@ -1,6 +1,6 @@ # Get the aliases and functions if [ -f /etc/bashrc ]; then - . /etc/bashrc + . /etc/bashrc fi # Update proxy settings in container @@ -23,17 +23,15 @@ function set_proxy() { # User specific environment and startup programs function ps1() { - _CONFIG_LAST_MODIFY=$(stat -c %Z /data/config.sh) - if [[ $_CONFIG_LAST_MODIFY -gt $_CONFIG_LAST_LOAD ]]; then - echo " -- Configuration Reloaded -- " - source /data/config.sh 2> /dev/null - source /data/proxy.sh 2> /dev/null - export _CONFIG_LAST_LOAD=$(date +%s) - set_proxy - fi - export PS1="[\u@${CLUSTER_NAME} \W]\$ " + _CONFIG_LAST_MODIFY=$(stat -c %Z /data/proxy.sh) + if [[ $_CONFIG_LAST_MODIFY -gt $_CONFIG_LAST_LOAD ]]; then + echo " -- Proxy Configuration Reloaded -- " + source /data/proxy.sh 2> /dev/null + export _CONFIG_LAST_LOAD=$(date +%s) + set_proxy + fi + export PS1="[\u@${CLUSTER_NAME} \W]\$ " } -export _CONFIG_LAST_LOAD="0" export PROMPT_COMMAND=ps1 export KUBECONFIG=/data/openshift-installer/auth/kubeconfig @@ -42,6 +40,4 @@ PYTHONUSERBASE=/deps/python ANSIBLE_COLLECTIONS_PATH=/deps/ansible PATH=/deps/python/bin:$PATH - - alias ll='ls -la' diff --git a/app/tmp/README.md b/pre/README.md similarity index 100% rename from app/tmp/README.md rename to pre/README.md diff --git a/app/tmp/ilorest-3.0.1-7.x86_64.rpm b/pre/ilorest-3.0.1-7.x86_64.rpm similarity index 100% rename from app/tmp/ilorest-3.0.1-7.x86_64.rpm rename to pre/ilorest-3.0.1-7.x86_64.rpm diff --git a/requirements.txt b/requirements.txt index bf23cef..858babe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -pyinquirer==1.0.3 ansible<2.11 +faros-config==0.2.0 # for management/ilo provider python-hpilo==4.4.3 diff --git a/version.txt b/version.txt index 6016e8a..0062ac9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -4.6.0 +5.0.0