diff options
| -rw-r--r-- | utils/docs/man/man1/atomic-openshift-installer.1 | 18 | ||||
| -rw-r--r-- | utils/docs/man/man1/atomic-openshift-installer.1.asciidoc.in | 6 | ||||
| -rw-r--r-- | utils/src/ooinstall/cli_installer.py | 447 | ||||
| -rw-r--r-- | utils/src/ooinstall/oo_config.py | 8 | ||||
| -rw-r--r-- | utils/src/ooinstall/openshift_ansible.py | 9 | ||||
| -rw-r--r-- | utils/src/ooinstall/utils.py | 11 | ||||
| -rw-r--r-- | utils/test/cli_installer_tests.py | 59 | ||||
| -rw-r--r-- | utils/test/fixture.py | 17 | 
8 files changed, 328 insertions, 247 deletions
diff --git a/utils/docs/man/man1/atomic-openshift-installer.1 b/utils/docs/man/man1/atomic-openshift-installer.1 index 4da82191b..072833ce8 100644 --- a/utils/docs/man/man1/atomic-openshift-installer.1 +++ b/utils/docs/man/man1/atomic-openshift-installer.1 @@ -2,12 +2,12 @@  .\"     Title: atomic-openshift-installer  .\"    Author: [see the "AUTHOR" section]  .\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/> -.\"      Date: 09/28/2016 +.\"      Date: 10/20/2016  .\"    Manual: atomic-openshift-installer  .\"    Source: atomic-openshift-utils 1.3  .\"  Language: English  .\" -.TH "ATOMIC\-OPENSHIFT\-I" "1" "09/28/2016" "atomic\-openshift\-utils 1\&.3" "atomic\-openshift\-installer" +.TH "ATOMIC\-OPENSHIFT\-I" "1" "10/20/2016" "atomic\-openshift\-utils 1\&.3" "atomic\-openshift\-installer"  .\" -----------------------------------------------------------------  .\" * Define some portability stuff  .\" ----------------------------------------------------------------- @@ -121,6 +121,17 @@ Show the usage help and exit\&.  \fBupgrade\fR  .RE  .sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +\fBscaleup\fR +.RE +.sp  The options specific to each command are described in the following sections\&.  .SH "INSTALL"  .sp @@ -158,6 +169,9 @@ Upgrade to the latest major version\&. For example, if you are running version  then this could upgrade you to  \fB3\&.3\fR\&.  .RE +.SH "SCALEUP" +.sp +The \fBscaleup\fR command is used to add new nodes to an existing cluster\&. This command has no additional options\&.  .SH "FILES"  .sp  \fB~/\&.config/openshift/installer\&.cfg\&.yml\fR \(em Installer configuration file\&. Can be used to generate an inventory later or start an unattended installation\&. diff --git a/utils/docs/man/man1/atomic-openshift-installer.1.asciidoc.in b/utils/docs/man/man1/atomic-openshift-installer.1.asciidoc.in index 64e5d14a3..9b02c4d14 100644 --- a/utils/docs/man/man1/atomic-openshift-installer.1.asciidoc.in +++ b/utils/docs/man/man1/atomic-openshift-installer.1.asciidoc.in @@ -73,6 +73,7 @@ COMMANDS  * **install**  * **uninstall**  * **upgrade** +* **scaleup**  The options specific to each command are described in the following  sections. @@ -122,6 +123,11 @@ Upgrade to the latest major version. For example, if you are running  version **3.2** then this could upgrade you to **3.3**. +SCALEUP +------- + +The **scaleup** command is used to add new nodes to an existing cluster. +This command has no additional options.  FILES  ----- diff --git a/utils/src/ooinstall/cli_installer.py b/utils/src/ooinstall/cli_installer.py index 1e3697776..7c7770207 100644 --- a/utils/src/ooinstall/cli_installer.py +++ b/utils/src/ooinstall/cli_installer.py @@ -1,28 +1,24 @@ -# 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,too-many-lines +# pylint: disable=missing-docstring,no-self-use,no-value-for-parameter,too-many-lines +import logging  import os -import re  import sys -import logging +  import click  from pkg_resources import parse_version -from ooinstall import openshift_ansible -from ooinstall.oo_config import OOConfig -from ooinstall.oo_config import OOConfigInvalidHostError -from ooinstall.oo_config import Host, Role +from ooinstall import openshift_ansible, utils +from ooinstall.oo_config import Host, OOConfig, OOConfigInvalidHostError, Role  from ooinstall.variants import find_variant, get_variant_version_combos -installer_log = logging.getLogger('installer') -installer_log.setLevel(logging.CRITICAL) -installer_file_handler = logging.FileHandler('/tmp/installer.txt') -installer_file_handler.setFormatter( +INSTALLER_LOG = logging.getLogger('installer') +INSTALLER_LOG.setLevel(logging.CRITICAL) +INSTALLER_FILE_HANDLER = logging.FileHandler('/tmp/installer.txt') +INSTALLER_FILE_HANDLER.setFormatter(      logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))  # Example output:  #   2016-08-23 07:34:58,480 - installer - DEBUG - Going to 'load_system_facts' -installer_file_handler.setLevel(logging.DEBUG) -installer_log.addHandler(installer_file_handler) +INSTALLER_FILE_HANDLER.setLevel(logging.DEBUG) +INSTALLER_LOG.addHandler(INSTALLER_FILE_HANDLER)  DEFAULT_ANSIBLE_CONFIG = '/usr/share/atomic-openshift-utils/ansible.cfg'  QUIET_ANSIBLE_CONFIG = '/usr/share/atomic-openshift-utils/ansible-quiet.cfg' @@ -58,17 +54,8 @@ def validate_ansible_dir(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): +    if hostname == '' or utils.is_valid_hostname(hostname):          return hostname      raise click.BadParameter('Invalid hostname. Please double-check this value and re-enter it.') @@ -84,7 +71,7 @@ passwordless sudo access.      return click.prompt('User for ssh access', default='root') -def get_master_routingconfig_subdomain(): +def get_routingconfig_subdomain():      click.clear()      message = """  You might want to override the default subdomain used for exposed routes. If you don't know what this is, use the default value. @@ -183,8 +170,9 @@ http://docs.openshift.com/enterprise/latest/architecture/infrastructure_componen          if masters_set or num_masters != 2:              more_hosts = click.confirm('Do you want to add additional hosts?') -    if num_masters >= 3: -        collect_master_lb(hosts) +    master_lb = collect_master_lb(hosts) +    if master_lb: +        hosts.append(master_lb)          roles.add('master_lb')      if not existing_env: @@ -193,7 +181,8 @@ http://docs.openshift.com/enterprise/latest/architecture/infrastructure_componen      return hosts, roles -def print_installation_summary(hosts, version=None): +# pylint: disable=too-many-branches +def print_installation_summary(hosts, version=None, verbose=True):      """      Displays a summary of all hosts configured thus far, and what role each      will play. @@ -214,35 +203,36 @@ def print_installation_summary(hosts, version=None):      click.echo('Total OpenShift masters: %s' % len(masters))      click.echo('Total OpenShift nodes: %s' % len(nodes)) -    if len(masters) == 1 and version != '3.0': -        ha_hint_message = """ +    if verbose: +        if len(masters) == 1 and version != '3.0': +            ha_hint_message = """  NOTE: Add a total of 3 or more masters to perform an HA installation.""" -        click.echo(ha_hint_message) -    elif len(masters) == 2: -        min_masters_message = """ +            click.echo(ha_hint_message) +        elif len(masters) == 2: +            min_masters_message = """  WARNING: A minimum of 3 masters are required to perform an HA installation.  Please add one more to proceed.""" -        click.echo(min_masters_message) -    elif len(masters) >= 3: -        ha_message = """ +            click.echo(min_masters_message) +        elif len(masters) >= 3: +            ha_message = """  NOTE: Multiple masters specified, this will be an HA deployment with a separate  etcd cluster. You will be prompted to provide the FQDN of a load balancer and  a host for storage once finished entering hosts. -""" -        click.echo(ha_message) +    """ +            click.echo(ha_message) -        dedicated_nodes_message = """ +            dedicated_nodes_message = """  WARNING: Dedicated nodes are recommended for an HA deployment. If no dedicated  nodes are specified, each configured master will be marked as a schedulable  node.""" -        min_ha_nodes_message = """ +            min_ha_nodes_message = """  WARNING: A minimum of 3 dedicated nodes are recommended for an HA  deployment.""" -        if len(dedicated_nodes) == 0: -            click.echo(dedicated_nodes_message) -        elif len(dedicated_nodes) < 3: -            click.echo(min_ha_nodes_message) +            if len(dedicated_nodes) == 0: +                click.echo(dedicated_nodes_message) +            elif len(dedicated_nodes) < 3: +                click.echo(min_ha_nodes_message)      click.echo('') @@ -270,6 +260,8 @@ def print_host_summary(all_hosts, host):              click.echo("  - Etcd (Embedded)")      if host.is_storage():          click.echo("  - Storage") +    if host.new_host: +        click.echo("  - NEW")  def collect_master_lb(hosts): @@ -307,14 +299,18 @@ hostname.                                           'please specify a separate host' % hostname)          return hostname -    host_props['connect_to'] = click.prompt('Enter hostname or IP address', -                                            value_proc=validate_prompt_lb) -    install_haproxy = \ -        click.confirm('Should the reference HAProxy load balancer be installed on this host?') -    host_props['preconfigured'] = not install_haproxy -    host_props['roles'] = ['master_lb'] -    master_lb = Host(**host_props) -    hosts.append(master_lb) +    lb_hostname = click.prompt('Enter hostname or IP address', +                               value_proc=validate_prompt_lb, +                               default='') +    if lb_hostname: +        host_props['connect_to'] = lb_hostname +        install_haproxy = \ +            click.confirm('Should the reference HAProxy load balancer be installed on this host?') +        host_props['preconfigured'] = not install_haproxy +        host_props['roles'] = ['master_lb'] +        return Host(**host_props) +    else: +        return None  def collect_storage_host(hosts): @@ -395,29 +391,29 @@ Notes:      default_facts_lines = []      default_facts = {} -    for h in hosts: -        if h.preconfigured: +    for host in hosts: +        if host.preconfigured:              continue          try: -            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[host.connect_to] = {} +            host.ip = callback_facts[host.connect_to]["common"]["ip"] +            host.public_ip = callback_facts[host.connect_to]["common"]["public_ip"] +            host.hostname = callback_facts[host.connect_to]["common"]["hostname"] +            host.public_hostname = callback_facts[host.connect_to]["common"]["public_hostname"]          except KeyError: -            click.echo("Problem fetching facts from {}".format(h.connect_to)) +            click.echo("Problem fetching facts from {}".format(host.connect_to))              continue -        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])) +        default_facts_lines.append(",".join([host.connect_to, +                                             host.ip, +                                             host.public_ip, +                                             host.hostname, +                                             host.public_hostname])) +        output = "%s\n%s" % (output, ",".join([host.connect_to, +                                               host.ip, +                                               host.public_ip, +                                               host.hostname, +                                               host.public_hostname]))      output = "%s\n%s" % (output, notes)      click.echo(output) @@ -534,7 +530,7 @@ def error_if_missing_info(oo_cfg):      oo_cfg.settings['variant_version'] = version.name      # check that all listed host roles are included -    listed_roles = get_host_roles_set(oo_cfg) +    listed_roles = oo_cfg.get_host_roles_set()      configured_roles = set([role for role in oo_cfg.deployment.roles])      if listed_roles != configured_roles:          missing_info = True @@ -544,16 +540,7 @@ def error_if_missing_info(oo_cfg):          sys.exit(1) -def get_host_roles_set(oo_cfg): -    roles_set = set() -    for host in oo_cfg.deployment.hosts: -        for role in host.roles: -            roles_set.add(role) - -    return roles_set - - -def get_proxy_hostnames_and_excludes(): +def get_proxy_hosts_excludes():      message = """  If a proxy is needed to reach HTTP and HTTPS traffic, please enter the  name below. This proxy will be configured by default for all processes @@ -635,7 +622,8 @@ https://docs.openshift.com/enterprise/latest/admin_guide/install/prerequisites.h          click.clear()      if 'master_routingconfig_subdomain' not in oo_cfg.deployment.variables: -        oo_cfg.deployment.variables['master_routingconfig_subdomain'] = get_master_routingconfig_subdomain() +        oo_cfg.deployment.variables['master_routingconfig_subdomain'] = \ +            get_routingconfig_subdomain()          click.clear()      # Are any proxy vars already presisted? @@ -644,7 +632,7 @@ https://docs.openshift.com/enterprise/latest/admin_guide/install/prerequisites.h      saved_proxy_vars = [pv for pv in proxy_vars                          if oo_cfg.deployment.variables.get(pv, 'UNSET') is not 'UNSET'] -    installer_log.debug("Evaluated proxy settings, found %s presisted values", +    INSTALLER_LOG.debug("Evaluated proxy settings, found %s presisted values",                          len(saved_proxy_vars))      current_version = parse_version(          oo_cfg.settings.get('variant_version', '0.0')) @@ -654,8 +642,8 @@ https://docs.openshift.com/enterprise/latest/admin_guide/install/prerequisites.h      # recognizes proxy parameters. We must prompt the user for values      # if this conditional is true.      if not saved_proxy_vars and current_version >= min_version: -        installer_log.debug("Prompting user to enter proxy values") -        http_proxy, https_proxy, proxy_excludes = get_proxy_hostnames_and_excludes() +        INSTALLER_LOG.debug("Prompting user to enter proxy values") +        http_proxy, https_proxy, proxy_excludes = get_proxy_hosts_excludes()          oo_cfg.deployment.variables['proxy_http'] = http_proxy          oo_cfg.deployment.variables['proxy_https'] = https_proxy          oo_cfg.deployment.variables['proxy_exclude_hosts'] = proxy_excludes @@ -709,82 +697,64 @@ def is_installed_host(host, callback_facts):      return version_found -# 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.deployment.hosts) +def get_hosts_to_run_on(oo_cfg, callback_facts, unattended, force): +    """ +    We get here once there are hosts in oo_cfg and we need to find out what +    state they are in. There are several different cases that might occur: + +    1. All hosts in oo_cfg are uninstalled. In this case, we should proceed +       with a normal installation. +    2. All hosts in oo_cfg are installed. In this case, ask the user if they +       want to force reinstall or exit. We can also hint in this case about +       the scaleup workflow. +    3. Some hosts are installed and some are uninstalled. In this case, prompt +       the user if they want to force (re)install all hosts specified or direct +       them to the scaleup workflow and exit. +    """ +    hosts_to_run_on = []      # Check if master or nodes already have something installed -    installed_hosts, uninstalled_hosts = get_installed_hosts(oo_cfg.deployment.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 +    installed_hosts, uninstalled_hosts = get_installed_hosts(oo_cfg.deployment.hosts, +                                                             callback_facts) +    nodes = [host for host in oo_cfg.deployment.hosts if host.is_node()] + +    # Case (1): All uninstalled hosts +    if len(uninstalled_hosts) == len(nodes): +        click.echo('All hosts in config are uninstalled. Proceeding with installation...') +        hosts_to_run_on = list(oo_cfg.deployment.hosts) +    else: +        # Case (2): All installed hosts +        if len(installed_hosts) == len(list(oo_cfg.deployment.hosts)): +            message = """ +All specified hosts in specified environment are installed. +""" +        # Case (3): Some installed, some uninstalled +        else: +            message = """ +A mix of installed and uninstalled hosts have been detected in your environment. +Please make sure your environment was installed successfully before adding new nodes. +""" + +        click.echo(message) + +        if not unattended: +            response = click.confirm('Do you want to (re)install the environment?\n\n' +                                     'Note: This will potentially erase any custom changes.') +            if response: +                hosts_to_run_on = list(oo_cfg.deployment.hosts) +                force = True +        elif unattended and force: +            hosts_to_run_on = list(oo_cfg.deployment.hosts)          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.is_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.is_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 -        # TODO: This logic is getting hard to understand. -        # we should revise all this to be cleaner. -        if not force and len(uninstalled_hosts) > 0: -            for uninstalled_host in uninstalled_hosts: -                click.echo("{} is currently uninstalled".format(uninstalled_host)) -            # Fall through -            click.echo('\nUninstalled hosts have been detected in your environment. ' -                       'Please make sure your environment was installed successfully ' -                       'before adding new nodes. If you want a fresh install, use ' -                       '`atomic-openshift-installer install --force`') +            message = """ +If you want to force reinstall of your environment, run: +`atomic-openshift-installer install --force` + +If you want to add new nodes to this environment, run: +`atomic-openshift-installer scaleup` +""" +            click.echo(message)              sys.exit(1) -        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(oo_cfg) - -                    hosts_to_run_on.extend(new_nodes) -                    oo_cfg.deployment.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.deployment.hosts, verbose) -                    if error or callback_facts is None: -                        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 @@ -800,6 +770,49 @@ def set_infra_nodes(hosts):          host.node_labels = "{'region': 'infra'}" +def run_config_playbook(oo_cfg, hosts_to_run_on, unattended, verbose, gen_inventory): +    # Write Ansible inventory file to disk: +    inventory_file = openshift_ansible.generate_inventory(hosts_to_run_on) + +    click.echo() +    click.echo('Wrote atomic-openshift-installer config: %s' % oo_cfg.config_path) +    click.echo("Wrote Ansible inventory: %s" % inventory_file) +    click.echo() + +    if gen_inventory: +        sys.exit(0) + +    click.echo('Ready to run installation process.') +    message = """ +If changes are needed please edit the config file above and re-run. +""" +    if not unattended: +        confirm_continue(message) + +    error = openshift_ansible.run_main_playbook(inventory_file, oo_cfg.deployment.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 +more: + +http://docs.openshift.com/enterprise/latest/admin_guide/overview.html +""" +        click.echo(message) + +  @click.group()  @click.pass_context  @click.option('--unattended', '-u', is_flag=True, default=False) @@ -846,8 +859,8 @@ def cli(ctx, unattended, configuration, ansible_playbook_directory, ansible_log_          # highest), anything below that (we only use debug/warning          # presently) is not logged. If '-d' is given though, we'll          # lower the threshold to debug (almost everything gets through) -        installer_log.setLevel(logging.DEBUG) -        installer_log.debug("Quick Installer debugging initialized") +        INSTALLER_LOG.setLevel(logging.DEBUG) +        INSTALLER_LOG.debug("Quick Installer debugging initialized")      ctx.obj = {}      ctx.obj['unattended'] = unattended @@ -857,8 +870,8 @@ def cli(ctx, unattended, configuration, ansible_playbook_directory, ansible_log_      try:          oo_cfg = OOConfig(ctx.obj['configuration']) -    except OOConfigInvalidHostError as e: -        click.echo(e) +    except OOConfigInvalidHostError as err: +        click.echo(err)          sys.exit(1)      # If no playbook dir on the CLI, check the config: @@ -916,7 +929,7 @@ def uninstall(ctx):  @click.option('--latest-minor', '-l', is_flag=True, default=False)  @click.option('--next-major', '-n', is_flag=True, default=False)  @click.pass_context -# pylint: disable=too-many-statements +# pylint: disable=too-many-statements,too-many-branches  def upgrade(ctx, latest_minor, next_major):      oo_cfg = ctx.obj['oo_cfg'] @@ -1013,15 +1026,17 @@ def upgrade(ctx, latest_minor, next_major):  def install(ctx, force, gen_inventory):      oo_cfg = ctx.obj['oo_cfg']      verbose = ctx.obj['verbose'] +    unattended = ctx.obj['unattended'] -    if ctx.obj['unattended']: +    if unattended:          error_if_missing_info(oo_cfg)      else:          oo_cfg = get_missing_info_from_user(oo_cfg) -    check_hosts_config(oo_cfg, ctx.obj['unattended']) +    check_hosts_config(oo_cfg, unattended) -    print_installation_summary(oo_cfg.deployment.hosts, oo_cfg.settings.get('variant_version', None)) +    print_installation_summary(oo_cfg.deployment.hosts, +                               oo_cfg.settings.get('variant_version', None))      click.echo('Gathering information from hosts...')      callback_facts, error = openshift_ansible.default_facts(oo_cfg.deployment.hosts,                                                              verbose) @@ -1031,62 +1046,92 @@ def install(ctx, force, gen_inventory):                     "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) +    hosts_to_run_on, callback_facts = get_hosts_to_run_on(oo_cfg, +                                                          callback_facts, +                                                          unattended, +                                                          force)      # 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 not ctx.obj['unattended'] and len(oo_cfg.calc_missing_facts()) > 0:          confirm_hosts_facts(oo_cfg, callback_facts)      # Write quick installer config file to disk:      oo_cfg.save_to_disk() -    # Write Ansible inventory file to disk: -    inventory_file = openshift_ansible.generate_inventory(hosts_to_run_on) +    run_config_playbook(oo_cfg, hosts_to_run_on, unattended, verbose, gen_inventory) -    click.echo() -    click.echo('Wrote atomic-openshift-installer config: %s' % oo_cfg.config_path) -    click.echo("Wrote Ansible inventory: %s" % inventory_file) -    click.echo() -    if gen_inventory: -        sys.exit(0) +@click.command() +@click.option('--gen-inventory', is_flag=True, default=False, +              help="Generate an Ansible inventory file and exit.") +@click.pass_context +def scaleup(ctx, gen_inventory): +    oo_cfg = ctx.obj['oo_cfg'] +    verbose = ctx.obj['verbose'] +    unattended = ctx.obj['unattended'] -    click.echo('Ready to run installation process.') +    installed_hosts = list(oo_cfg.deployment.hosts) + +    if len(installed_hosts) == 0: +        click.echo('No hosts specified.') +        sys.exit(1) + +    click.echo('Welcome to the OpenShift Enterprise 3 Scaleup utility.') + +    print_installation_summary(installed_hosts, +                               oo_cfg.settings['variant_version'], +                               verbose=False,)      message = """ -If changes are needed please edit the config file above and re-run. -""" -    if not ctx.obj['unattended']: -        confirm_continue(message) +--- -    error = openshift_ansible.run_main_playbook(inventory_file, oo_cfg.deployment.hosts, -                                                hosts_to_run_on, verbose) +We have detected this previously installed OpenShift environment. -    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. +This tool will guide you through the process of adding additional +nodes to your cluster.  """ -        click.echo(message) +    confirm_continue(message) + +    error_if_missing_info(oo_cfg) +    check_hosts_config(oo_cfg, True) + +    installed_masters = [host for host in installed_hosts if host.is_master()] +    new_nodes = collect_new_nodes(oo_cfg) + +    oo_cfg.deployment.hosts.extend(new_nodes) +    hosts_to_run_on = installed_masters + new_nodes + +    openshift_ansible.set_config(oo_cfg) +    click.echo('Gathering information from hosts...') +    callback_facts, error = openshift_ansible.default_facts(oo_cfg.deployment.hosts, verbose) +    if error or callback_facts is None: +        click.echo("There was a problem fetching the required information. See " +                   "{} for details.".format(oo_cfg.settings['ansible_log_path']))          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 -more: +    print_installation_summary(oo_cfg.deployment.hosts, +                               oo_cfg.settings.get('variant_version', None)) +    click.echo('Gathering information from hosts...') +    callback_facts, error = openshift_ansible.default_facts(oo_cfg.deployment.hosts, +                                                            verbose) + +    if error or callback_facts is None: +        click.echo("There was a problem fetching the required information. " +                   "Please see {} for details.".format(oo_cfg.settings['ansible_log_path'])) +        sys.exit(1) + +    # We already verified this is not the case for unattended installs, so this can +    # only trigger for live CLI users: +    if not ctx.obj['unattended'] and len(oo_cfg.calc_missing_facts()) > 0: +        confirm_hosts_facts(oo_cfg, callback_facts) + +    # Write quick installer config file to disk: +    oo_cfg.save_to_disk() +    run_config_playbook(oo_cfg, hosts_to_run_on, unattended, verbose, gen_inventory) -http://docs.openshift.com/enterprise/latest/admin_guide/overview.html -""" -        click.echo(message)  cli.add_command(install) +cli.add_command(scaleup)  cli.add_command(upgrade)  cli.add_command(uninstall) diff --git a/utils/src/ooinstall/oo_config.py b/utils/src/ooinstall/oo_config.py index 697ac9c08..e6bff7133 100644 --- a/utils/src/ooinstall/oo_config.py +++ b/utils/src/ooinstall/oo_config.py @@ -436,3 +436,11 @@ class OOConfig(object):              if host.connect_to == name:                  return host          return None + +    def get_host_roles_set(self): +        roles_set = set() +        for host in self.deployment.hosts: +            for role in host.roles: +                roles_set.add(role) + +        return roles_set diff --git a/utils/src/ooinstall/openshift_ansible.py b/utils/src/ooinstall/openshift_ansible.py index 80a79a6d2..764cc1e56 100644 --- a/utils/src/ooinstall/openshift_ansible.py +++ b/utils/src/ooinstall/openshift_ansible.py @@ -48,9 +48,6 @@ def set_config(cfg):  def generate_inventory(hosts):      global CFG -    masters = [host for host in hosts if host.is_master()] -    multiple_masters = len(masters) > 1 -      new_nodes = [host for host in hosts if host.is_node() and host.new_host]      scaleup = len(new_nodes) > 0 @@ -61,7 +58,7 @@ def generate_inventory(hosts):      write_inventory_children(base_inventory, scaleup) -    write_inventory_vars(base_inventory, multiple_masters, lb) +    write_inventory_vars(base_inventory, lb)      # write_inventory_hosts      for role in CFG.deployment.roles: @@ -106,7 +103,7 @@ def write_inventory_children(base_inventory, scaleup):  # pylint: disable=too-many-branches -def write_inventory_vars(base_inventory, multiple_masters, lb): +def write_inventory_vars(base_inventory, lb):      global CFG      base_inventory.write('\n[OSEv3:vars]\n') @@ -123,7 +120,7 @@ def write_inventory_vars(base_inventory, multiple_masters, lb):      if CFG.deployment.variables['ansible_ssh_user'] != 'root':          base_inventory.write('ansible_become=yes\n') -    if multiple_masters and lb is not None: +    if lb is not None:          base_inventory.write('openshift_master_cluster_method=native\n')          base_inventory.write("openshift_master_cluster_hostname={}\n".format(lb.hostname))          base_inventory.write( diff --git a/utils/src/ooinstall/utils.py b/utils/src/ooinstall/utils.py index eb27a57e4..85a77c75e 100644 --- a/utils/src/ooinstall/utils.py +++ b/utils/src/ooinstall/utils.py @@ -1,4 +1,6 @@  import logging +import re +  installer_log = logging.getLogger('installer') @@ -8,3 +10,12 @@ def debug_env(env):          if k.startswith("OPENSHIFT") or k.startswith("ANSIBLE") or k.startswith("OO"):              installer_log.debug("{key}: {value}".format(                  key=k, value=env[k])) + + +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(".")) diff --git a/utils/test/cli_installer_tests.py b/utils/test/cli_installer_tests.py index 34392777b..36dc18034 100644 --- a/utils/test/cli_installer_tests.py +++ b/utils/test/cli_installer_tests.py @@ -842,7 +842,7 @@ class AttendedCliTests(OOCliFixture):      # interactive with config file and some installed some uninstalled hosts      @patch('ooinstall.openshift_ansible.run_main_playbook')      @patch('ooinstall.openshift_ansible.load_system_facts') -    def test_add_nodes(self, load_facts_mock, run_playbook_mock): +    def test_scaleup_hint(self, load_facts_mock, run_playbook_mock):          # Modify the mock facts to return a version indicating OpenShift          # is already installed on our master, and the first node. @@ -866,13 +866,12 @@ class AttendedCliTests(OOCliFixture):          result = self.runner.invoke(cli.cli,                                      self.cli_args,                                      input=cli_input) -        self.assert_result(result, 0) -        self._verify_load_facts(load_facts_mock) -        self._verify_run_playbook(run_playbook_mock, 3, 2) +        # This is testing the install workflow so we want to make sure we +        # exit with the appropriate hint. +        self.assertTrue('scaleup' in result.output) +        self.assert_result(result, 1) -        written_config = read_yaml(self.config_file) -        self._verify_config_hosts(written_config, 3)      @patch('ooinstall.openshift_ansible.run_main_playbook')      @patch('ooinstall.openshift_ansible.load_system_facts') @@ -897,30 +896,30 @@ class AttendedCliTests(OOCliFixture):          written_config = read_yaml(config_file)          self._verify_config_hosts(written_config, 3) -    #interactive with config file and all installed hosts -    @patch('ooinstall.openshift_ansible.run_main_playbook') -    @patch('ooinstall.openshift_ansible.load_system_facts') -    def test_get_hosts_to_run_on(self, load_facts_mock, run_playbook_mock): -        mock_facts = copy.deepcopy(MOCK_FACTS) -        mock_facts['10.0.0.1']['common']['version'] = "3.0.0" -        mock_facts['10.0.0.2']['common']['version'] = "3.0.0" - -        cli_input = build_input(hosts=[ -            ('10.0.0.1', True, False), -            ], -                                      add_nodes=[('10.0.0.2', False, False)], -                                      ssh_user='root', -                                      variant_num=1, -                                      schedulable_masters_ok=True, -                                      confirm_facts='y', -                                      storage='10.0.0.1',) - -        self._verify_get_hosts_to_run_on(mock_facts, load_facts_mock, -                                         run_playbook_mock, -                                         cli_input, -                                         exp_hosts_len=2, -                                         exp_hosts_to_run_on_len=2, -                                         force=False) +#    #interactive with config file and all installed hosts +#    @patch('ooinstall.openshift_ansible.run_main_playbook') +#    @patch('ooinstall.openshift_ansible.load_system_facts') +#    def test_get_hosts_to_run_on(self, load_facts_mock, run_playbook_mock): +#        mock_facts = copy.deepcopy(MOCK_FACTS) +#        mock_facts['10.0.0.1']['common']['version'] = "3.0.0" +#        mock_facts['10.0.0.2']['common']['version'] = "3.0.0" +# +#        cli_input = build_input(hosts=[ +#            ('10.0.0.1', True, False), +#            ], +#                                      add_nodes=[('10.0.0.2', False, False)], +#                                      ssh_user='root', +#                                      variant_num=1, +#                                      schedulable_masters_ok=True, +#                                      confirm_facts='y', +#                                      storage='10.0.0.1',) +# +#        self._verify_get_hosts_to_run_on(mock_facts, load_facts_mock, +#                                         run_playbook_mock, +#                                         cli_input, +#                                         exp_hosts_len=2, +#                                         exp_hosts_to_run_on_len=2, +#                                         force=False)      #interactive multimaster: one more node than master      @patch('ooinstall.openshift_ansible.run_main_playbook') diff --git a/utils/test/fixture.py b/utils/test/fixture.py index a883e5c56..62135c761 100644 --- a/utils/test/fixture.py +++ b/utils/test/fixture.py @@ -138,8 +138,8 @@ class OOCliFixture(OOInstallFixture):              written_config = read_yaml(config_file)              self._verify_config_hosts(written_config, exp_hosts_len) -        if "Uninstalled" in result.output: -            # verify we exited on seeing uninstalled hosts +        if "If you want to force reinstall" in result.output: +            # verify we exited on seeing installed hosts              self.assertEqual(result.exit_code, 1)          else:              self.assert_result(result, 0) @@ -156,7 +156,7 @@ class OOCliFixture(OOInstallFixture):  #pylint: disable=too-many-arguments,too-many-branches,too-many-statements  def build_input(ssh_user=None, hosts=None, variant_num=None,                  add_nodes=None, confirm_facts=None, schedulable_masters_ok=None, -                master_lb=None, storage=None): +                master_lb=('', False), storage=None):      """      Build an input string simulating a user entering values in an interactive      attended install. @@ -204,11 +204,11 @@ def build_input(ssh_user=None, hosts=None, variant_num=None,              i += 1      # You can pass a single master_lb or a list if you intend for one to get rejected: -    if master_lb: -        if isinstance(master_lb[0], list) or isinstance(master_lb[0], tuple): -            inputs.extend(master_lb[0]) -        else: -            inputs.append(master_lb[0]) +    if isinstance(master_lb[0], list) or isinstance(master_lb[0], tuple): +        inputs.extend(master_lb[0]) +    else: +        inputs.append(master_lb[0]) +    if master_lb[0]:          inputs.append('y' if master_lb[1] else 'n')      if storage: @@ -248,6 +248,7 @@ def build_input(ssh_user=None, hosts=None, variant_num=None,      inputs.extend([          confirm_facts,          'y',  # lets do this +        'y',      ])      return '\n'.join(inputs)  | 
