diff --git a/ansible/bootstrap.py b/ansible/bootstrap.py new file mode 100755 index 000000000..e9817a39d --- /dev/null +++ b/ansible/bootstrap.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import datetime +import logging +import os +import sys +import time + +installer_types = {} + + +def main(args): + working_dir = os.path.dirname(os.path.realpath(__file__)) + + parser = argparse.ArgumentParser( + description="Browbeat bootstrap Ansible. Generates files for Ansible interactions to the " + "OpenStack Cloud.", prog="bootstrap.py") + parser.add_argument("-d", "--debug", action="store_true", help="Enable Debug messages") + subparsers = parser.add_subparsers(dest="installer") + + # Load pluggable installers + pluggable_installers_dir = os.path.join(working_dir, "bootstrap") + installer_plugins = [x[:-3] for x in os.listdir(pluggable_installers_dir) if x.endswith(".py")] + sys.path.insert(0, pluggable_installers_dir) + for installer in sorted(installer_plugins): + installer_mod = __import__(installer) + if hasattr(installer_mod, "register"): + installer_mod.register(installer_types, subparsers) + + cliargs = parser.parse_args(args) + + LOG = logging.getLogger("browbeat") + LOG.setLevel(logging.DEBUG) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + formatter.converter = time.gmtime + handler = logging.StreamHandler(stream=sys.stdout) + if cliargs.debug: + handler.setLevel(logging.DEBUG) + else: + handler.setLevel(logging.INFO) + handler.setFormatter(formatter) + LOG.addHandler(handler) + + LOG.debug("CLI Args: {}".format(cliargs)) + + installer_types[cliargs.installer]().bootstrap(working_dir, cliargs) + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/ansible/bootstrap/sample.py b/ansible/bootstrap/sample.py new file mode 100644 index 000000000..8d07f9423 --- /dev/null +++ b/ansible/bootstrap/sample.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import time + +LOG = logging.getLogger("browbeat.sample") + + +def register(installer_types, subparser): + installer_types["sample"] = sample + sample_parser = subparser.add_parser("sample") + sample_parser.add_argument("-s", "--sample-arg") + + +class sample(object): + + def bootstrap(self, working_dir, cliargs): + """Sample bootstrap installer.""" + start_time = time.time() + LOG.info("Bootstrap via sample installer") + + # Insert code to generate ssh-config and hosts files for Ansible + LOG.info("This is provided as a sample to copy and paste to make a new installer plugin.") + + LOG.info("Completed bootstrap in {}".format(round(time.time() - start_time, 2))) diff --git a/ansible/bootstrap/tripleo.py b/ansible/bootstrap/tripleo.py new file mode 100644 index 000000000..56ea27599 --- /dev/null +++ b/ansible/bootstrap/tripleo.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import defaultdict +import logging +import os +import re +import shutil +import time + +LOG = logging.getLogger("browbeat.tripleo") + + +def register(installer_types, subparser): + installer_types["tripleo"] = tripleo + tripleo_parser = subparser.add_parser("tripleo", + description="Bootstrap implementation for tripleo clouds") + help_text = ("IP address of tripleo undercloud. Defaults to 'localhost'. Currently only " + "localhost is supported.") + tripleo_parser.add_argument("-i", "--tripleo-ip", default="localhost", type=str, help=help_text) + help_text = "User used for tripleo install. Defaults to 'stack'." + tripleo_parser.add_argument("-u", "--user", default="stack", type=str, help=help_text) + + +class tripleo(object): + + def bootstrap(self, working_dir, cliargs): + """Generates three files in order to install and operate tripleo cloud for Browbeat.""" + start_time = time.time() + LOG.info("Bootstrap via tripleo installer") + + stack_id_rsa = "/home/stack/.ssh/id_rsa" + ha_id_rsa = os.path.join(working_dir, "heat-admin-id_rsa") + ssh_config = os.path.join(working_dir, "ssh-config") + ansible_inventory_file = os.path.join(working_dir, "hosts") + LOG.debug("working directory: {}".format(working_dir)) + LOG.debug("ssh-config file: {}".format(ssh_config)) + LOG.debug("Ansible inventory file: {}".format(ansible_inventory_file)) + + if cliargs.tripleo_ip == "localhost" or cliargs.tripleo_ip == "127.0.0.1": + if os.path.isfile(stack_id_rsa): + shutil.copy2(stack_id_rsa, ha_id_rsa) + else: + LOG.error("Tripleo ip is localhost but unable to find {}".format(stack_id_rsa)) + exit(1) + else: + LOG.error("Currently only localhost is supported for tripleo.bootstrap") + exit(1) + + if "OS_USERNAME" not in os.environ: + LOG.error("OS_USERNAME not found in environment variables: source stackrc") + exit(1) + + os_username = os.environ["OS_USERNAME"] + os_password = os.environ["OS_PASSWORD"] + os_auth_url = os.environ["OS_AUTH_URL"] + + if "OS_PROJECT_NAME" in os.environ: + project_name = os.environ["OS_PROJECT_NAME"] + elif "OS_TENANT_NAME" in os.environ: + project_name = os.environ["OS_TENANT_NAME"] + else: + LOG.error("Missing OS_PROJECT_NAME or OS_TENANT_NAME in rc file") + exit(1) + + LOG.debug("os_username: {}".format(os_username)) + LOG.debug("os_password: {}".format(os_password)) + LOG.debug("os_auth_url: {}".format(os_auth_url)) + LOG.debug("project_name: {}".format(project_name)) + + # Lazy import due to pluggable bootstrapping + from openstack import connection + + conn = connection.Connection( + auth_url=os_auth_url, + username=os_username, + password=os_password, + project_name=project_name, + compute_api_version="2", + identity_interface="internal") + + # Get the data needed + servers = [server for server in conn.compute.servers()] + baremetal_nodes = [node for node in conn.bare_metal.nodes()] + + inventory = defaultdict(dict) + + for server in conn.compute.servers(): + for baremetal_node in baremetal_nodes: + if server.id == baremetal_node.instance_id: + group = re.search(r"overcloud-([a-zA-Z0-9_]+)-[0-9]+$", server.name).group(1) + LOG.debug("Matched: {} : ironic_uuid={}".format(server.name, baremetal_node.id)) + if group: + # Unfortunately the easiest way to work with this data is to examine the + # server's name and see if it contains the "group" we most likely associate + # with the server's role. + if "compute" in group: + group = "compute" + inventory[group][server.name] = "{} ironic_uuid={}".format( + server.name, baremetal_node.id) + else: + LOG.warn("Unidentified group for server: {}".format(server.name)) + inventory["other"][server.name] = "{} ironic_uuid={}".format( + server.name, baremetal_node.id) + break + + self._generate_ssh_config(ssh_config, ha_id_rsa, cliargs.tripleo_ip, cliargs.user, servers) + self._generate_inventory(ansible_inventory_file, inventory) + + LOG.info("Completed bootstrap in {}".format(round(time.time() - start_time, 2))) + + def _generate_inventory(self, inventoryfile, inventory): + with open(inventoryfile, "w") as f: + f.write("# Generated by bootstrap.py (tripleo) from Browbeat\n") + f.write("[browbeat]\n") + f.write("# Pick host depending on desired install\n") + f.write("localhost\n") + f.write("#undercloud\n") + f.write("\n") + f.write("[undercloud]\n") + f.write("undercloud\n") + f.write("\n") + for group in inventory.keys(): + f.write("[{}]\n".format(group)) + for node in sorted(inventory[group].keys()): + f.write("{}\n".format(inventory[group][node])) + f.write("\n") + f.write("[overcloud:children]\n") + for group in inventory.keys(): + f.write("{}\n".format(group)) + f.write("\n") + + def _generate_ssh_config(self, sshfile, ha_id_rsa, tripleo_ip, tripleo_user, servers): + with open(sshfile, "w") as f: + f.write("# Generated by bootstrap.py (tripleo) from Browbeat\n") + f.write("\n") + f.write("Host undercloud\n") + f.write(" Hostname {}\n".format(tripleo_ip)) + f.write(" IdentityFile ~/.ssh/id_rsa\n") + f.write(" StrictHostKeyChecking no\n") + f.write(" UserKnownHostsFile=/dev/null\n") + f.write("\n") + f.write("Host undercloud-root\n") + f.write(" Hostname {}\n".format(tripleo_ip)) + f.write(" User root\n") + f.write(" IdentityFile ~/.ssh/id_rsa\n") + f.write(" StrictHostKeyChecking no\n") + f.write(" UserKnownHostsFile=/dev/null\n") + f.write("\n") + f.write("Host undercloud-{}\n".format(tripleo_user)) + f.write(" Hostname {}\n".format(tripleo_ip)) + f.write(" User {}\n".format(tripleo_user)) + f.write(" IdentityFile ~/.ssh/id_rsa\n") + f.write(" StrictHostKeyChecking no\n") + f.write(" UserKnownHostsFile=/dev/null\n") + pcmd = (" ProxyCommand ssh -F {} -o UserKnownHostsFile=/dev/null" + " -o StrictHostKeyChecking=no -o ConnectTimeout=60 -i ~/.ssh/id_rsa" + " {}@{} -W {}:22\n") + for server in servers: + LOG.debug("{} - {}".format(server.name, server.addresses["ctlplane"][0]["addr"])) + f.write("\n") + f.write("Host {}\n".format(server.name)) + f.write(pcmd.format(sshfile, tripleo_user, tripleo_ip, + server.addresses["ctlplane"][0]["addr"])) + f.write(" User heat-admin\n") + f.write(" IdentityFile {}\n".format(ha_id_rsa)) + f.write(" StrictHostKeyChecking no\n") + f.write(" UserKnownHostsFile=/dev/null\n") diff --git a/doc/source/installation.rst b/doc/source/installation.rst index edab9dc66..677a98ca5 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -40,8 +40,9 @@ On the Undercloud $ ssh undercloud-root [root@undercloud ~]# su - stack [stack@undercloud ~]$ git clone https://github.com/openstack/browbeat.git + [stack@undercloud ~]$ source stackrc [stack@undercloud ~]$ cd browbeat/ansible - [stack@undercloud ansible]$ ./generate_tripleo_hostfile.sh -t localhost + [stack@undercloud ansible]$ ./bootstrap.py tripleo [stack@undercloud ansible]$ sudo easy_install pip [stack@undercloud ansible]$ sudo pip install ansible [stack@undercloud ansible]$ vi install/group_vars/all.yml # Make sure to edit the dns_server to the correct ip address diff --git a/tests/bootstrap/__init__.py b/tests/bootstrap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/bootstrap/test_bootstrap.py b/tests/bootstrap/test_bootstrap.py new file mode 100644 index 000000000..322ec4c8c --- /dev/null +++ b/tests/bootstrap/test_bootstrap.py @@ -0,0 +1,54 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +sys.path.append(os.path.abspath('ansible')) + +import pytest + +import bootstrap + +def test_bootstrap_help(capsys): + """Tests to see if bootstrap.py help text is correct and that it loads sample/tripleo plugins""" + help_text = ("usage: bootstrap.py [-h] [-d] {sample,tripleo} ...\n\n" + "Browbeat bootstrap Ansible. Generates files for Ansible interactions to the\n" + "OpenStack Cloud.\n\n" + "positional arguments:\n" + " {sample,tripleo}\n\n" + "optional arguments:\n" + " -h, --help show this help message and exit\n" + " -d, --debug Enable Debug messages\n") + with pytest.raises(SystemExit) as pytest_wrapped_e: + bootstrap.main(["-h",]) + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + out, err = capsys.readouterr() + assert out == help_text + +def test_bootstrap_tripleo_help(capsys): + """Tests to see if bootstrap.py tripleo plugin help text is correct.""" + help_text = ("usage: bootstrap.py tripleo [-h] [-i TRIPLEO_IP] [-u USER]\n\n" + "Bootstrap implementation for tripleo clouds\n\n" + "optional arguments:\n" + " -h, --help show this help message and exit\n" + " -i TRIPLEO_IP, --tripleo-ip TRIPLEO_IP\n" + " IP address of tripleo undercloud. Defaults to\n" + " 'localhost'. Currently only localhost is supported.\n" + " -u USER, --user USER User used for tripleo install. Defaults to 'stack'.\n") + + with pytest.raises(SystemExit) as pytest_wrapped_e: + bootstrap.main(["tripleo", "-h"]) + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + out, err = capsys.readouterr() + assert out == help_text