CloudHacks.blog F5,vRealise Automation,vRealise Orcastration Part 2: Manage your F5 Pool Members via vRA

Part 2: Manage your F5 Pool Members via vRA



As a F5 Admin I generally get requests from application owners to help them to disable and enable pool members to enable them to do maintenance work without impact, and in most cases it outside of hours on a Friday night or a weekend. And frankly I have better things to do, then cancel my weekend and do a 5-minute change.

So today we are going to look at using vRA to enable application owners to enable and disable pool members on a F5 VIP. As well as show to them the status of their Nodes and add RBAC to insure someone does not accidently disable someone else’s application. Let’s get started.

Similar to Part 1, In which we used vRA and vRO to provision VIPs using HTTP-RESTful API (aka iControl) we are going to do the same. To achieve this automation, we need to create new vRO Actions:

  • List all Partitions
  • List all Pools in Partition
  • List all available pool members and status

These actions will make the form inputs which we will pass as parameters into the main workflow.

The workflow will comprise of the following sub-workflows:

  • Read Description of Pool and extract App Owner AD Group
  • Enable/Disable Pool Member
  • Sync Configuration

Actions

List all Partitions

F5 partitions are useful to help segregate applications or environments, however they make API calls a bit more complicated as you need to know which Partition your services sit in to be able to call it. Fortunately the call to get the list or partitions is simple:

 GET /tm/sys/folder

In vRO Javascript this will look like this:

var requestURI = "/tm/sys/folder"
var requestMethord = "GET";
var request = restHost.createRequest(requestMethord,requestURI);
//set the request content type
request.contentType = "";
System.log("Request: " + request);
System.log("Request URL: " + request.fullUrl);
//execute request
//Do not edit 
var response = request.execute();
//prepare output parameters
System.log("Response: " + response);
statusCode = response.statusCode;
statusCodeAttribute = statusCode;
System.log("Status code: " + statusCode);
contentLength = response.contentLength;
headers = response.getAllHeaders();
contentAsString = response.contentAsString;
System.log("Content as string: " + contentAsString);
var resJSON = JSON.parse(contentAsString);
var Paritions = resJSON.items
var partitionList = new Array ()

for each ( var item in Paritions )
{
 partitionList.push (item.name)
    System.log(item.name)
}
return partitionList

This code will output a list of all the available partitions on the F5 appliance (input as restHost). Attaching this to the F5Parition input of the workflow:

List all Pools in Partition

Using the partition from the last code we can now run a GET to list all available Pools in the partition

GET /tm/ltm/pool

In JavaScript Code, we can use this to get an array of Pools in the format of /${partition}/${pool}:

var requestURI = "/tm/ltm/pool"
var requestMethord = "GET";
//var restOpeartion = restHost.getOperation();
var request = restHost.createRequest(requestMethord,requestURI);
//set the request content type
request.contentType = "";
System.log("Request: " + request);
System.log("Request URL: " + request.fullUrl);

var response = request.execute();
//prepare output parameters
System.log("Response: " + response);
statusCode = response.statusCode;
statusCodeAttribute = statusCode;
System.log("Status code: " + statusCode);
contentLength = response.contentLength;
headers = response.getAllHeaders();
contentAsString = response.contentAsString;
System.log("Content as string: " + contentAsString);
var resJSON = JSON.parse(contentAsString);
var Pools = resJSON.items
var poolList = new Array ()
//var json = =
for each ( var item in Pools )
{
    if(item.partition == Partition){
 poolList.push (item.fullPath)
    System.log(item.fullPath)
    }
}
return poolList

We will attach this to the Pools parameter in the workflow:

Get a list Pool Members

The last action we will use to make the form is to list the Pool Members and their current status

GET /tm/ltm/pool/${PoolPath}/members

In Code, we will extract the Pool Member Name and the Status of that Pool Member

var poolPathAPI = PathName.replace(/\//g,"~")
var requestURI = "/tm/ltm/pool/"+ poolPathAPI+"/members"
var requestMethord = "GET";

var request = restHost.createRequest(requestMethord,requestURI);
//set the request content type
request.contentType = "";
System.log("Request: " + request);
System.log("Request URL: " + request.fullUrl);
//Do not edit 
var response = request.execute();
//prepare output parameters
System.log("Response: " + response);
statusCode = response.statusCode;
statusCodeAttribute = statusCode;
System.log("Status code: " + statusCode);
contentLength = response.contentLength;
headers = response.getAllHeaders();
contentAsString = response.contentAsString;
System.log("Content as string: " + contentAsString);

var resJSON = JSON.parse(contentAsString);
var PoolMembers = resJSON.items
var poolMemberList = new Array ()
for each ( var item in PoolMembers )
{
    System.log(item.fullPath)
    System.log(item.state)
    System.log(item.session)
    
    if(item.state == "up" && item.session == "monitor-enabled")
        {var poolStatus = ",Status:Up"}
    else if(item.state == "up" && item.session == "user-disabled")
        {var poolStatus = ",Status:Disabled"}
    else if(item.state == "user-down" && item.session == "user-disabled")
        {var poolStatus = ",Status:Force Offline"}
    else
        {var poolStatus = ",Status:Down"}
 poolMemberList.push (item.fullPath+poolStatus)

}
return poolMemberList

The usual output for the API Call is the below we use our JS to make the output more readable and easier for application admins to understand.

{
  "kind": "tm:ltm:pool:members:membersstate",
  "name": "10.1.1.1:443",
  "partition": "Common",
  "fullPath": "/Common/10.1.1.1:443",
  "generation": 34444,
  "selfLink": "https://localhost/mgmt/tm/ltm/pool/~Common~P_testmonty_au_deloitte.com_443_Common/members/~Common~10.1.1.1:443?ver=14.1.2.6",
  "address": "10.1.1.1",
  "connectionLimit": 0,
  "dynamicRatio": 1,
  "ephemeral": "false",
  "fqdn":{"autopopulate": "disabled"},
  "inheritProfile": "enabled",
  "logging": "disabled",
  "monitor": "default",
  "priorityGroup": 0,
  "rateLimit": "disabled",
  "ratio": 1,
  "session": "monitor-enabled",
  "state": "down"
}

We will assign this as a list for the pool member in the form:

This being a list of Array’s and also an Array input we will make this as a checkbox input to allow the users to select multiple nodes they want to manage.

Workflow

Now we got the input for the workflow, lets start working on the main thing, the workflow itself.

Validating Permissions

Because the API is all run via a central automation account trying to setup RBAC is difficult and something we need to design, to do this we are going to AD Groups and the Description field to specify the AD Group to have permissions to manage the Pool. In Part 1, when we were creating the VIPs and Pools we were inputting an XML Metadata into the Pool’s Description, part of this description is an AD Group name:

So the first thing we are going to extract is the Pool Owner AD Group and validate the requestor is in the AD Group. Here we will extract this Owner field of the Description and check it against AD for Members, and then validate if the requestor in vRA is a member of the AD Group.

To pull the AD Group is relativity easy using the same GET /tm/ltm/pool/${PoolPath}/ we can pull the Description and Parse it through an XML Reader and extract the <Owner> tag data.

var poolPathAPI = PathName.replace(/\//g,"~")
var requestURI = "/tm/ltm/pool/"+ poolPathAPI
var requestMethord = "GET";
var request = restHost.createRequest(requestMethord,requestURI);
//set the request content type
request.contentType = "";
System.log("Request: " + request);
System.log("Request URL: " + request.fullUrl);
var response = request.execute();

System.log("Response: " + response);
statusCode = response.statusCode;
statusCodeAttribute = statusCode;
System.log("Status code: " + statusCode);
contentLength = response.contentLength;
headers = response.getAllHeaders();
contentAsString = response.contentAsString;
System.log("Content as string: " + contentAsString);

var resJSON = JSON.parse(contentAsString);
var PoolDescription = resJSON.description;
    
    if (PoolDescription != null){
            if(PoolDescription.indexOf("<PoolDescription>")!== -1) {
                var document = new XML(PoolDescription)
                adminGroupOwner = document.Owner.toString();
            } else {
                adminGroupOwner = "AU_SEC_F5_Admin"
            }
        }else{
        	adminGroupOwner = "AU_SEC_F5_Admin"
        }
System.log(adminGroupOwner)
return adminGroupOwner

We now validate the AD Group members and insure the requestor field (which takes the UPN of the vRA user logged in) and check if they are a member. If they are then the workflow will go ahead to disable/enable the node pools if they are no then it will fail with access denied.

Disable the Pool Member.

To disable the pool member we run a POST to the Pool Member object changing the state and session status.

var content
//Define the Disable String
if (ForceOffline == true) {
    var contentString = '{';
    contentString += '"session":"user-disabled",'
    contentString += '"state":"user-down"';
    contentString += '}';
} else {
    var contentString = '{';
    contentString += '"session":"user-disabled"'
    contentString += '}';
}

var poolPathAPI = F5Pool.replace(/\//g, "~")
var poolMemberPathAPI
var requestURI
var requestMethord = "PUT";
var request
var response

for each(Member in F5PoolMember){
    MemberSplit = Member.split(",");
    poolMemberPathAPI = MemberSplit[0].replace(/\//g, "~")
poolMemberPathAPI = poolMemberPathAPI.replace(/%/g, "%25")

requestURI = "/tm/ltm/pool/" + poolPathAPI + "/members/" + poolMemberPathAPI //Define URL to block access

request = F5Instance.createRequest(requestMethord, requestURI, contentString);

    request.contentType = "application/json";
    System.log("Request: " + request);
    System.log("Request URL: " + request.fullUrl);

    response = request.execute();
    //prepare output parameters
    System.log("Response: " + response);
    statusCode = response.statusCode;
    statusCodeAttribute = statusCode;
    System.log("Status code: " + statusCode);
    contentLength = response.contentLength;
    headers = response.getAllHeaders();
    contentAsString = response.contentAsString;
    System.log("Content as string: " + contentAsString);
}

Using the above command, we can change the state to either Disabled or Force-Offline based on what they want. Similarly, to re-enable we would run.

var contentString = '{';
contentString += '"session":"user-enabled",'
contentString += '"state":"user-up"';
contentString += '}';

Running a Config Sync

Last but not least is syncing up the config between the HA devices.

var requestURI = "/tm/cm/device-group"
var requestMethord = "GET";
var request = restHost.createRequest(requestMethord, requestURI);
//set the request content type
request.contentType = "";
System.log("Request: " + request);
System.log("Request URL: " + request.fullUrl);

var response = request.execute();
//prepare output parameters
System.log("Response: " + response);
statusCode = response.statusCode;
statusCodeAttribute = statusCode;
System.log("Status code: " + statusCode);
contentLength = response.contentLength;
headers = response.getAllHeaders();
contentAsString = response.contentAsString;
System.log("Content as string: " + contentAsString);
var resJSON = JSON.parse(contentAsString);
var deviceGroups = resJSON.items
var SyncGroupsList = new Array()
for each( var item in deviceGroups )
{
    if (item.type == "sync-failover") {
        SyncGroupsList.push(item.name)
        System.log(item.name)
    }
}

var contentString = '{';
contentString += '"command":"run"'
contentString += '}';

for each(item in SyncGroupsList)
{
        requestURI = "/tm/cm/config-sync?options=to-group%20" + item
        requestMethord = "POST";
        request = restHost.createRequest(requestMethord, requestURI, contentString);
        request.contentType = "application/json";
        var response = request.execute();
        System.log("Response: " + response);
        statusCode = response.statusCode;
        statusCodeAttribute = statusCode;
        System.log("Status code: " + statusCode);
        contentLength = response.contentLength;
        headers = response.getAllHeaders();
        contentAsString = response.contentAsString;
        System.log("Content as string: " + contentAsString);

}

And there we have a workflow to manage our pool members and a Friday night I do not need to work 😊.