Define hosts as dict and allow to define same host with different port multiple times in inventory
Showing 5 of 8 files from the diff.
suitable/utils.py
changed.
suitable/api.py
changed.
suitable/module_runner.py
changed.
Newly tracked file
suitable/inventory.py
created.
suitable/callback.py
changed.
Other files ignored by Codecov
suitable/tests/test_api.py
has changed.
suitable/tests/test_inventory.py
is new.
suitable/tests/test_utils.py
was deleted.
@@ -1,32 +1,3 @@
Loading
1 | - | def to_host_and_port(server): |
|
2 | - | # [ipv6]:port |
|
3 | - | if server.startswith('['): |
|
4 | - | host, port = server.rsplit(':', 1) |
|
5 | - | host = host.strip('[]') |
|
6 | - | ||
7 | - | # host:port |
|
8 | - | elif server.count(':') == 1: |
|
9 | - | host, port = server.split(':', 1) |
|
10 | - | ||
11 | - | # host |
|
12 | - | else: |
|
13 | - | host, port = server, None |
|
14 | - | ||
15 | - | return host, port and int(port) or None |
|
16 | - | ||
17 | - | ||
18 | - | def to_server(host, port): |
|
19 | - | if port is None: |
|
20 | - | return host |
|
21 | - | ||
22 | - | if ':' in host: |
|
23 | - | form = '[{host}]:{port}' |
|
24 | - | else: |
|
25 | - | form = '{host}:{port}' |
|
26 | - | ||
27 | - | return form.format(host=host, port=port) |
|
28 | - | ||
29 | - | ||
30 | 1 | def options_as_class(dictionary): |
|
31 | 2 | ||
32 | 3 | class Options(object): |
@@ -5,10 +5,10 @@
Loading
5 | 5 | from ansible.plugins.loader import module_loader |
|
6 | 6 | from ansible.plugins.loader import strategy_loader |
|
7 | 7 | from contextlib import contextmanager |
|
8 | - | from suitable.compat import string_types |
|
9 | 8 | from suitable.errors import UnreachableError, ModuleError |
|
10 | 9 | from suitable.module_runner import ModuleRunner |
|
11 | - | from suitable.utils import to_host_and_port, options_as_class |
|
10 | + | from suitable.utils import options_as_class |
|
11 | + | from suitable.inventory import Inventory |
|
12 | 12 | ||
13 | 13 | ||
14 | 14 | VERBOSITY = { |
@@ -43,7 +43,8 @@
Loading
43 | 43 | ): |
|
44 | 44 | """ |
|
45 | 45 | :param servers: |
|
46 | - | A list of servers or a string with space-delimited servers. The |
|
46 | + | A list of servers, a string with space-delimited servers or a dict |
|
47 | + | with server name as key and ansible host variables as values. The |
|
47 | 48 | api instances will operate on these servers only. Servers which |
|
48 | 49 | cannot be reached or whose use triggers an error are taken out |
|
49 | 50 | of the list for the lifetime of the object. |
@@ -53,27 +54,9 @@
Loading
53 | 54 | api = Api(['web.example.org', 'db.example.org']) |
|
54 | 55 | api = Api('web.example.org') |
|
55 | 56 | api = Api('web.example.org db.example.org') |
|
56 | - | ||
57 | - | Each server may optionally contain the port in the form of |
|
58 | - | ``host:port``. If the host part is an ipv6 address you need to |
|
59 | - | use the following form to specify the port: ``[host]:port``. |
|
60 | - | ||
61 | - | For example:: |
|
62 | - | ||
63 | - | api = Api('remote.example.org:2222') |
|
64 | - | api = Api('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:1234') |
|
65 | - | ||
66 | - | Note that there's currently no support for passing the same host |
|
67 | - | more than once (like in the case of a bastion host). Ansible |
|
68 | - | groups these kind of calls together and only calls the first |
|
69 | - | server. |
|
70 | - | ||
71 | - | So this won't work as expected:: |
|
72 | - | ||
57 | + | api = Api({'web.example.org': {'ansible_host': '10.10.5.1'}}) |
|
73 | 58 | api = Api(['example.org:2222', 'example.org:2223']) |
|
74 | 59 | ||
75 | - | As a work around you should define aliases for these hosts in your |
|
76 | - | ssh config or your hosts file. |
|
77 | 60 | ||
78 | 61 | :param ignore_unreachable: |
|
79 | 62 | If true, unreachable servers will not trigger an exception. They |
@@ -160,20 +143,13 @@
Loading
160 | 143 | `<http://docs.ansible.com/ansible/developing_api.html>`_ |
|
161 | 144 | ||
162 | 145 | """ |
|
163 | - | if isinstance(servers, string_types): |
|
164 | - | self.servers = servers.split(u' ') |
|
165 | - | else: |
|
166 | - | self.servers = list(servers) |
|
146 | + | # Create Inventory |
|
147 | + | self.inventory = Inventory(options.get('connection', None), |
|
148 | + | hosts=servers) |
|
167 | 149 | ||
168 | - | # if the target is the local host but the transport is not set default |
|
169 | - | # to transport = 'local' as it's usually what you want |
|
150 | + | # Set connection to smart (if not set by user) |
|
170 | 151 | if 'connection' not in options: |
|
171 | - | for host, port in self.hosts_with_ports: |
|
172 | - | if host in ('localhost', '127.0.0.1', '::1'): |
|
173 | - | options['connection'] = 'local' |
|
174 | - | break |
|
175 | - | else: |
|
176 | - | options['connection'] = 'smart' |
|
152 | + | options['connection'] = 'smart' |
|
177 | 153 | ||
178 | 154 | # sudo is just a shortcut that is easier to remember than this: |
|
179 | 155 | if not ('become' in options or 'become_user' in options): |
@@ -238,11 +214,6 @@
Loading
238 | 214 | for runner in (ModuleRunner(m) for m in list_ansible_modules()): |
|
239 | 215 | runner.hookup(self) |
|
240 | 216 | ||
241 | - | @property |
|
242 | - | def hosts_with_ports(self): |
|
243 | - | for server in self.servers: |
|
244 | - | yield to_host_and_port(server) |
|
245 | - | ||
246 | 217 | def on_unreachable_host(self, module, host): |
|
247 | 218 | """ If you want to customize your error handling, this would be |
|
248 | 219 | the point to write your own method in a subclass. |
@@ -18,7 +18,6 @@
Loading
18 | 18 | from suitable.common import log |
|
19 | 19 | from suitable.runner_results import RunnerResults |
|
20 | 20 | ||
21 | - | ||
22 | 21 | try: |
|
23 | 22 | from ansible import context |
|
24 | 23 | except ImportError: |
@@ -155,8 +154,10 @@
Loading
155 | 154 | loader = DataLoader() |
|
156 | 155 | inventory_manager = SourcelessInventoryManager(loader=loader) |
|
157 | 156 | ||
158 | - | for host, port in self.api.hosts_with_ports: |
|
159 | - | inventory_manager._inventory.add_host(host, group='all', port=port) |
|
157 | + | for host, host_variables in self.api.inventory.items(): |
|
158 | + | inventory_manager._inventory.add_host(host, group='all') |
|
159 | + | for key, value in host_variables.items(): |
|
160 | + | inventory_manager._inventory.set_variable(host, key, value) |
|
160 | 161 | ||
161 | 162 | for key, value in self.api.options.extra_vars.items(): |
|
162 | 163 | inventory_manager._inventory.set_variable('all', key, value) |
@@ -254,7 +255,6 @@
Loading
254 | 255 | task_queue_manager.cleanup() |
|
255 | 256 | ||
256 | 257 | if set_global_context: |
|
257 | - | ||
258 | 258 | # Ansible 2.8 introduces a global context which persists |
|
259 | 259 | # during the lifetime of the process - for Suitable this |
|
260 | 260 | # singleton/cache needs to be cleared after each call |
@@ -272,7 +272,7 @@
Loading
272 | 272 | def ignore_further_calls_to_server(self, server): |
|
273 | 273 | """ Takes a server out of the list. """ |
|
274 | 274 | log.error(u'ignoring further calls to {}'.format(server)) |
|
275 | - | self.api.servers.remove(server) |
|
275 | + | del self.api.inventory[server] |
|
276 | 276 | ||
277 | 277 | def trigger_event(self, server, method, args): |
|
278 | 278 | try: |
@@ -0,0 +1,49 @@
Loading
1 | + | from suitable.compat import string_types |
|
2 | + | ||
3 | + | ||
4 | + | class Inventory(dict): |
|
5 | + | ||
6 | + | def __init__(self, ansible_connection=None, hosts=None): |
|
7 | + | super(Inventory, self).__init__() |
|
8 | + | self.ansible_connection = ansible_connection |
|
9 | + | if hosts: |
|
10 | + | self.add_hosts(hosts) |
|
11 | + | ||
12 | + | def add_host(self, server, host_variables): |
|
13 | + | self[server] = {} |
|
14 | + | ||
15 | + | # [ipv6]:port |
|
16 | + | if server.startswith('['): |
|
17 | + | host, port = server.rsplit(':', 1) |
|
18 | + | self[server]['ansible_host'] = host = host.strip('[]') |
|
19 | + | self[server]['ansible_port'] = int(port) |
|
20 | + | ||
21 | + | # host:port |
|
22 | + | elif server.count(':') == 1: |
|
23 | + | host, port = server.split(':', 1) |
|
24 | + | self[server]['ansible_host'] = host |
|
25 | + | self[server]['ansible_port'] = int(port) |
|
26 | + | ||
27 | + | # Add vars |
|
28 | + | self[server].update(host_variables) |
|
29 | + | ||
30 | + | # Localhost |
|
31 | + | if not self.ansible_connection: |
|
32 | + | # Get hostname (either ansible_host or server) |
|
33 | + | host = self[server].get('ansible_host', server) |
|
34 | + | if host in ('localhost', '127.0.0.1', '::1'): |
|
35 | + | self[server]['ansible_connection'] = 'local' |
|
36 | + | ||
37 | + | def add_hosts(self, servers): |
|
38 | + | if isinstance(servers, string_types): |
|
39 | + | for server in servers.split(u' '): |
|
40 | + | self.add_host(server, {}) |
|
41 | + | elif isinstance(servers, (list, set, tuple)): |
|
42 | + | for server in list(servers): |
|
43 | + | self.add_host(server, {}) |
|
44 | + | elif isinstance(servers, dict): |
|
45 | + | for server, host_variables in servers.items(): |
|
46 | + | self.add_host(server, host_variables) |
|
47 | + | else: |
|
48 | + | raise TypeError("Not a valid type. Only String, List, Set, Tuple " |
|
49 | + | "or Dict is allowed!") |
@@ -1,5 +1,4 @@
Loading
1 | 1 | from ansible.plugins.callback import CallbackBase |
|
2 | - | from suitable.utils import to_server |
|
3 | 2 | ||
4 | 3 | ||
5 | 4 | class SilentCallbackModule(CallbackBase): |
@@ -12,29 +11,17 @@
Loading
12 | 11 | self.unreachable = {} |
|
13 | 12 | self.contacted = {} |
|
14 | 13 | ||
15 | - | def adapt_result(self, result): |
|
16 | - | host = result._host.name |
|
17 | - | port = result._host.vars.get('ansible_port') |
|
18 | - | ||
19 | - | return to_server(host, port), result._result |
|
20 | - | ||
21 | 14 | def v2_runner_on_ok(self, result): |
|
22 | - | server, result = self.adapt_result(result) |
|
23 | - | ||
24 | - | self.contacted[server] = { |
|
15 | + | self.contacted[result._host.name] = { |
|
25 | 16 | 'success': True, |
|
26 | - | 'result': result |
|
17 | + | 'result': result._result |
|
27 | 18 | } |
|
28 | 19 | ||
29 | 20 | def v2_runner_on_failed(self, result, ignore_errors=False): |
|
30 | - | server, result = self.adapt_result(result) |
|
31 | - | ||
32 | - | self.contacted[server] = { |
|
21 | + | self.contacted[result._host.name] = { |
|
33 | 22 | 'success': False, |
|
34 | - | 'result': result |
|
23 | + | 'result': result._result |
|
35 | 24 | } |
|
36 | 25 | ||
37 | 26 | def v2_runner_on_unreachable(self, result): |
|
38 | - | server, result = self.adapt_result(result) |
|
39 | - | ||
40 | - | self.unreachable[server] = result |
|
27 | + | self.unreachable[result._host.name] = result._result |
Files | Coverage |
---|---|
suitable | 93.91% |
Project Totals (11 files) | 93.91% |
124.12
TRAVIS_PYTHON_VERSION=3.6 TRAVIS_OS_NAME=linux TOXENV=py36-ansible27
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file.
The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files.
The size and color of each slice is representing the number of statements and the coverage, respectively.