Python ssh-config+Ansible Inventory Generator
Converting some of generate_tripleo_hostfile.sh into Python * Use API bindings rather than cli commands * Pluggable design to allow other Ansible ssh-config/inventory generator * Two integration tests for testing cli of bootstrap.py and bootstrap/tripleo.py Change-Id: I0669d96904891f1d54d0b805fbb0acadb4a7bf57
This commit is contained in:
parent
163004099f
commit
df1a4764bb
61
ansible/bootstrap.py
Executable file
61
ansible/bootstrap.py
Executable file
@ -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:]))
|
36
ansible/bootstrap/sample.py
Normal file
36
ansible/bootstrap/sample.py
Normal file
@ -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)))
|
178
ansible/bootstrap/tripleo.py
Normal file
178
ansible/bootstrap/tripleo.py
Normal file
@ -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")
|
@ -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
|
||||
|
0
tests/bootstrap/__init__.py
Normal file
0
tests/bootstrap/__init__.py
Normal file
54
tests/bootstrap/test_bootstrap.py
Normal file
54
tests/bootstrap/test_bootstrap.py
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user