(Click to open topic with navigation)
MWS provides functionality to intercept and modify data sent to and returned from web services for all available resources. This is done by creating hooks in Groovy files located in a sub-directory of the MWS_HOME directory (by default, /opt/mws/hooks).
Please see Reference in this topic for the full reference for available hooks and methods available to them.
The directory of the hooks folder may be changed by providing a value for mws.hooks.location in the configuration file. If the directory starts with a path separator (ie /path/to/hooks), it will be treated as an absolute path. Otherwise, it will be used relative to the location of the MWS home directory (for more information, see Configuring Moab Web Services).
For example, if the MWS home directory is set to /opt/mws, the hooks directory by default would be in /opt/mws/hooks. Changing the mws.hooks.location property to myhooks would result in the hooks directory being located at /opt/mws/myhooks. Due to the default location of the MWS home directory, the default directory of the hooks directory is /opt/mws/hooks.
On startup, if the hooks directory does not exist, it will be created with a simple README.txt file with instructions on how to create hooks, the objects available, and the hooks available. If the folder or file is unable to be created, a message will be printed on the log with the full location of a README file, copied into a temporary directory.
5.218.2 Defining Hooks for a Resource
Hooks are defined for resources by creating groovy class files in the hooks directory (MWS_HOME/hooks by default). Each groovy file must be named by the resource URL it is associated with and end in ".groovy". The following table shows some possible hook files that may be created. Notice that the virtual machines hook file is abbreviated as vms, just as the URL for virtual machines is /rest/vms. In most cases, the hook file names will exactly match the URLs. However, in cases of nested URLs—such as with "accounting/users"—the hook file name must replace slashes with periods. For example:
Resource | Hook filename |
---|---|
Jobs | jobs.groovy |
Nodes | nodes.groovy |
Virtual Machines | vms.groovy |
Accounting Users | accounting.users.groovy |
Accounting Funds Reports Statement | accounting.funds.reports.statement.groovy |
Accounting Charge Rates | accounting.charge-rates.groovy |
url | url.groovy |
plugins.rm.groovy is a valid hook filename. It works for the following URL: /rest/plugins/<pluginID or all>/rm/<query or action> (for example, /rest/plugins/plugin1/rm/cluster-query).
A complete example of a hook file is as follows:
Complete Hook File ------------------------------------ // Example before hook def beforeList = { // Perform actions here // Return true to allow the API call to execute normally return true } def beforeShow = { // Perform actions here // Render messages to the user with a 405 Method Not Allowed // HTTP response code renderMessages("Custom message here", 405) // Return false to stop normal execution of the API call return false } // Example after hook def afterList = { o -> if (!isSuccess()) { // Handle error here return false } // Perform actions here return o }
You must convert all actions or queries that are separated by dashes to a camel case. For example, the hooks called for "cluster-query" should be beforeClusterQuery and afterClusterQuery.
As the specific format for the hooks for before and after are different, each will be explained separately.
As shown above, before hooks require no arguments. They can directly act on several properties, objects, and methods as described in Reference. The return value is one of the most important aspects of a before hook. If it is false, a renderMessages, renderObject, renderList, render, or redirect method must first be called. This signifies that the API call should be interrupted and the render or redirect action specified within the hook is to be completed immediately.
A return value of true signifies that the API call should continue normally. Parameters, session variables, request and response variables may all be modified within a before hook.
If no return value is explicitly given, the result of the last statement in the before hook to be executed will be returned. This may cause unexpected behavior if the last statement resolves to false.
For all methods available to before hooks as well as specific examples, see beforeSave.
After hooks are always passed one argument: the object or list that is to be rendered as JSON. This may be modified as desired, but note that the object or list value is either a JSONArray or JSONObject. Therefore, it may not be accessed and modified as a typical groovy Map.
Unlike before hooks, after hooks should not call the render* methods directly. This method will automatically be called on the resulting object or list returned. The redirect and render methods should also not be called at this point. Instead, if a custom object or list is desired to be used, the serializeObject and serializeList methods are available to create suitable results to return.
The return value of an after hook may be one of two possibilities:
The return value of the after hook, if not null or false, must be the modified object passed into the hook or an object or list created with the serialize* methods.
For all methods available to after hooks as well as specific examples, see afterSave.
After hooks, unlike the before hooks, have the possibility of handling errors encountered during the course of the request. Handling errors is as simple as adding a one-line check to the hook as shown above or in the following code:
if (!isSuccess()) { // Handle error return false }
It is recommended that each after hook contain at least these lines of code to prevent confusion on what the input object or list represents or should look like.
The isSuccess() function is false if and only if the HTTP response code is 400 or higher, such as a 404 Not Found, 400 Bad Request, or 500 Internal Server Error and the cause of the error state was not in the associated before hook. In other words, objects and lists rendered in the before hook with any HTTP response code will never run the associated after hook.
When handling errors, the passed in object will always contain a messages property containing a list of strings describing the error(s) encountered.
Sometimes it is beneficial to create hooks which are executed for all calls of a certain type, such as a beforeList hook that is executed during the course of listing any resource. These are possible using an all.groovy file. The format of this file is exactly the same as other hook files. The order of execution is as follows:
This page gives specific examples and reference for implementing hooks in MWS.
Available hooks
The following table lists the available hooks for each resource with their associated HTTP method and description.
If a resource does not support a certain operation, any hooks for that operation will simply be ignored—such as beforeSave and afterSave hooks for the Node resource, where saving is not supported.
Available properties
The following table lists the properties, objects, and methods available in all hooks. Note that although it is possible to directly call the render* methods in the after hooks, it is not recommended.
Name | Type | Description |
---|---|---|
params | Map | Contains all URL parameters as well as the body of the request as parsed JSON. |
request | HttpServletRequest | Contains properties of the HTTP request. |
response | HttpServletResponse | Contains properties of the HTTP response which can be modified directly. |
session | HttpSession | Contains the session parameters which can be modified directly. |
flash | Map | Temporary storage that stores objects within the session for the next request only. |
controllerName | String | The name of the controller responding to the request. Only available in before hooks. |
actionName | String | The name of the action to be run on the controller. Only available in before hooks. |
apiVersion | String | The API version for the current request (for example, 1 for 7.0 and 7.1, 2 for 7.2). |
The parsed JSON may be accessed in before hooks as a simple groovy Map with params[controllerName].
In addition, several methods are available to the hooks. These are described in the following sections.
Redirect
The redirect method may be used to redirect the request to another API call or an arbitrary URL.
redirect(uri:'/rest/jobs') // uri is used for internal redirection within MWS redirect(url:'http://adaptivecomputing.com') // url is used for external redirection redirect(uri:'http://adaptivecomputing.com', params:[lang:'en']) // params may be used for URL parameters
The redirect method will use the GET HTTP method for the resulting redirected request.
See the redirect method's documentation for more information.
Rendering objects, lists, or messages
There are several render* methods available to handle any case where objects or lists are desired to be rendered directly from the hook without continuing to the API call. Three different methods may be used depending on the desired output object type:
Render object ------------------------------------ // Object that should be rendered as JSON def objectToRender = … // HTTP response code (bad request) def responseCode = 400 // Render a simple object renderObject(objectToRender) // Render a simple object with a custom response code renderObject(objectToRender, responseCode)
Render list ------------------------------------ // List that should be rendered as JSON def listToRender = … // If the totalCount property differs from resultCount, use this value instead def totalCount = … // HTTP response code (bad request) def responseCode = 400 // Render a simple list // Dynamically adds "resultCount" and "totalCount" properties based on the size of the input list renderList(listToRender) // Render a simple list with a custom "totalCount" renderList(listToRender, totalCount) // Render a simple list without changing the "totalCount" but with a custom response code renderList(listToRender, null, responseCode) // Render a simple list with a custom "totalCount" and response code renderList(listToRender, totalCount, responseCode)
Render message(s) ------------------------------------ // Messages def messageToRender = "Single message" def messagesListToRender = ["Message 1", "Message 2"] // HTTP response code (bad request) def responseCode = 400 // Render messages as an object with a property of "messages" containing a list of the messages passed in renderMessages(messageToRender) renderMessages(messageToRender, responseCode) // Supports either a single String or list of Strings renderMessages(messagesListToRender) renderMessages(messagesListToRender, responseCode)
It is not recommended to call any of these methods from an after hook.
Render
Less commonly used, the render method is also available directly. This may be used to render text directly, change the content-type of the output, and many other functions. See the render method's documentation for more information.
It is not recommended to call this method from an after hook.
Serialize objects
The serializeObject and serializeList methods may be used to convert a custom object or list respectively into a format usable for returning in the after hooks. Simply pass in the object or list and a serialized version will be returned from the method.
def afterShow = { def objectToRender = … def serializedObject = serializeObject(objectToRender) return serializedObject }
def afterShow = { def listToRender = [...] def serializedList = serializeList(listToRender) return serializedList }
Error handling
Error handling is only available in after hooks by using the following check:
if (!isSuccess()) { // Handle error return … // False or modified object/list to render }
Usage examples
Override an API call
The following hook would serve to override an entire API call, the list call in this case, and return a messages list containing a single element of "Action is not supported" and a HTTP response code of 405 (Method Not Allowed):
def beforeList = { renderMessages("Action is not supported", 405) return false }
To be even more specific and disallow the deletion of virtual machines, the following may be used as the vms.groovy file:
def beforeDelete = { renderMessages("Virtual Machine deletion is not allowed", 405) return false }
Add an additional property during job creation
To add an additional property to a job definition during creation, create a beforeSave hook in the jobs.groovy file as follows:
def beforeSave = { // params[controllerName] is equivalent to params["job"] or params.job params[controllerName].user = "myuser" }
This would cause the created job to have a user of myuser.
Redirect based on URL parameter
To redirect an API call if a certain URL parameter exists, create a beforeSave hook in the jobs.groovy file as follows:
def beforeSave = { if (params.external) { redirect(url:'http://example.com/create-job') return false; // Stop API call } }
This would cause an API call of PUT /rest/jobs?external=1 to redirect to GET http://example.com/create-job.
Remove a property from getting a single job
To remove a property from the output of getting a single job, create an afterShow hook in the jobs.groovy file as follows:
def afterShow = { o -> o.discard("group") return o }
This will cause the resulting JSON to be missing the group property of the job resource. Note again that these calls must use the JSONArray and JSONObject classes as mentioned in After Hooks.
Filter list items
To filter the items in a list nodes request based on user provided query parameter in the URL, use the following in the nodes.groovy file. A sample request that would activate the filter is http://localhost:8080/mws/rest/nodes?api-version=3&filter-power=On.
def afterList = { o -> // Do not filter if the user did not ask for it if (!params['filter-power']) return o // o = {resultCount: x, totalCount: x, results:[...]} // Using a built-in groovy method findAll to return all // list items that return true from the block def results = o.results.findAll { node -> // Includes the node only if the power equals the user input return params['filter-power'].equalsIgnoreCase(node.power) } // Sets the results on the return object and updates the counts o.element("results", results) o.element("resultCount", results.size()) return o }
To filter the items in a list nodes request based on values within the list itself, such as variable values, use the following in the nodes.groovy file.
def afterList = { o -> // o = {resultCount: x, totalCount: x, results:[...]} // Using a built-in groovy method findAll to return all // list items that return true from the block def results = o.results.findAll { node -> // Includes the node only if the variable "included" is set to "true" return node.variables?.included=="true" } // Sets the results on the return object and updates the counts o.element("results", results) o.element("resultCount", results.size()) return o }
Related Topics