January 2, 2018

Cloudify properties and intrinsic functions

WORDS BY   Tadej Borovšak

POSTED IN   cloudify | devops | python


Passing data between nodes in Cloudify blueprints is one of the basic things blueprint author might want to do. In this post, we will have a look at how can we exchange and manipulate data in Cloudify blueprints, with emphasis on using intrinsic functions and their limitations.

Seting up our sandbox

If you would like to play along with our examples, you will need Cloudify's command line tool cfy installed. In order to keep our system clean, we will install cfy in a virtual environment by running

$ cd /tmp
$ virtualenv -p python2 venv
(venv) $ . venv/bin/activate
(venv) $ pip install 'cloudify<4'

Make sure you create a virtual environment for python 2, since Cloudify does not support python 3. Also, we will be using latest Cloudify in 3.x release. All of this can be done using Cloudify 4, but commands need to be changed a bit. Those modifications are left to the reader as an exercise.

Another thing that we will need is blueprint, that is available in this repo. We will clone it into /tmp/blueprint by running

(venv) $ git clone --branch init \
  https://github.com/xlab-si/cloudify-intrinsic-functions blueprint
(venv) $ cd blueprint

And with this being done, we are ready to start running some cfy commands.

Initial blueprint incarnation

At the end of this post, we would like to have a blueprint that deploys node_a and node_b components, where node_b needs node_a to be running. Additionally, node_b also needs some information about node_a at installation time. But for starters, we will write down the blueprint that simply creates both components.

tosca_definitions_version: cloudify_dsl_1_3

imports:
  - http://www.getcloudify.org/spec/cloudify/3.4.2/types.yaml


node_types:

  type_a:
    derived_from: cloudify.nodes.Root
    properties:
      property_a: { default: property_a_value }
    interfaces:
      cloudify.interfaces.lifecycle:
        create:
          implementation: scripts/create_a.py
          executor: central_deployment_agent

  type_b:
    derived_from: cloudify.nodes.Root
    properties:
      property_b1: { required: true }
      property_b2: { required: true }
    interfaces:
      cloudify.interfaces.lifecycle:
        create:
          implementation: scripts/create_b.py
          executor: central_deployment_agent


node_templates:

  node_a:
    type: type_a

  node_b:
    type: type_b
    properties:
      property_b1: some data from node_a
      property_b2: more data from node_a

This is a fairly simple blueprint that we can install locally by running

(venv) $ cfy local install -p blueprint.yaml
... Starting 'install' workflow execution
... [node_b_foxvkq] Creating node
... [node_a_ctosv3] Creating node
... [node_b_foxvkq.create] Sending task 'script_runner.tasks.run'
... [node_a_ctosv3.create] Sending task 'script_runner.tasks.run'
... [node_b_foxvkq.create] Task started 'script_runner.tasks.run'
... [node_b_foxvkq.create] INFO: Creating node of type B
... [node_b_foxvkq.create] INFO: property_b1 = some data from node_a
... [node_b_foxvkq.create] INFO: property_b2 = more data from node_a
... [node_b_foxvkq.create] Task succeeded 'script_runner.tasks.run'
... [node_a_ctosv3.create] Task started 'script_runner.tasks.run'
... [node_a_ctosv3.create] INFO: Creating node of type A
... [node_a_ctosv3.create] Task succeeded 'script_runner.tasks.run'
... [node_a_ctosv3] Configuring node
... [node_b_foxvkq] Configuring node
... [node_b_foxvkq] Starting node
... [node_a_ctosv3] Starting node
... 'install' workflow execution succeeded

Output of the command clearly indicates that we have some work left to do, since node_b has been created before node_a.

Adding order to execution

Official documentation states that execution order can be controlled by specifying relationships between nodes. So let us do this by adding relationships key to our node_b definition. This will take care of order issue that we had before.

What we also need to do is retrieve some information from node_a and use it while creating node_b. To do this, we will use get_property and get_attribute intrinsic functions. And if you are wondering where is attribute_a coming from, it is set by the scripts/create_a.py. Unfortunately, Cloudify' blueprint DSL does not allow us to declare attributes, so we need to grep around a bit to find the information we need.

After our modifications, node_b template looks like this:

node_b:
  type: type_b
  properties:
    property_b1: { get_property:  [ node_a, property_a  ] }
    property_b2: { get_attribute: [ node_a, attribute_a ] }
  relationships:
    - type: cloudify.relationships.connected_to
      target: node_a

If we try installing updated blueprint again, we get

(venv) $ git checkout order
(venv) $ cfy local install -p blueprint.yaml
... Starting 'install' workflow execution
... [node_a_aky88e] Creating node
... [node_a_aky88e.create] Sending task 'script_runner.tasks.run'
... [node_a_aky88e.create] Task started 'script_runner.tasks.run'
... [node_a_aky88e.create] INFO: Creating node of type A
... [node_a_aky88e.create] Task succeeded 'script_runner.tasks.run'
... [node_a_aky88e] Configuring node
... [node_a_aky88e] Starting node
... [node_b_fe0sl4] Creating node
... [node_b_fe0sl4.create] Sending task 'script_runner.tasks.run'
... [node_b_fe0sl4.create] Task started 'script_runner.tasks.run'
... [node_b_fe0sl4.create] INFO: Creating node of type B
... [node_b_fe0sl4.create] INFO: property_b1 = property_a_value
... [node_b_fe0sl4.create] INFO: property_b2 = {u'get_attribute': [u'node_a', u'attribute_a']}
... [node_b_fe0sl4.create] Task succeeded 'script_runner.tasks.run'
... [node_b_fe0sl4] Configuring node
... [node_b_fe0sl4] Starting node
... 'install' workflow execution succeeded

Node creation order is OK now, and get_property function behaves as expected, but get_attribute call was not evaluated and made it through verbatim. Well, let us sort this out.

But wait, before we tackle this problem, let us have a quick look at the creation scripts to see what they are actually doing. Creating node_a is as simple as it gets, since all we do is log (un)informative message and set the attribute_a.

from cloudify import ctx

ctx.logger.info("Creating node of type A")
ctx.instance.runtime_properties["attribute_a"] = "attribute_a_value"

Creating node_b is a bit more interesting, since we also access two properties of the node.

from cloudify import ctx

ctx.logger.info("Creating node of type B")

props = ctx.node.properties
ctx.logger.info("property_b1 = {}".format(props["property_b1"]))
ctx.logger.info("property_b2 = {}".format(props["property_b2"]))

Having seen these scripts, we can resume our quest to get the get_attribute function call sorted out.

Down the rabbit hole

Since the intrinsic functions' documentation does not list our use case among the examples, we must assume that direct property access does not evaluate the get_attribute function.

From the examples in the documentation we can see that we can use it when adding inputs to lifecycle operation. So let us try this. We modify our node_b template to this:

node_b:
  type: type_b
  properties:
    property_b1: { get_property:  [ node_a, property_a  ] }
    property_b2: { get_attribute: [ node_a, attribute_a ] }
  relationships:
    - type: cloudify.relationships.connected_to
      target: node_a
  interfaces:
    cloudify.interfaces.lifecycle:
      create:
        implementation: scripts/create_b.py
        executor: central_deployment_agent
        inputs:
          input_b2: { get_attribute: [ node_a, attribute_a ] }

This time, we also need to modify the creation script, since we need to handle the input:

from cloudify import ctx
from cloudify.state import ctx_parameters as inputs

ctx.logger.info("Creating node of type B")

props = ctx.node.properties
ctx.logger.info("property_b1 = {}".format(props["property_b1"]))
ctx.logger.info("input_b2 = {}".format(inputs["input_b2"]))

If we now run the installation again, we get what we were after:

(venv) $ git checkout inputs
(venv) $ cfy local install -p blueprint.yaml
...
... [node_b_t0mdbd] Creating node
... [node_b_t0mdbd.create] Sending task 'script_runner.tasks.run'
... [node_b_t0mdbd.create] Task started 'script_runner.tasks.run'
... [node_b_t0mdbd.create] INFO: Creating node of type B
... [node_b_t0mdbd.create] INFO: property_b1 = property_a_value
... [node_b_t0mdbd.create] INFO: input_b2 = attribute_a_value
... [node_b_t0mdbd.create] Task succeeded 'script_runner.tasks.run'
... [node_b_t0mdbd] Configuring node
... [node_b_t0mdbd] Starting node
... 'install' workflow execution succeeded

But at what cost! We now need to configure operation inputs on each node template instead of hiding this mess inside the node type. Sure we can do better, right?

Digging a hole inside the rabbit hole

By the fundamental theorem of software engineering, there must be another level of indirection that will resolve the conundrum that we got ourselves into. And indeed there is.

Our initial output contained a line:

INFO: property_b2 = {u'get_attribute': [u'node_a', u'attribute_a']}

So the function call is still there, we just need to trick Cloudify into evaluating this for us without resorting to duplication. And we can do this by resetting node_b template back to previous state:

node_b:
  type: type_b
  properties:
    property_b1: { get_property:  [ node_a, property_a  ] }
    property_b2: { get_attribute: [ node_a, attribute_a ] }
  relationships:
    - type: cloudify.relationships.connected_to
      target: node_a

Next, we employ another level of indirection inside the type_b definition:

type_b:
  derived_from: cloudify.nodes.Root
  properties:
    property_b1: { required: true }
    property_b2: { required: true }
  interfaces:
    cloudify.interfaces.lifecycle:
      create:
        implementation: scripts/create_b.py
        executor: central_deployment_agent
        inputs:
          input_b2: { default: { get_property: [ SELF, property_b2 ] } }

The trick is in the last line. Since we know that get_attribute function call is evaluated before the operation is executed, we use the get_property function to retrieve the "stored" get_attributes call and evaluate it.

And just to verify our hypothesis, here is the installation log:

(venv) $ git checkout master
(venv) $ cfy local install -p blueprint.yaml
...
... [node_b_h8yzi9] Creating node
... [node_b_h8yzi9.create] Sending task 'script_runner.tasks.run'
... [node_b_h8yzi9.create] Task started 'script_runner.tasks.run'
... [node_b_h8yzi9.create] INFO: Creating node of type B
... [node_b_h8yzi9.create] INFO: property_b1 = property_a_value
... [node_b_h8yzi9.create] INFO: input_b2 = attribute_a_value
... [node_b_h8yzi9.create] Task succeeded 'script_runner.tasks.run'
... [node_b_h8yzi9] Configuring node
... [node_b_h8yzi9] Starting node
... 'install' workflow execution succeeded

Final thoughts

As we learned today, intrinsic functions can be a bit counter-intuitive to work with, but if we stick to the following rule, most of the problem will resolve itself: life-cycle operation implementation should get all of its data as inputs and should not access properties and/or attributes directly. And by following this rule, we will also gain ability to use concat intrinsic function in properties for free;)

There are other, more advanced and flexible, ways of passing data around the blueprint (like creating custom relationship type that copies attributes between node instances, etc.), but sometimes this approach can simplify our implementation quite a bit.

Cheers!