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,12 @@
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), hosts=servers)
167 148
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
149 +
        # Set connection to smart (if not set by user)
170 150
        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'
151 +
            options['connection'] = 'smart'
177 152
178 153
        # sudo is just a shortcut that is easier to remember than this:
179 154
        if not ('become' in options or 'become_user' in options):
@@ -238,11 +213,6 @@
Loading
238 213
        for runner in (ModuleRunner(m) for m in list_ansible_modules()):
239 214
            runner.hookup(self)
240 215
241 -
    @property
242 -
    def hosts_with_ports(self):
243 -
        for server in self.servers:
244 -
            yield to_host_and_port(server)
245 -
246 216
    def on_unreachable_host(self, module, host):
247 217
        """ If you want to customize your error handling, this would be
248 218
        the point to write your own method in a subclass.

@@ -19,6 +19,7 @@
Loading
19 19
from suitable.runner_results import RunnerResults
20 20
21 21
22 +
22 23
try:
23 24
    from ansible import context
24 25
except ImportError:
@@ -155,8 +156,10 @@
Loading
155 156
        loader = DataLoader()
156 157
        inventory_manager = SourcelessInventoryManager(loader=loader)
157 158
158 -
        for host, port in self.api.hosts_with_ports:
159 -
            inventory_manager._inventory.add_host(host, group='all', port=port)
159 +
        for host, host_variables in self.api.inventory.items():
160 +
            inventory_manager._inventory.add_host(host, group='all')
161 +
            for key, value in host_variables.items():
162 +
                inventory_manager._inventory.set_variable(host, key, value)
160 163
161 164
        for key, value in self.api.options.extra_vars.items():
162 165
            inventory_manager._inventory.set_variable('all', key, value)
@@ -272,7 +275,7 @@
Loading
272 275
    def ignore_further_calls_to_server(self, server):
273 276
        """ Takes a server out of the list. """
274 277
        log.error(u'ignoring further calls to {}'.format(server))
275 -
        self.api.servers.remove(server)
278 +
        del self.api.inventory[server]
276 279
277 280
    def trigger_event(self, server, method, args):
278 281
        try:

@@ -0,0 +1,48 @@
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().__init__()
8 +
        self.ansible_connection = ansible_connection
9 +
        if hosts:
10 +
            self.add_hosts(hosts)
11 +
12 +
    def add_host(self, server: str, host_variables: dict):
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 or Dict is allowed!")

@@ -1,6 +1,4 @@
Loading
1 1
from ansible.plugins.callback import CallbackBase
2 -
from suitable.utils import to_server
3 -
4 2
5 3
class SilentCallbackModule(CallbackBase):
6 4
    """ A callback module that does not print anything, but keeps tabs
@@ -12,29 +10,17 @@
Loading
12 10
        self.unreachable = {}
13 11
        self.contacted = {}
14 12
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 13
    def v2_runner_on_ok(self, result):
22 -
        server, result = self.adapt_result(result)
23 -
24 -
        self.contacted[server] = {
14 +
        self.contacted[result._host.name] = {
25 15
            'success': True,
26 -
            'result': result
16 +
            'result': result._result
27 17
        }
28 18
29 19
    def v2_runner_on_failed(self, result, ignore_errors=False):
30 -
        server, result = self.adapt_result(result)
31 -
32 -
        self.contacted[server] = {
20 +
        self.contacted[result._host.name] = {
33 21
            'success': False,
34 -
            'result': result
22 +
            'result': result._result
35 23
        }
36 24
37 25
    def v2_runner_on_unreachable(self, result):
38 -
        server, result = self.adapt_result(result)
39 -
40 -
        self.unreachable[server] = result
26 +
        self.unreachable[result._host.name] = result._result
Files Coverage
suitable 93.91%
Project Totals (11 files) 93.91%
120.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