From 080c2d97f51e95386bdb2c2edd9fb4c5b8f933ce Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sat, 13 Aug 2016 16:21:21 +0200 Subject: Add nosetest and tox setup --- .gitignore | 3 +++ requirements.txt | 3 +++ setup.cfg | 6 ++++++ setup.py | 1 + tox.ini | 18 ++++++++++++++++++ 5 files changed, 31 insertions(+) create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 8934ee2..e8629a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.index *.cache *.params +*.facts +*.pyc +.tox/ build/ dist/ *.egg-info/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8ebf0ae --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +nose +requests +responses diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0a5283b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[nosetests] +with-doctest=1 +exe=1 + +[flake8] +ignore = E501,F821 diff --git a/setup.py b/setup.py index 1dbfc7e..8149b30 100644 --- a/setup.py +++ b/setup.py @@ -35,4 +35,5 @@ setup(name = "foreman_ansible_inventory", ], scripts = ['foreman_ansible_inventory.py'], requires = ["requests"], + tests_require=['responses'], ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..432d303 --- /dev/null +++ b/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = pep8, py27 + +[testenv] +install_command = pip install {opts} {packages} +deps = -r{toxinidir}/requirements.txt +sitepackages = True + +[testenv:pep8] +deps = hacking +commands = + flake8 {posargs} + +[testenv:py27] +setenv = + PYTHONHASHSEED = 0 +commands = + python setup.py nosetests --verbosity=3 -- cgit v1.2.3 From 409c195d2fdf62fe38c0d54edd6241314b7c5cd6 Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sat, 13 Aug 2016 16:51:30 +0200 Subject: Don't perform all the work in the constructor This is a left over from the cobbler inventory this is based on. Instead add a run() method that performs all the work and split out config file parsing and output to ease testability. --- foreman_ansible_inventory.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/foreman_ansible_inventory.py b/foreman_ansible_inventory.py index 2395811..bce9d88 100755 --- a/foreman_ansible_inventory.py +++ b/foreman_ansible_inventory.py @@ -37,16 +37,22 @@ class ForemanInventory(object): def __init__(self): """ Main execution path """ self.inventory = dict() # A list of groups and the hosts in that group - self.cache = dict() # Details about hosts in the inventory - self.params = dict() # Params of each host - self.facts = dict() # Facts of each host + self.cache = dict() # Details about hosts in the inventory + self.params = dict() # Params of each host + self.facts = dict() # Facts of each host self.hostgroups = dict() # host groups + def run(self): + self._read_settings() + self._get_inventory() + self._print_data() + + def _read_settings(self): # Read settings and parse CLI arguments self.read_settings() self.parse_cli_args() - # Cache + def _get_inventory(self): if self.args.refresh_cache: self.update_cache() elif not self.is_cache_valid(): @@ -57,9 +63,8 @@ class ForemanInventory(object): self.load_facts_from_cache() self.load_cache_from_cache() + def _print_data(self): data_to_print = "" - - # Data to print if self.args.host: data_to_print += self.get_host_info() else: @@ -264,7 +269,9 @@ class ForemanInventory(object): self.params[dns_name] = params self.facts[dns_name] = self._get_facts(host) self.push(self.inventory, 'all', dns_name) + self._write_cache() + def _write_cache(self): self.write_to_cache(self.cache, self.cache_path_cache) self.write_to_cache(self.inventory, self.cache_path_inventory) self.write_to_cache(self.params, self.cache_path_params) @@ -343,5 +350,5 @@ class ForemanInventory(object): return json.dumps(data) if __name__ == '__main__': - ForemanInventory() - + inv = ForemanInventory() + inv.run() -- cgit v1.2.3 From c4f81ff47b43931beefe4dcd671d8d71387a80b2 Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sat, 13 Aug 2016 16:35:48 +0200 Subject: Add doctest for safe_word --- foreman_ansible_inventory.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/foreman_ansible_inventory.py b/foreman_ansible_inventory.py index bce9d88..a71f29a 100755 --- a/foreman_ansible_inventory.py +++ b/foreman_ansible_inventory.py @@ -336,8 +336,15 @@ class ForemanInventory(object): cache.write(json_data) cache.close() - def to_safe(self, word): - ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups ''' + @staticmethod + def to_safe(word): + ''' + Converts 'bad' characters in a string to underscores so + they can be used as Ansible groups + + >>> ForemanInventory.to_safe("foo-bar baz") + 'foo_barbaz' + ''' regex = "[^A-Za-z0-9\_]" return re.sub(regex, "_", word.replace(" ", "")) -- cgit v1.2.3 From 1c5e75d047040ba09e63442d0360e4f87836fce5 Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sat, 13 Aug 2016 17:20:16 +0200 Subject: Add a test for methods invoking _get_json() --- foreman_ansible_inventory.py | 3 ++ tests/test_get_json.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tests/test_get_json.py diff --git a/foreman_ansible_inventory.py b/foreman_ansible_inventory.py index a71f29a..afe20b9 100755 --- a/foreman_ansible_inventory.py +++ b/foreman_ansible_inventory.py @@ -167,10 +167,13 @@ class ForemanInventory(object): break ret.raise_for_status() json = ret.json() + # /hosts/:id has not results key if not json.has_key('results'): return json + # Facts are returned as dict in results not list if type(json['results']) == type({}): return json['results'] + # List of all hosts is returned paginaged results = results + json['results'] if len(results) >= json['total']: break diff --git a/tests/test_get_json.py b/tests/test_get_json.py new file mode 100644 index 0000000..d424936 --- /dev/null +++ b/tests/test_get_json.py @@ -0,0 +1,76 @@ +# vim: set fileencoding=utf-8 : + +import responses +import unittest + +from foreman_ansible_inventory import ForemanInventory + + +class TestGetJson(unittest.TestCase): + def setUp(self): + self.inv = ForemanInventory() + self.inv.foreman_url = 'http://localhost:3000' + self.inv.foreman_user = 'doesnot' + self.inv.foreman_pw = 'mastter' + self.inv.foreman_ssl_verify = True + + @responses.activate + def test_get_hosts(self): + url = 'http://localhost:3000/api/v2/hosts' + responses.add(responses.GET, + url, + json={'results': [{'name': 'foo'}, + {'name': 'bar'}], + 'total': 4}, + status=200) + + ret = self.inv._get_hosts() + self.assertEqual(sorted(ret), + sorted([{u'name': u'foo'}, + {u'name': u'bar'}, + {u'name': u'foo'}, + {u'name': u'bar'}])) + self.assertEqual(len(responses.calls), 2) + self.assertEqual(responses.calls[0].request.url, + '%s?per_page=250&page=1' % url) + self.assertEqual(responses.calls[1].request.url, + '%s?per_page=250&page=2' % url) + + @responses.activate + def test_get_facts(self): + self.inv.want_facts = True + url = 'http://localhost:3000/api/v2/hosts/10/facts' + responses.add(responses.GET, + url, + json={'results': {'facts': {'fact1': 'val1', + 'fact2': 'val2', + } + }, + }, + status=200) + + ret = self.inv._get_facts({'id': 10}) + self.assertEqual(ret, {u'fact2': u'val2', u'fact1': u'val1'}) + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].request.url, + '%s?per_page=250&page=1' % url) + + @responses.activate + def test_resolve_params(self): + url = 'http://localhost:3000/api/v2/hosts/10' + responses.add(responses.GET, + url, + json={'all_parameters': + [{'name': 'param1', + 'value': 'value1'}, + {'name': 'param2', + 'value': 'value2'}]}, + status=200) + + ret = self.inv._resolve_params({'id': 10}) + self.assertEqual(sorted(ret.items()), + sorted({'param1': 'value1', + 'param2': 'value2'}.items())) + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].request.url, + '%s?per_page=250&page=1' % url) -- cgit v1.2.3 From 2187d21d029ea474221bcfe1d5ae70cf42f262c5 Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sun, 14 Aug 2016 13:26:59 +0200 Subject: Make setup.py pep8 compliant --- setup.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 8149b30..4877532 100644 --- a/setup.py +++ b/setup.py @@ -18,22 +18,20 @@ # # END OF COPYRIGHT # -import subprocess -from setuptools import setup, find_packages -import os +from setuptools import setup -setup(name = "foreman_ansible_inventory", - version = "0.0.3", - author = u'Guido Günther', - author_email = 'agx@sigxcpu.org', - description = 'Ansible dynamic inventory that queries the Foreman', - license = 'GPLv3+', - classifiers = [ +setup(name="foreman_ansible_inventory", + version="0.0.3", + author=u'Guido Günther', + author_email='agx@sigxcpu.org', + description='Ansible dynamic inventory that queries the Foreman', + license='GPLv3+', + classifiers=[ 'Environment :: Console', 'Programming Language :: Python :: 2', 'Operating System :: POSIX :: Linux', ], - scripts = ['foreman_ansible_inventory.py'], - requires = ["requests"], + scripts=['foreman_ansible_inventory.py'], + requires=["requests"], tests_require=['responses'], -) + ) -- cgit v1.2.3 From 03f652e6678bf7933c1e6f5078648e216089f182 Mon Sep 17 00:00:00 2001 From: Guido Günther Date: Sun, 14 Aug 2016 13:29:36 +0200 Subject: Make inventory script pep8 compliant --- foreman_ansible_inventory.py | 49 ++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/foreman_ansible_inventory.py b/foreman_ansible_inventory.py index afe20b9..ea0fd2f 100755 --- a/foreman_ansible_inventory.py +++ b/foreman_ansible_inventory.py @@ -23,9 +23,9 @@ import ConfigParser import copy import os import re -from time import time import requests from requests.auth import HTTPBasicAuth +from time import time try: import json @@ -35,7 +35,6 @@ except ImportError: class ForemanInventory(object): def __init__(self): - """ Main execution path """ self.inventory = dict() # A list of groups and the hosts in that group self.cache = dict() # Details about hosts in the inventory self.params = dict() # Params of each host @@ -82,20 +81,19 @@ class ForemanInventory(object): print(data_to_print) def is_cache_valid(self): - """ Determines if the cache files have expired, or if it is still valid """ - + """Determines if the cache is still valid""" if os.path.isfile(self.cache_path_cache): mod_time = os.path.getmtime(self.cache_path_cache) current_time = time() if (mod_time + self.cache_max_age) > current_time: if (os.path.isfile(self.cache_path_inventory) and os.path.isfile(self.cache_path_params) and - os.path.isfile(self.cache_path_facts)): + os.path.isfile(self.cache_path_facts)): return True return False def read_settings(self): - """ Reads the settings from the foreman.ini file """ + """Reads the settings from the foreman.ini file""" config = ConfigParser.SafeConfigParser() config_paths = [ @@ -146,7 +144,7 @@ class ForemanInventory(object): self.cache_max_age = config.getint('cache', 'max_age') def parse_cli_args(self): - """ Command line argument processing """ + """Command line argument processing""" parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on foreman') parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)') @@ -168,10 +166,10 @@ class ForemanInventory(object): ret.raise_for_status() json = ret.json() # /hosts/:id has not results key - if not json.has_key('results'): + if 'results' not in json: return json # Facts are returned as dict in results not list - if type(json['results']) == type({}): + if isinstance(json['results'], dict): return json['results'] # List of all hosts is returned paginaged results = results + json['results'] @@ -192,7 +190,8 @@ class ForemanInventory(object): def _get_all_params_by_id(self, hid): url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid) ret = self._get_json(url, [404]) - if ret == []: ret = {} + if ret == []: + ret = {} return ret.get('all_parameters', {}) def _get_facts_by_id(self, hid): @@ -200,9 +199,7 @@ class ForemanInventory(object): return self._get_json(url) def _resolve_params(self, host): - """ - Fetch host params and convert to dict - """ + """Fetch host params and convert to dict""" params = {} for param in self._get_all_params_by_id(host['id']): @@ -212,9 +209,7 @@ class ForemanInventory(object): return params def _get_facts(self, host): - """ - Fetch all host facts of the host - """ + """Fetch all host facts of the host""" if not self.want_facts: return {} @@ -242,7 +237,7 @@ class ForemanInventory(object): if val: safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower())) self.push(self.inventory, safe_key, dns_name) - + for group in ['lifecycle_environment', 'content_view']: val = host.get('content_facet_attributes', {}).get('%s_name' % group) if val: @@ -281,7 +276,7 @@ class ForemanInventory(object): self.write_to_cache(self.facts, self.cache_path_facts) def get_host_info(self): - """ Get variables about a specific host """ + """Get variables about a specific host""" if not self.cache or len(self.cache) == 0: # Need to load index from cache @@ -304,21 +299,21 @@ class ForemanInventory(object): d[k] = [v] def load_inventory_from_cache(self): - """ Reads the index from the cache file sets self.index """ + """Read the index from the cache file sets self.index""" cache = open(self.cache_path_inventory, 'r') json_inventory = cache.read() self.inventory = json.loads(json_inventory) def load_params_from_cache(self): - """ Reads the index from the cache file sets self.index """ + """Read the index from the cache file sets self.index""" cache = open(self.cache_path_params, 'r') json_params = cache.read() self.params = json.loads(json_params) def load_facts_from_cache(self): - """ Reads the index from the cache file sets self.index """ + """Read the index from the cache file sets self.facts""" if not self.want_facts: return cache = open(self.cache_path_facts, 'r') @@ -326,14 +321,14 @@ class ForemanInventory(object): self.facts = json.loads(json_facts) def load_cache_from_cache(self): - """ Reads the cache from the cache file sets self.cache """ + """Read the cache from the cache file sets self.cache""" cache = open(self.cache_path_cache, 'r') json_cache = cache.read() self.cache = json.loads(json_cache) def write_to_cache(self, data, filename): - """ Writes data in JSON format to a file """ + """Write data in JSON format to a file""" json_data = self.json_format_dict(data, True) cache = open(filename, 'w') cache.write(json_data) @@ -341,9 +336,9 @@ class ForemanInventory(object): @staticmethod def to_safe(word): - ''' - Converts 'bad' characters in a string to underscores so - they can be used as Ansible groups + '''Converts 'bad' characters in a string to underscores + + so they can be used as Ansible groups >>> ForemanInventory.to_safe("foo-bar baz") 'foo_barbaz' @@ -352,7 +347,7 @@ class ForemanInventory(object): return re.sub(regex, "_", word.replace(" ", "")) def json_format_dict(self, data, pretty=False): - """ Converts a dict to a JSON object and dumps it as a formatted string """ + """Converts a dict to a JSON object and dumps it as a formatted string""" if pretty: return json.dumps(data, sort_keys=True, indent=2) -- cgit v1.2.3