Skip to content

Instantly share code, notes, and snippets.

@grkvlt
Last active May 30, 2016 16:01
Show Gist options
  • Save grkvlt/e094c1968915a1d3ba2a09588bfff8c5 to your computer and use it in GitHub Desktop.
Save grkvlt/e094c1968915a1d3ba2a09588bfff8c5 to your computer and use it in GitHub Desktop.
Proposal for updates to Brooklyn effectors.

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 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.

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 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.

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:

- 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment