Recently while updating an application to consume the cloudstack 4.2.x API I started to run against some issues regarding the loadbalancer stickness policy attribute validation.

On version 4.1.x and lower the API CreateLBSticknessPolicy would accept the policy attributes as raw values, however, on 4.2.x it started to complain about some validation rules.

Screen Shot 2014-02-05 at 12.20.22 PM

There are several things wrong here, lets start from the beginning:

1.
The rest api has a very rudimentary interface to deal with policy parameters, instead of defining a key for each supported parameter it forces you to create a string matching this format

param[0].name=cookiename&param[0].value=LBCookie

That’s not even a json obejct/array. It is just a plain string which creates some overhead when you are consuming the api since you need to parse the params manually.
Plus there is no way to know which attributes are valid or not, you need to dig in on the documentation or the source code to see what is accepted by the api.

2.
Leaving the api interface aside, I started to look at the UI to check which format they were sending the requests. To my surprise I got the same error I was getting when consuming the api from a third party app.
Screen Shot 2014-02-05 at 12.25.39 PM

As you can see from the picture above there is not indication of what the format should be.
Looking at their docs there is also no mention.

3.
With no choice I had to dig through their source code.
Doing a full search on the project for the string “Failed LB in validation rule id
I found two occurrences:

This is the piece that does the validation:

 
 public static boolean validateHAProxyLBRule(LoadBalancingRule rule) {  
 String timeEndChar = "dhms";

 for (LbStickinessPolicy stickinessPolicy : rule.getStickinessPolicies()) {  
 List> paramsList = stickinessPolicy  
 .getParams();

 if (StickinessMethodType.LBCookieBased.getName().equalsIgnoreCase(  
 stickinessPolicy.getMethodName())) {

 } else if (StickinessMethodType.SourceBased.getName()  
 .equalsIgnoreCase(stickinessPolicy.getMethodName())) {  
 String tablesize = "200k"; // optional  
 String expire = "30m"; // optional

 /* overwrite default values with the stick parameters */  
 for (Pair paramKV : paramsList) {  
 String key = paramKV.first();  
 String value = paramKV.second();  
 if ("tablesize".equalsIgnoreCase(key))  
 tablesize = value;  
 if ("expire".equalsIgnoreCase(key))  
 expire = value;  
 }  
 if ((expire != null)  
 && !containsOnlyNumbers(expire, timeEndChar)) {  
 throw new InvalidParameterValueException(  
 "Failed LB in validation rule id: " + rule.getId()  
 + " Cause: expire is not in timeformat: "  
 + expire);  
 }  
 if ((tablesize != null)  
 && !containsOnlyNumbers(tablesize, "kmg")) {  
 throw new InvalidParameterValueException(  
 "Failed LB in validation rule id: "  
 + rule.getId()  
 + " Cause: tablesize is not in size format: "  
 + tablesize);

 }  
 } else if (StickinessMethodType.AppCookieBased.getName()  
 .equalsIgnoreCase(stickinessPolicy.getMethodName())) {  
 /*  
 * FORMAT : appsession len timeout  
 * [request-learn] [prefix] [mode  
 * ]  
 */  
 /* example: appsession JSESSIONID len 52 timeout 3h */  
 String cookieName = null; // optional  
 String length = null; // optional  
 String holdTime = null; // optional

 for (Pair paramKV : paramsList) {  
 String key = paramKV.first();  
 String value = paramKV.second();  
 if ("cookie-name".equalsIgnoreCase(key))  
 cookieName = value;  
 if ("length".equalsIgnoreCase(key))  
 length = value;  
 if ("holdtime".equalsIgnoreCase(key))  
 holdTime = value;  
 }

 if ((length != null) && (!containsOnlyNumbers(length, null))) {  
 throw new InvalidParameterValueException(  
 "Failed LB in validation rule id: " + rule.getId()  
 + " Cause: length is not a number: "  
 + length);  
 }  
 if ((holdTime != null)  
 && (!containsOnlyNumbers(holdTime, timeEndChar) && !containsOnlyNumbers(  
 holdTime, null))) {  
 throw new InvalidParameterValueException(  
 "Failed LB in validation rule id: " + rule.getId()  
 + " Cause: holdtime is not in timeformat: "  
 + holdTime);  
 }  
 }  
 }  
 return true;  
 }  

Which is the same in both classes, only difference is the formatting.
Actually, the whole class re-implements most methods, not sure why they can’t share a helper class or extend some base class that implements the common methods.
Might be that they are treated as separated projects so there are some dependencies overhead involved.
Anyway, the validation itself is pretty straight forward for SourceBased rules:

  • table size attribute must end with k, m or g
  • expire attribute must end with d, h, m, or s.