|
7 | 7 | import ntpath
|
8 | 8 | import os
|
9 | 9 | from datetime import timedelta
|
| 10 | +import sys |
| 11 | +import traceback |
10 | 12 |
|
11 | 13 | import six
|
12 | 14 |
|
|
17 | 19 | OperationError,
|
18 | 20 | OperationTypeError,
|
19 | 21 | )
|
20 |
| -from pyinfra.api.util import get_file_sha1 |
| 22 | +from pyinfra.api.util import get_file_sha1, get_template |
| 23 | + |
| 24 | +from jinja2 import TemplateRuntimeError, TemplateSyntaxError, UndefinedError |
| 25 | +from os import makedirs, path as os_path, walk |
21 | 26 |
|
22 | 27 | from .util.compat import fspath
|
23 | 28 | from .util.files import ensure_mode_int
|
@@ -323,6 +328,133 @@ def file(
|
323 | 328 | # yield chown(path, user, group)
|
324 | 329 |
|
325 | 330 |
|
| 331 | +@operation |
| 332 | +def template( |
| 333 | + src, dest, |
| 334 | + user=None, group=None, mode=None, create_remote_dir=True, |
| 335 | + state=None, host=None, |
| 336 | + **data |
| 337 | +): |
| 338 | + ''' |
| 339 | + Generate a template using jinja2 and write it to the remote system. |
| 340 | +
|
| 341 | + + src: local template filename |
| 342 | + + dest: remote filename |
| 343 | + + user: user to own the files |
| 344 | + + group: group to own the files |
| 345 | + + mode: permissions of the files |
| 346 | + + create_remote_dir: create the remote directory if it doesn't exist |
| 347 | +
|
| 348 | + ``create_remote_dir``: |
| 349 | + If the remote directory does not exist it will be created using the same |
| 350 | + user & group as passed to ``files.put``. The mode will *not* be copied over, |
| 351 | + if this is required call ``files.directory`` separately. |
| 352 | +
|
| 353 | + Notes: |
| 354 | + Common convention is to store templates in a "templates" directory and |
| 355 | + have a filename suffix with '.j2' (for jinja2). |
| 356 | +
|
| 357 | + For information on the template syntax, see |
| 358 | + `the jinja2 docs <https://jinja.palletsprojects.com>`_. |
| 359 | +
|
| 360 | + Examples: |
| 361 | +
|
| 362 | + .. code:: python |
| 363 | +
|
| 364 | + files.template( |
| 365 | + name='Create a templated file', |
| 366 | + src='templates/somefile.conf.j2', |
| 367 | + dest='/etc/somefile.conf', |
| 368 | + ) |
| 369 | +
|
| 370 | + files.template( |
| 371 | + name='Create service file', |
| 372 | + src='templates/myweb.service.j2', |
| 373 | + dest='/etc/systemd/system/myweb.service', |
| 374 | + mode='755', |
| 375 | + user='root', |
| 376 | + group='root', |
| 377 | + ) |
| 378 | +
|
| 379 | + # Example showing how to pass python variable to template file. You can also |
| 380 | + # use dicts and lists. The .j2 file can use `{{ foo_variable }}` to be interpolated. |
| 381 | + foo_variable = 'This is some foo variable contents' |
| 382 | + foo_dict = { |
| 383 | + "str1": "This is string 1", |
| 384 | + "str2": "This is string 2" |
| 385 | + } |
| 386 | + foo_list = [ |
| 387 | + "entry 1", |
| 388 | + "entry 2" |
| 389 | + ] |
| 390 | + files.template( |
| 391 | + name='Create a templated file', |
| 392 | + src='templates/foo.yml.j2', |
| 393 | + dest='/tmp/foo.yml', |
| 394 | + foo_variable=foo_variable, |
| 395 | + foo_dict=foo_dict, |
| 396 | + foo_list=foo_list |
| 397 | + ) |
| 398 | +
|
| 399 | + .. code:: yml |
| 400 | +
|
| 401 | + # templates/foo.j2 |
| 402 | + name: "{{ foo_variable }}" |
| 403 | + dict_contents: |
| 404 | + str1: "{{ foo_dict.str1 }}" |
| 405 | + str2: "{{ foo_dict.str2 }}" |
| 406 | + list_contents: |
| 407 | + {% for entry in foo_list %} |
| 408 | + - "{{ entry }}" |
| 409 | + {% endfor %} |
| 410 | + ''' |
| 411 | + |
| 412 | + if state.deploy_dir: |
| 413 | + src = os_path.join(state.deploy_dir, src) |
| 414 | + |
| 415 | + # Ensure host/state/inventory are available inside templates (if not set) |
| 416 | + data.setdefault('host', host) |
| 417 | + data.setdefault('state', state) |
| 418 | + data.setdefault('inventory', state.inventory) |
| 419 | + |
| 420 | + # Render and make file-like it's output |
| 421 | + try: |
| 422 | + output = get_template(src).render(data) |
| 423 | + except (TemplateRuntimeError, TemplateSyntaxError, UndefinedError) as e: |
| 424 | + trace_frames = traceback.extract_tb(sys.exc_info()[2]) |
| 425 | + trace_frames = [ |
| 426 | + frame for frame in trace_frames |
| 427 | + if frame[2] in ('template', '<module>', 'top-level template code') |
| 428 | + ] # thank you https://github.com/saltstack/salt/blob/master/salt/utils/templates.py |
| 429 | + |
| 430 | + line_number = trace_frames[-1][1] |
| 431 | + |
| 432 | + # Quickly read the line in question and one above/below for nicer debugging |
| 433 | + with open(src, 'r') as f: |
| 434 | + template_lines = f.readlines() |
| 435 | + |
| 436 | + template_lines = [line.strip() for line in template_lines] |
| 437 | + relevant_lines = template_lines[max(line_number - 2, 0):line_number + 1] |
| 438 | + |
| 439 | + raise OperationError('Error in template: {0} (L{1}): {2}\n...\n{3}\n...'.format( |
| 440 | + src, line_number, e, '\n'.join(relevant_lines), |
| 441 | + )) |
| 442 | + |
| 443 | + # api/connectors/winrm._put_file expects binary |
| 444 | + output_file = six.BytesIO(six.ensure_binary(output)) |
| 445 | + # Set the template attribute for nicer debugging |
| 446 | + output_file.template = src |
| 447 | + |
| 448 | + # Pass to the put function |
| 449 | + yield put( |
| 450 | + output_file, dest, |
| 451 | + user=user, group=group, mode=mode, |
| 452 | + add_deploy_dir=False, |
| 453 | + create_remote_dir=create_remote_dir, |
| 454 | + state=state, host=host, |
| 455 | + ) |
| 456 | + |
| 457 | + |
326 | 458 | def windows_file(*args, **kwargs):
|
327 | 459 | # COMPAT
|
328 | 460 | # TODO: remove this
|
|
0 commit comments