Chef and vRO Integration Pattern

Today, I’m sharing our Chef / vRO pattern – my previous article on our strategy and why we’re using Chef got a lot of traffic, so now that I’ve wet your whistle on the intersection of Chef, VMWare, and strategy, I thought I’d take you on a tour of how we manage the interaction between Chef and vRO.

Recipe Conundrum

We started out probably where everyone who is integrating Chef and vRO starts out at… one vRA blueprint, one vRO Machine Provisioned workflow, one Chef recipe.  Want IIS on Windows, that’s its own blueprint with its own Machine Provisioned workflow, that takes the information the user entered and converts it to JSON to pass into Chef, which is also running its own recipe.  Want Python on Windows?  That’s another blueprint, another workflow, and another recipe.  By the time we were done with our proof of concept, we had 6 blueprints, with 6 workflows, and 6 recipes behind them.  And all the workflows were doing much the same thing, so if we had to fix something, we had to fix it in 6 places.  Workflow churn was literally eating up all our working hours!

As mentioned in my previous article, one of our main goals was to support our end developers and future proof our design by going wide on as many OSes and application stacks as possible.  It doesn’t take a math whiz to figure out we were going to have hundreds of things to manage before we got done, and that just wasn’t a productive use of our time.

Slimming Down to Go Wide

We had to slim down the work per OS and per application stack to the bare minimum necessary to support our application stack.  In team discussion, we decided we would have one blueprint per OS and an overall machine provisioned workflow that would then call workflows to do the customization per chef recipe that we still required.  That’s still a workflow per application stack, but that workflow is just doing the additional needed complexity on top of the base build, and for some application stacks it actually turns out to be an empty workflow.  For a lot of others, all it does is add the vRA properties to the JSON for Chef to parse and then exits.

vRA-vRO-Chef-Pattern

Conventional Thinking

Those of you who know me, know I’m not one that’s likely to be accused of conventional thinking!  That said, we started thinking about conventions once we decided on the overall structure of our vRO/Chef integration.  To whit, we understood that coming up with a naming convention and following it would make all the coding much easier than it could otherwise be…

Firstly, we had to decide how to control in vRA what would inform vRO of the recipes to be built.  We decided on the convention of a custom property added to the blueprint of the form “Chef.AppStack” to indicate to vRO that we needed a particular recipe added.  Then in vRA, we would run a workflow of the name “ChefAppStack“.  Finally, in Chef we would run a recipe of the name “OS-ChefAppStack“, which would allow for, say, doing something different in Windows as opposed to Linux when we are installing Python.

vRA Blueprints

Following the conventional thinking, we would need to add a property of “Chef.AppStack” to the appropriate blueprint.  This property gets an english name, and is usually presented to the user as a checkbox.  We add any other information needed to support the build, for example the name of the Site object to create in IIS as part of the build, as their own custom properties (with whatever non-“Chef.” name makes sense to us), and add them all to a single Build Profile called “Install AppStack“.  This lets us add an application stack to, say, all the flavors of Linux we build, just by including the one Build Profile in each blueprint.  In cases like the IIS Site, where its optional and exposes additional functionality over and above the base, we actually separate that into its own Chef Recipe and its own vRO Workflow – and when we do that, we name the actual textbox custom property “Chef.AppStack.Feature“.

vRO Machine Provisioned Workflow

Our Machine Provisioned workflow does a lot of things like install and configure Chef before we process the Chef.* properties per the above convention; we won’t cover those here, they’re pretty simple and that particular step is well documented on the chef site; we just run their bootstrap powershell script on Windows, for instance.  Once all that is done and we’re ready to actually run some Chef, we start building up the JSON file we’re going to pass into Chef with the following workflow.

ProcessWorkflows

The JSON will have a run_list telling Chef what recipes to run as well as additional data reflecting the user’s choices in vRA.  We use a role for the base run_list, so the code that starts building the JSON looks like this:

if (machineType == “Linux”) {
runListJSON = ‘{“run_list”:[“role[Linux-ChefBase]”‘;
} else {
runListJSON = ‘{“run_list”:[“role[Windows-ChefBase]”‘;
}

Then, we need to cycle through the Custom Properties to identify any that match our convention:

for each (var key in vCACVmProperties.keys) {
if (key.match(wfRegex)) {
System.log(“Found key: ” + key);
        if (vCACVmProperties.get(key) != “False” && vCACVmProperties.get(key)) {
            keys.push(key);
        }
    }
}

Where wfRegex is “Chef.” to catch just the “Chef.*” properties.  To finish preparing to run the workflows, we build up the run_list with the below code.  We sort the keys array to ensure that sub-workflows come immediately after their parents, and execute in their intended order in both vRO and Chef.

keys.sort();

for (var i = 0; i < keys.length; i++) {
    var split = keys[i].split(“.”);
    var joinkey = “”;
    for (var j = 0;j < split.length;j++) {
        joinkey += split[j];
    }

    if (split.length == 2) {
        dependentArr += split[1];
        array.push(joinkey);
        if (machineType == “Linux”){
            runListJSON = runListJSON + ‘,”recipe[Linux-‘ + joinkey + ‘]”‘;
        } else {
            runListJSON = runListJSON + ‘,”recipe[Windows-‘ + joinkey + ‘]”‘;
        }

        System.log(“Key added to ChefArray: ” + joinkey);
    } else {
        if (dependentArr.indexOf(split[split.length – 2]) > -1) {
            dependentArr += split[split.length – 1];
            array.push(joinkey);
            if (machineType == “Linux”) {
                runListJSON = runListJSON + ‘,”recipe[Linux-‘ + joinkey + ‘]”‘;
            } else {
                runListJSON = runListJSON + ‘,”recipe[Windows-‘ + joinkey + ‘]”‘;
            }
            System.log(“Key added to ChefArray: ” + joinkey);
        } else {
            System.log(“Key was not Added, missing dependent key: ” + joinkey);
        }
    }
}
if (machineType == “Linux”) {
    runListJSON = runListJSON + ‘]’;
else {
    runListJSON = runListJSON + ‘,”recipe[Windows-ChefFinal]”]’;
}

Then we need to actually run the ChefAppStack workflows, so we loop through the wfArray in our vRO workflow, passing the current wfArray[i] to the getWorkflowByName action, and then passing that Workflow object to the following code, which will pass all of the stub parameters forward to be used by the ChefAppStack workflow:

var wfTokens = new Array();

//Set all the input varibles for workflow
var workflowParameters = new Properties();
for (var i=0;i < Workflow.inParameters.length; i++) {
    var workflowInputParameterName = Workflow.inParameters[i].name;
    System.log(“Input parameter ” + (i + 1) + “: ” + workflowInputParameterName);
    if (workflowInputParameterName == “VM”) {
        workflowParameters.put(workflowInputParameterName, rmtHost);
    }
    if (workflowInputParameterName == “vmUsername”) {
        workflowParameters.put(workflowInputParameterName, vmUsername);
    }
    if (workflowInputParameterName == “vmPassword”) {
        workflowParameters.put(workflowInputParameterName, vmPassword);
    }
    if (workflowInputParameterName == “vCACVm”) {
        workflowParameters.put(workflowInputParameterName, vCACVm);
    }
    if (workflowInputParameterName == “vCenterVm”) {
        workflowParameters.put(workflowInputParameterName, vCenterVm);
    }
}

System.log(“Attempting to execute workflow”);
var wfToken = Workflow.execute(workflowParameters);

wfTokens.push(wfToken);

We then run a waitForCompletion action on that wfToken and when its done, we parse the output (as a string) into our JSON we are compiling workflow by workflow.  I couldn’t get if(array[0] == NULL) or any variation thereof to detect an empty string thus the test for == “EMPTY” you see below; a workflow that doesn’t want to add any JSON needs to return the string “EMPTY”

var array = new Array();

for each (var out in wfTokens[0].getOutputParameters()) {
    array.push(out);
    System.log(“output: ” + out);
}

if (array[0] == “EMPTY” || array[0] == “__NULL__” || array[0] === null) {
    System.log(“Recipe has no user input”);
else {
    answersJSON = answersJSON + ‘,’ + array[0];
}

Chef Workflows and Recipes

What do the Chef vRO Workflows look like?  They can be pretty basic, like the ChefIISSite workflow:

ChefIISSite

The first action, getPropertiesFromVirtualMachine, just creates a Properties object (vCACVMProperties) we can access in the script action, which we use to add a name/value pair to the answersJSON that is this workflow’s output, in this case “sitename”: “Sitename

var siteName = vCACVMProperties.get(“Chef.IIS.Site”);

answersJSON = ‘”sitename”:”‘ + siteName + ‘”‘;
System.log(“JSON string: ” + answersJSON);

The ChefAppstack workflow can also be pretty complex, like the following ChefSQL workflow that adds multiple drives to the VM, choosing from among the datastores available for each one as it goes.  I won’t be able to cover this one in detail as there is some secret sauce from my employer in it, but as an example of complexity, I think you get the idea:

ChefSQL

The actual Chef recipes are an exercise for the reader, but to parse the data entered by the user in vRA is pretty simple.  This is from the Windows-ChefIISSite recipe, parsing the JSON created above:

# In our actual chef recipe, we use a databag to store constants… this could also be inetpub=”C:\\Inetpub” or whatever
inetpub = data_bag_item(“Globals”, “default”)[“Inetpub”]
sitename = node[“sitename”]

#bindings not idempotent!!!! If this site exists, it is assumed to be configured properly.
iis_site “#{sitename}” do
    bindings “http://*:80,https://*:443&#8221;
    path “#{inetpub}\\#{sitename}”
    action [:add,:start]
end

Conclusion

That’s it for today!  Post a comment, what have you done to manage the overall complexity of your private cloud solution? Are you going to adapt and use the tips here in your own practice?  I and my readers would love to know!

As always, follow @merlinjim on twitter to be notified when I write new articles!

Advertisements
About

I've been automating everything I can get my hands on since I was a wee lad, these days its mostly Office, UC4, or VMWare - but I have a strong interest in AI, microfluidics, and 3D printing when I'm not slaving for "da man"

Tagged with: , , , ,
Posted in Automation, Chef, vRA

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Head Shot of Jim

Jim McCracken

Enter your email address to follow this blog and receive notifications of new posts by email.

Follow me on Twitter
Past Posts
Automate The Cloud pages
%d bloggers like this: