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:
-
Mechanism to enable defining arbitrary scripted effectors in YAML, using JSR-223 supported scripting languages.
-
DSL syntax to allow calling effectors in YAML.
-
Enable new entities to be defined in YAML that conform to existing interfaces by implementing the required effectors.
-
Change the
SoftwareProcess
based lifecycle task logic to call a series of effectors on the entity, rather than driver methods. -
Use AOP style mechanisms to enable before- and after- advice as effector extension points, rather than inconsistent
preX()
andpostX()
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.
The rest of this document will be a detailed description of the capabilities listed above, as a starting point for further discussion.
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
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.
- 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.
A working sandbox for scripted effectors should perform the following tasks:
-
Mediate access to Brooklyn objects, including the current entity, task, and management context.
-
Prevent creation and loading of arbitrary Brooklyn objects and execution of methods on them unless authorized.
-
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.
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
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:
$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.
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:
- 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.
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.
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.
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