Send vRA Deployment Lease Expiration Mails [CB10109]

  1. Steps
  2. Final Script

A simple use-case where we want to notify the vRA users about the deployments lease expiration over the email via vRO script. Use this script and modify accordingly. Let me quickly explain what I am doing here.

Steps

  • Get all the deployments by hitting the vRA Deployments API (/deployment/api/deployments) available at https://{{vRA_FQDN}}/automation-ui/api-docs
// typeof(host) == vRA:Host
var restClient = host.createRestClient();
//Assuming there are less than 10000 deployments in your environment
var request = restClient.createRequest("GET", "/deployment/api/deployments?$limit=10000", null);
request.setHeader("Content-Type", "application/json");
var response = restClient.execute(request);

System.log("****Status Code****");
var statusCode = response.statusCode;
System.log("Status code: " + statusCode);

System.log("****Content As String****");
var contentAsString = response.contentAsString;
System.log("Content as string: " + contentAsString);

Note Make sure to deal with pagination in REST API if you have a large number of deployments. Find the pagination code in final script.


We will get response something like,

{
   "content":[
      {
         "id":"xyz123-abcd-efgh-9ijk-lmnop123rst",
         "name":"new_deploy01-2023-03-1T21:13:55.799Z",
         "orgId":"xyz123-abcd-efgh-9ijk-lmnop123rst",
         "catalogItemId":"xyz123-abcd-efgh-9ijk-lmnop123rst",
         "catalogItemVersion":"1",
         "blueprintId":"xyz123-abcd-efgh-9ijk-lmnop123rst",
         "blueprintVersion":"1",
         "iconId":"xyz123-abcd-efgh-9ijk-lmnop123rst",
         "createdAt":"2023-03-1T21:14:11.773990Z",
         "createdBy":"mayank.goyal",
         "ownedBy":"mayank.goyal",
         "lastUpdatedAt":"2023-03-1T21:23:42.518212Z",
         "lastUpdatedBy":"new_deploy01",
         "leaseExpireAt":"2023-05-19T21:23:00Z",
         "inputs":{
            "name":"vm_sql01",
            "vm-size":"Small (1vCPU|2Gb)",
            "os-image":"Win2016",
            "leaseDays":60,
            "textField_b8ad97ef":"2023-03-1T21:13:55.799Z"
         },
         "projectId":"xyz123-abcd-efgh-9ijk-lmnop123rst",
         "status":"CREATE_SUCCESSFUL"
      },
.
.
.
.
  ]
}
  • Parse the response JSON to get properties like lease expiration date, deployment owner & deployment name.
.
.
.
for (var i = 0; i < contentAsString.content.length; i++){
    System.log("//------ Deployment "+contentAsString.content[i].inputs.name+" ------//");
    System.log("Lease expiring at "+contentAsString.content[i].leaseExpireAt);
.
.
var toAddress = contentAsString.content[i].ownedBy + "@email.domain";
var emailBody = "Deployment "+ contentAsString.content[i].inputs.name +" is about to expire";
.
.
.
  • Find out how many days remaining for the deployment lease to expire. Using some date and time operations, we are doing the calculation of days difference between today and leaseDate.
//2023-05-19T21:23:00Z -> vRO doesn't understand this format. So, we are converting it.
var dateString = contentAsString.content[i].leaseExpireAt;
var dateFromUI = dateString.split("Z")[0].split("T")[0];
var timeFromUI = dateString.split("Z")[0].split("T")[1];
var dateParts = dateFromUI.split("-");
var timeParts = timeFromUI.split(":");
var date = new Date(dateParts[0], dateParts[1]-1, dateParts[2], timeParts[0], timeParts[1], timeParts[2]);
System.warn("Expiration Date: "+date);
var todayDate = new Date();
System.warn("Present Date: "+todayDate);
System.log("How many days remaining for lease to expire? "+ getDaysDiffBetweenDates(todayDate, date));
if((getDaysDiffBetweenDates(todayDate, date) == 14) || (getDaysDiffBetweenDates(todayDate, date) == 7) || (getDaysDiffBetweenDates(todayDate, date) == 1)){
var toAddress = contentAsString.content[i].ownedBy + "@email.domain";
var emailBody = "Deployment "+ contentAsString.content[i].inputs.name +" is about to expire in "+getDaysDiffBetweenDates(todayDate, date)+" days. Take action now!";

function getDaysDiffBetweenDates(initialDate, finalDate){
    return Math.floor((finalDate - initialDate) / (1000 * 3600 * 24));
}
  • And then we have to send notification mails to the users at different time interval, lets say at 14 days , 7 days and 1 day before expiration.
if((getDaysDiffBetweenDates(todayDate, date) == 14) || (getDaysDiffBetweenDates(todayDate, date) == 7) || (getDaysDiffBetweenDates(todayDate, date) == 1)){
        var toAddress = contentAsString.content[i].ownedBy + "@email.domain";
        var emailBody = "Deployment "+ contentAsString.content[i].inputs.name +" is about to expire in "+getDaysDiffBetweenDates(todayDate, date)+" days. Take action now!";
        sendMail(toAddress,emailBody);

function sendMail(toAddress,emailBody){
    var message = new EmailMessage();
    var smtpHost = "[INPUT]your SMTP server fqdn/relay address";
    message.smtpHost = smtpHost;
    message.subject = "Take Action now! Deployment Expiring Soon";
    message.toAddress = toAddress;
    message.fromAddress = "[INPUT] from address";
    message.addMimePart(emailBody, "text/html; charset=UTF-8");
    message.sendMessage();    
    System.log("Mail sent to user "+toAddress);
}

Final Script

Create vRO Action with an input host of type vRA:Host and edit values like smtp server address, email domain, fromAddress.

// Input typeof(host) == vRA:Host

var restClient = host.createRestClient();
var items = [];
var path = "/deployment/api/deployments"; 
var page = 0; 
var page_size = 200; 
var base_path = path + "?$top=" + page_size; 
while (true) {    
    var skipFilter = page * page_size; 
    System.log(base_path + "&$skip=" + skipFilter);
    var request = restClient.createRequest("GET", base_path + "&$skip=" + skipFilter, null);
    request.setHeader("Content-Type", "application/json");
    var response = restClient.execute(request);
    var statusCode = response.statusCode;
    System.log("Status code: " + statusCode);
    var contentAsString = response.contentAsString;
    contentAsString = JSON.parse(contentAsString);

    for (var i = 0; i< contentAsString.content.length; i++)
        items.push(contentAsString.content[i]); 
    page++; 
    if (page >= contentAsString.totalPages) break  
};


var completedDeploymentCount = 0;
System.log("Total number of records fetched: "+ items.length);

for (var i = 0; i < items.length; i++) {
    if (items[i].inputs.name &&items[i].status != "CREATE_INPROGRESS" && items[i].status != "CREATE_FAILED"  && items[i].status != "UPDATE_FAILED" && items[i].status != "UPDATE_INPROGRESS") {
        completedDeploymentCount++; 
        System.log("*** Deployment #" + completedDeploymentCount + " " + items[i].inputs.name + " ***");
        if (items[i].leaseExpireAt) { 
            var dateString = items[i].leaseExpireAt; 
            var dateFromUI = dateString.split("Z")[0].split("T")[0];
            var timeFromUI = dateString.split("Z")[0].split("T")[1];
            var dateParts = dateFromUI.split("-");
            var timeParts = timeFromUI.split(":");
            var date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2], timeParts[0], timeParts[1], timeParts[2]);
            System.log("Lease expiring on " + date);
            var todayDate = new Date();
            System.log("How many days remaining for the lease to expire? " + getDaysDiffBetweenDates(todayDate, date));  
            if ((getDaysDiffBetweenDates(todayDate, date) == 30) || (getDaysDiffBetweenDates(todayDate, date) == 14) || (getDaysDiffBetweenDates(todayDate, date) == 7) || (getDaysDiffBetweenDates(todayDate, date) == 1)) {
              if(items[i].ownedBy){  
                var toAddress =items[i].ownedBy + "@domain.com";
                var emailBody = "Deployment " + items[i].inputs.name + "'s lease is about to expire in " + getDaysDiffBetweenDates(todayDate, date) + " days.";
                sendMail(toAddress, emailBody);
              } else {
                 System.error("\"ownedBy\" attribute not found in response JSON for deployment: " + items[i].inputs.name + ". Cannot send mail!"); 
              }
            }
        } else {
            System.error("\"leaseExpireAt\" attribute not found in response JSON for deployment: " + items[i].inputs.name +". Skipping...");
        }
    }
}

function sendMail(toAddress, emailBody) {
    var message = new EmailMessage();
    var smtpHost = "x";
    message.smtpHost = smtpHost;
    message.subject = "Take Action Now!";
    message.toAddress = toAddress;
    message.fromAddress = "fromAddress@domain.com";
    message.addMimePart(emailBody, "text/html; charset=UTF-8");
    message.sendMessage();
    System.log("Mail sent to user " + toAddress);
}

function getDaysDiffBetweenDates(initialDate, finalDate) {
    return Math.ceil((finalDate - initialDate) / (1000 * 3600 * 24));
}

Over 100+ people have signed up.
Join the COMMUNITY.

Enter your mail to get the latest to your inbox, delivered weekly.

Advertisement

Upload a vRO package using vRO Node.js action [CB10108]

  1. Thought Process
  2. Steps
  3. The JavaScript Code that didn’t work
    1. Choice 1 Using Simple REST operations
    2. Choice 2 Generating Multi-Part Content line by line

Thought Process

Recently, I had a requirement to upload a vRO package from one vRO to other. I was very sure that vRO offers the REST APIs to do so and it just the matter of writing a simple JavaScript code to fetch the package, save it in local file system and upload it to other vRO. Clearly, I was not thinking straight. I immediately realized that vRO doesn’t work really well with Multi-Part MIME Type which is what the /packages/ API is using.

I was struggling through it when I found a tweet by Joerg Lew from 2017 where he is uploading a workflow using same Multi-Part MIME type. From what I see in the replies, that code worked. Now I had to make it work with a package. After many attempts and modifications, I failed badly. You can check what I did here.

That was a lot of efforts and I dropped that idea. But I started wondering maybe I can find my answer in Node.js or Python and of course there it is. One thing constantly biting me was whether Polyglot Scripts can access the local filesystem just as JavaScript and clearly, they don’t. Another setback.

In my last attempt, I created a node.js script which was working fine on my local machine but I had to make it work inside vRO. Then, I thought what if I use vRO Polyglot bundles as a local filesystem. It kinda worked for me. However, I had to do a trade-off. The filesystem inside these bundles are read-only. Updating anything from inside would not possible but I can read anything I want. That’s exactly what I did. I created a package in which I kept everything I need on my remote vRO, put it inside the bundle, and uploaded it to vRO. BINGO! After many trails, I managed to somehow fulfill my original requirement which was to upload a package to vRO from vRO.

Steps

  • Here is the Nodejs script I used in the bundle. Download the bundle “uploadPackageToVro.zip” from here.
/**
 * @description Uploads a .package file to vRO
 *
 * @param {string} vroFqdn 
 * @param {string} username 
 * @param {string} password 
 * @param {string} packageNameWithExtension packagename + ".package"
 *
 * @outputType void
 *
 */
exports.handler = (context, inputs, callback) => {
	const process = require('process');
	const request = require('request');
	const fs = require('fs');
	process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0; //throws a warning in vRO log, ignore it.
	console.log('Inputs were ' + JSON.stringify(inputs));
	var options = {
	  'method': 'POST',
	  'url': 'https://'+ inputs.vroFqdn +'/vco/api/packages',
	  'auth': {
			'user': inputs.username,
			'password': inputs.password
		},
	  'headers': {
                // Uncomment this and comment out the auth attribute above
                //'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', // for vRO Basic Authentication
                //'Authorization: 'Bearer AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F', // for vRA Access Token Authentication
		'Accept': 'application/xml'
	  },
	  formData: {
		'file': {
		  'value': fs.createReadStream(inputs.packageNameWithExtension),
		  'options': {
			'filename': inputs.packageNameWithExtension,
			'contentType': null
		  }
		}
	  }
	};
	request(options, function (error, response) {
	  if (error) throw new Error(error);
	  console.log(response.body);
	});
	callback(undefined, {status: "done"});
}
  • Extract the zip and replace the package “com.mayank.emptyPackage” with your custom package and rezip it either using 7-zip or Windows Compressor.
  • Create a new action in vRO, Select the Runtime, Type, Entry handler as shown below and Click Import to import the bundle.
  • Add Inputs as shown here as well as set the memory limits and timeout accordingly.
  • Run the action and provide input values. packageName should match the package that you have bundled in the zip.
  • Once the action is completed, you should see the package in other vRO.

The JavaScript Code that didn’t work

Below are my initial efforts where I spent a lot of time but didn’t work for me. I am still hopeful though that this code can be improved and should work.

Choice 1 Using Simple REST operations

var basicvROUsername = "username@domain";
var basicvROPassword = "pa$$w0rd!";
var vROUrl = "https://"+getvROName()+"/vco/api"; 
var packageName = "com.mayank.emptyPackage";

//Save package to a file in local filesystem
var vroHost = RESTHostManager.createHost("vROHost");
vroHost.url = vROUrl;
var host = RESTHostManager.createTransientHostFrom(vroHost);
host.authentication = RESTAuthenticationManager.createAuthentication('Basic', ['Shared Session', basicvROUsername, basicvROPassword]);
//System.log(host.authentication);
var url = "/content/packages/" + packageName + "?allowedOperations=vef&exportConfigSecureStringAttributeValues=false&exportConfigurationAttributeValues=true&exportExtensionData=false&exportGlobalTags=true&exportVersionHistory=true";
// vco/api/content/packages/com.mayank.emptyPackage
//System.debug(url);
var request = host.createRequest("GET", url, null);
request.setHeader("Accept-Encoding","gzip, deflate")
request.setHeader("Accept", "application/zip")
request.setHeader("Content-Type", "application/zip;charset=utf-8")
var response = request.execute();
//System.log(response);

if (response.statusCode != 200) { // 200 - HTTP status OK
    System.warn("Request failed with status code " + response.statusCode);
    if(response.statusCode == 401)
        System.warn("User is not authorized. Cannot Proceed!");
} else {
    System.log(response.contentAsString);
    System.log(" >>> Time to save the content as a file");
    var fileDir = System.getTempDirectory();
    var filePath  = fileDir + "/"+packageName +".package";
    System.log(filePath);
    var writer = new FileWriter(filePath);
    writer.open();
    writer.write(response.contentAsString);
    writer.close();

}

//Upload .package file to vRO REST Host
var vroHost2 = RESTHostManager.createHost("vROHost2");
vroHost2.url = vROUrl;
var host2 = RESTHostManager.createTransientHostFrom(vroHost2);
host2.authentication = RESTAuthenticationManager.createAuthentication('Basic', ['Shared Session', basicvROUsername, basicvROPassword]);
var url = "/content/packages";
var myFileReader = new FileReader(filePath);
myFileReader.open();
var fileContent = myFileReader.readAll(); 
var request = host2.createRequest("POST", url, fileContent);
request.setHeader("Accept", "application/json");
request.setHeader("Content-Type", "multipart/form-data");
var response = request.execute();

System.log(response.contentAsString);
if (response.statusCode != 200) { // 200 - HTTP status OK
    System.warn("Request failed with status code " + response.statusCode);
    if(response.statusCode == 401)
        System.warn("User is not authorized. Cannot Proceed!");
} else {
 System.log("Check if package uploaded");
}

function getvROName(){ //works on 8.x
    var com = new Command(["printenv", "|", "fgrep", "JVM_OPTS"]);
    var res = com.execute(true);
    var output = com.output;
    var nodeName = output.match(/-Dvco.app.hostname=([^\s]+)/)[1];
    return nodeName;
};

Choice 2 Generating Multi-Part Content line by line

I used Postman code generator to get WebKit strings.

var boundary = System.nextUUID();
var crlf = "\r\n";
var wf_zip_path = filePath;
var packageName = "com.mayank.emptyPackage";
System.log(filePath);
System.log(rest_Host);

//Generate multipart content
// var content = "";
// content += "--" + boundary + crlf;
// content += "Content-Disposition: form-data; name=\"file\"; filename=\"com.mayank.emptyPackage\"" + crlf;
// content += "Content-Type: application/zip" + crlf + crlf;
var fr = new FileReader(wf_zip_path);
fr.open();
//content += fr.readAll() + crlf;
// content += "--" + boundary + crlf;
// // content += "Content-Disposition: form-data; name=\"categoryId\"" + crlf + crlf;
// // content += System.getObjectId(wf_folder) + crlf;
// // content += "--" + boundary + crlf;
// content += "Content-Disposition: form-data; name=\"overwrite\"" + crlf + crlf;
// content += true + crlf;
// content += "--" + boundary + "--" + crlf;
content = "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name=\"file\"; filename=\"com.mayank.emptyPackage.package\"\r\nContent-Type: \"application/octet-stream\"\r\n\r\n" + fr.readAll() + "\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--";

var req = rest_Host.createRequest("POST", "content/packages", content);
req.contentType = "multipart/form-data; boundary=" + "----WebKitFormBoundary7MA4YWxkTrZu0gW";
req.setHeader("Accept", "application/xml");

var reponse = req.execute();

System.log("URL: " + req.fullUrl + " -> " + reponse.statusCode);
if (reponse.statusCode != 202) {
    System.warn("Error during workflow import.");
    System.log("---- Request ----");
    System.log("URL: " + req.fullUrl);
    System.log("Content:\r\n" + content);
    System.log("");
    System.log("");
    System.log("---Response---");
    System.log("Status Code: " + reponse.statusCode);
    System.log("Content: " + reponse.contentAsString);
    var headers = reponse.getAllHeaders();
    // for each(var k in headers.keys) {
    //     System.log("Header [" + k + "]:" + headers.get(k));
    // }
    throw "Error during workflow import (HTTP Code: " + reponse.statusCode + ")";
}

That’s all in this post. Hope you like it. See you on other posts. Thanks a lot.

Basics of REST

So what is REST? At a high level REST, or REpresentational State Transfer, is an architectural style for distributed hypermedia ystems. It was created from a combination of other architectural styles and enlists several constraints. Roy Fielding, its creator, said that “REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.”

This guide will cover:

  • REST HTTP requests
  • REST HTTP responses
  • Constraints needed for an API to be RESTful

Requests

The client has to make a request to the server to get or modify data on the server. A request contains the following:

  • HTTP verb
  • headers
  • path: to a resource
  • [optional] message body: data

Consider the example of a todo list. An example request could look like this:

GET /todos
Accept: application/json

HTTP Methods/Verbs

HTTP methods, or verbs, define what kind of operation to perform on a resource. Some of the primary ones used with RESTful APIs are:

  • GET: get a resource or collection of resources
  • POST: create a new resource
  • PUT: update a resource
  • PATCH: partially modify a resource
  • DELETE: delete a resource

HTTP Headers

The client uses headers to pass along request information to the server. Requests can have an Accept request HTTP header which specifies what content type the client can accept from the server. The values are of the media, or MIME type. The simplest MIME types are of the format type/subtype. For example, this could be text/html which is for a text file containing HTML. Or application/json for a JSON file. To see a list of common Accept values check out the MDN web docs.

Path

Requests need to contain a path to the resource it is trying to perform the HTTP request on. In the above example, that is /todos. This is the resource that you are looking to read from or write to.

One important thing to note is the difference between addressing a collection and an individual item in that collection. The path /todos would be addressing all of the items on the todo list. A POST to this path could create a new item on that list, and a request to GET /todos would return all items. On the other hand, GET /todos/2 would return just the second item on the todo list.

Response

After the client sends a request to the server, the server sends a response back to the client. This response consists of a: status code headers message body: data

For the previous request:

GET /todos
Accept: application/json

The response could contain the following for a successful request:

HTTP/1.1 200 OK
Content-Type: application/json
[
    {
        "name": "pay rent",
        "due": 1589031653,
        "completed": false
    },
    {
        "name": "get groceries",
        "due": 1588869295,
        "completed": true
    }
]

HTTP Headers

The responses have a Content-Type entity header which specifies the MIME type of the resource. Its value should match one of the Accept types sent by the client. In the above example, both the Content-Type and Accept header values are application/json.

Headers can contain information on a wide array of topics including how to handle the TCP connection, authentication, caching, and more. Some REST APIs may have headers specific to them, but there’s also some headers that have a universal definition.

Response Status Code

One of the return values in a response is a response status code. Each code has a specific designation; for example, 200 is OK and means the request was successful. The response code categories are as follows:

  • 1XX: Informational
  • 2XX: Successful
  • 3XX: Redirects
  • 4XX: Client Errors
  • 5XX: Server Errors

To see the detailed list of response codes and their meaning, check out the MDN web docs.

Body

The body contains the data the client requested in the MIME type specified in the Content-Type header. In the example above, the body of the response is:

[
    {
        "name": "pay rent",
        "due": 1589031653,
        "completed": false
    },
    {
        "name": "get groceries",
        "due": 1588869295,
        "completed": true
    }
]

Constraints

In order to create RESTful APIs, they need to adhere to six style constraints:

  1. Client-Server Separation
  2. Stateless
  3. Cache
  4. Uniform Interface
  5. Layered System
  6. [Optional] Code-On-Demand

Client-Server

First, there needs to be a separation of client and server. This means that the client and server each need to work independent of each other. Their only form of interaction is when a client makes requests and a server sends a response whenever it receives a request. One of the benefits is that the client and server can each be updated or modified independently without affecting the other.

For example, if you make a call to a restaurant to place a takeout order, the only interaction is you asking for a specific item and them responding with an ok or saying they don’t have it.

Stateless

Next, the client and server communication needs to be stateless: the server and client don’t need to know the state of the other. The server doesn’t store state from the client and therefore the client can’t depend on it. Therefore the client needs to send everything the server would need to process a request every time and any storing needs to happen client-side.

To continue with the analogy, when you make your call, you don’t need to know what the restaurant has in stock, and they don’t need to know what you want until you order. Additionally, you’re responsible for keeping track of what you like to order, not the restaurant.

Cache

Cache constraints require that the response be labeled as cacheable or non-cacheable. If it’s cacheable, then the client can choose to use this data for future requests that are equivalent. For instance, if the data is valid for a set time, the cached data will be used instead of making a request to the server.

For your takeout call, the restaurant may tell you that a specific item is not available until a certain date. You can then remember to not order it again if you make another request before that date.

Uniform Interface

As mentioned, the client and server are independent of each other, meaning they evolve and change independently. For this reason, it’s imperative that the interface between the client and server expect some commonality in their communication. This constraint can actually be broken down even further:

  1. Resource-Based: This is two-fold: First, individual resources should be identifiable in the request. Often this is done in the path of the URI of the request. For example, /todos/2 would directly address the todo item with the ID of 2. Second, the presentation of the resource does not necessarily need to match the internal representation by the server. The todo item may be returned as JSON, but more realistically the server is storing this in a database in another format.
  2. Manipulate Resources Through Representations: When the client receives a resource, the representation of that resource contains enough information to update or delete it. This could be seen as the flip-side of the “stateless” constraint. Using the same example of a todo list, if a client requests all items in a todo list, each of those items would likely include an ID so that it could be individually addressed.
  3. Self-Descriptive Messages: Each message or resource should include enough information so that the client knows how to process that message. For example, if a client has requested a resource that’s returned as JSON, the response should also include a Content-Type header with the value application/json.
  4. Hypermedia as the Engine of Application Sate: AKA “HATEOAS”. This could be a whole conversation on it’s own, and it’s encouraged to read on this topic as well, but in short each response should include related links that the client can use to discover other actions and resources. Continuing the ToDo example, requesting an individual todo item may include links to the list that it is a part of.

Layered System

Layered system constraints are used to scope hierarchical layers based on behavior and have each layer be unable to have visibility past the layer it is interacting with. That’s to say, a client may send a request to a server, which in turn may send a request to a data service, which sends a request to an authentication service. All of this is invisible to the client and the client can not and should not distinguish between a direct request to the data or one that has multiple requests server-side. This is also true for infrastructure and operational components such as proxies and load balancers. Introducing these components to the server architecture should require no updates from the client.

[Optional] Code-On-Demand

This constraint states that a server can extend the functionality of a client by providing it executable code. Probably the most common example of this is client-side scripting with JavaScript, but this can take many forms. While this keeps clients simpler and smaller, it also reduces visibility of features and can introduce ambiguity. Because of this, while the absence of other constraints may mean a service isn’t actually RESTful, this constraint is optional.

Conclusion

Well, this is the end of this coverage of the basics of REST. Reading Roy Fielding’s REST dissertation is a great place to start if you haven’t already.