@@ -33,6 +33,8 @@ fs.mkdir(project_name).notify(initialize_project)
3333This playbook creates a ` my-test-project ` playbook, and puts an empty ` README ` file in it,
3434then initializes git.
3535
36+ ## Being Declarative
37+
3638The ` notify() ` makes this playbook declarative. A ` notify() ` sets up a function (known as
3739a "handler") to be called later, ** only when a task makes a change** . uPlaybook Tasks
3840will determine if they change the system (in this case, if the directory already exists,
@@ -42,17 +44,17 @@ directory is created.
4244This is a useful trait of a playbook because you don't want to overwrite the ` README ` , or
4345re-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)
5456Initialized 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: -->
0 commit comments