Properties
==========

Names
-----

The names of properties may not end in the character ``.``, because that has a
special meaning in unevaluated property application specifications.

Properties with ``:APPLY`` subroutines occupy the function cells of symbols,
so except in the case of properties with no ``:APPLY`` subroutine, do not try
to define an ordinary function with the same name as a property.

Working directories
-------------------

Except where specified otherwise in property docstrings, relative paths are
relative to the remote home directory.  ``:LISP`` properties may assume they
will be executed in the remote home directory, and ``:POSIX`` properties may
assume that commands will be executed in the remote home directory, and that
relative paths passed to ``READ-REMOTE-FILE`` and ``WRITE-REMOTE-FILE`` are
relative to the remote home directory.  Use ``WITH-REMOTE-CURRENT-DIRECTORY``
to change the remote working directory in a way which ensures it will get
changed back.

.. _property-subroutines:

Property subroutines
--------------------

A property is composed of up to five subroutines, which all have the same
lambda list (take the same arguments).  At least one of ``:hostattrs``,
``:apply`` or ``:unapply`` must be present.

``:desc`` subroutines
~~~~~~~~~~~~~~~~~~~~~

Pure function of the property's arguments which returns a description of
applying the property, to be used in stdout by deployments to inform the user
what work is being done.

``:preprocess`` subroutines
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Pure function executed to modify the arguments that will be passed to the
other subroutines; should return a fresh list of the new arguments.  This
subroutine is called on each atomic property application within a property
application specification before the effects of property combinators have been
applied.  That is, it is effectively executed on atomic property applications
in isolation from the property application specifications in which they occur.

``:hostattrs`` subroutines
~~~~~~~~~~~~~~~~~~~~~~~~~~

Executed in the root Lisp to (i) add static informational attributes of hosts
to which this property is applied or is to be applied; and (ii) check that
applying this property makes sense -- e.g. that we're not trying to install a
package using apt(1) on a FreeBSD host.

Can retrieve existing static informational attributes using ``GET-HOSTATTRS``,
or things which wrap ``GET-HOSTATTRS``, such as ``GET-HOSTNAME``.  Should
signal the condition ``INCOMPATIBLE-PROPERTY`` if existing static
informational attributes indicate that the property should not be applied to
this host.  Can use ``PUSH-HOSTATTRS`` and ``REQUIRE-DATA`` to add new entries
to the host's static information atributes.

Other than as described in the previous paragraph, should be a pure function.
In particular, should not examine the actual state of the host.  Essentially a
conversion of the arguments to the property to appropriate static
informational attributes.

``:check`` subroutines
~~~~~~~~~~~~~~~~~~~~~~

Determine whether or not the property is already applied to the host and
return a generalised boolean indicating such.  Whether or not the ``:apply``
and ``:unapply`` subroutines get called depends on this return value.  If
absent, it is always assumed the property is unapplied, i.e., an attempt to
apply the property will always be made.

``:apply`` and ``:unapply`` subroutines
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Apply or unapply the property.  Should return ``:no-change`` if the property
was already applied; any other return value is interpreted as meaning that the
property was not (fully) applied before we ran, but now it is.  (If the
``:check`` function indicated that neither ``:apply`` nor ``:unapply`` should
be run, then this is equivalent to those subroutines returning ``:no-change``.)

The point of having both these return value semantics and the ``:check``
subroutine is that a property might only be able to check whether it made a
change after trying to apply itself -- it might check whether running a
command actually made a change to a particular file, for example.

Errors in attempting to apply a property are indicated by signalling a
``FAILED-CHANGE`` error condition.

``:posix`` vs. ``:lisp`` properties
-----------------------------------

``:posix`` properties should not make any assumptions about what localhost is
-- they may be running in the root Lisp, but they might be running in a Lisp
image running on an intermediary host, or even on the host to be configured.
They should perform I/O only by calling ``RUN``, ``RUNLINES``,
``READ-REMOTE-FILE``, ``WRITE-REMOTE-FILE``, requesting prerequisite data, and
applying or unapplying other ``:posix`` properties.  Otherwise, they should be
pure functions.

``:lisp`` properties, by contrast, may (and should) assume that they are
running in a Lisp image on the host to which they are to be applied, so they
can perform arbitrary I/O in that context.  They can also make use of ``RUN``,
``RUNLINES``, ``READ-REMOTE-FILE`` and ``WRITE-REMOTE-FILE`` if desired.

``:posix`` properties are characterised by the limited set of ways in which
they perform I/O, not by the use of only facilities defined in the Single UNIX
Specification.  Nevertheless, if a ``:posix`` property or function intended to
be called by ``:posix`` properties uses non-POSIX facilities, but it is not
obvious given the stated purpose of the property that it will do this, it is
good to mention the use of non-POSIX facilities in the docstring.  For
examples of this, see ``USER:HAS-LOGIN-SHELL`` and ``USER:PASSWD-FIELD``.

API reference
-------------

Properties
~~~~~~~~~~

Function: ``PROPAPP-TYPE``
^^^^^^^^^^^^^^^^^^^^^^^^^^

``(propapp-type propapp)``

Function: ``PROPAPP-ARGS``
^^^^^^^^^^^^^^^^^^^^^^^^^^

``(propapp-args propapp)``

Function: ``COMBINE-PROPAPP-TYPES``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(combine-propapp-types &rest lists)``

Function: ``PROPAPP-DESC``
^^^^^^^^^^^^^^^^^^^^^^^^^^

``(propapp-desc propapp)``

Function: ``PROPAPP-ATTRS``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(propapp-attrs propapp)``

Function: ``CHECK-PROPAPP``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(check-propapp propapp)``

Function: ``APPLY-PROPAPP``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(apply-propapp propapp)``

Function: ``UNAPPLY-PROPAPP``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(unapply-propapp propapp)``

Macro: ``IGNORING-HOSTATTRS``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(ignoring-hostattrs form)``

Where FORM is a programmatic call to a property which has a :HOSTATTRS
subroutine, muffle warnings about calling a property with a :HOSTATTRS
subroutine programmatically.  Use this only when you know that the :HOSTATTRS
subroutine does not push any new hostattrs.

Macro: ``DEFPROPLIST``
^^^^^^^^^^^^^^^^^^^^^^

``(defproplist name type lambda &body properties)``

Like DEFPROPSPEC, but define the function which yields the propspec using the
unevaluated property application specification PROPERTIES, where the implicit
surrounding combinator is ESEQPROPS.

If the first element of PROPERTIES is a string, it will be considered a
docstring for the resulting property.  If the first element of PROPERTIES
after any such string is a list beginning with :DESC, the remainder will be
used as the :DESC subroutine for the resulting property, like DEFPROP.
Supplying :CHECK and :HOSTATTRS subroutines in the same way is also supported.

Otherwise, the body should not contain any references to variables other than
those in LAMBDA.  LAMBDA is an ordinary lambda list, so you can use &AUX
variables to compute intermediate values.  The evaluation of arguments to
propapps in PROPERTIES, and the evaluation of any &AUX variables in LAMBDA,
will happen at :HOSTATTRS-time for the host to which the resulting property is
to be applied, so you can retrieve static informational attributes set by
other properties applied to the host (unlike with unevaluated property
application specifications appearing in DEFHOST forms).  The evaluation should
otherwise be purely functional.

You will usually be able to use DEFPROPLIST instead of DEFPROPSPEC.  However,
sometimes you will need to fall back on DEFPROPSPEC.  For example, an
unevaluated property application specification cannot express passing values
other than constant values and propapps to property combinators.

hostattrs in property subroutines
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Simple error: ``INAPPLICABLE-PROPERTY``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(inapplicable-property)``

Signal, in a :HOSTATTRS subroutine, that the host's hostattrs indicate that
this property cannot be applied to this host.  E.g. the property will try to
install an apt package but the host is FreeBSD.

Function: ``GET-HOSTATTRS``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(get-hostattrs k &optional host)``

Retrieve the list of static informational attributes of type KEY.

Called by property :HOSTATTRS, :APPLY and :UNAPPLY subroutines.

Function: ``GET-HOSTATTRS-CAR``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(get-hostattrs-car k &optional host)``

Function: ``GET-PARENT-HOSTATTRS``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(get-parent-hostattrs k &optional host)``

Function: ``GET-PARENT-HOSTATTRS-CAR``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(get-parent-hostattrs-car k &optional host)``

Function: ``PUSH-HOSTATTR``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(push-hostattr k v)``

Push new static informational attribute V of type K.

Called by property :HOSTATTRS subroutines.

Function: ``PUSH-HOSTATTRS``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(push-hostattrs k vs)``

Push new static informational attributes VS of type K.

Called by property :HOSTATTRS subroutines.

Function: ``PUSHNEW-HOSTATTR``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(pushnew-hostattr k v &key test)``

Push new static informational attribute V of type K.
TEST is passed on to PUSHNEW.  Called by property :HOSTATTRS subroutines.

Function: ``PUSHNEW-HOSTATTRS``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(pushnew-hostattrs k vs &key test)``

Push new static informational attributes VS of type K.
VS is a list of items.  TEST is passed on to PUSHNEW.  Called by property
:HOSTATTRS subroutines.

Function: ``REQUIRE-DATA``
^^^^^^^^^^^^^^^^^^^^^^^^^^

``(require-data iden1 iden2)``

Wrapper around PUSHNEW-HOSTATTR to indicate that a piece of prerequisite data
is needed to deploy a property.

Called by property :HOSTATTRS subroutines.

Function: ``GET-HOSTNAME``
^^^^^^^^^^^^^^^^^^^^^^^^^^

``(get-hostname &optional host)``

Get the hostname of HOST, defaulting to the host to which properties are
being applied.

Called by property subroutines.

Function: ``GET-SHORT-HOSTNAME``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(get-short-hostname &optional host)``

Get the short hostname of HOST, defaulting to the host to which properties
are being applied.

Called by property subroutines.

:APPLY subroutines
~~~~~~~~~~~~~~~~~~

Simple error: ``FAILED-CHANGE``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(failed-change)``

Signal problems with the connection and errors while actually attempting to
apply or unapply properties.

Simple error: ``ABORTED-CHANGE``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(aborted-change)``

Like FAILED-CHANGE, except the attempt to apply or unapply the property has
failed before any changes have been made to the system.  Signalled when a
property is able to determine that it cannot be applied/unapplied by examining
the actual state of the host but without making any changes.

Not to be confused with INAPPLICABLE-PROPERTY.

Function: ``MAYBE-WRITE-REMOTE-FILE-STRING``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(maybe-write-remote-file-string path content &key mode)``

Wrapper around WRITE-REMOTE-FILE which returns :NO-CHANGE and avoids writing
PATH if PATH already has the specified CONTENT and MODE.

Function: ``ASSERT-REMOTE-EUID-ROOT``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(assert-remote-euid-root)``

Assert that the remote user has uid 0 (root)

Macro: ``WITH-CHANGE-IF-CHANGES-FILE``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(with-change-if-changes-file file &body forms)``

Execute FORMS and yield :NO-CHANGE if FILE does not change.
Since stat(1) is not POSIX, this is implemented by calling ``ls -dlL`` and
cksum(1), and seeing if any of the information reported there, except for the
number of links, has changed.  Thus, you should not use this macro to detect
changes in properties which will change the file but not the output of
``ls -dlL`` and cksum(1).

Macro: ``WITH-CHANGE-IF-CHANGES-FILES``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(with-change-if-changes-files &rest &body forms)``

Execute FORMS and yield :NO-CHANGE if none of FILES change.
See WITH-CHANGE-IF-CHANGES-FILE docstring regarding the sense of 'change'.

Macro: ``WITH-CHANGE-IF-CHANGES-FILE-CONTENT``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

``(with-change-if-changes-file-content file &body forms)``

Execute FORMS and yield :NO-CHANGE if FILE has the same content afterwards.
