CloudBolt Plug-ins

How Plug-ins are Written

Plug-ins are written in Python 2.7, although compatibility with Python 3.6+ is recommended for upgradeability to future versions of CloudBolt. For more information on how to write cross-compatible plug-ins, refer to the guide at https://support.cloudbolt.io/hc/en-us/articles/115003824766. Plug-ins are stored on the filesystem of the CloudBolt server or externally and loaded dynamically via a URL. Plug-ins run on the CloudBolt server and have access to the internal CloudBolt APIs, which read and write data to the CloudBolt database by using Django’s object–relational mapper (ORM). This Python interface is simple to learn, see the section on Resources for Writing Plug-ins for more info.

Structure of a CloudBolt Plug-in

A CloudBolt plug-in modules must contain a function called run(). CloudBolt’s job engine will call that function at particular points during a job’s execution.

run(job, **kwargs)

Is passed a CloudBolt Job object and a set of keyword arguments meant to simplify access to common CloudBolt objects the plug-in might need to interact with.

Helpful keywords arguments are passed to any CloudBolt plug-in run method, but the values will be set to None if the object referenced is not applicable to the context of the hook execution. Keyword arguments include:

  • logger: standard Python Logger object (http://docs.python.org/2/library/logging.html).
  • hook_point: A string representation of the hook_point where the action is called from. If the action is not run in the context of an orchestration, hook_point is None.
  • servers: an iterable collection of all servers that are the target of the plug-in. If there is only one target server, the “server” keyword is also available for simplicity.
  • resources: an iterable collection of all deployed resources that are the target of the plug-in. If there is only one target resource, the “resource” keyword is also available for simplicity.

The run method of CloudBolt plug-ins should always return a tuple of 3 strings: (status, output, errors). On success, the plug-in should return (“”, “”, “”). If a non-empty status string is returned, the job engine will save the status, output, and errors to the job object and terminate any further steps in the job that would have been run. A pre-job plug-in can use this behavior to short circuit the execution of the job if certain prerequisite conditions are not met.

Valid Status Values

These are the valid string values that your plug-in can return for status, along with the corresponding display text that will be shown to the user for the job:

  • ‘SUCCESS’ — ‘Completed successfully’
  • ‘WARNING’ — ‘Completed with warnings’
  • ‘FAILURE’ — ‘Completed with errors’
  • ‘CANCELED’ — ‘Canceled by user’

Note

There are a few types of plug-ins that are exceptions to the structure described here and may have different method names, method signatures, and return values. These include plug-ins in rules, Generated Parameter Options plug-ins, Display Condition Plug-ins for Server Actions, and Discovery Plug-ins on Blueprints.

CloudBolt Plug-in Parameterization

When uploading or setting a URL for a new file, for a CloudBolt plug-in, CloudBolt will scan the file contents for template style variables like {{ variable }} and will automatically create action inputs for each variable it discovers. See Action Input Parameters for more details on auto generated action inputs and Action Context for a complete reference list of objects available to CloudBolt plug-ins on different contexts.

CloudBolt will handle prompting users for those inputs, and then render them in the action before executing it. The inputs will be escaped so they can’t execute arbitrary code while still rendering as expected. However, this depends on the parameters being quoted properly in the action code. Ensure that any string inputs also include surrounding quote marks so the value is properly assigned to its variable in Python. An example: hostname = “{{server.hostname}}”. Otherwise, a user could enter arbitrary Python code into an action input and it would be executed as if it had been written by the author of the plug-in.

Action Input Options

Action inputs can be constrained to specific, pre-determined values. This can be done by starting at the action’s details page, clicking on the name of the action input, and then defining global options for that action input.

For action input whose options need to be determined at run time, the options can be provided by the plug-in code. Specifically, for each action input in the plug-in you can define a method called generate_options_for_<action-input-name> that returns the options for that action input.

For example, if we wanted to give the user options for the prefix for the hostname for a server, we could define a variable to use in its run method like prefix = “{{ hostname_prefix }}”. This would create an action input called hostname_prefix. One possible implementation of the method for generating its options is as follows.

def generate_options_for_hostname_prefix(server=None, **kwargs):
  options = [('test', 'test'), ('example', 'example')]
  if server:
     options.append((server.group.id, server.group.name))
     options.append((server.environment.id, server.environment.name))
   return options

The method returns a list of tuples where every tuple contains a value to use and a label to display for the option. The context passed to the method should match the contexts described elsewhere, depending where the plug-in is used. Best practice is to define the method to accept arbitrary kwargs so it can gracefully handle if more items are passed to the context in the future.

It is also possible to define an initial value for the action input as part of the options. Another possible implementation of the method that does so is as follows.

def generate_options_for_hostname_prefix(server=None, **kwargs):
  options = [('test', 'test'), ('example', 'example')]
  if server:
     options.append((server.group.id, server.group.name))
     options.append((server.environment.id, server.environment.name))
   return {'initial_value': server.group.id, 'options': options}

In this case, the method returns a dictionary with keys for the initial value and options, where the options are a list of tuples as before. The initial value should match the first item in one of the option tuples.

These options will appear both when setting default values for the action input and when choosing a value at execution time. If an initial value is provided, it will simply be the option that is first selected when the widget loads.

For more information on all the possible ways to control action input options see the Generated Parameter Options Appendix.

CloudBolt Plug-in Examples

Places to find example CloudBolt Plug-ins: * CloudBolt hosts a content library with a range of example plug-ins. These can be imported in your CloudBolt UI on most pages where actions can be created (ex. the Server Actions page, the Orchestration Actions page). * The code for these plug-ins is stored in a public github repository and can be browsed here: https://github.com/CloudBoltSoftware/cloudbolt-forge/tree/master/actions/cloudbolt_plugins

To demonstrate a few key concepts, a few examples have been included here.

Here’s a simple plug-in, which can be used in Provision Server job’s Pre-Create Resource trigger point . This action simply adds a message to the job progress (and log file) stating the IP address of the server. It then waits for 60 seconds, to give the user watching the job in CloudBolt’s UI a chance to notice this new progress message.

import time
from common.methods import set_progress


def run(job, server=None, **kwargs):
    msg = "In Pre-Create Resource for job #{}, the server's IP is {}".format(job.id, server.ip)
    set_progress(msg)
    time.sleep(60)
    return "","",""

This example plug-in can be used in Provision Server job’s Pre-Create Resource trigger point to change the hostname for the server, prefixing what the user entered with the value of a parameter called org_id.

import sys
from common.methods import set_progress


def run(job, server=None, **kwargs):
    set_progress("In Pre-Create Resource job.id={}".format(job.id))
    # Plug-in to add a prefix to the hostname of any server whose group
    # has an Organization ID parameter associated with it
    agency_id = server.org_id
    if not org_id:
        prog_msg = "Not altering the hostname as no org_id was found."
        set_progress(prog_msg)
        return "","",""
    old_hostname = server.hostname
    server.hostname = "{}{}".format(org_id, server.hostname)
    server.save()
    prog_msg = "Prefixed hostname with Organization ID. {} -> {}".format(
        old_hostname, server.hostname)
    set_progress(prog_msg)

    return "","",""

The next example demonstrates how to use multi-value parameters with a server action that updates a hypothetical “Users” parameter on the server using a “New Users” action input that will be specified when the action is run. Both the parameter and the action input should be set to allow multiple values.

from common.methods import set_progress


def run(job, server=None, **kwargs):
   users = server.users
   new_users = {{ new_users }}
   for user in new_users:
       if user not in users:
           job.set_progress("Adding {}".format(user))
           users.append(user)
   server.users = users
   return "", "", ""

The following is a more advanced example, thst should be run at Provision Server job’s Post-Provision trigger point that reads the enable_monitoring parameter and, if it is set to true, emails the CloudBolt admin to let them know. The email step could be changed to email someone else, or even to call into the monitoring system to automatically enable monitoring for the new server. This plug-in should be configured to only run when the job is successful.

"""
For each successful job, if a custom field 'enable_monitoring' is set, send an
email to the CloudBolt admin.
"""
from common.methods import set_progress
from django.conf import settings
from utilities import mail


def run(job, logger, server=None, **kwargs):
    if server.enable_monitoring == True:
        set_progress("Enable monitoring is set for this job.")

        if hasattr(settings, 'ENABLE_MONITORING_EMAIL'):
            subject = render_to_string('email/enable-monitoring-subj.template', {
                'servers': servers}).strip()
            body = render_to_string('email/enable-monitoring-body.template', {
                'owner': job.owner,
                'servers': servers})
            sender = None # use default
            recipient = getattr(settings, 'ENABLE_MONITORING_EMAIL')

            try:
                mail.send_mail(subject, body, sender, [recipient])
            except Exception as err:
                logger.exception("Aborting due to email error")
                return ("FAILURE", "Aborting due to email error. ", str(err))
            #mail utility already logs this, so just update job progress
            set_progress("Email sent to {}".format(recipient))
        else:
            set_progress("Email not sent because customer_settings.ENABLE_MONITORING_EMAIL is not set.")
    else:
        set_progress("Monitoring not enabled.")
    return "", "", ""

Special Case Plug-ins

There are a few special CloudBolt plug-ins, where they might be executed directly from the front-end appliance and outside of the context of a job executions, or they have specific return types that differ from the default CloudBolt Plug-in return tuple. Those include:

Order Approval Plug-ins

When the Order Approval plug-in is enabled, CloudBolt’s default behavior of emailing approvers when an order is submitted is replaced by the logic in the plug-in. This provides a mechanism to integrate CloudBolt with a separate change management system.

Example Order Approval Plug-in

import sys
import time
from common.methods import set_progress

def run(order, **kwargs):
    set_progress("In order approval plug-in order.id={}".format(order.id))
    # randomly approve or deny the order, replace this logic with your own
    if int(time.time()) % 2 == 0:
        order.approve()
        return ("", "Order approved", "")
    else:
        order.deny()
        # note: the plug-in was still successful, so we return an empty status,
        # even though we have rejected the order.
        return ("", "Order denied", "")

Generate Parameter Option Plug-ins

Using a CloudBolt Plug-in to generate options for a parameter is one of the most complex ways to alter the out-of-the-box CloudBolt behavior and warrants it’s own document section. See the Generated Parameter Options Documentation for details.

Auto-Select Environment Plug-ins

Using a CloudBolt Plug-in to determine the best execution venue for a given Server Tier in a Blueprint is one of many powerful features in CloudBolt. Setting up the feature and all the plug-in specific details is covered in detail in the Auto-Select Environment Documentation.

Plug-in Development Tips

Storing Credentials

If you need to use credentials in a plug-in, one way to store them securely is to use Connection Info. Go to Admin > Connection Info and create a Connection Info entry with an easily identifiable name and the information you need. Then in your plug-in you can access and make use of the Connection Info using something like the following code snippet.

connection_info = ConnectionInfo.objects.get(name='my_connection_info')
ip = connection_info.ip
username = connection_info.username
password = connection_info.password

Making requests using the Requests library

Requests is a common library for making HTTP requests in Python. When making requests to HTTPS endpoints, the SSL certificates will be verified against a list of Root Certificates provided by the Certifi library. CloudBolt also provides a way to append additional certificates to the default list, or to disable verification altogether if the CloudBolt installation requires it. Those settings are available at Admin SSL Root Certificates. CloudBolt provides a method for fetching the appropriate settings, which can be used in plug-ins that would work across CloudBolt instances.

import requests
from utilities.helpers import get_ssl_verification
reponse = requests.get('https://github.com', verify=get_ssl_verification())

Resources for Writing Plug-ins

Interacting with CloudBolt’s Models

  • CloudBolt’s auto-generated model documentation can be seen by navigating to: http://your_CloudBolt_server/alladmin/doc/models/
  • CloudBolt features an interactive shell, which is very useful for testing out commands before adding them to your CloudBolt plug-in. To open this shell:
  1. ssh to the CloudBolt server
  2. Run /opt/cloudbolt/manage.py shell_plus. This will open an interactive Python shell and import all of CloudBolt’s models for you to use
  3. From here, you can interact with CloudBolt’s models. Ex.
# Reboot the server with hostname "devtest0032"
server = Server.objects.get(hostname="devtest0032")
server.reboot()

# Re-assign all of David's servers to Lisa
lisa = UserProfile.objects.get(user__username='lisa')
server.objects.filter(owner__user__username='david').update(owner=lisa)

Danger

Using CloudBolt’s API can be destructive (to CloudBolt and potentially to the servers it manages). We highly recommend doing all plug-in development work in a dev CloudBolt with only development servers under management.

Learning Django’s ORM

CloudBolt’s models are exposed through Django’s Object Relational Model, so understanding this ORM is key to effectively querying CloudBolt.

Learning Python

Next Steps

This section covers the basics of CloudBolt plug-in authoring, but if you have any additional questions or want to discuss a more specific use case you have, we would love to hear from you. Just send us a note at support@cloudboltsoftware.com with your preferred method of contact.