Skip to content

Commit 1aae7ba

Browse files
committed
Add windows_file.template operation
Mostly copy/pasted from file.template. One tweak is making the IO buffer-type bytes since win_rm _put_file expects a bytes input. Therefore output_file was changed from: ``` output_file = six.StringIO(output) ``` to ``` output_file = six.BytesIO(six.ensure_binary(output)) ``` References https://github.com/Fizzadar/pyinfra/issues/645
1 parent a044008 commit 1aae7ba

File tree

1 file changed

+133
-1
lines changed

1 file changed

+133
-1
lines changed

pyinfra/operations/windows_files.py

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import ntpath
88
import os
99
from datetime import timedelta
10+
import sys
11+
import traceback
1012

1113
import six
1214

@@ -17,7 +19,10 @@
1719
OperationError,
1820
OperationTypeError,
1921
)
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
2126

2227
from .util.compat import fspath
2328
from .util.files import ensure_mode_int
@@ -323,6 +328,133 @@ def file(
323328
# yield chown(path, user, group)
324329

325330

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+
326458
def windows_file(*args, **kwargs):
327459
# COMPAT
328460
# TODO: remove this

0 commit comments

Comments
 (0)