瀏覽代碼

Merge pull request #2563 from smunilla/BZ1339621

a-o-i: Separate install and scaleup workflows
Andrew Butcher 8 年之前
父節點
當前提交
65a478443e

+ 16 - 2
utils/docs/man/man1/atomic-openshift-installer.1

@@ -2,12 +2,12 @@
 .\"     Title: atomic-openshift-installer
 .\"     Title: atomic-openshift-installer
 .\"    Author: [see the "AUTHOR" section]
 .\"    Author: [see the "AUTHOR" section]
 .\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
 .\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
-.\"      Date: 09/28/2016
+.\"      Date: 10/20/2016
 .\"    Manual: atomic-openshift-installer
 .\"    Manual: atomic-openshift-installer
 .\"    Source: atomic-openshift-utils 1.3
 .\"    Source: atomic-openshift-utils 1.3
 .\"  Language: English
 .\"  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
 .\" * Define some portability stuff
 .\" -----------------------------------------------------------------
 .\" -----------------------------------------------------------------
@@ -121,6 +121,17 @@ Show the usage help and exit\&.
 \fBupgrade\fR
 \fBupgrade\fR
 .RE
 .RE
 .sp
 .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\&.
 The options specific to each command are described in the following sections\&.
 .SH "INSTALL"
 .SH "INSTALL"
 .sp
 .sp
@@ -158,6 +169,9 @@ Upgrade to the latest major version\&. For example, if you are running version
 then this could upgrade you to
 then this could upgrade you to
 \fB3\&.3\fR\&.
 \fB3\&.3\fR\&.
 .RE
 .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"
 .SH "FILES"
 .sp
 .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\&.
 \fB~/\&.config/openshift/installer\&.cfg\&.yml\fR \(em Installer configuration file\&. Can be used to generate an inventory later or start an unattended installation\&.

+ 6 - 0
utils/docs/man/man1/atomic-openshift-installer.1.asciidoc.in

@@ -73,6 +73,7 @@ COMMANDS
 * **install**
 * **install**
 * **uninstall**
 * **uninstall**
 * **upgrade**
 * **upgrade**
+* **scaleup**
 
 
 The options specific to each command are described in the following
 The options specific to each command are described in the following
 sections.
 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**.
 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
 FILES
 -----
 -----

+ 246 - 201
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 os
-import re
 import sys
 import sys
-import logging
+
 import click
 import click
 from pkg_resources import parse_version
 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
 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'))
     logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
 # Example output:
 # Example output:
 #   2016-08-23 07:34:58,480 - installer - DEBUG - Going to 'load_system_facts'
 #   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'
 DEFAULT_ANSIBLE_CONFIG = '/usr/share/atomic-openshift-utils/ansible.cfg'
 QUIET_ANSIBLE_CONFIG = '/usr/share/atomic-openshift-utils/ansible-quiet.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))
     #     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):
 def validate_prompt_hostname(hostname):
-    if hostname == '' or is_valid_hostname(hostname):
+    if hostname == '' or utils.is_valid_hostname(hostname):
         return hostname
         return hostname
     raise click.BadParameter('Invalid hostname. Please double-check this value and re-enter it.')
     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')
     return click.prompt('User for ssh access', default='root')
 
 
 
 
-def get_master_routingconfig_subdomain():
+def get_routingconfig_subdomain():
     click.clear()
     click.clear()
     message = """
     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.
 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:
         if masters_set or num_masters != 2:
             more_hosts = click.confirm('Do you want to add additional hosts?')
             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')
         roles.add('master_lb')
 
 
     if not existing_env:
     if not existing_env:
@@ -193,7 +181,8 @@ http://docs.openshift.com/enterprise/latest/architecture/infrastructure_componen
     return hosts, roles
     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
     Displays a summary of all hosts configured thus far, and what role each
     will play.
     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 masters: %s' % len(masters))
     click.echo('Total OpenShift nodes: %s' % len(nodes))
     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."""
 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.
 WARNING: A minimum of 3 masters are required to perform an HA installation.
 Please add one more to proceed."""
 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
 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
 etcd cluster. You will be prompted to provide the FQDN of a load balancer and
 a host for storage once finished entering hosts.
 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
 WARNING: Dedicated nodes are recommended for an HA deployment. If no dedicated
 nodes are specified, each configured master will be marked as a schedulable
 nodes are specified, each configured master will be marked as a schedulable
 node."""
 node."""
 
 
-        min_ha_nodes_message = """
+            min_ha_nodes_message = """
 WARNING: A minimum of 3 dedicated nodes are recommended for an HA
 WARNING: A minimum of 3 dedicated nodes are recommended for an HA
 deployment."""
 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('')
     click.echo('')
 
 
@@ -270,6 +260,8 @@ def print_host_summary(all_hosts, host):
             click.echo("  - Etcd (Embedded)")
             click.echo("  - Etcd (Embedded)")
     if host.is_storage():
     if host.is_storage():
         click.echo("  - Storage")
         click.echo("  - Storage")
+    if host.new_host:
+        click.echo("  - NEW")
 
 
 
 
 def collect_master_lb(hosts):
 def collect_master_lb(hosts):
@@ -307,14 +299,18 @@ hostname.
                                          'please specify a separate host' % hostname)
                                          'please specify a separate host' % hostname)
         return 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):
 def collect_storage_host(hosts):
@@ -395,29 +391,29 @@ Notes:
 
 
     default_facts_lines = []
     default_facts_lines = []
     default_facts = {}
     default_facts = {}
-    for h in hosts:
-        if h.preconfigured:
+    for host in hosts:
+        if host.preconfigured:
             continue
             continue
         try:
         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:
         except KeyError:
-            click.echo("Problem fetching facts from {}".format(h.connect_to))
+            click.echo("Problem fetching facts from {}".format(host.connect_to))
             continue
             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)
     output = "%s\n%s" % (output, notes)
     click.echo(output)
     click.echo(output)
@@ -534,7 +530,7 @@ def error_if_missing_info(oo_cfg):
     oo_cfg.settings['variant_version'] = version.name
     oo_cfg.settings['variant_version'] = version.name
 
 
     # check that all listed host roles are included
     # 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])
     configured_roles = set([role for role in oo_cfg.deployment.roles])
     if listed_roles != configured_roles:
     if listed_roles != configured_roles:
         missing_info = True
         missing_info = True
@@ -544,16 +540,7 @@ def error_if_missing_info(oo_cfg):
         sys.exit(1)
         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 = """
     message = """
 If a proxy is needed to reach HTTP and HTTPS traffic, please enter the
 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
 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()
         click.clear()
 
 
     if 'master_routingconfig_subdomain' not in oo_cfg.deployment.variables:
     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()
         click.clear()
 
 
     # Are any proxy vars already presisted?
     # 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
     saved_proxy_vars = [pv for pv in proxy_vars
                         if oo_cfg.deployment.variables.get(pv, 'UNSET') is not 'UNSET']
                         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))
                         len(saved_proxy_vars))
     current_version = parse_version(
     current_version = parse_version(
         oo_cfg.settings.get('variant_version', '0.0'))
         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
     # recognizes proxy parameters. We must prompt the user for values
     # if this conditional is true.
     # if this conditional is true.
     if not saved_proxy_vars and current_version >= min_version:
     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_http'] = http_proxy
         oo_cfg.deployment.variables['proxy_https'] = https_proxy
         oo_cfg.deployment.variables['proxy_https'] = https_proxy
         oo_cfg.deployment.variables['proxy_exclude_hosts'] = proxy_excludes
         oo_cfg.deployment.variables['proxy_exclude_hosts'] = proxy_excludes
@@ -709,82 +697,64 @@ def is_installed_host(host, callback_facts):
     return version_found
     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
     # 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 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)
             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
     return hosts_to_run_on, callback_facts
 
 
@@ -800,6 +770,49 @@ def set_infra_nodes(hosts):
         host.node_labels = "{'region': 'infra'}"
         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.group()
 @click.pass_context
 @click.pass_context
 @click.option('--unattended', '-u', is_flag=True, default=False)
 @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
         # highest), anything below that (we only use debug/warning
         # presently) is not logged. If '-d' is given though, we'll
         # presently) is not logged. If '-d' is given though, we'll
         # lower the threshold to debug (almost everything gets through)
         # 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 = {}
     ctx.obj['unattended'] = unattended
     ctx.obj['unattended'] = unattended
@@ -857,8 +870,8 @@ def cli(ctx, unattended, configuration, ansible_playbook_directory, ansible_log_
 
 
     try:
     try:
         oo_cfg = OOConfig(ctx.obj['configuration'])
         oo_cfg = OOConfig(ctx.obj['configuration'])
-    except OOConfigInvalidHostError as e:
-        click.echo(e)
+    except OOConfigInvalidHostError as err:
+        click.echo(err)
         sys.exit(1)
         sys.exit(1)
 
 
     # If no playbook dir on the CLI, check the config:
     # 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('--latest-minor', '-l', is_flag=True, default=False)
 @click.option('--next-major', '-n', is_flag=True, default=False)
 @click.option('--next-major', '-n', is_flag=True, default=False)
 @click.pass_context
 @click.pass_context
-# pylint: disable=too-many-statements
+# pylint: disable=too-many-statements,too-many-branches
 def upgrade(ctx, latest_minor, next_major):
 def upgrade(ctx, latest_minor, next_major):
     oo_cfg = ctx.obj['oo_cfg']
     oo_cfg = ctx.obj['oo_cfg']
 
 
@@ -1013,15 +1026,17 @@ def upgrade(ctx, latest_minor, next_major):
 def install(ctx, force, gen_inventory):
 def install(ctx, force, gen_inventory):
     oo_cfg = ctx.obj['oo_cfg']
     oo_cfg = ctx.obj['oo_cfg']
     verbose = ctx.obj['verbose']
     verbose = ctx.obj['verbose']
+    unattended = ctx.obj['unattended']
 
 
-    if ctx.obj['unattended']:
+    if unattended:
         error_if_missing_info(oo_cfg)
         error_if_missing_info(oo_cfg)
     else:
     else:
         oo_cfg = get_missing_info_from_user(oo_cfg)
         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...')
     click.echo('Gathering information from hosts...')
     callback_facts, error = openshift_ansible.default_facts(oo_cfg.deployment.hosts,
     callback_facts, error = openshift_ansible.default_facts(oo_cfg.deployment.hosts,
                                                             verbose)
                                                             verbose)
@@ -1031,62 +1046,92 @@ def install(ctx, force, gen_inventory):
                    "Please see {} for details.".format(oo_cfg.settings['ansible_log_path']))
                    "Please see {} for details.".format(oo_cfg.settings['ansible_log_path']))
         sys.exit(1)
         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
     # We already verified this is not the case for unattended installs, so this can
     # only trigger for live CLI users:
     # 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:
     if not ctx.obj['unattended'] and len(oo_cfg.calc_missing_facts()) > 0:
         confirm_hosts_facts(oo_cfg, callback_facts)
         confirm_hosts_facts(oo_cfg, callback_facts)
 
 
     # Write quick installer config file to disk:
     # Write quick installer config file to disk:
     oo_cfg.save_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 = """
     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)
         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(install)
+cli.add_command(scaleup)
 cli.add_command(upgrade)
 cli.add_command(upgrade)
 cli.add_command(uninstall)
 cli.add_command(uninstall)
 
 

+ 8 - 0
utils/src/ooinstall/oo_config.py

@@ -436,3 +436,11 @@ class OOConfig(object):
             if host.connect_to == name:
             if host.connect_to == name:
                 return host
                 return host
         return None
         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

+ 3 - 6
utils/src/ooinstall/openshift_ansible.py

@@ -48,9 +48,6 @@ def set_config(cfg):
 def generate_inventory(hosts):
 def generate_inventory(hosts):
     global CFG
     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]
     new_nodes = [host for host in hosts if host.is_node() and host.new_host]
     scaleup = len(new_nodes) > 0
     scaleup = len(new_nodes) > 0
 
 
@@ -61,7 +58,7 @@ def generate_inventory(hosts):
 
 
     write_inventory_children(base_inventory, scaleup)
     write_inventory_children(base_inventory, scaleup)
 
 
-    write_inventory_vars(base_inventory, multiple_masters, lb)
+    write_inventory_vars(base_inventory, lb)
 
 
     # write_inventory_hosts
     # write_inventory_hosts
     for role in CFG.deployment.roles:
     for role in CFG.deployment.roles:
@@ -106,7 +103,7 @@ def write_inventory_children(base_inventory, scaleup):
 
 
 
 
 # pylint: disable=too-many-branches
 # pylint: disable=too-many-branches
-def write_inventory_vars(base_inventory, multiple_masters, lb):
+def write_inventory_vars(base_inventory, lb):
     global CFG
     global CFG
     base_inventory.write('\n[OSEv3:vars]\n')
     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':
     if CFG.deployment.variables['ansible_ssh_user'] != 'root':
         base_inventory.write('ansible_become=yes\n')
         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_method=native\n')
         base_inventory.write("openshift_master_cluster_hostname={}\n".format(lb.hostname))
         base_inventory.write("openshift_master_cluster_hostname={}\n".format(lb.hostname))
         base_inventory.write(
         base_inventory.write(

+ 11 - 0
utils/src/ooinstall/utils.py

@@ -1,4 +1,6 @@
 import logging
 import logging
+import re
+
 
 
 installer_log = logging.getLogger('installer')
 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"):
         if k.startswith("OPENSHIFT") or k.startswith("ANSIBLE") or k.startswith("OO"):
             installer_log.debug("{key}: {value}".format(
             installer_log.debug("{key}: {value}".format(
                 key=k, value=env[k]))
                 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("."))

+ 29 - 30
utils/test/cli_installer_tests.py

@@ -842,7 +842,7 @@ class AttendedCliTests(OOCliFixture):
     # interactive with config file and some installed some uninstalled hosts
     # interactive with config file and some installed some uninstalled hosts
     @patch('ooinstall.openshift_ansible.run_main_playbook')
     @patch('ooinstall.openshift_ansible.run_main_playbook')
     @patch('ooinstall.openshift_ansible.load_system_facts')
     @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
         # Modify the mock facts to return a version indicating OpenShift
         # is already installed on our master, and the first node.
         # is already installed on our master, and the first node.
@@ -866,13 +866,12 @@ class AttendedCliTests(OOCliFixture):
         result = self.runner.invoke(cli.cli,
         result = self.runner.invoke(cli.cli,
                                     self.cli_args,
                                     self.cli_args,
                                     input=cli_input)
                                     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.run_main_playbook')
     @patch('ooinstall.openshift_ansible.load_system_facts')
     @patch('ooinstall.openshift_ansible.load_system_facts')
@@ -897,30 +896,30 @@ class AttendedCliTests(OOCliFixture):
         written_config = read_yaml(config_file)
         written_config = read_yaml(config_file)
         self._verify_config_hosts(written_config, 3)
         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
     #interactive multimaster: one more node than master
     @patch('ooinstall.openshift_ansible.run_main_playbook')
     @patch('ooinstall.openshift_ansible.run_main_playbook')

+ 9 - 8
utils/test/fixture.py

@@ -138,8 +138,8 @@ class OOCliFixture(OOInstallFixture):
             written_config = read_yaml(config_file)
             written_config = read_yaml(config_file)
             self._verify_config_hosts(written_config, exp_hosts_len)
             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)
             self.assertEqual(result.exit_code, 1)
         else:
         else:
             self.assert_result(result, 0)
             self.assert_result(result, 0)
@@ -156,7 +156,7 @@ class OOCliFixture(OOInstallFixture):
 #pylint: disable=too-many-arguments,too-many-branches,too-many-statements
 #pylint: disable=too-many-arguments,too-many-branches,too-many-statements
 def build_input(ssh_user=None, hosts=None, variant_num=None,
 def build_input(ssh_user=None, hosts=None, variant_num=None,
                 add_nodes=None, confirm_facts=None, schedulable_masters_ok=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
     Build an input string simulating a user entering values in an interactive
     attended install.
     attended install.
@@ -204,11 +204,11 @@ def build_input(ssh_user=None, hosts=None, variant_num=None,
             i += 1
             i += 1
 
 
     # You can pass a single master_lb or a list if you intend for one to get rejected:
     # 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')
         inputs.append('y' if master_lb[1] else 'n')
 
 
     if storage:
     if storage:
@@ -248,6 +248,7 @@ def build_input(ssh_user=None, hosts=None, variant_num=None,
     inputs.extend([
     inputs.extend([
         confirm_facts,
         confirm_facts,
         'y',  # lets do this
         'y',  # lets do this
+        'y',
     ])
     ])
 
 
     return '\n'.join(inputs)
     return '\n'.join(inputs)