summaryrefslogtreecommitdiff
path: root/bin/ansible
blob: 41149ae80ebff06550e6e2253ec757d912086ed7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

########################################################

import sys
import getpass
import shlex
import time
from optparse import OptionParser

import ansible.runner
import ansible.constants as C
from ansible import utils
from ansible import errors
from ansible import callbacks

########################################################

class Cli(object):
    ''' code behind bin/ansible '''
    
    # ----------------------------------------------     

    def __init__(self):
        self.stats = callbacks.AggregateStats()
        self.callbacks = callbacks.CliRunnerCallbacks()
        self.silent_callbacks = callbacks.DefaultRunnerCallbacks()

    # ----------------------------------------------     

    def parse(self):
        ''' create an options parser for bin/ansible '''

        parser = OptionParser(usage = 'ansible <host-pattern> [options]')
        parser.add_option("-a", "--args", dest="module_args",
            help="module arguments", default=C.DEFAULT_MODULE_ARGS)
        parser.add_option("-B", "--background", dest="seconds", type='int', default=0,
            help="run asynchronously, failing after X seconds")
        parser.add_option('-f','--forks', dest='forks', default=C.DEFAULT_FORKS, type='int',
            help='number of parallel processes to use')
        parser.add_option("-i", "--inventory-file", dest="inventory",
           help="inventory host file", default=C.DEFAULT_HOST_LIST)
        parser.add_option("-k", "--ask-pass", default=False, action="store_true",
            help="ask for SSH password")
        parser.add_option("-M", "--module-path", dest="module_path",
           help="path to module library", default=C.DEFAULT_MODULE_PATH)
        parser.add_option("-m", "--module-name", dest="module_name",
            help="module name to execute", default=C.DEFAULT_MODULE_NAME)
        parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
            help="condense output")
        parser.add_option('-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int',
            dest='poll_interval', help="set the poll interval if using -B")
        parser.add_option('-t', '--tree', dest='tree', default=None,
            help="log output to this directory")
        parser.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int',
            dest='timeout', help="set the SSH timeout in seconds")
        parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER,
            dest='remote_user', help='connect as this user')
        options, args = parser.parse_args()
        self.callbacks.options = options        

        if len(args) == 0 or len(args) > 1:
            parser.print_help()
            sys.exit(1)
        return (options, args)   
    
    # ----------------------------------------------     
 
    def run(self, options, args):
        ''' use Runner lib to do SSH things '''

        pattern = args[0]
        sshpass = None
        if options.ask_pass:
            sshpass = getpass.getpass(prompt="SSH password: ")
        
        if options.tree:
            utils.prepare_writeable_dir(options.tree)

        if options.seconds:
            print "background launch...\n\n"

        runner = ansible.runner.Runner(
            module_name=options.module_name, module_path=options.module_path,
            module_args=shlex.split(options.module_args),
            remote_user=options.remote_user, remote_pass=sshpass,
            host_list=options.inventory, timeout=options.timeout,
            forks=options.forks, background=options.seconds, pattern=pattern, 
            callbacks=self.callbacks, verbose=True,
        )
        return (runner, runner.run())


    # ----------------------------------------------

    def get_polling_runner(self, old_runner, hosts, jid):
        return ansible.runner.Runner(
            module_name='async_status', module_path=old_runner.module_path,
            module_args=[ "jid=%s" % jid ], remote_user=old_runner.remote_user,
            remote_pass=old_runner.remote_pass, host_list=hosts,
            timeout=old_runner.timeout, forks=old_runner.forks,
            pattern='*', callbacks=self.silent_callbacks, verbose=True,
        )

    # ----------------------------------------------

    def hosts_to_poll(self, results):
        hosts = []
        for (host, res) in results['contacted'].iteritems():
            if res.get('started',False):
                hosts.append(host)
        return hosts

    # ----------------------------------------------     

    def poll_if_needed(self, runner, results, options, args):
        ''' summarize results from Runner '''

        if results is None:
           exit("No hosts matched")

        # BACKGROUND POLL LOGIC when -B and -P are specified
        # FIXME: refactor
        if options.seconds and options.poll_interval > 0:
            poll_hosts = results['contacted'].keys()
            if len(poll_hosts) == 0:
                exit("no jobs were launched successfully")
            ahost = poll_hosts[0]
            jid = results['contacted'][ahost].get('ansible_job_id', None)
            if jid is None:
                exit("unexpected error: unable to determine jid")

            clock = options.seconds
            while (clock >= 0):
                polling_runner = self.get_polling_runner(runner, poll_hosts, jid)
                poll_results = polling_runner.run()
                if poll_results is None:
                    break
                for (host, host_result) in poll_results['contacted'].iteritems():
                    # override last result with current status result for report
                    results['contacted'][host] = host_result
                    print utils.async_poll_status(jid, host, clock, host_result)
                for (host, host_result) in poll_results['dark'].iteritems():
                    print "FAILED: %s => %s" % (host, host_result)
                clock = clock - options.poll_interval
                time.sleep(options.poll_interval)
                poll_hosts = self.hosts_to_poll(poll_results)

########################################################

if __name__ == '__main__':
    cli = Cli()
    (options, args) = cli.parse()
    try:
        (runner, results) = cli.run(options, args)
    except errors.AnsibleError as e:
        # Generic handler for ansible specific errors
        print "ERROR: %s" % str(e)
        sys.exit(1)
    else:
        cli.poll_if_needed(runner, results, options, args)