123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606 |
- # TODO: Temporarily disabled due to importing old code into openshift-ansible
- # repo. We will work on these over time.
- # pylint: disable=bad-continuation,missing-docstring,no-self-use,invalid-name,no-value-for-parameter
- import click
- import os
- import re
- import sys
- from ooinstall import openshift_ansible
- from ooinstall import OOConfig
- from ooinstall.oo_config import Host
- from ooinstall.variants import find_variant, get_variant_version_combos
- DEFAULT_ANSIBLE_CONFIG = '/usr/share/atomic-openshift-utils/ansible.cfg'
- DEFAULT_PLAYBOOK_DIR = '/usr/share/ansible/openshift-ansible/'
- def validate_ansible_dir(path):
- if not path:
- raise click.BadParameter('An ansible path must be provided')
- return path
- # if not os.path.exists(path)):
- # raise click.BadParameter("Path \"{}\" doesn't exist".format(path))
- def is_valid_hostname(hostname):
- if not hostname or len(hostname) > 255:
- return False
- if hostname[-1] == ".":
- hostname = hostname[:-1] # strip exactly one dot from the right, if present
- allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
- return all(allowed.match(x) for x in hostname.split("."))
- def validate_prompt_hostname(hostname):
- if '' == hostname or is_valid_hostname(hostname):
- return hostname
- raise click.BadParameter('"{}" appears to be an invalid hostname. ' \
- 'Please double-check this value i' \
- 'and re-enter it.'.format(hostname))
- def get_ansible_ssh_user():
- click.clear()
- message = """
- This installation process will involve connecting to remote hosts via ssh. Any
- account may be used however if a non-root account is used it must have
- passwordless sudo access.
- """
- click.echo(message)
- return click.prompt('User for ssh access', default='root')
- def list_hosts(hosts):
- hosts_idx = range(len(hosts))
- for idx in hosts_idx:
- click.echo(' {}: {}'.format(idx, hosts[idx]))
- def delete_hosts(hosts):
- while True:
- list_hosts(hosts)
- del_idx = click.prompt('Select host to delete, y/Y to confirm, ' \
- 'or n/N to add more hosts', default='n')
- try:
- del_idx = int(del_idx)
- hosts.remove(hosts[del_idx])
- except IndexError:
- click.echo("\"{}\" doesn't match any hosts listed.".format(del_idx))
- except ValueError:
- try:
- response = del_idx.lower()
- if response in ['y', 'n']:
- return hosts, response
- click.echo("\"{}\" doesn't coorespond to any valid input.".format(del_idx))
- except AttributeError:
- click.echo("\"{}\" doesn't coorespond to any valid input.".format(del_idx))
- return hosts, None
- def collect_hosts():
- """
- Collect host information from user. This will later be filled in using
- ansible.
- Returns: a list of host information collected from the user
- """
- click.clear()
- click.echo('***Host Configuration***')
- message = """
- The OpenShift Master serves the API and web console. It also coordinates the
- jobs that have to run across the environment. It can even run the datastore.
- For wizard based installations the database will be embedded. It's possible to
- change this later using etcd from Red Hat Enterprise Linux 7.
- Any Masters configured as part of this installation process will also be
- configured as Nodes. This is so that the Master will be able to proxy to Pods
- from the API. By default this Node will be unscheduleable but this can be changed
- after installation with 'oadm manage-node'.
- The OpenShift Node provides the runtime environments for containers. It will
- host the required services to be managed by the Master.
- http://docs.openshift.com/enterprise/latest/architecture/infrastructure_components/kubernetes_infrastructure.html#master
- http://docs.openshift.com/enterprise/latest/architecture/infrastructure_components/kubernetes_infrastructure.html#node
- """
- click.echo(message)
- hosts = []
- more_hosts = True
- while more_hosts:
- host_props = {}
- hostname_or_ip = click.prompt('Enter hostname or IP address:',
- default='',
- value_proc=validate_prompt_hostname)
- host_props['connect_to'] = hostname_or_ip
- host_props['master'] = click.confirm('Will this host be an OpenShift Master?')
- host_props['node'] = True
- #TODO: Reenable this option once container installs are out of tech preview
- #rpm_or_container = click.prompt('Will this host be RPM or Container based (rpm/container)?',
- # type=click.Choice(['rpm', 'container']),
- # default='rpm')
- #if rpm_or_container == 'container':
- # host_props['containerized'] = True
- #else:
- # host_props['containerized'] = False
- host_props['containerized'] = False
- host = Host(**host_props)
- hosts.append(host)
- more_hosts = click.confirm('Do you want to add additional hosts?')
- return hosts
- def confirm_hosts_facts(oo_cfg, callback_facts):
- hosts = oo_cfg.hosts
- click.clear()
- message = """
- A list of the facts gathered from the provided hosts follows. Because it is
- often the case that the hostname for a system inside the cluster is different
- from the hostname that is resolveable from command line or web clients
- these settings cannot be validated automatically.
- For some cloud providers the installer is able to gather metadata exposed in
- the instance so reasonable defaults will be provided.
- Plese confirm that they are correct before moving forward.
- """
- notes = """
- Format:
- connect_to,IP,public IP,hostname,public hostname
- Notes:
- * The installation host is the hostname from the installer's perspective.
- * The IP of the host should be the internal IP of the instance.
- * The public IP should be the externally accessible IP associated with the instance
- * The hostname should resolve to the internal IP from the instances
- themselves.
- * The public hostname should resolve to the external ip from hosts outside of
- the cloud.
- """
- # For testing purposes we need to click.echo only once, so build up
- # the message:
- output = message
- default_facts_lines = []
- default_facts = {}
- for h in hosts:
- default_facts[h.connect_to] = {}
- h.ip = callback_facts[h.connect_to]["common"]["ip"]
- h.public_ip = callback_facts[h.connect_to]["common"]["public_ip"]
- h.hostname = callback_facts[h.connect_to]["common"]["hostname"]
- h.public_hostname = callback_facts[h.connect_to]["common"]["public_hostname"]
- default_facts_lines.append(",".join([h.connect_to,
- h.ip,
- h.public_ip,
- h.hostname,
- h.public_hostname]))
- output = "%s\n%s" % (output, ",".join([h.connect_to,
- h.ip,
- h.public_ip,
- h.hostname,
- h.public_hostname]))
- output = "%s\n%s" % (output, notes)
- click.echo(output)
- facts_confirmed = click.confirm("Do the above facts look correct?")
- if not facts_confirmed:
- message = """
- Edit %s with the desired values and run `atomic-openshift-installer --unattended install` to restart the install.
- """ % oo_cfg.config_path
- click.echo(message)
- # Make sure we actually write out the config file.
- oo_cfg.save_to_disk()
- sys.exit(0)
- return default_facts
- def get_variant_and_version():
- message = "\nWhich variant would you like to install?\n\n"
- i = 1
- combos = get_variant_version_combos()
- for (variant, version) in combos:
- message = "%s\n(%s) %s %s" % (message, i, variant.description,
- version.name)
- i = i + 1
- click.echo(message)
- response = click.prompt("Choose a variant from above: ", default=1)
- product, version = combos[response - 1]
- return product, version
- def confirm_continue(message):
- click.echo(message)
- click.confirm("Are you ready to continue?", default=False, abort=True)
- return
- def error_if_missing_info(oo_cfg):
- missing_info = False
- if not oo_cfg.hosts:
- missing_info = True
- click.echo('For unattended installs, hosts must be specified on the '
- 'command line or in the config file: %s' % oo_cfg.config_path)
- sys.exit(1)
- if 'ansible_ssh_user' not in oo_cfg.settings:
- click.echo("Must specify ansible_ssh_user in configuration file.")
- sys.exit(1)
- # Lookup a variant based on the key we were given:
- if not oo_cfg.settings['variant']:
- click.echo("No variant specified in configuration file.")
- sys.exit(1)
- ver = None
- if 'variant_version' in oo_cfg.settings:
- ver = oo_cfg.settings['variant_version']
- variant, version = find_variant(oo_cfg.settings['variant'], version=ver)
- if variant is None or version is None:
- err_variant_name = oo_cfg.settings['variant']
- if ver:
- err_variant_name = "%s %s" % (err_variant_name, ver)
- click.echo("%s is not an installable variant." % err_variant_name)
- sys.exit(1)
- oo_cfg.settings['variant_version'] = version.name
- missing_facts = oo_cfg.calc_missing_facts()
- if len(missing_facts) > 0:
- missing_info = True
- click.echo('For unattended installs, facts must be provided for all masters/nodes:')
- for host in missing_facts:
- click.echo('Host "%s" missing facts: %s' % (host, ", ".join(missing_facts[host])))
- if missing_info:
- sys.exit(1)
- def get_missing_info_from_user(oo_cfg):
- """ Prompts the user for any information missing from the given configuration. """
- click.clear()
- message = """
- Welcome to the OpenShift Enterprise 3 installation.
- Please confirm that following prerequisites have been met:
- * All systems where OpenShift will be installed are running Red Hat Enterprise
- Linux 7.
- * All systems are properly subscribed to the required OpenShift Enterprise 3
- repositories.
- * All systems have run docker-storage-setup (part of the Red Hat docker RPM).
- * All systems have working DNS that resolves not only from the perspective of
- the installer but also from within the cluster.
- When the process completes you will have a default configuration for Masters
- and Nodes. For ongoing environment maintenance it's recommended that the
- official Ansible playbooks be used.
- For more information on installation prerequisites please see:
- https://docs.openshift.com/enterprise/latest/admin_guide/install/prerequisites.html
- """
- confirm_continue(message)
- click.clear()
- if oo_cfg.settings.get('ansible_ssh_user', '') == '':
- oo_cfg.settings['ansible_ssh_user'] = get_ansible_ssh_user()
- click.clear()
- if not oo_cfg.hosts:
- oo_cfg.hosts = collect_hosts()
- click.clear()
- if oo_cfg.settings.get('variant', '') == '':
- variant, version = get_variant_and_version()
- oo_cfg.settings['variant'] = variant.name
- oo_cfg.settings['variant_version'] = version.name
- click.clear()
- return oo_cfg
- def collect_new_nodes():
- click.clear()
- click.echo('***New Node Configuration***')
- message = """
- Add new nodes here
- """
- click.echo(message)
- return collect_hosts()
- def get_installed_hosts(hosts, callback_facts):
- installed_hosts = []
- for host in hosts:
- if(host.connect_to in callback_facts.keys()
- and 'common' in callback_facts[host.connect_to].keys()
- and callback_facts[host.connect_to]['common'].get('version', '')
- and callback_facts[host.connect_to]['common'].get('version', '') != 'None'):
- installed_hosts.append(host)
- return installed_hosts
- # pylint: disable=too-many-branches
- # This pylint error will be corrected shortly in separate PR.
- def get_hosts_to_run_on(oo_cfg, callback_facts, unattended, force, verbose):
- # Copy the list of existing hosts so we can remove any already installed nodes.
- hosts_to_run_on = list(oo_cfg.hosts)
- # Check if master or nodes already have something installed
- installed_hosts = get_installed_hosts(oo_cfg.hosts, callback_facts)
- if len(installed_hosts) > 0:
- click.echo('Installed environment detected.')
- # This check has to happen before we start removing hosts later in this method
- if not force:
- if not unattended:
- click.echo('By default the installer only adds new nodes to an installed environment.')
- response = click.prompt('Do you want to (1) only add additional nodes or ' \
- '(2) reinstall the existing hosts ' \
- 'potentially erasing any custom changes?',
- type=int)
- # TODO: this should be reworked with error handling.
- # Click can certainly do this for us.
- # This should be refactored as soon as we add a 3rd option.
- if response == 1:
- force = False
- if response == 2:
- force = True
- # present a message listing already installed hosts and remove hosts if needed
- for host in installed_hosts:
- if host.master:
- click.echo("{} is already an OpenShift Master".format(host))
- # Masters stay in the list, we need to run against them when adding
- # new nodes.
- elif host.node:
- click.echo("{} is already an OpenShift Node".format(host))
- # force is only used for reinstalls so we don't want to remove
- # anything.
- if not force:
- hosts_to_run_on.remove(host)
- # Handle the cases where we know about uninstalled systems
- new_hosts = set(hosts_to_run_on) - set(installed_hosts)
- if len(new_hosts) > 0:
- for new_host in new_hosts:
- click.echo("{} is currently uninstalled".format(new_host))
- # Fall through
- click.echo('Adding additional nodes...')
- else:
- if unattended:
- if not force:
- click.echo('Installed environment detected and no additional nodes specified: ' \
- 'aborting. If you want a fresh install, use ' \
- '`atomic-openshift-installer install --force`')
- sys.exit(1)
- else:
- if not force:
- new_nodes = collect_new_nodes()
- hosts_to_run_on.extend(new_nodes)
- oo_cfg.hosts.extend(new_nodes)
- openshift_ansible.set_config(oo_cfg)
- click.echo('Gathering information from hosts...')
- callback_facts, error = openshift_ansible.default_facts(oo_cfg.hosts, verbose)
- if error:
- click.echo("There was a problem fetching the required information. " \
- "See {} for details.".format(oo_cfg.settings['ansible_log_path']))
- sys.exit(1)
- else:
- pass # proceeding as normal should do a clean install
- return hosts_to_run_on, callback_facts
- @click.group()
- @click.pass_context
- @click.option('--unattended', '-u', is_flag=True, default=False)
- @click.option('--configuration', '-c',
- type=click.Path(file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True),
- default=None)
- @click.option('--ansible-playbook-directory',
- '-a',
- type=click.Path(exists=True,
- file_okay=False,
- dir_okay=True,
- readable=True),
- # callback=validate_ansible_dir,
- default=DEFAULT_PLAYBOOK_DIR,
- envvar='OO_ANSIBLE_PLAYBOOK_DIRECTORY')
- @click.option('--ansible-config',
- type=click.Path(file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True),
- default=None)
- @click.option('--ansible-log-path',
- type=click.Path(file_okay=True,
- dir_okay=False,
- writable=True,
- readable=True),
- default="/tmp/ansible.log")
- @click.option('-v', '--verbose',
- is_flag=True, default=False)
- #pylint: disable=too-many-arguments
- # Main CLI entrypoint, not much we can do about too many arguments.
- def cli(ctx, unattended, configuration, ansible_playbook_directory, ansible_config, ansible_log_path, verbose):
- """
- atomic-openshift-installer makes the process for installing OSE or AEP easier by interactively gathering the data needed to run on each host.
- It can also be run in unattended mode if provided with a configuration file.
- Further reading: https://docs.openshift.com/enterprise/latest/install_config/install/quick_install.html
- """
- ctx.obj = {}
- ctx.obj['unattended'] = unattended
- ctx.obj['configuration'] = configuration
- ctx.obj['ansible_config'] = ansible_config
- ctx.obj['ansible_log_path'] = ansible_log_path
- ctx.obj['verbose'] = verbose
- oo_cfg = OOConfig(ctx.obj['configuration'])
- # If no playbook dir on the CLI, check the config:
- if not ansible_playbook_directory:
- ansible_playbook_directory = oo_cfg.settings.get('ansible_playbook_directory', '')
- # If still no playbook dir, check for the default location:
- if not ansible_playbook_directory and os.path.exists(DEFAULT_PLAYBOOK_DIR):
- ansible_playbook_directory = DEFAULT_PLAYBOOK_DIR
- validate_ansible_dir(ansible_playbook_directory)
- oo_cfg.settings['ansible_playbook_directory'] = ansible_playbook_directory
- oo_cfg.ansible_playbook_directory = ansible_playbook_directory
- ctx.obj['ansible_playbook_directory'] = ansible_playbook_directory
- if ctx.obj['ansible_config']:
- oo_cfg.settings['ansible_config'] = ctx.obj['ansible_config']
- elif os.path.exists(DEFAULT_ANSIBLE_CONFIG):
- # If we're installed by RPM this file should exist and we can use it as our default:
- oo_cfg.settings['ansible_config'] = DEFAULT_ANSIBLE_CONFIG
- oo_cfg.settings['ansible_log_path'] = ctx.obj['ansible_log_path']
- ctx.obj['oo_cfg'] = oo_cfg
- openshift_ansible.set_config(oo_cfg)
- @click.command()
- @click.pass_context
- def uninstall(ctx):
- oo_cfg = ctx.obj['oo_cfg']
- verbose = ctx.obj['verbose']
- if len(oo_cfg.hosts) == 0:
- click.echo("No hosts defined in: %s" % oo_cfg['configuration'])
- sys.exit(1)
- click.echo("OpenShift will be uninstalled from the following hosts:\n")
- if not ctx.obj['unattended']:
- # Prompt interactively to confirm:
- for host in oo_cfg.hosts:
- click.echo(" * %s" % host.connect_to)
- proceed = click.confirm("\nDo you wish to proceed?")
- if not proceed:
- click.echo("Uninstall cancelled.")
- sys.exit(0)
- openshift_ansible.run_uninstall_playbook(verbose)
- @click.command()
- @click.pass_context
- def upgrade(ctx):
- oo_cfg = ctx.obj['oo_cfg']
- verbose = ctx.obj['verbose']
- if len(oo_cfg.hosts) == 0:
- click.echo("No hosts defined in: %s" % oo_cfg.config_path)
- sys.exit(1)
- # Update config to reflect the version we're targetting, we'll write
- # to disk once ansible completes successfully, not before.
- old_variant = oo_cfg.settings['variant']
- old_version = oo_cfg.settings['variant_version']
- if oo_cfg.settings['variant'] == 'enterprise':
- oo_cfg.settings['variant'] = 'openshift-enterprise'
- version = find_variant(oo_cfg.settings['variant'])[1]
- oo_cfg.settings['variant_version'] = version.name
- click.echo("Openshift will be upgraded from %s %s to %s %s on the following hosts:\n" % (
- old_variant, old_version, oo_cfg.settings['variant'],
- oo_cfg.settings['variant_version']))
- for host in oo_cfg.hosts:
- click.echo(" * %s" % host.connect_to)
- if not ctx.obj['unattended']:
- # Prompt interactively to confirm:
- proceed = click.confirm("\nDo you wish to proceed?")
- if not proceed:
- click.echo("Upgrade cancelled.")
- sys.exit(0)
- retcode = openshift_ansible.run_upgrade_playbook(verbose)
- if retcode > 0:
- click.echo("Errors encountered during upgrade, please check %s." %
- oo_cfg.settings['ansible_log_path'])
- else:
- oo_cfg.save_to_disk()
- click.echo("Upgrade completed! Rebooting all hosts is recommended.")
- @click.command()
- @click.option('--force', '-f', is_flag=True, default=False)
- @click.pass_context
- def install(ctx, force):
- oo_cfg = ctx.obj['oo_cfg']
- verbose = ctx.obj['verbose']
- if ctx.obj['unattended']:
- error_if_missing_info(oo_cfg)
- else:
- oo_cfg = get_missing_info_from_user(oo_cfg)
- click.echo('Gathering information from hosts...')
- callback_facts, error = openshift_ansible.default_facts(oo_cfg.hosts,
- verbose)
- if error:
- click.echo("There was a problem fetching the required information. " \
- "Please see {} for details.".format(oo_cfg.settings['ansible_log_path']))
- sys.exit(1)
- hosts_to_run_on, callback_facts = get_hosts_to_run_on(
- oo_cfg, callback_facts, ctx.obj['unattended'], force, verbose)
- click.echo('Writing config to: %s' % oo_cfg.config_path)
- # We already verified this is not the case for unattended installs, so this can
- # only trigger for live CLI users:
- # TODO: if there are *new* nodes and this is a live install, we may need the user
- # to confirm the settings for new nodes. Look into this once we're distinguishing
- # between new and pre-existing nodes.
- if len(oo_cfg.calc_missing_facts()) > 0:
- confirm_hosts_facts(oo_cfg, callback_facts)
- oo_cfg.save_to_disk()
- click.echo('Ready to run installation process.')
- message = """
- If changes are needed to the values recorded by the installer please update {}.
- """.format(oo_cfg.config_path)
- if not ctx.obj['unattended']:
- confirm_continue(message)
- error = openshift_ansible.run_main_playbook(oo_cfg.hosts,
- hosts_to_run_on, verbose)
- if error:
- # The bootstrap script will print out the log location.
- message = """
- An error was detected. After resolving the problem please relaunch the
- installation process.
- """
- click.echo(message)
- sys.exit(1)
- else:
- message = """
- The installation was successful!
- If this is your first time installing please take a look at the Administrator
- Guide for advanced options related to routing, storage, authentication and much
- more:
- http://docs.openshift.com/enterprise/latest/admin_guide/overview.html
- """
- click.echo(message)
- click.pause()
- cli.add_command(install)
- cli.add_command(upgrade)
- cli.add_command(uninstall)
- if __name__ == '__main__':
- # This is expected behaviour for context passing with click library:
- # pylint: disable=unexpected-keyword-arg
- cli(obj={})
|