- 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
