Skip to content

Commit a8a5654

Browse files
committed
ignore_failures -> ignore_failure, IgnoreFailure moved to core, lots of docs enhancements
1 parent 75d7af0 commit a8a5654

File tree

9 files changed

+207
-36
lines changed

9 files changed

+207
-36
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ fs.builder(state="exists", path="testfile", mode="a=rX", group="sean")
159159
Produces the following status output:
160160

161161
```
162-
=> run(command=rm -rf testdir, shell=True, ignore_failures=False, change=True)
162+
=> run(command=rm -rf testdir, shell=True, ignore_failure=False, change=True)
163163
==> mkdir(path=testdir, mode=a=rX,u+w, parents=True)
164164
==# chmod(path=testdir, mode=493)
165165
=> builder(path=testdir, mode=a=rX,u+w, state=directory)

docs/getting_started.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,16 @@ You can create a test "skeleton" playbook called "my-test-playbook" by running `
4949
new-uplaybook my-test-playbook`:
5050

5151
$ up new-uplaybook my-test-playbook
52-
=! exists(dst=my-test-playbook, ignore_failures=True) (failure ignored)
52+
=! exists(dst=my-test-playbook, ignore_failure=True) (failure ignored)
5353
=> mkdir(dst=my-test-playbook, parents=True)
5454
=# cd(dst=my-test-playbook)
5555
=> cp(dst=playbook, src=playbook.j2, template=True, template_filenames=True, recursive=True) (Contents)
5656
=# cd(dst=/home/sean/projects/uplaybook)
5757
>> *** Starting handler: git_init
5858
=# cd(dst=my-test-playbook)
59-
=> run(command=git init, shell=True, ignore_failures=False, change=True)
59+
=> run(command=git init, shell=True, ignore_failure=False, change=True)
6060
Initialized empty Git repository in /home/sean/projects/uplaybook/my-test-playbook/.git/
61-
=> run(command=git add ., shell=True, ignore_failures=False, change=True)
61+
=> run(command=git add ., shell=True, ignore_failure=False, change=True)
6262
=# cd(dst=/home/sean/projects/uplaybook)
6363
>> *** Done with handlers
6464

docs/playbooks/basics.md

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ fs.mkdir(project_name).notify(initialize_project)
3333
This playbook creates a `my-test-project` playbook, and puts an empty `README` file in it,
3434
then initializes git.
3535

36+
## Being Declarative
37+
3638
The `notify()` makes this playbook declarative. A `notify()` sets up a function (known as
3739
a "handler") to be called later, **only when a task makes a change**. uPlaybook Tasks
3840
will determine if they change the system (in this case, if the directory already exists,
@@ -42,17 +44,17 @@ directory is created.
4244
This is a useful trait of a playbook because you don't want to overwrite the `README`, or
4345
re-run the `git` commands if the project has already been created.
4446

45-
Output:
47+
Output of the above playbook, if run twice, is:
4648

4749
```bash
4850
$ up my-test-playbook
4951
=> mkdir(dst=my-test-project, parents=True)
5052
>> *** Starting handler: initialize_project
5153
=# cd(dst=my-test-project)
5254
=> mkfile(dst=README)
53-
=> run(command=git init, shell=True, ignore_failures=False, change=True)
55+
=> run(command=git init, shell=True, ignore_failure=False, change=True)
5456
Initialized empty Git repository in /home/sean/projects/uplaybook/my-test-project/.git/
55-
=> run(command=git add ., shell=True, ignore_failures=False, change=True)
57+
=> run(command=git add ., shell=True, ignore_failure=False, change=True)
5658
=# cd(dst=/home/sean/projects/uplaybook)
5759
>> *** Done with handlers
5860

@@ -64,4 +66,163 @@ $ up my-test-playbook
6466
*** RECAP: total=2 changed=0 failure=0
6567
```
6668

69+
Note the first run creates the directory, creates the README, and runs git. The second
70+
run skips the `mkdir` (that's what the "=#" denotes: no change was made), and because of
71+
that it does not run the handler.
72+
73+
## Calling Tasks
74+
75+
The most basic component of playbooks is calling tasks, such as `fs.mkdir` or
76+
`core.run` above. `core` and `fs` are uPlaybook modules of "core functionality" and
77+
"filesystem tasks" respectively.
78+
79+
These tasks are the heart of uPlaybook. All uPlaybook tasks are declarative, as described
80+
[above](#being-declarative).
81+
82+
## Task Return()s
83+
84+
Tasks return a object called `Return()`. This has some notable features:
85+
86+
- It has a `notify()` method which registers a [handler](#handlers) if the task determines
87+
it has changed the system.
88+
- It can be checked to see if the task failed. This only applies to tasks that can ignore
89+
failures, like [core.run](../tasks/core.md#uplaybook.core.run). For example: `if
90+
not core.run("false", ignore_failur=False):`
91+
- Some tasks can be used as context managers, see [fs.cd](../tasks/fs.md#uplaybook.fs.cd)
92+
- Capture output: the `output` attribute stores output of the task, see
93+
[core.run](../tasks/core.md#uplaybook.core.run) for an example.
94+
- Extra data: The `extra` attribute stores additional information the task may return, see
95+
for example (fs.stat)[/tasks/fs#uplaybook.fs.stat] stores information about the file in
96+
`extra`.
97+
98+
## Extra Return Data
99+
100+
Some tasks return extra data in the `extra` attribute of the return object. For example:
101+
102+
```python
103+
stats = fs.stat("{{project_dir}}/README")
104+
print(f"Permissions: {stats.extra.perms:o}")
105+
if stats.S_ISDIR:
106+
core.fail(msg="The README is a directory, that's unexpected!")
107+
```
108+
109+
## Ignoring Failures
110+
111+
Some tasks, such as [core.run](../tasks/core.md#uplaybook.core.run), take an `ignore_failure`
112+
option for one-shot failure ignoring.
113+
114+
There is also an "IgnoreFailure" context manager to ignore failures for a block of
115+
tasks:
116+
117+
```python
118+
with core.IgnoreFailures():
119+
core.run("false")
120+
if not mkdir("/root/fail"):
121+
print("You are not root")
122+
```
123+
124+
## Getting Help
125+
126+
The `up` command-line can be used to get documentation on the uPlaybook tasks with the
127+
`--up-doc` argument. For example:
128+
129+
up --up-doc fs
130+
[Displays a list of tasks in the "fs" module]
131+
up --up-doc core
132+
[Displays a list of tasks in the "core" module]
133+
up --up-doc core.run
134+
[Dislays documentation for the "core.run" task]
135+
136+
## Handlers
137+
138+
An idea taken from Ansible, handlers are functions that are called only if changes are
139+
made to the system. They are deferred, either until the end of the playbook run, or until
140+
(core.flush_handlers)[../tasks/core#uplaybook.core.handlers] is called.
141+
142+
They are deferred so that multiple tasks can all register handlers, but only run them once
143+
rather than running multiple times. For example, if you are installing multiple Apache
144+
modules, and writing several configuration files, these all may "notify" the
145+
"restart_apache" handler, but only run the handler once:
146+
147+
```python
148+
def restart_apache():
149+
core.run("systemctl restart apache2")
150+
core.run("apt -y install apache2", creates="/etc/apache2").notify(restart_apache)
151+
fs.cp(src="site1.conf.j2", dst="/etc/apache2/sites-enabled/site1.conf").notify(restart_apache)
152+
fs.cp(src="site2.conf.j2", dst="/etc/apache2/sites-enabled/site2.conf").notify(restart_apache)
153+
fs.cp(src="site3.conf.j2", dst="/etc/apache2/sites-enabled/site3.conf").notify(restart_apache)
154+
core.flush_handlers()
155+
# ensure apache is running
156+
run("wget -O /dev/null http://localhost/")
157+
```
158+
159+
## Arguments
160+
161+
Playbooks can include arguments and options for customizing the playbook run. For
162+
example:
163+
164+
core.playbook_args(
165+
core.Argument(name="playbook_name",
166+
description="Name of playbook to create, creates directory of this name."),
167+
core.Argument(name="git", default=False, type="bool",
168+
description="Initialize git (only for directory-basd playbooks)."),
169+
core.Argument(name="single-file", default=False, type="bool",
170+
description="Create a single-file uplaybook rather than a directory."),
171+
core.Argument(name="force", default=False, type="bool",
172+
description="Reset the playbook back to the default if it "
173+
"already exists (default is to abort if playbook already exists)."),
174+
)
175+
176+
This set up an argument of "playbook_name" and options of "--git", "--single-file", and
177+
"--force".
178+
179+
These can be accessed as `ARGS.playbook_name`, `ARGS.git`, `ARGS.single_file`, etc...
180+
181+
!!! Note "Note on dash in name"
182+
183+
A dash in the argument name is converted to an underscore in the `ARGS` list.
184+
185+
See [core.Argument](../tasks/core#uplaybook.core.Argument) for full documentation.
186+
187+
## Item Lists
188+
189+
Another idea taken from Ansible is looping over items. See
190+
[core.Item](../tasks/core#uplaybook.core.Item) and
191+
[fs.builder](../tasks/fs#uplaybook.fs.builder) for some examples on how to effectively use
192+
item lists.
193+
194+
Example:
195+
196+
for item in [
197+
core.Item(dst="foo", action="directory", owner="nobody"),
198+
core.Item(dst="bar", action="exists"),
199+
core.Item(dst="/etc/apache2/sites-enabled/foo", notify=restart_apache),
200+
]:
201+
fs.builder(**item)
202+
203+
fs.builder is an incredibly powerful paradigm for managing the state on files and
204+
directories on a system.
205+
206+
## Keeping Playbooks Declarative
207+
208+
You have the flexibility to determine whether to make your playbooks declarative or not.
209+
210+
The benefits of declarative playbooks are that they can be updated and re-run to update
211+
the system configuration, for "configuration as code" usage. For example: you could have
212+
a playbook that sets up your user environment, or configures a web server. Rather than
213+
updating the configurations directly, if your configuration is a playbook you can update
214+
the playbook and then run it on system reinstallation, or across a cluster of systems.
215+
216+
However, as you have the full power of Python at your command, you need to be aware of
217+
whether you are trying to make a declarative playbook or not.
218+
219+
For example, a playbook that creates scaffolding for a new project may not be something
220+
you can re-run. Since scaffolding is a starting point for user customization, it may not
221+
be possible or reasonable to re-run the playbook at a later time. In this case, you may
222+
wish to detect a re-run, say by checking if the project directory already exists, and
223+
abort the run.
224+
225+
To make a declarative playbook, you need to ensure that all steps of the playbook,
226+
including Python code, is repeatable when re-run.
227+
67228
<!-- vim: set tw=90: -->

examples/new-uplaybook/playbook

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ core.playbook_args(
1717
core.Argument(name="single-file", default=False, type="bool",
1818
description="Create a single-file uplaybook rather than a directory."),
1919
core.Argument(name="force", default=False, type="bool",
20-
description="Reset the playbook back to the default if it already exists (default is to abort if playbook already exists)."),
20+
description="Reset the playbook back to the default if it already "
21+
"exists (default is to abort if playbook already exists)."),
2122
)
2223

2324
if ARGS.git and ARGS.single_file:

mkdocs.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ extra_css:
1414
extra_javascript:
1515
- assets/versions.js
1616
markdown_extensions:
17+
- toc:
18+
permalink: true
19+
toc_depth: 3
1720
- markdown_include.include
1821
- codehilite:
1922
css_class: highlight
2023
- admonition
21-
- toc:
22-
permalink: true
2324
- pymdownx.superfences
2425
repo_url: https://github.com/linsomniac/uplaybook
2526
site_name: uPlaybook - Declarative System/Project Setup

tests/test_basics/basics.pb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ with fs.cd(dst="testdir"):
1111
core.run(command="date")
1212
r = core.run(command="true")
1313
assert r
14-
r = core.run(command="false", ignore_failures=True)
14+
r = core.run(command="false", ignore_failure=True)
1515
assert not r
1616

1717
var = "bar"

uplaybook/__init__.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,3 @@
1717

1818
up_context = internals.up_context
1919
ARGS = up_context.context["ARGS"]
20-
21-
22-
class IgnoreFailure:
23-
"""A context-manager to ignore failures in wrapped tasks"""
24-
25-
def __enter__(self):
26-
up_context.ignore_failure_count += 1
27-
return self
28-
29-
def __exit__(self, exc_type, exc_value, traceback) -> None:
30-
up_context.ignore_failure_count -= 1
31-
assert up_context.ignore_failure_count >= 0

uplaybook/core.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@
2626
import re
2727

2828

29+
class IgnoreFailure:
30+
"""A context-manager to ignore failures in wrapped tasks.
31+
32+
Example:
33+
34+
with core.IgnoreFailures():
35+
core.run("false")
36+
if not core.mkdir("/root/failure"):
37+
print("You are not root")
38+
"""
39+
40+
def __enter__(self):
41+
up_context.ignore_failure_count += 1
42+
return self
43+
44+
def __exit__(self, exc_type, exc_value, traceback) -> None:
45+
up_context.ignore_failure_count -= 1
46+
assert up_context.ignore_failure_count >= 0
47+
48+
2949
class Item(dict):
3050
"""
3151
An (ansible-like) item for processing in a playbook (a file, directory, user...)
@@ -173,7 +193,7 @@ def render(s: TemplateStr) -> str:
173193
def run(
174194
command: TemplateStr,
175195
shell: bool = True,
176-
ignore_failures: bool = False,
196+
ignore_failure: bool = False,
177197
change: bool = True,
178198
creates: Optional[TemplateStr] = None,
179199
) -> object:
@@ -186,7 +206,7 @@ def run(
186206
shell: If False, run `command` without a shell. Safer. Default is True:
187207
allows shell processing of `command` for things like output
188208
redirection, wildcard expansion, pipelines, etc. (optional, bool)
189-
ignore_failures: If True, do not treat non-0 return code as a fatal failure.
209+
ignore_failure: If True, do not treat non-0 return code as a fatal failure.
190210
This allows testing of return code within playbook. (optional, bool)
191211
change: By default, all shell commands are assumed to have caused a change
192212
to the system and will trigger notifications. If False, this `command`
@@ -209,7 +229,7 @@ def run(
209229
print(f"Current date/time: {{r.output}}")
210230
print(f"Return code: {{r.extra.returncode}}")
211231
212-
if core.run(command="grep -q ^user: /etc/passwd", ignore_failures=True, change=False):
232+
if core.run(command="grep -q ^user: /etc/passwd", ignore_failure=True, change=False):
213233
print("User exists")
214234
```
215235
@@ -233,9 +253,9 @@ def run(
233253
failure=failure,
234254
output=p.stdout.rstrip(),
235255
extra=extra,
236-
ignore_failure=ignore_failures,
256+
ignore_failure=ignore_failure,
237257
raise_exc=Failure(f"Exit code {p.returncode}")
238-
if failure and not ignore_failures
258+
if failure and not ignore_failure
239259
else None,
240260
)
241261

@@ -511,7 +531,7 @@ def grep(
511531
path: TemplateStr,
512532
search: TemplateStr,
513533
regex: bool = True,
514-
ignore_failures: bool = True,
534+
ignore_failure: bool = True,
515535
) -> object:
516536
"""
517537
Look for `search` in the file `path`
@@ -520,7 +540,7 @@ def grep(
520540
path: File location to look for a match in. (templateable)
521541
search: The string (or regex) to look for. (templateable)
522542
regex: Do a regex search, if False do a simple string search. (bool, default=True)
523-
ignore_failures: If True, do not treat file absence as a fatal failure.
543+
ignore_failure: If True, do not treat file absence as a fatal failure.
524544
(optional, bool, default=True)
525545
526546
Examples:
@@ -546,8 +566,8 @@ def grep(
546566
return Return(
547567
changed=False,
548568
failure=True,
549-
ignore_failure=ignore_failures,
550-
raise_exc=Failure("No match found") if not ignore_failures else None,
569+
ignore_failure=ignore_failure,
570+
raise_exc=Failure("No match found") if not ignore_failure else None,
551571
)
552572

553573

uplaybook/fs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -707,14 +707,14 @@ def builder(
707707
@template_args
708708
def exists(
709709
dst: TemplateStr,
710-
ignore_failures: bool = True,
710+
ignore_failure: bool = True,
711711
) -> object:
712712
"""
713713
Does `dst` exist?
714714
715715
Args:
716716
dst: File location to see if it exists. (templateable).
717-
ignore_failures: If True, do not treat file absence as a fatal failure.
717+
ignore_failure: If True, do not treat file absence as a fatal failure.
718718
(optional, bool, default=True)
719719
720720
Examples:
@@ -733,8 +733,8 @@ def exists(
733733
return Return(
734734
changed=False,
735735
failure=True,
736-
ignore_failure=ignore_failures,
736+
ignore_failure=ignore_failure,
737737
raise_exc=Failure(f"File does not exist: {dst}")
738-
if not ignore_failures
738+
if not ignore_failure
739739
else None,
740740
)

0 commit comments

Comments
 (0)