Use custom ansible inventory generator

Drops kargo-cli as a heavy dependency.
Added py.test to tox config to run unit testing.

Change-Id: I3e282ba079d22656483b1c3f94eaedb260b5020c
This commit is contained in:
Matthew Mosesohn 2016-07-22 18:22:19 +03:00
parent 23b0007244
commit 38084ae2f7
9 changed files with 412 additions and 18 deletions

View File

@ -22,7 +22,7 @@ sys.path.insert(0, os.path.abspath('../..'))
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
#'sphinx.ext.intersphinx',
# 'sphinx.ext.intersphinx',
'oslosphinx'
]
@ -72,4 +72,4 @@ latex_documents = [
]
# Example configuration for intersphinx: refer to the Python standard library.
#intersphinx_mapping = {'http://docs.python.org/': None}
# intersphinx_mapping = {'http://docs.python.org/': None}

View File

@ -1 +1,2 @@
netaddr
configparser>=3.3.0

View File

@ -4,3 +4,5 @@ oslosphinx>=2.5.0,!=3.4.0 # Apache-2.0
sphinx>=1.2.1,!=1.3b1,<1.3 # BSD
hacking>=0.10.2
pytest>=2.8.0
mock>=1.3.0

209
tests/test_inventory.py Normal file
View File

@ -0,0 +1,209 @@
# Copyright 2016 Mirantis, Inc.
#
# 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 unittest
from collections import OrderedDict
import sys
path = "./utils/kargo"
if path not in sys.path:
sys.path.append(path)
import inventory
class TestInventory(unittest.TestCase):
def setUp(self):
super(TestInventory, self).setUp()
self.data = ['10.90.3.2', '10.90.3.3', '10.90.3.4']
self.inv = inventory.KargoInventory()
def test_get_ip_from_opts(self):
optstring = "ansible_ssh_host=10.90.3.2 ip=10.90.3.2"
expected = "10.90.3.2"
result = self.inv.get_ip_from_opts(optstring)
self.assertEqual(expected, result)
def test_get_ip_from_opts_invalid(self):
optstring = "notanaddr=value something random!chars:D"
self.assertRaisesRegexp(ValueError, "IP parameter not found",
self.inv.get_ip_from_opts, optstring)
def test_ensure_required_groups(self):
groups = ['group1', 'group2']
self.inv.ensure_required_groups(groups)
for group in groups:
self.assertTrue(group in self.inv.config.sections())
def test_get_host_id(self):
hostnames = ['node99', 'no99de01', '01node01', 'node1.domain',
'node3.xyz123.aaa']
expected = [99, 1, 1, 1, 3]
for hostname, expected in zip(hostnames, expected):
result = self.inv.get_host_id(hostname)
self.assertEqual(expected, result)
def test_get_host_id_invalid(self):
bad_hostnames = ['node', 'no99de', '01node', 'node.111111']
for hostname in bad_hostnames:
self.assertRaisesRegexp(ValueError, "Host name must end in an",
self.inv.get_host_id, hostname)
def test_build_hostnames_add_one(self):
changed_hosts = ['10.90.0.2']
expected = OrderedDict([('node1',
'ansible_ssh_host=10.90.0.2 ip=10.90.0.2')])
result = self.inv.build_hostnames(changed_hosts)
self.assertEqual(expected, result)
def test_build_hostnames_add_duplicate(self):
changed_hosts = ['10.90.0.2']
expected = OrderedDict([('node1',
'ansible_ssh_host=10.90.0.2 ip=10.90.0.2')])
self.inv.config['all'] = expected
result = self.inv.build_hostnames(changed_hosts)
self.assertEqual(expected, result)
def test_build_hostnames_add_two(self):
changed_hosts = ['10.90.0.2', '10.90.0.3']
expected = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
self.inv.config['all'] = OrderedDict()
result = self.inv.build_hostnames(changed_hosts)
self.assertEqual(expected, result)
def test_build_hostnames_delete_first(self):
changed_hosts = ['-10.90.0.2']
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
self.inv.config['all'] = existing_hosts
expected = OrderedDict([
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
result = self.inv.build_hostnames(changed_hosts)
self.assertEqual(expected, result)
def test_exists_hostname_positive(self):
hostname = 'node1'
expected = True
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
result = self.inv.exists_hostname(existing_hosts, hostname)
self.assertEqual(expected, result)
def test_exists_hostname_negative(self):
hostname = 'node99'
expected = False
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
result = self.inv.exists_hostname(existing_hosts, hostname)
self.assertEqual(expected, result)
def test_exists_ip_positive(self):
ip = '10.90.0.2'
expected = True
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
result = self.inv.exists_ip(existing_hosts, ip)
self.assertEqual(expected, result)
def test_exists_ip_negative(self):
ip = '10.90.0.200'
expected = False
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
result = self.inv.exists_ip(existing_hosts, ip)
self.assertEqual(expected, result)
def test_delete_host_by_ip_positive(self):
ip = '10.90.0.2'
expected = OrderedDict([
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
self.inv.delete_host_by_ip(existing_hosts, ip)
self.assertEqual(expected, existing_hosts)
def test_delete_host_by_ip_negative(self):
ip = '10.90.0.200'
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3')])
self.assertRaisesRegexp(ValueError, "Unable to find host",
self.inv.delete_host_by_ip, existing_hosts, ip)
def test_purge_invalid_hosts(self):
proper_hostnames = ['node1', 'node2']
bad_host = 'doesnotbelong2'
existing_hosts = OrderedDict([
('node1', 'ansible_ssh_host=10.90.0.2 ip=10.90.0.2'),
('node2', 'ansible_ssh_host=10.90.0.3 ip=10.90.0.3'),
('doesnotbelong2', 'whateveropts=ilike')])
self.inv.config['all'] = existing_hosts
self.inv.purge_invalid_hosts(proper_hostnames)
self.assertTrue(bad_host not in self.inv.config['all'].keys())
def test_add_host_to_group(self):
group = 'etcd'
host = 'node1'
opts = 'ip=10.90.0.2'
self.inv.add_host_to_group(group, host, opts)
self.assertEqual(self.inv.config[group].get(host), opts)
def test_set_kube_master(self):
group = 'kube-master'
host = 'node1'
self.inv.set_kube_master([host])
self.assertTrue(host in self.inv.config[group])
def test_set_all(self):
group = 'all'
hosts = OrderedDict([
('node1', 'opt1'),
('node2', 'opt2')])
self.inv.set_all(hosts)
for host, opt in hosts.items():
self.assertEqual(self.inv.config[group].get(host), opt)
def test_set_k8s_cluster(self):
group = 'k8s-cluster:children'
expected_hosts = ['kube-node', 'kube-master']
self.inv.set_k8s_cluster()
for host in expected_hosts:
self.assertTrue(host in self.inv.config[group])
def test_set_kube_node(self):
group = 'kube-node'
host = 'node1'
self.inv.set_kube_node([host])
self.assertTrue(host in self.inv.config[group])
def test_set_etcd(self):
group = 'etcd'
host = 'node1'
self.inv.set_etcd([host])
self.assertTrue(host in self.inv.config[group])

View File

@ -1,12 +1,17 @@
[tox]
minversion = 1.6
skipsdist = True
envlist = bashate,pep8
envlist = bashate, pep8, py27
[testenv]
whitelist_externals = py.test
usedevelop = True
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
setenv = VIRTUAL_ENV={envdir}
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
commands = py.test -vv #{posargs:tests}
[testenv:doc8]
commands = doc8 doc
@ -38,7 +43,7 @@ commands = bash -c "find {toxinidir} -type f -name '*.sh' -not -path '*/.tox/*'
usedevelop = False
whitelist_externals = bash
commands =
bash -c "find {toxinidir} -type f name '*.py' -print0 | xargs -0 flake8"
bash -c "find {toxinidir}/* -type f -name '*.py' -print0 | xargs -0 flake8"
[testenv:venv]
commands = {posargs}

View File

@ -98,15 +98,13 @@ for slaveip in ${SLAVE_IPS[@]}; do
# Add VM label:
ssh $SSH_OPTIONS $ADMIN_USER@$slaveip "echo $VM_LABEL > /home/${ADMIN_USER}/vm_label"
deploy_args+=" node${current_slave}[ansible_ssh_host=${slaveip},ip=${slaveip}]"
inventory_args+=" ${slaveip}"
((current_slave++))
done
echo "Setting up required dependencies..."
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP sudo apt-get update
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP sudo apt-get install -y git python-dev python3-dev python-pip gcc libssl-dev libffi-dev vim software-properties-common
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP "sudo easy_install setuptools"
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP "sudo pip install 'cryptography>=1.3.2' 'cffi>=1.6.0'"
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP sudo apt-get install -y git vim software-properties-common
echo "Setting up ansible..."
case $NODE_BASE_OS in
@ -126,18 +124,15 @@ case $NODE_BASE_OS in
esac
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP 'sudo sh -c "apt-get install -y ansible"'
echo "Setting up kargo-cli..."
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP git clone https://github.com/kubespray/kargo-cli.git
# Workaround for kargo prepare bug
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP "sudo sh -c 'cd kargo-cli && git checkout 4fabe51301ba805f57024d5a511c78f58b6d9aa9'"
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP "sudo sh -c 'cd kargo-cli && python setup.py install'"
echo "Checking out kargo playbook..."
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP git clone $KARGO_REPO
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP "sh -c 'cd kargo && git checkout $KARGO_COMMIT'"
echo "Preparing kargo node..."
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP kargo prepare -y --noclone --nodes $deploy_args
echo "Setting up primary node for deployment..."
scp $SSH_OPTIONS ${BASH_SOURCE%/*}/../kargo/inventory.py $ADMIN_USER@$ADMIN_IP:inventory.py
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP chmod +x inventory.py
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP env CONFIG_FILE=kargo/inventory/inventory.cfg python3 inventory.py ${SLAVE_IPS[@]}
cat $WORKSPACE/id_rsa | ssh $SSH_OPTIONS $ADMIN_USER@${SLAVE_IPS[0]} "cat - > .ssh/id_rsa"
ssh $SSH_OPTIONS $ADMIN_USER@$ADMIN_IP chmod 600 .ssh/id_rsa

184
utils/kargo/inventory.py Normal file
View File

@ -0,0 +1,184 @@
#!/usr/bin/python
# Usage: kargo_inventory.py ip1 [ip2 ...]
# Examples: kargo_inventory.py 10.10.1.3 10.10.1.4 10.10.1.5
#
# Advanced usage:
# Add another host after initial creation: kargo_inventory.py 10.10.1.5
# Delete a host: kargo_inventory.py -10.10.1.3
# Delete a host by id: kargo_inventory.py -node1
from collections import OrderedDict
try:
import configparser
except ImportError:
import ConfigParser as configparser
import os
import re
import sys
ROLES = ['kube-master', 'all', 'k8s-cluster:children', 'kube-node', 'etcd']
PROTECTED_NAMES = ROLES
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
def get_var_as_bool(name, default):
value = os.environ.get(name, '')
return _boolean_states.get(value.lower(), default)
CONFIG_FILE = os.environ.get("CONFIG_FILE", "./inventory.cfg")
DEBUG = get_var_as_bool("DEBUG", True)
HOST_PREFIX = os.environ.get("HOST_PREFIX", "node")
class KargoInventory(object):
def __init__(self, changed_hosts=None, config_file=None):
self.config = configparser.ConfigParser(allow_no_value=True,
delimiters=('\t', ' '))
if config_file:
self.config.read(config_file)
self.ensure_required_groups(ROLES)
if changed_hosts:
self.hosts = self.build_hostnames(changed_hosts)
self.purge_invalid_hosts(self.hosts.keys(), PROTECTED_NAMES)
self.set_kube_master(list(self.hosts.keys())[:2])
self.set_all(self.hosts)
self.set_k8s_cluster()
self.set_kube_node(self.hosts.keys())
self.set_etcd(list(self.hosts.keys())[:3])
if config_file:
with open(config_file, 'w') as f:
self.config.write(f)
def debug(self, msg):
if DEBUG:
print("DEBUG: {0}".format(msg))
def get_ip_from_opts(self, optstring):
opts = optstring.split(' ')
for opt in opts:
if '=' not in opt:
continue
k, v = opt.split('=')
if k == "ip":
return v
raise ValueError("IP parameter not found in options")
def ensure_required_groups(self, groups):
for group in groups:
try:
self.config.add_section(group)
except configparser.DuplicateSectionError:
pass
def get_host_id(self, host):
'''Returns integer host ID (without padding) from a given hostname.'''
try:
short_hostname = host.split('.')[0]
return int(re.findall("\d+$", short_hostname)[-1])
except IndexError:
raise ValueError("Host name must end in an integer")
def build_hostnames(self, changed_hosts):
existing_hosts = OrderedDict()
highest_host_id = 0
try:
for host, opts in self.config.items('all'):
existing_hosts[host] = opts
host_id = self.get_host_id(host)
if host_id > highest_host_id:
highest_host_id = host_id
except configparser.NoSectionError:
pass
# FIXME(mattymo): Fix condition where delete then add reuses highest id
next_host_id = highest_host_id + 1
all_hosts = existing_hosts.copy()
for host in changed_hosts:
if host[0] == "-":
realhost = host[1:]
if self.exists_hostname(all_hosts, realhost):
self.debug("Marked {0} for deletion.".format(realhost))
all_hosts.pop(realhost)
elif self.exists_ip(all_hosts, realhost):
self.debug("Marked {0} for deletion.".format(realhost))
self.delete_host_by_ip(all_hosts, realhost)
elif host[0].isdigit():
if self.exists_hostname(all_hosts, host):
self.debug("Skipping existing host {0}.".format(host))
continue
elif self.exists_ip(all_hosts, host):
self.debug("Skipping existing host {0}.".format(host))
continue
next_host = "{0}{1}".format(HOST_PREFIX, next_host_id)
next_host_id += 1
all_hosts[next_host] = "ansible_ssh_host={0} ip={1}".format(
host, host)
elif host[0].isalpha():
raise Exception("Adding hosts by hostname is not supported.")
return all_hosts
def exists_hostname(self, existing_hosts, hostname):
return hostname in existing_hosts.keys()
def exists_ip(self, existing_hosts, ip):
for host_opts in existing_hosts.values():
if ip == self.get_ip_from_opts(host_opts):
return True
return False
def delete_host_by_ip(self, existing_hosts, ip):
for hostname, host_opts in existing_hosts.items():
if ip == self.get_ip_from_opts(host_opts):
del existing_hosts[hostname]
return
raise ValueError("Unable to find host by IP: {0}".format(ip))
def purge_invalid_hosts(self, hostnames, protected_names=[]):
for role in self.config.sections():
for host, _ in self.config.items(role):
if host not in hostnames and host not in protected_names:
self.debug("Host {0} removed from role {1}".format(host,
role))
self.config.remove_option(role, host)
def add_host_to_group(self, group, host, opts=""):
self.debug("adding host {0} to group {1}".format(host, group))
self.config.set(group, host, opts)
def set_kube_master(self, hosts):
for host in hosts:
self.add_host_to_group('kube-master', host)
def set_all(self, hosts):
for host, opts in hosts.items():
self.add_host_to_group('all', host, opts)
def set_k8s_cluster(self):
self.add_host_to_group('k8s-cluster:children', 'kube-node')
self.add_host_to_group('k8s-cluster:children', 'kube-master')
def set_kube_node(self, hosts):
for host in hosts:
self.add_host_to_group('kube-node', host)
def set_etcd(self, hosts):
for host in hosts:
self.add_host_to_group('etcd', host)
def main(argv=None):
if not argv:
argv = sys.argv[1:]
KargoInventory(argv, CONFIG_FILE)
if __name__ == "__main__":
sys.exit(main())

View File

@ -50,5 +50,4 @@ python-setuptools
apt-get -y install $PACKAGES
#Installer/CCP tools
pip install git+https://github.com/kubespray/kargo-cli.git --upgrade
pip install git+https://git.openstack.org/openstack/fuel-ccp.git --upgrade

View File

@ -25,5 +25,4 @@ ansible
apt-get -y install $PACKAGES
#Installer/CCP tools
pip install git+https://github.com/kubespray/kargo-cli.git --upgrade
pip install git+https://git.openstack.org/openstack/fuel-ccp.git --upgrade