seantis / suitable

@@ -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

No yaml found.

Create your codecov.yml to customize your Codecov experience

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.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading