File: __main__.py

package info (click to toggle)
pyinfra 0.2.2%2Bgit20161227.ec708ef-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 11,804 kB
  • ctags: 677
  • sloc: python: 5,944; sh: 71; makefile: 11
file content (357 lines) | stat: -rw-r--r-- 10,392 bytes parent folder | download
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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
#!/usr/bin/env python
# pyinfra
# File: bin/pyinfra
# Desc: the CLI in front of the API

'''
pyinfra
Docs: pyinfra.readthedocs.io

Usage:
    pyinfra -i INVENTORY DEPLOY [-vv options]
    pyinfra -i INVENTORY --run OP ARGS [-vv options]
    pyinfra -i INVENTORY --run COMMAND [-vv options]
    pyinfra -i INVENTORY --fact FACT [-vv options]
    pyinfra -i INVENTORY [DEPLOY] --debug-data [options]
    pyinfra (--facts | --help | --version)

Deploy options:
    DEPLOY               Deploy script filename.
    -i INVENTORY         Inventory script filename or single hostname.
    --run OP ARGS        Run a single operation with args.
          COMMAND        Run a command using the server.shell operation.
    --fact FACT          Name of fact to run/test.
    --limit HOSTNAME     Limit the inventory, supports *wildcards and group names.
    --serial             Run commands on one host at a time.
    --no-wait             Don't wait for all hosts at each operation.
    -v -vv               Prints remote input/output in realtime. -vv prints facts output.
    --dry                Only print proposed changes.
    --debug              Print debug info.
    --debug-data         Print inventory hosts, data and exit (no connect/deploy).
    --debug-state        Print state information and exit (no deploy like --dry).

Config options:
    -p --port PORT       SSH port number.
    -u --user USER       SSH user.
    --key FILE           SSH private key.
    --key-password PASS  SSH key password.
    --sudo               Use sudo.
    --sudo-user USER     Which user to sudo to.
    --su-user USER
    --password PASS      SSH password auth (bad).
    --parallel NUM       Number of parallel processes.
    --fail-percent NUM   Percentage of hosts that can fail before exiting.

Experimental options:
    --enable-pipelining  Enable fact pipelining.
'''

from __future__ import division, unicode_literals, print_function

from gevent import monkey
monkey.patch_all()  # noqa

import sys
import signal
import logging
from os import getcwd, path

from docopt import docopt
from termcolor import colored

# Colorama patch to enable termcolor on Windows
from colorama import init as colorama_init
colorama_init()  # noqa

from pyinfra import logger, pseudo_state, pseudo_host, pseudo_inventory, hook, __version__
from pyinfra.local import exec_file
from pyinfra.cli import (
    FakeHost, FakeState,
    run_hook, dump_state, dump_trace, setup_logging,
    make_inventory, load_config, load_deploy_config, setup_arguments,
    print_meta, print_results, print_fact, print_facts_list, print_data
)

from pyinfra.api import State
from pyinfra.api.ssh import connect_all
from pyinfra.api.operation import add_op
from pyinfra.api.operations import run_ops
from pyinfra.api.attrs import FallbackAttrData
from pyinfra.api.facts import get_facts
from pyinfra.api.exceptions import PyinfraError


# Don't write out deploy.pyc/config.pyc etc
sys.dont_write_bytecode = True

# Make sure imported files (deploy.py/etc) behave as if imported from the cwd
sys.path.append('.')


# Handle ctrl+c
def _signal_handler(signum, frame):
    print('Exiting upon user request!')
    sys.exit(0)

signal.signal(signal.SIGINT, _signal_handler)


# Exit handler
def _exit(code=0):
    print()
    print('<-- Thank you, goodbye')
    print()

    sys.exit(code)


# Exception handler
def _exception(name, e, always_dump=False):
    print()
    if pseudo_host.isset():
        sys.stderr.write('--> [{0}]: {1}: '.format(
            colored(pseudo_host.name, attrs=['bold']),
            colored(name, 'red', attrs=['bold'])
        ))
    else:
        sys.stderr.write('--> {0}: '.format(colored(name, 'red', attrs=['bold'])))

    if e:
        logger.warning(e)

    if arguments.get('debug') or always_dump:
        dump_trace(sys.exc_info())

    _exit(1)


# Get arguments
arguments = docopt(__doc__, version='pyinfra-{0}'.format(__version__))

print()
print('### {0}'.format(colored('Welcome to pyinfra', attrs=['bold'])))
print()

# Setup logging
log_level = logging.DEBUG if arguments['--debug'] else logging.INFO
setup_logging(log_level)


try:
    # Setup arguments
    arguments = setup_arguments(arguments)

    # Quickly list facts & exit if desired
    if arguments['list_facts']:
        logger.info('Available facts list:')
        print_facts_list()
        _exit()

    deploy_dir = getcwd()

    # This is the most common case: we have a deploy file so use it's pathname
    if arguments['deploy'] is not None:
        deploy_dir = path.dirname(arguments['deploy'])

    # If we have a valid inventory, look in it's path and it's parent for group_data or
    # config.py to indicate deploy_dir (--fact, --run)
    elif arguments['inventory'] and path.isfile(arguments['inventory']):
        inventory_dir, _ = path.split(arguments['inventory'])
        above_inventory_dir, _ = path.split(inventory_dir)

        for inventory_path in (inventory_dir, above_inventory_dir):
            if any((
                path.isdir(path.join(inventory_path, 'group_data')),
                path.isfile(path.join(inventory_path, 'config.py'))
            )):
                deploy_dir = inventory_path

    # Load up the inventory from the filesystem
    inventory, inventory_group = make_inventory(
        arguments['inventory'],
        deploy_dir=deploy_dir,
        limit=arguments['limit'],
        ssh_user=arguments['user'],
        ssh_key=arguments['key'],
        ssh_key_password=arguments['key_password'],
        ssh_port=arguments['port'],
        ssh_password=arguments['password'],
    )

    # If --debug-data dump & exit
    if arguments['debug_data']:
        print_data(inventory)
        _exit()

    # Attach to pseudo inventory
    pseudo_inventory.set(inventory)

    # Set a fake state/host
    pseudo_state.set(FakeState())
    pseudo_host.set(FakeHost())

    # Load up any config.py from the filesystem
    config = load_config(deploy_dir)

    # Load any hooks/config from the deploy file
    load_deploy_config(arguments['deploy'], config)

    # Unset fake state/host
    pseudo_host.reset()
    pseudo_state.reset()

    # Arg based config overrides
    if arguments['sudo']:
        config.SUDO = True
        if arguments['sudo_user']:
            config.SUDO_USER = arguments['sudo_user']

    if arguments['su_user']:
        config.SU_USER = arguments['su_user']

    if arguments['parallel']:
        config.PARALLEL = arguments['parallel']

    if arguments['fail_percent'] is not None:
        config.FAIL_PERCENT = arguments['fail_percent']

    # Create/set the state
    state = State(inventory, config)
    state.is_cli = True
    state.deploy_dir = deploy_dir

    # Setup printing on the new state
    print_output = arguments['verbose'] > 0
    if arguments['deploy'] is None and arguments['op'] is None:
        print_fact_output = print_output
    else:
        print_fact_output = arguments['verbose'] > 1

    state.print_output = print_output  # -v
    state.print_fact_info = print_output  # -v
    state.print_fact_output = print_fact_output  # -vv
    state.print_lines = True

    # Attach to pseudo state
    pseudo_state.set(state)

    # Setup the data to be passed to config hooks
    hook_data = FallbackAttrData(
        state.inventory.get_override_data(),
        state.inventory.get_group_data(inventory_group),
        state.inventory.get_data()
    )

    # Run the before_connect hook if provided
    run_hook(state, 'before_connect', hook_data)

    # Connect to all the servers
    print('--> Connecting to hosts...')
    connect_all(state)

    print()

    # Run the before_connect hook if provided
    run_hook(state, 'before_facts', hook_data)

    # Just getting a fact?
    if arguments['fact']:
        fact_data = get_facts(
            state, arguments['fact'], args=arguments['fact_args'],
            sudo=arguments['sudo'], sudo_user=arguments['sudo_user'],
            su_user=arguments['su_user']
        )
        print_fact(fact_data)
        _exit()

    # We're building a deploy!
    print('--> Building deploy scripts...')

    # Deploy file
    if arguments['deploy']:
        def loop_hosts():
            # This actually does the op build
            for host in inventory:
                pseudo_host.set(host)
                exec_file(arguments['deploy'])
                state.ready_host(host)

                logger.info('{0} {1}'.format(
                    '[{}]'.format(colored(host.name, attrs=['bold'])),
                    colored('Ready', 'green')
                ))

        if arguments['pipelining']:
            with state.pipeline_facts:
                loop_hosts()
        else:
            loop_hosts()

        # Remove any pseudo host
        pseudo_host.reset()

        # Un-ready the hosts - this is so that any hooks or callbacks during the deploy
        # can still use facts as expected.
        state.ready_hosts = set()

    # One off op run
    else:
        # Setup args if present
        args, kwargs = [], {}
        if isinstance(arguments['op_args'], tuple):
            args, kwargs = arguments['op_args']

        # Add the op w/args
        add_op(
            state, arguments['op'],
            *args, **kwargs
        )

    # Always show meta output
    print()
    print('--> Proposed changes:')
    print_meta(state)

    # If --debug-state, dump state (ops, op order, op meta) now & exit
    if arguments['debug_state']:
        dump_state(state)
        _exit()

    # Run the operations we generated with the deploy file
    if not arguments['dry']:
        print()

        # Run the before_deploy hook if provided
        run_hook(state, 'before_deploy', hook_data)

        print('--> Beginning operation run...')
        run_ops(
            state,
            serial=arguments['serial'],
            no_wait=arguments['no_wait']
        )

        # Run the after_deploy hook if provided
        run_hook(state, 'after_deploy', hook_data)

        print('--> Results:')
        print_results(state)

# Hook errors
except hook.Error as e:
    _exception('hook error', e)

# Internal exceptions
except PyinfraError as e:
    _exception('pyinfra error', e)

# IO errors
except IOError as e:
    _exception('local IO error', e)

# Unexpected exceptions/everything else
except Exception as e:
    _exception('unknown error', e, always_dump=True)


_exit()