- https://gist.github.com/grkvlt/e094c1968915a1d3ba2a09588bfff8c5

# Enabling Effective Effectors

As part of the work I have been doing on Clocker, it became evident that
the way effectors are integrated into Brooklyn is not sufficient for them
to be considered first-class citizens. In particular, it is currently
impossible to use effectors in any meaningful way when writing blueprints
or catalogue items in YAML. To fix this, I think we need the following
capabilities:

1.  Mechanism to enable defining arbitrary scripted effectors in YAML, using
JSR-223 supported scripting languages.
2.  DSL syntax to allow calling effectors in YAML.
3.  Enable new entities to be defined in YAML that conform to existing
interfaces
by implementing the required effectors.
4.  Change the `SoftwareProcess` based lifecycle task logic to call a series
of effectors on the entity, rather than driver methods.
5.  Use AOP style mechanisms to enable _before-_ and _after-_ advice
as effector
extension points, rather than inconsistent `preX()` and `postX()` methods.

To accomplish these tasks will require quite a lot of refactoring, as well
as some pieces of extra work like sandboxing of scripts, but some parts can be
implemented immediately to validate the concept.

I have been working on the first two items, and there are pull requests on the
`brooklyn-server` repository for these with initial, working code.

## Capabilities

The rest of this document will be a detailed description of the
capabilities listed above, as a starting point for further discussion.

### Scripted Effectors

This involves being able to define new effectors in a YAML file by writing the
code to be executed in a scripting language that can be interpreted by the
JVM. Java uses JSR-223 to implement this feature, and it is part of Java 7
onwards, with a JavaScript implementation included by default.

I have written an initial implementation as [brooklyn-server#153](
https://github.com/apache/brooklyn-server/pull/153) which uses the
`AddEffector` initializer to create a new effector on an entity, in the
same way as `SshCommandEffector` does. The initializer can be configured to
use code from a file, given as a URL, or embedded in the blueprint.
Effector parameters for input can be defined, and the output type
configured. The effector return value can either be obtained from the
default return value of the script, often the value of the last expression
to be evaluated, or from a particular variable set by the script.

The following YAML shows an effector written in JavaScript that returns a
string value. The code does not interact with any other Brooklyn features, and
simply operates on its arguments.

```YAML
- type: org.apache.brooklyn.core.effector.script.ScriptEffector
  brooklyn.config:
    name: testEffector
    description: |
      An effector implemented in JavaScript
    parameters:
      one:
        type: java.lang.String
        description: "A string argument"
        defaultValue: "one"
      two:
        type: java.lang.Integer
        description: "An integer argument"
        defaultValue: 2
    script.language: "JavaScript"
    script.return.type: java.lang.String
    script.content: |
      var n = 0;
      var out = "";
      while (n < two) {
        out += one;
        n++;
      }
      out;
```

Most JVM scripting languages provide some way of interacting with native Java
code, such as instantiating Java objects and executing methods on them.
This allows much more useful effectors to be written, that can call other
parts of the Brooklyn code, but also opens up the Brooklyn internals in a
way that can be unsafe. We would prefer to have some way of sandboxing the
effector code in order to ensure that the operations it performs are safe
and do not interfere with other running Brooklyn applications.

The Brooklyn API call to add an item to the catalogue allows YAML to
be submitted
that includes a `brooklyn.library` section listing Jar files to be added to
the CLASSPATH. These files contain code that will be executed in the
Brooklyn server context and will be trusted in the same way as the actual
core Brooklyn code. The entitlements system therefore has a check provided
to restrict access to this API call, preventing abuse by non-privileged
users. Unfortunately, it is not possible to do the same with scripted
effectors, since any blueprint YAML is able to use the
`brooklyn.initializers` section where effectors are added.

#### Sandboxing

A working sandbox for scripted effectors should perform the following tasks:

1.  Mediate access to Brooklyn objects, including the current entity, task,
and management context.
2.  Prevent creation and loading of arbitrary Brooklyn objects and execution
of methods on them unless authorized.
3.  Prevent access to the Brooklyn server itself, including the filesystem,
processes and network, except through trusted APIs.

These requirements can probably be fulfilled using a combination of Java
security policy, custom class loaders and adding further checks to the
entitlements API. Untrusted users should only be able to use scripted
effectors to perform the same tasks as they could perform via the Brooklyn
YAML DSL, whereas a user with the right entitlements would have access to
the Brooklyn internals. However, there is also the issue of runtime access,
since a trusted user could define the effector to perform some privileged
operation that we wish to allow an untrusted user to execute when they
create the entity. This aspect of scripted effectors will require further
investigation and careful design and testing.

The current implementation of scripted effectors injects a series of objects
into the script context, allowing it access to them. These are the current
entity, its management context, the current task and the configuration map.
One possibility for protecting access to Brooklyn internals is to inject a
single object that allows methods to be called on it in a similar fashion
to the `DslComponent` object in the actual CAMP YAML. The object would be
injected as `brooklyn` and would have methods like `entity(id)`,
`sensor(name)`, `config(key)` and `attributeWhenReady(name)` just as with
the `$brooklyn` functions. The implementations of these methods could call
an appropriate entitlements API check to decide whether the effector is
able to execute them, based on the current user. Other checks, like those for
Brooklyn server filesystem access, can be managed using the Java security
policy, and a custom classloader.

## Calling Effectors

The YAML blueprint specification allows entities to be defined with sensors
and to access the value of sensors for use in configuration. However,
although an entity can include effectors defined in the Java classes, or
using scripting languages (see above) and SSH commands, it is not possible
to execute effectors and retrieve their results anywhere in a blueprint.

The code in [brooklyn-server#155](
https://github.com/apache/brooklyn-server/pull/155) implements a new
function for the `DslComponent` class that can execute an effector on an
entity and evaluates to its return value. This new function is used as
follows:

```YAML
$brooklyn:entity("other").effector("findInformation"):
  args:
    arg1: "value"
    arg2: 3.14159d
    arg3: $brooklyn:attributeWhenReady("host.address")
```

Here we see the effector `findInformation` being evaluated with three
arguments,
on the entity with id `other`. One of the arguments is an
`attributeWhenReady` call, thus causing the execution to be delayed until
the sensor data is available.

### Implementing Interfaces

It should be possible to define an entity in YAML by specifying the
set of interfaces
it should implement, and then the code for each of the methods declared in
the interfaces. The new entity could simply extend `AnstractEntity` or some
other base class could be specified. This could possibly also be used to
add extra interface implementations to an existing entity as mixins.

The rationale for this capability is that many existing entities require that
the components they communicate with are entities of a specific type, defined
by the Java interfaces is implements. For example, the `Startable` and
`Resizable`
interfaces each define methods that a calling entity can know will exist.

One idea for accomplishing this is to define an entity in YAML using
the `Entity`
interface as the type, and add effectors, specifying which interface they
are implementing, as follows:

```YAML
- type: org.apache.brooklyn.api.entity.Entity
  brooklyn.initializsers:
  - type: org.apache.brooklyn.core.effector.script.ScriptEffector
    brooklyn.config:
      name: start
      interface: org.apache.brooklyn.core.entity.trait.Startable
      script.content: |
        // start the entity
```

There are still problems with this approach, such as interfaces with
multiple methods. It is also possible that entity developers will wish to
extend other base classes than the top level `AbstractEntity`. However, it
is certainly possible to use JSR-223 scripting to implement an interface,
and the ability to develop entities that implement Brooklyn interfaces
would add a great deal of flexibility and power to the application
blueprint creator.

### Lifecycle Logic

There has been some discussion on the Brooklyn mailing list about how to
manage entity configuration in YAML when extending an entity. This has
mostly centered around maps of configuration data, but the
`VanillaSoftwareProcess` entity and its `install.command`,
`pre.install.command` and similar keys have also been discussed. The issue
here is that in an entity definition extending another entity type it is
not possible to add extra commands to these keys in the case where the
`pre` and `post` configuration is already defined.

In many cases it would be useful to redefine completely the `install`
or `launch`
lifecycle phase, or even add an additional phase, but these are not actually
effectors. Instead, the `SoftwareProcessDriverLifecycleEffectorTasks` class
describes the order in which methods on the entity driver are to be executed.
A better way to handle this would be to define a minimal set of phases (for
example, _install_, _customize_ and _launch_) as effectors in an interface
that can be implemented by `SoftwareProcess`. The default order of execution
could be described in a configuration key, and additional phases could be
added to the configuration, causing extra effectors to be executed. the
default implementation of these effectors would be to execute the commands in
a configuration key named after the phase, so `install.command` for
the _install_
phase, and to execute the method with the same name on the driver if no
such configuration exists. This would allow the current entities to
remain unchanged,
but also enable easy customisation and extension of the lifecycle.

### Effector Aspects

The lifecycle changes described in the previous section enable changes
and additions
to the entity startup lifecycle, but the issue of modifying an existing
phase still exists. Instead of the single _pre_ and _post_ modifiers available,
it would be interesting to consider a solution modelled on AOP; Aspect-Oriented
Programming. Aspects are points in the execution of code where additional
functionality can be inserted, often for cross-cutting concerns like
security or auditing, or for extension of APIs or code that is maintained
by a third party and cannot be modified. A _before_ aspect executes before
a particular method, and an _after_ aspect executes afterwards.

The suggestion here is to allow effectors to be defined not just as
named pieces
of code executed on an entity, but also as advice on the entity. For
example, if the `install` effector is defined, then an entity could be added
in a catalog entry that used `before` advice to add some extra setup of the
environment, and would not preclude additional entities that extend it
adding their own _before_ advice to do further setup. This chain of
advice would
be managed by Brooklyn (rather that an external library like AspectJ) and
when the `install` effector is executed all configured advice would be
executed in order.

## Conclusion


I believe that the first two capabilities (adding support for defining
and executing
effectors to YAML blueprints) are the simplest to implement, but sandboxing
and entitlement concerns will require some further thought before a final
design can be agreed on. These capabilities are also pre-requisites for the
other three, in particular the ability to define effectors in YAML.

This is not limited to the points mentioned, and another possibility
not discussed
above is the ability to define the logic for policies using scripts in the
YAML blueprint. There may be other parts of Brooklyn that can also benefit
from embedding code in blueprints.

Extending the implementation of effectors to allow defining entities in
YAML based on interfaces is the obvious next  step, although some of the
syntax for this is still unclear and more discussion is needed to
understand how this could work and what the main use cases are.

The last two capabilities mentioned, involving the `SoftwareProcess`
entity lifecycle,
will probably require the most work in refactoring of existing code but
have a lot of potential to make entity definitions much more powerful. Again,
more discussion as to the best way to approach this is still needed.

I am very interested to know what people think about this proposal, and which
parts should be worked on further.

----

_Andrew Kennedy; <mailto:[email protected]>; 30 May 2016_
-- 

Andrew Kennedy ; Founder clocker.io project ; @grkvlt ; Cloudsoft

Reply via email to