summaryrefslogtreecommitdiffstats
path: root/utils/src
diff options
context:
space:
mode:
Diffstat (limited to 'utils/src')
-rw-r--r--utils/src/ooinstall/cli_installer.py256
-rw-r--r--utils/src/ooinstall/oo_config.py49
-rw-r--r--utils/src/ooinstall/openshift_ansible.py105
3 files changed, 352 insertions, 58 deletions
diff --git a/utils/src/ooinstall/cli_installer.py b/utils/src/ooinstall/cli_installer.py
index 0b3af8829..dc88cb1ad 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(version=None, masters_set=False, print_summary=True):
"""
Collect host information from user. This will later be filled in using
ansible.
@@ -80,19 +80,31 @@ def collect_hosts(master_set=False):
Returns: a list of host information collected from the user
"""
click.clear()
- click.echo('***Host Configuration***')
+ click.echo('*** Host Configuration ***')
message = """
-The OpenShift Master serves the API and web console. It also coordinates the
-jobs that have to run across the environment. It can even run the datastore.
-For wizard based installations the database will be embedded. It's possible to
-change this later using etcd from Red Hat Enterprise Linux 7.
+You must now specify the hosts that will compose your OpenShift cluster.
+
+Please enter an IP or hostname to connect to for each system in the cluster.
+You will then be prompted to identify what role you would like this system to
+serve in the cluster.
+
+OpenShift Masters serve the API and web console and coordinate the jobs to run
+across the environment. If desired you can specify multiple Master systems for
+an HA deployment, in which case you will be prompted to identify a *separate*
+system to act as the load balancer for your cluster after all Masters and Nodes
+are defined.
+
+If only one Master is specified, an etcd instance embedded within the OpenShift
+Master service will be used as the datastore. This can be later replaced with a
+separate etcd instance if desired. If multiple Masters are specified, a
+separate etcd cluster will be configured with each Master serving as a member.
Any Masters configured as part of this installation process will also be
configured as Nodes. This is so that the Master will be able to proxy to Pods
-from the API. By default this Node will be unscheduleable but this can be changed
+from the API. By default this Node will be unschedulable but this can be changed
after installation with 'oadm manage-node'.
-The OpenShift Node provides the runtime environments for containers. It will
+OpenShift Nodes provide the runtime environments for containers. They will
host the required services to be managed by the Master.
http://docs.openshift.com/enterprise/latest/architecture/infrastructure_components/kubernetes_infrastructure.html#master
@@ -102,17 +114,19 @@ 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',
+ 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 version == '3.0':
+ masters_set = True
host_props['node'] = True
#TODO: Reenable this option once container installs are out of tech preview
@@ -129,9 +143,142 @@ http://docs.openshift.com/enterprise/latest/architecture/infrastructure_componen
hosts.append(host)
- more_hosts = click.confirm('Do you want to add additional hosts?')
+ if print_summary:
+ print_installation_summary(hosts)
+
+ # If we have one master, this is enough for an all-in-one deployment,
+ # thus we can start asking if you wish to proceed. Otherwise we assume
+ # you must.
+ 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)
+
return hosts
+
+def print_installation_summary(hosts):
+ """
+ Displays a summary of all hosts configured thus far, and what role each
+ will play.
+
+ Shows total nodes/masters, hints for performing/modifying the deployment
+ with additional setup, warnings for invalid or sub-optimal configurations.
+ """
+ click.clear()
+ click.echo('*** Installation Summary ***\n')
+ click.echo('Hosts:')
+ for host in hosts:
+ print_host_summary(hosts, host)
+
+ masters = [host for host in hosts if host.master]
+ nodes = [host for host in hosts if host.node]
+ dedicated_nodes = [host for host in hosts if host.node and not host.master]
+ click.echo('')
+ click.echo('Total OpenShift Masters: %s' % len(masters))
+ click.echo('Total OpenShift Nodes: %s' % len(nodes))
+
+ if len(masters) == 1:
+ 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 = """
+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 = """
+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 once
+finished entering hosts."""
+ click.echo(ha_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 = """
+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)
+
+ click.echo('')
+
+
+def print_host_summary(all_hosts, host):
+ click.echo("- %s" % host.connect_to)
+ if host.master:
+ click.echo(" - OpenShift Master")
+ if host.node:
+ if host.is_dedicated_node():
+ click.echo(" - OpenShift Node (Dedicated)")
+ elif host.is_schedulable_node(all_hosts):
+ click.echo(" - OpenShift Node")
+ else:
+ click.echo(" - OpenShift Node (Unscheduled)")
+ if host.master_lb:
+ if host.preconfigured:
+ click.echo(" - Load Balancer (Preconfigured)")
+ else:
+ click.echo(" - Load Balancer (HAProxy)")
+ if host.master:
+ if host.is_etcd_member(all_hosts):
+ click.echo(" - Etcd Member")
+ else:
+ click.echo(" - Etcd (Embedded)")
+
+
+def collect_master_lb(hosts):
+ """
+ Get a valid load balancer from the user and append it to the list of
+ hosts.
+
+ Ensure user does not specify a system already used as a master/node as
+ this is an invalid configuration.
+ """
+ message = """
+Setting up High Availability Masters requires a load balancing solution.
+Please provide a the FQDN of a host that will be configured as a proxy. This
+can be either an existing load balancer configured to balance all masters on
+port 8443 or a new host that will have HAProxy installed on it.
+
+If the host provided does is not yet configured, a reference haproxy load
+balancer will be installed. It's important to note that while the rest of the
+environment will be fault tolerant this reference load balancer will not be.
+It can be replaced post-installation with a load balancer with the same
+hostname.
+"""
+ click.echo(message)
+ host_props = {}
+
+ # Using an embedded function here so we have access to the hosts list:
+ def validate_prompt_lb(hostname):
+ # Run the standard hostname check first:
+ hostname = validate_prompt_hostname(hostname)
+
+ # Make sure this host wasn't already specified:
+ for host in hosts:
+ if host.connect_to == hostname and (host.master or host.node):
+ raise click.BadParameter('Cannot re-use "%s" as a load balancer, '
+ '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['master'] = False
+ host_props['node'] = False
+ host_props['master_lb'] = True
+ master_lb = Host(**host_props)
+ hosts.append(master_lb)
+
def confirm_hosts_facts(oo_cfg, callback_facts):
hosts = oo_cfg.hosts
click.clear()
@@ -169,6 +316,8 @@ Notes:
default_facts_lines = []
default_facts = {}
for h in hosts:
+ if h.preconfigured == True:
+ continue
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"]
@@ -199,7 +348,50 @@ 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, unattended):
+ click.clear()
+ masters = [host for host in oo_cfg.hosts if host.master]
+
+ if len(masters) == 2:
+ click.echo("A minimum of 3 Masters are required for HA deployments.")
+ sys.exit(1)
+
+ if len(masters) > 1:
+ master_lb = [host for host in oo_cfg.hosts if host.master_lb]
+ if len(master_lb) > 1:
+ click.echo('ERROR: More than one Master load balancer specified. Only one is allowed.')
+ sys.exit(1)
+ elif len(master_lb) == 1:
+ if master_lb[0].master or master_lb[0].node:
+ click.echo('ERROR: The Master load balancer is configured as a master or node. Please correct this.')
+ sys.exit(1)
+ else:
+ message = """
+ERROR: No master load balancer specified in config. You must provide the FQDN
+of a load balancer to balance the API (port 8443) on all Master hosts.
+
+https://docs.openshift.org/latest/install_config/install/advanced_install.html#multiple-masters
+"""
+ click.echo(message)
+ sys.exit(1)
+
+ dedicated_nodes = [host for host in oo_cfg.hosts if host.node and not host.master]
+ if len(dedicated_nodes) == 0:
+ message = """
+WARNING: No dedicated Nodes specified. By default, colocated Masters have
+their Nodes set to unschedulable. If you proceed all nodes will be labelled
+as schedulable.
+"""
+ if unattended:
+ click.echo(message)
+ else:
+ 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,13 +403,16 @@ 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]
return product, version
def confirm_continue(message):
- click.echo(message)
+ if message:
+ click.echo(message)
click.confirm("Are you ready to continue?", default=False, abort=True)
return
@@ -292,27 +487,27 @@ 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
def collect_new_nodes():
click.clear()
- click.echo('***New Node Configuration***')
+ click.echo('*** New Node Configuration ***')
message = """
Add new nodes here
"""
click.echo(message)
- return collect_hosts(True)
+ return collect_hosts(masters_set=True, print_summary=False)
def get_installed_hosts(hosts, callback_facts):
installed_hosts = []
@@ -487,7 +682,7 @@ def uninstall(ctx):
verbose = ctx.obj['verbose']
if len(oo_cfg.hosts) == 0:
- click.echo("No hosts defined in: %s" % oo_cfg['configuration'])
+ click.echo("No hosts defined in: %s" % oo_cfg.config_path)
sys.exit(1)
click.echo("OpenShift will be uninstalled from the following hosts:\n")
@@ -555,6 +750,9 @@ def install(ctx, force):
else:
oo_cfg = get_missing_info_from_user(oo_cfg)
+ check_hosts_config(oo_cfg, ctx.obj['unattended'])
+
+ print_installation_summary(oo_cfg.hosts)
click.echo('Gathering information from hosts...')
callback_facts, error = openshift_ansible.default_facts(oo_cfg.hosts,
verbose)
@@ -580,8 +778,8 @@ def install(ctx, force):
click.echo('Ready to run installation process.')
message = """
-If changes are needed to the values recorded by the installer please update {}.
-""".format(oo_cfg.config_path)
+If changes are needed please edit the config file above and re-run.
+"""
if not ctx.obj['unattended']:
confirm_continue(message)
diff --git a/utils/src/ooinstall/oo_config.py b/utils/src/ooinstall/oo_config.py
index 9c97e6e93..031b82bc1 100644
--- a/utils/src/ooinstall/oo_config.py
+++ b/utils/src/ooinstall/oo_config.py
@@ -14,7 +14,8 @@ PERSIST_SETTINGS = [
'variant_version',
'version',
]
-REQUIRED_FACTS = ['ip', 'public_ip', 'hostname', 'public_hostname']
+DEFAULT_REQUIRED_FACTS = ['ip', 'public_ip', 'hostname', 'public_hostname']
+PRECONFIGURED_REQUIRED_FACTS = ['hostname', 'public_hostname']
class OOConfigFileError(Exception):
@@ -36,19 +37,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.preconfigured = kwargs.get('preconfigured', 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.")
+ raise OOConfigInvalidHostError("You must specify either an ip " \
+ "or hostname as '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,12 +68,38 @@ 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', 'preconfigured']:
# If the property is defined (not None or False), export it:
if getattr(self, prop):
d[prop] = getattr(self, prop)
return d
+ def is_etcd_member(self, all_hosts):
+ """ Will this host be a member of a standalone etcd cluster. """
+ if not self.master:
+ return False
+ masters = [host for host in all_hosts if host.master]
+ if len(masters) > 1:
+ return True
+ return False
+
+ def is_dedicated_node(self):
+ """ Will this host be a dedicated node. (not a master) """
+ return self.node and not self.master
+
+ def is_schedulable_node(self, all_hosts):
+ """ Will this host be a node marked as schedulable. """
+ if not self.node:
+ return False
+ if not self.master:
+ return True
+
+ masters = [host for host in all_hosts if host.master]
+ nodes = [host for host in all_hosts if host.node]
+ if len(masters) == len(nodes):
+ return True
+ return False
+
class OOConfig(object):
default_dir = os.path.normpath(
@@ -177,7 +209,12 @@ class OOConfig(object):
for host in self.hosts:
missing_facts = []
- for required_fact in REQUIRED_FACTS:
+ if host.preconfigured:
+ required_facts = PRECONFIGURED_REQUIRED_FACTS
+ else:
+ required_facts = DEFAULT_REQUIRED_FACTS
+
+ for required_fact in required_facts:
if not getattr(host, required_fact):
missing_facts.append(required_fact)
if len(missing_facts) > 0:
diff --git a/utils/src/ooinstall/openshift_ansible.py b/utils/src/ooinstall/openshift_ansible.py
index 372f27bda..fd2cd7fbd 100644
--- a/utils/src/ooinstall/openshift_ansible.py
+++ b/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 = determine_proxy_configuration(hosts)
+ 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,24 +48,66 @@ 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:
- scheduleable = False
- write_host(node, base_inventory, scheduleable)
+ # Let the fact defaults decide if we're not a master:
+ schedulable = None
+
+ # If the node is also a master, we must explicitly set schedulablity:
+ if node.master:
+ schedulable = node.is_schedulable_node(hosts)
+ write_host(node, base_inventory, schedulable)
+
+ if not getattr(proxy, 'preconfigured', True):
+ base_inventory.write('\n[lb]\n')
+ write_host(proxy, base_inventory)
+
base_inventory.close()
return base_inventory_path
+def determine_proxy_configuration(hosts):
+ proxy = next((host for host in hosts if host.master_lb), None)
+ if proxy:
+ if proxy.hostname == None:
+ proxy.hostname = proxy.connect_to
+ proxy.public_hostname = proxy.connect_to
+ return proxy
+
+ return None
+
+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 not getattr(proxy, 'preconfigured', True):
+ 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 and proxy is not None:
+ 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):
+def write_host(host, inventory, schedulable=None):
global CFG
facts = ''
@@ -76,8 +121,16 @@ def write_host(host, inventory, scheduleable=True):
facts += ' openshift_public_hostname={}'.format(host.public_hostname)
# TODO: For not write_host is handles both master and nodes.
# Technically only nodes will ever need this.
- if not scheduleable:
- facts += ' openshift_scheduleable=False'
+
+ # Distinguish between three states, no schedulability specified (use default),
+ # explicitly set to True, or explicitly set to False:
+ if schedulable is None:
+ pass
+ elif schedulable:
+ facts += ' openshift_schedulable=True'
+ elif not schedulable:
+ facts += ' openshift_schedulable=False'
+
installer_host = socket.gethostname()
if installer_host in [host.connect_to, host.hostname, host.public_hostname]:
facts += ' ansible_connection=local'
@@ -104,9 +157,15 @@ def load_system_facts(inventory_file, os_facts_path, env_vars, verbose=False):
status = subprocess.call(args, env=env_vars, stdout=FNULL)
if not status == 0:
return [], 1
- callback_facts_file = open(CFG.settings['ansible_callback_facts_yaml'], 'r')
- callback_facts = yaml.load(callback_facts_file)
- callback_facts_file.close()
+
+ with open(CFG.settings['ansible_callback_facts_yaml'], 'r') as callback_facts_file:
+ try:
+ callback_facts = yaml.safe_load(callback_facts_file)
+ except yaml.YAMLError, exc:
+ print "Error in {}".format(CFG.settings['ansible_callback_facts_yaml']), exc
+ print "Try deleting and rerunning the atomic-openshift-installer"
+ sys.exit(1)
+
return callback_facts, 0
@@ -118,6 +177,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:
@@ -130,10 +190,10 @@ def run_main_playbook(hosts, hosts_to_run_on, verbose=False):
inventory_file = generate_inventory(hosts_to_run_on)
if len(hosts_to_run_on) != len(hosts):
main_playbook_path = os.path.join(CFG.ansible_playbook_directory,
- 'playbooks/common/openshift-cluster/scaleup.yml')
+ 'playbooks/byo/openshift-cluster/scaleup.yml')
else:
main_playbook_path = os.path.join(CFG.ansible_playbook_directory,
- 'playbooks/byo/config.yml')
+ 'playbooks/byo/openshift-cluster/config.yml')
facts_env = os.environ.copy()
if 'ansible_log_path' in CFG.settings:
facts_env['ANSIBLE_LOG_PATH'] = CFG.settings['ansible_log_path']
@@ -176,4 +236,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)
-