From 0ee19018001ed6a0a578aa5b7b75e096d6c014d6 Mon Sep 17 00:00:00 2001
From: Samuel Munilla <smunilla@redhat.com>
Date: Wed, 18 Nov 2015 10:59:04 -0500
Subject: atomic-openshift-installer: HA for quick installer

This adds the ability to quickly set up a multi-master environment.
---
 utils/src/ooinstall/cli_installer.py     | 82 ++++++++++++++++++++++++++++----
 utils/src/ooinstall/oo_config.py         |  9 +++-
 utils/src/ooinstall/openshift_ansible.py | 36 ++++++++++++--
 3 files changed, 110 insertions(+), 17 deletions(-)

(limited to 'utils/src')

diff --git a/utils/src/ooinstall/cli_installer.py b/utils/src/ooinstall/cli_installer.py
index 0b3af8829..1b4a67259 100644
--- a/utils/src/ooinstall/cli_installer.py
+++ b/utils/src/ooinstall/cli_installer.py
@@ -72,7 +72,7 @@ def delete_hosts(hosts):
                 click.echo("\"{}\" doesn't coorespond to any valid input.".format(del_idx))
     return hosts, None
 
-def collect_hosts(master_set=False):
+def collect_hosts(masters_set=False):
     """
         Collect host information from user. This will later be filled in using
         ansible.
@@ -102,17 +102,20 @@ http://docs.openshift.com/enterprise/latest/architecture/infrastructure_componen
 
     hosts = []
     more_hosts = True
+    num_masters = 0
     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
-        if not master_set:
-            is_master = click.confirm('Will this host be an OpenShift Master?')
-            host_props['master'] = is_master
-            master_set = is_master
+        host_props['connect_to'] = click.prompt('Enter hostname or IP address:',
+                                                default='',
+                                                value_proc=validate_prompt_hostname)
+
+        if not masters_set:
+            if click.confirm('Will this host be an OpenShift Master?'):
+                host_props['master'] = True
+                num_masters += 1
+                if num_masters >= 3:
+                    masters_set = True
+                    hosts.append(collect_ha_proxy())
         host_props['node'] = True
 
         #TODO: Reenable this option once container installs are out of tech preview
@@ -132,6 +135,29 @@ http://docs.openshift.com/enterprise/latest/architecture/infrastructure_componen
         more_hosts = click.confirm('Do you want to add additional hosts?')
     return hosts
 
+def collect_ha_proxy():
+    """
+    Get an HA proxy from the user
+    """
+    message = """
+Setting up High Availability Masters requires a load balancing solution.
+Please provide a host that will be configured as a proxy. This can either be
+an existing load balancer configured to balance all masters on port 8443 or a
+new host that will have HAProxy installed on it.
+"""
+    click.echo(message)
+    host_props = {}
+    host_props['connect_to'] = click.prompt('Enter hostname or IP address:',
+                                            default='',
+                                            value_proc=validate_prompt_hostname)
+    host_props['run_on'] = click.confirm('Is this a clean host you want to install HAProxy on?')
+    host_props['master'] = False
+    host_props['node'] = False
+    host_props['ha_proxy'] = True
+    ha_proxy = Host(**host_props)
+
+    return ha_proxy
+
 def confirm_hosts_facts(oo_cfg, callback_facts):
     hosts = oo_cfg.hosts
     click.clear()
@@ -199,6 +225,40 @@ Edit %s with the desired values and run `atomic-openshift-installer --unattended
         sys.exit(0)
     return default_facts
 
+
+
+def check_hosts_config(oo_cfg):
+    click.clear()
+    masters = [host for host in oo_cfg.hosts if host.master]
+    if len(masters) > 1:
+        ha_proxy = [host for host in oo_cfg.hosts if host.ha_proxy]
+        click.echo(ha_proxy)
+        if len(ha_proxy) > 1:
+            click.echo('More than one HAProxy specified. Only one proxy is allowed.')
+            sys.exit(0)
+        elif len(ha_proxy) == 1:
+            if ha_proxy[0].master or ha_proxy[0].node:
+                click.echo('HAProxy is configured as a master or node. Please correct this.')
+                sys.exit(0)
+        else:
+            message = """
+No HAProxy given in config. Either specify one or provide a load balancing solution
+of your choice to balance the master API (port 8443) on all master hosts.
+
+https://docs.openshift.org/latest/install_config/install/advanced_install.html#multiple-masters
+"""
+            confirm_continue(message)
+
+    nodes = [host.node for host in oo_cfg.hosts]
+    if len(masters) == len(nodes):
+        message = """
+No dedicated Nodes specified. By default, colocated Masters have their Nodes set to unscheduleable.
+Would you like to label the colocated masters as scheduleable?
+"""
+        confirm_continue(message)
+
+    return
+
 def get_variant_and_version():
     message = "\nWhich variant would you like to install?\n\n"
 
@@ -555,6 +615,8 @@ def install(ctx, force):
     else:
         oo_cfg = get_missing_info_from_user(oo_cfg)
 
+    check_hosts_config(oo_cfg)
+
     click.echo('Gathering information from hosts...')
     callback_facts, error = openshift_ansible.default_facts(oo_cfg.hosts,
         verbose)
diff --git a/utils/src/ooinstall/oo_config.py b/utils/src/ooinstall/oo_config.py
index 9c97e6e93..6b4f36204 100644
--- a/utils/src/ooinstall/oo_config.py
+++ b/utils/src/ooinstall/oo_config.py
@@ -36,19 +36,24 @@ class Host(object):
         self.public_ip = kwargs.get('public_ip', None)
         self.public_hostname = kwargs.get('public_hostname', None)
         self.connect_to = kwargs.get('connect_to', None)
+        self.run_on = kwargs.get('run_on', None)
 
         # Should this host run as an OpenShift master:
         self.master = kwargs.get('master', False)
 
         # Should this host run as an OpenShift node:
         self.node = kwargs.get('node', False)
+
+        # Should this host run as an HAProxy:
+        self.ha_proxy = kwargs.get('ha_proxy', False)
+
         self.containerized = kwargs.get('containerized', False)
 
         if self.connect_to is None:
             raise OOConfigInvalidHostError("You must specify either and 'ip' " \
                                            "or 'hostname' to connect to.")
 
-        if self.master is False and self.node is False:
+        if self.master is False and self.node is False and self.ha_proxy is False:
             raise OOConfigInvalidHostError(
                 "You must specify each host as either a master or a node.")
 
@@ -62,7 +67,7 @@ class Host(object):
         """ Used when exporting to yaml. """
         d = {}
         for prop in ['ip', 'hostname', 'public_ip', 'public_hostname',
-                     'master', 'node', 'containerized', 'connect_to']:
+                     'master', 'node', 'ha_proxy', 'containerized', 'connect_to', 'run_on']:
             # If the property is defined (not None or False), export it:
             if getattr(self, prop):
                 d[prop] = getattr(self, prop)
diff --git a/utils/src/ooinstall/openshift_ansible.py b/utils/src/ooinstall/openshift_ansible.py
index 372f27bda..ec97c4144 100644
--- a/utils/src/ooinstall/openshift_ansible.py
+++ b/utils/src/ooinstall/openshift_ansible.py
@@ -17,14 +17,31 @@ def set_config(cfg):
 
 def generate_inventory(hosts):
     global CFG
+    masters = [host for host in hosts if host.master]
+    nodes = [host for host in hosts if host.node]
+    proxy = next((host for host in hosts if host.ha_proxy), None)
+    multiple_masters = len(masters) > 1
 
     base_inventory_path = CFG.settings['ansible_inventory_path']
     base_inventory = open(base_inventory_path, 'w')
-    base_inventory.write('\n[OSEv3:children]\nmasters\nnodes\n')
+
+    base_inventory.write('\n[OSEv3:children]\n')
+    base_inventory.write('masters\n')
+    base_inventory.write('nodes\n')
+    if multiple_masters:
+        base_inventory.write('etcd\n')
+    if getattr(proxy, 'run_on', False):
+        base_inventory.write('lb\n')
+
     base_inventory.write('\n[OSEv3:vars]\n')
     base_inventory.write('ansible_ssh_user={}\n'.format(CFG.settings['ansible_ssh_user']))
     if CFG.settings['ansible_ssh_user'] != 'root':
         base_inventory.write('ansible_become=true\n')
+    if multiple_masters:
+        base_inventory.write('openshift_master_cluster_method=native\n')
+        base_inventory.write("openshift_master_cluster_hostname={}\n".format(proxy.hostname))
+        base_inventory.write("openshift_master_cluster_public_hostname={}\n".format(proxy.public_hostname))
+
 
     # Find the correct deployment type for ansible:
     ver = find_variant(CFG.settings['variant'],
@@ -45,19 +62,28 @@ def generate_inventory(hosts):
             "'enabled': 1, 'gpgcheck': 0}}]\n".format(os.environ['OO_INSTALL_PUDDLE_REPO']))
 
     base_inventory.write('\n[masters]\n')
-    masters = (host for host in hosts if host.master)
     for master in masters:
         write_host(master, base_inventory)
+
+    if len(masters) > 1:
+        base_inventory.write('\n[etcd]\n')
+        for master in masters:
+            write_host(master, base_inventory)
+
     base_inventory.write('\n[nodes]\n')
-    nodes = (host for host in hosts if host.node)
     for node in nodes:
         # TODO: Until the Master can run the SDN itself we have to configure the Masters
         # as Nodes too.
         scheduleable = True
         # If there's only one Node and it's also a Master we want it to be scheduleable:
-        if node in masters and len(masters) != 1:
+        if node in masters and len(masters) != len(nodes):
             scheduleable = False
         write_host(node, base_inventory, scheduleable)
+
+    if getattr(proxy, 'run_on', False):
+        base_inventory.write('\n[lb]\n')
+        write_host(proxy, base_inventory)
+
     base_inventory.close()
     return base_inventory_path
 
@@ -118,6 +144,7 @@ def default_facts(hosts, verbose=False):
     facts_env = os.environ.copy()
     facts_env["OO_INSTALL_CALLBACK_FACTS_YAML"] = CFG.settings['ansible_callback_facts_yaml']
     facts_env["ANSIBLE_CALLBACK_PLUGINS"] = CFG.settings['ansible_plugins_directory']
+    facts_env["OPENSHIFT_MASTER_CLUSTER_METHOD"] = 'native'
     if 'ansible_log_path' in CFG.settings:
         facts_env["ANSIBLE_LOG_PATH"] = CFG.settings['ansible_log_path']
     if 'ansible_config' in CFG.settings:
@@ -176,4 +203,3 @@ def run_upgrade_playbook(verbose=False):
     if 'ansible_config' in CFG.settings:
         facts_env['ANSIBLE_CONFIG'] = CFG.settings['ansible_config']
     return run_ansible(playbook, inventory_file, facts_env, verbose)
-
-- 
cgit v1.2.3