Browse Source

Merge branch 'quick_ha' of https://github.com/smunilla/openshift-ansible into quick_ha

Brenton Leanhardt 9 years ago
parent
commit
7be7104ebe

+ 87 - 17
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(version=None, masters_set=False):
     """
         Collect host information from user. This will later be filled in using
         ansible.
@@ -102,17 +102,23 @@ 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 > 1:
+                    hosts.append(collect_master_lb())
+
+                if num_masters >= 3 or version == '3.0':
+                    masters_set = True
         host_props['node'] = True
 
         #TODO: Reenable this option once container installs are out of tech preview
@@ -129,9 +135,35 @@ http://docs.openshift.com/enterprise/latest/architecture/infrastructure_componen
 
         hosts.append(host)
 
-        more_hosts = click.confirm('Do you want to add additional hosts?')
+        if num_masters <= 1 or num_masters >= 3:
+            more_hosts = click.confirm('Do you want to add additional hosts?')
     return hosts
 
+def collect_master_lb():
+    """
+    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.
+
+This will also require you to set a third master.
+"""
+    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['master_lb'] = True
+    master_lb = Host(**host_props)
+
+    return master_lb
+
 def confirm_hosts_facts(oo_cfg, callback_facts):
     hosts = oo_cfg.hosts
     click.clear()
@@ -199,7 +231,41 @@ Edit %s with the desired values and run `atomic-openshift-installer --unattended
         sys.exit(0)
     return default_facts
 
-def get_variant_and_version():
+
+
+def check_hosts_config(oo_cfg):
+    click.clear()
+    masters = [host for host in oo_cfg.hosts if host.master]
+    if len(masters) > 1:
+        master_lb = [host for host in oo_cfg.hosts if host.master_lb]
+        click.echo(master_lb)
+        if len(master_lb) > 1:
+            click.echo('More than one HAProxy specified. Only one proxy is allowed.')
+            sys.exit(0)
+        elif len(master_lb) == 1:
+            if master_lb[0].master or master_lb[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(multi_master=False):
     message = "\nWhich variant would you like to install?\n\n"
 
     i = 1
@@ -211,6 +277,8 @@ def get_variant_and_version():
     message = "%s\n" % message
 
     click.echo(message)
+    if multi_master:
+        click.echo('NOTE: 3.0 installations are not')
     response = click.prompt("Choose a variant from above: ", default=1)
     product, version = combos[response - 1]
 
@@ -292,16 +360,16 @@ https://docs.openshift.com/enterprise/latest/admin_guide/install/prerequisites.h
         oo_cfg.settings['ansible_ssh_user'] = get_ansible_ssh_user()
         click.clear()
 
-    if not oo_cfg.hosts:
-        oo_cfg.hosts = collect_hosts()
-        click.clear()
-
     if oo_cfg.settings.get('variant', '') == '':
         variant, version = get_variant_and_version()
         oo_cfg.settings['variant'] = variant.name
         oo_cfg.settings['variant_version'] = version.name
         click.clear()
 
+    if not oo_cfg.hosts:
+        oo_cfg.hosts = collect_hosts(version=oo_cfg.settings['variant_version'])
+        click.clear()
+
     return oo_cfg
 
 
@@ -312,7 +380,7 @@ def collect_new_nodes():
 Add new nodes here
     """
     click.echo(message)
-    return collect_hosts(True)
+    return collect_hosts(masters_set=True)
 
 def get_installed_hosts(hosts, callback_facts):
     installed_hosts = []
@@ -555,6 +623,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)

+ 7 - 2
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.master_lb = kwargs.get('master_lb', 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.master_lb 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', 'master_lb', '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)

+ 43 - 9
utils/src/ooinstall/openshift_ansible.py

@@ -17,14 +17,17 @@ 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.master_lb), 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: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')
+
+    write_inventory_children(base_inventory, multiple_masters, proxy)
+
+    write_inventory_vars(base_inventory, multiple_masters, proxy)
 
     # Find the correct deployment type for ansible:
     ver = find_variant(CFG.settings['variant'],
@@ -45,22 +48,53 @@ 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
 
+def write_inventory_children(base_inventory, multiple_masters, proxy):
+    global CFG
+
+    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')
+
+def write_inventory_vars(base_inventory, multiple_masters, proxy):
+    global CFG
+    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))
+
 
 def write_host(host, inventory, scheduleable=True):
     global CFG
@@ -118,6 +152,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 +211,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)
-

+ 132 - 3
utils/test/cli_installer_tests.py

@@ -41,6 +41,41 @@ MOCK_FACTS = {
     },
 }
 
+MOCK_FACTS_QUICKHA = {
+    '10.0.0.1': {
+        'common': {
+            'ip': '10.0.0.1',
+            'public_ip': '10.0.0.1',
+            'hostname': 'master-private.example.com',
+            'public_hostname': 'master.example.com'
+        }
+    },
+    '10.0.0.2': {
+        'common': {
+            'ip': '10.0.0.2',
+            'public_ip': '10.0.0.2',
+            'hostname': 'node1-private.example.com',
+            'public_hostname': 'node1.example.com'
+        }
+    },
+    '10.0.0.3': {
+        'common': {
+            'ip': '10.0.0.3',
+            'public_ip': '10.0.0.3',
+            'hostname': 'node2-private.example.com',
+            'public_hostname': 'node2.example.com'
+        }
+    },
+    '10.0.0.4': {
+        'common': {
+            'ip': '10.0.0.4',
+            'public_ip': '10.0.0.4',
+            'hostname': 'proxy-private.example.com',
+            'public_hostname': 'proxy.example.com'
+        }
+    },
+}
+
 # Substitute in a product name before use:
 SAMPLE_CONFIG = """
 variant: %s
@@ -91,6 +126,38 @@ hosts:
     node: true
 """
 
+QUICKHA_CONFIG = """
+variant: %s
+ansible_ssh_user: root
+hosts:
+  - connect_to: 10.0.0.1
+    ip: 10.0.0.1
+    hostname: master-private.example.com
+    public_ip: 24.222.0.1
+    public_hostname: master.example.com
+    master: true
+    node: true
+  - connect_to: 10.0.0.2
+    ip: 10.0.0.2
+    hostname: node1-private.example.com
+    public_ip: 24.222.0.2
+    public_hostname: node1.example.com
+    master: true
+    node: true
+  - connect_to: 10.0.0.3
+    ip: 10.0.0.3
+    hostname: node2-private.example.com
+    public_ip: 24.222.0.3
+    public_hostname: node2.example.com
+    node: true
+  - connect_to: 10.0.0.4
+    ip: 10.0.0.4
+    hostname: proxy-private.example.com
+    public_ip: 24.222.0.4
+    public_hostname: proxy.example.com
+    master_lb: true
+"""
+
 class OOCliFixture(OOInstallFixture):
 
     def setUp(self):
@@ -504,6 +571,40 @@ class UnattendedCliTests(OOCliFixture):
         assert result.exit_code == 1
         assert result.output == "You must specify either and 'ip' or 'hostname' to connect to.\n"
 
+    #unattended with two masters, one node, and haproxy
+    @patch('ooinstall.openshift_ansible.run_main_playbook')
+    @patch('ooinstall.openshift_ansible.load_system_facts')
+    def test_quick_ha_full_run(self, load_facts_mock, run_playbook_mock):
+        load_facts_mock.return_value = (MOCK_FACTS_QUICKHA, 0)
+        run_playbook_mock.return_value = 0
+
+        config_file = self.write_config(os.path.join(self.work_dir,
+            'ooinstall.conf'), QUICKHA_CONFIG % 'openshift-enterprise')
+
+        self.cli_args.extend(["-c", config_file, "install"])
+        result = self.runner.invoke(cli.cli, self.cli_args)
+        self.assert_result(result, 0)
+
+        load_facts_args = load_facts_mock.call_args[0]
+        self.assertEquals(os.path.join(self.work_dir, ".ansible/hosts"),
+            load_facts_args[0])
+        self.assertEquals(os.path.join(self.work_dir,
+            "playbooks/byo/openshift_facts.yml"), load_facts_args[1])
+        env_vars = load_facts_args[2]
+        self.assertEquals(os.path.join(self.work_dir,
+            '.ansible/callback_facts.yaml'),
+            env_vars['OO_INSTALL_CALLBACK_FACTS_YAML'])
+        self.assertEqual('/tmp/ansible.log', env_vars['ANSIBLE_LOG_PATH'])
+        # If user running test has rpm installed, this might be set to default:
+        self.assertTrue('ANSIBLE_CONFIG' not in env_vars or
+            env_vars['ANSIBLE_CONFIG'] == cli.DEFAULT_ANSIBLE_CONFIG)
+
+        # Make sure we ran on the expected masters and nodes:
+        hosts = run_playbook_mock.call_args[0][0]
+        hosts_to_run_on = run_playbook_mock.call_args[0][1]
+        self.assertEquals(4, len(hosts))
+        self.assertEquals(4, len(hosts_to_run_on))
+
 class AttendedCliTests(OOCliFixture):
 
     def setUp(self):
@@ -527,6 +628,9 @@ class AttendedCliTests(OOCliFixture):
         if ssh_user:
             inputs.append(ssh_user)
 
+        if variant_num:
+            inputs.append(str(variant_num))  # Choose variant + version
+
         if hosts:
             i = 0
             for (host, is_master) in hosts:
@@ -539,9 +643,6 @@ class AttendedCliTests(OOCliFixture):
                     inputs.append('n')  # Done adding hosts
                 i += 1
 
-        if variant_num:
-            inputs.append(str(variant_num))  # Choose variant + version
-
         # TODO: support option 2, fresh install
         if add_nodes:
             inputs.append('1')  # Add more nodes
@@ -613,6 +714,7 @@ class AttendedCliTests(OOCliFixture):
         result = self.runner.invoke(cli.cli,
                                     self.cli_args,
                                     input=cli_input)
+        print result
         self.assert_result(result, 0)
 
         self._verify_load_facts(load_facts_mock)
@@ -667,6 +769,33 @@ class AttendedCliTests(OOCliFixture):
                                          exp_hosts_to_run_on_len=2,
                                          force=False)
 
+    #interactive multimaster
+    @patch('ooinstall.openshift_ansible.run_main_playbook')
+    @patch('ooinstall.openshift_ansible.load_system_facts')
+    def test_quick_ha(self, load_facts_mock, run_playbook_mock):
+        load_facts_mock.return_value = (MOCK_FACTS, 0)
+        run_playbook_mock.return_value = 0
+
+        cli_input = self._build_input(hosts=[
+            ('10.0.0.1', True),
+            ('10.0.0.2', True),
+            ('10.0.0.3', False),
+            ('10.0.0.4', True)],
+                                      ssh_user='root',
+                                      variant_num=1,
+                                      confirm_facts='y')
+        self.cli_args.append("install")
+        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, 3)
+
+        written_config = self._read_yaml(self.config_file)
+        self._verify_config_hosts(written_config, 3)
+
+        return
 # TODO: test with config file, attended add node
 # TODO: test with config file, attended new node already in config file
 # TODO: test with config file, attended new node already in config file, plus manually added nodes