Ansible Tower REST API Part 2

This is the second in a two part blog on the basics of using the REST API with Ansible Tower to configure the Cisco ACI APIC. You can find part one here. In this second part we will look at the javascript code to login to Tower and the code to submit a job to Tower. We will briefly use the Tower REST API HTML interface to gather some information we need to submit the job.
 

Logging Into Tower

We login to Tower using the following function which requires that global variables ‘username’ and ‘pswd’ have the users Ansible Tower credentials assigned. The code uses the JavaScript XMLHTTPRequest method to pass the username and password as data in a JSON format to the REST API URL of Tower (https://hostname/api/v1/authtoken/). We are using the POST method as we are providing the credentials via the body of the request and by sending this to the URI ‘/api/v1/authtoken/’ we are expecting to get a token in return for providing valid credentials. We save this token to a global variable to reuse when we submit requests instead of having to perform the login again. The function for the Tower login is shown below.


function loginTower()
{
    var bodyJSON = '{"username": "' + username + '", "password": "' + pswd + '"}';
    var xhr = new XMLHttpRequest();
    var defaultPostFunc = 
        function () 
        {
            if (this.readyState === 4) 
            {
                if(this.status != 200 )                                              
                {
                    console.log("**** login failed **** status=", this.status);
                    if(this.status > 399 && this.status < 500)
                    {
                        loginFailedRetries+=1;
                    }
                    return;
                }

                if(this.status == 200)  
                {
                    respJson = JSON.parse(this.responseText) ;

                    if(respJson.hasOwnProperty('token'))
                    {
                        loginToken = respJson.token;
                        loginFailedRetries = 0;
                        return;
                    }
                    return;
                }
            }
         };

    
    xhr.onerror = function(e){
        appMsg.postMessage("Unknown Login Error Occured. Server response not received. [" + e + "]", "error");
        console.log("xhr.onerror: ", e);
	};     

    xhr.addEventListener("readystatechange",  defaultPostFunc);
    xhr.open("POST", "https://" + location.hostname + "/api/v1/authtoken/", true);
	xhr.setRequestHeader("Content-type", "application/json");
	xhr.send(bodyJSON);
}

 

Submitting Jobs to Tower

The following code submits a job request to Tower. A job template has an numeric ID associated with it, we use this ID to identify the template we want to use to run a job. We need to submit this ID and any required parameters (variables) that the playbook requires. There are two functions below, the main one being the ‘sendRequest’ function which creates and sends the POST request to the Tower REST API interface. The function requires two inputs, first the job ID and second the required parameters for the playbook in a dictionary format. This dictionary is converted to a string and a new dictionary created with a key of extra_vars and the value being the string we created from the passed dictionary parameter ‘vars’. This is how Tower requires the parameters to to formatted. Finally we convert the last dictionary to a string to be sent to the Tower REST API.

/*
* vars should be in the dict format with values as strings as follows {  name : str_value, name : str_value }
*/
function sendRequest(templateid, vars)
{
    var data = {};
    // create string from dict 'vars', the assign then assign the string to a dict with key 'extra_vars'
    data["extra_vars"] = JSON.stringify(vars);
    // create a string to POST from the dict 'data'
    strData = JSON.stringify(data);
  
    var xhr = new XMLHttpRequest();
    var defaultPostFunc = 
        function () 
        {
            if (this.readyState === 4) 
            {
                if(this.status > 400 && this.status < 500)
                {
                    if(this.status === 401){
                        loginTower();}
                    return;
                }

                if((this.status < 200) && (this.status > 299) )                                              
                {
                    return;
                }

                if((this.status > 200) && (this.status < 300) )  
                {
                    respJson = JSON.parse(this.responseText) ;
                    if(respJson.hasOwnProperty('id'))
                    {
                        submittedJobList.push(respJson.id);
                        return;
                    }
                    return;
                }
            }
        };
    
    xhr.onerror = function(e){ console.log("xhr.onerror: ", e);	}; 
    xhr.addEventListener("readystatechange",  defaultPostFunc);
    xhr.open("POST", "https://" + location.hostname + "/api/v1/job_templates/" + templateid + "/launch/", true);
    xhr.setRequestHeader("Content-type", "application/json");
    xhr.setRequestHeader("Authorization", "Token " + loginToken) ;
    xhr.send(strData);
}

We need to know what the ID is of the job template we want to use, to this this we use the Tower REST API as its not possible from the Tower GUI to get this. We can use the REST API directly on Tower, if we open the URI ‘/api/’ on the Tower instance, so for example ‘https://192.168.1.1/api’ will take us to a page which provides links to available API versions, select the latest version, in this case we end up at ‘https://192.168.1.1/api/v1/’. This page will provide a number of links to all parts of the available API. You will find one called “job_templates”, click this link which will take you to ‘https://192.168.1.1/api/v1/job_templates/’. You will be prompted to provide credentials at this point, any user account with access to Tower can be authenticated as this is just using the REST API as you would via the GUI or via the code accessing the REST API on the custom html/javascript pages.
We can shorten this process, you could just enter the job_templates URI or you can use the URL
‘https://192.168.242.130/api/v1/job_templates/?name=New_Customer_L2_Setup’ where this asks the REST API on Tower to return the job template that has the name “New_Customer_L2_Setup”, once the data is returned you are able to grab the ID from the “id” attribute. The following screenshot shows a portion of the screen and data returned for a job template for the job template we created in the first part of this blog (‘New_Customer_L2_Setup’) and shows the job template ID is 15. If the name you gave for the job template has spaces in you will need to replace these with %20 in the URL.

Putting it all together

To make all this useful, I have created an interface using the metroui framework and using tiles for each job template which when selected prompt for the required parameters before the job request is submitted to Tower. The following is a screenshot of the main screen. It is coded so its very easy to add new jobs and easy to create new pages. The parameter fields have special attributes assigned so the submission code is generic and can find the relevant DOM objects and turn them into parameters for the job submission.

We select the tile called “Add Existing Customer L2 Domain” (hovering over the tile slides the content on the tile to provide a full description of the job – not shown).

Once submitted, we check the Job Queue page to monitor the status;

We can double click the job to get more information on the job status and associated parameters. This is done by submitting a REST API request to Tower using the URI ‘”/api/v1/jobs/’. It is possible to filter these also with URI parameters for example;

‘/api/v1/jobs?order_by=-created&created_by__username=devops1&created__gte=2017-11-16T00:00&created__lte=2017-11-16T23:59&or__status=new&or__status=pending&or__status=waiting&or__status=running&or__status=successful&or__status=failed&or__status=error&or__status=canceled’

We can query the job as mentioned by submitting a GET request to Tower with a URL of ‘/api/v1/jobs/301/’ where the 301 is the job ID – this is not the Job Template ID we used before, this is the specific Job ID running. This is returned on successful submission of a job or can be taken from each job as shown in the job queue. Some of the example output from a Job GET request is shown below and useful for reporting status or troubleshooting. Look at the “extra_vars” field, you will see the actual format of the parameters sent to Tower and therefore sent to the Ansible playbook.

{
  "id": 301,
  "type": "job",
  "url": "/api/v1/jobs/301/",
  "related": {
    "created_by": "/api/v1/users/3/",
    "labels": "/api/v1/jobs/301/labels/",
    "inventory": "/api/v1/inventories/4/",
    "project": "/api/v1/projects/14/",
    "credential": "/api/v1/credentials/3/",
    "network_credential": "/api/v1/credentials/5/",
    "unified_job_template": "/api/v1/job_templates/15/",
    "stdout": "/api/v1/jobs/301/stdout/",
    "notifications": "/api/v1/jobs/301/notifications/",
    "job_host_summaries": "/api/v1/jobs/301/job_host_summaries/",
    "job_events": "/api/v1/jobs/301/job_events/",
    "activity_stream": "/api/v1/jobs/301/activity_stream/",
    "job_template": "/api/v1/job_templates/15/",
    "start": "/api/v1/jobs/301/start/",
    "cancel": "/api/v1/jobs/301/cancel/",
    "relaunch": "/api/v1/jobs/301/relaunch/"
  },
  "summary_fields": {
    "network_credential": {
      "id": 5,
      "name": "APIC-F1",
      "description": "APIC fab 1 credentials",
      "kind": "net"
    },
    "job_template": {
      "id": 15,
      "name": "New_Customer_L2_Setup",
      "description": "Creates new default L2 customer"
    },
    "inventory": {
      "id": 4,
      "name": "APIC-F1",
      "description": "APIC Fabric 1",
      "has_active_failures": true,
      "total_hosts": 1,
      "hosts_with_active_failures": 1,
      "total_groups": 0,
      "groups_with_active_failures": 0,
      "has_inventory_sources": false,
      "total_inventory_sources": 0,
      "inventory_sources_with_failures": 0
    },
    "credential": {
      "id": 3,
      "name": "EmptyMachineCred",
      "description": "",
      "kind": "ssh",
      "cloud": false
    },
    "unified_job_template": {
      "id": 15,
      "name": "New_Customer_L2_Setup",
      "description": "Creates new default L2 customer",
      "unified_job_type": "job"
    },
    "project": {
      "id": 14,
      "name": "New_Customer_Deployments",
      "description": "New Customer Deployments on DC Fabric",
      "status": "ok",
      "scm_type": ""
    },
    "created_by": {
      "id": 3,
      "username": "devops1",
      "first_name": "Dev",
      "last_name": "Ops"
    },
    "user_capabilities": {
      "start": true,
      "delete": false
    },
    "labels": {
      "count": 0,
      "results": []
    }
  },
  "created": "2017-11-16T09:04:23.903Z",
  "modified": "2017-11-16T09:04:28.144Z",
  "name": "New_Customer_L2_Setup",
  "description": "Creates new default L2 customer",
  "job_type": "run",
  "inventory": 4,
  "project": 14,
  "playbook": "mtenant_tower.yml",
  "credential": 3,
  "cloud_credential": null,
  "network_credential": 5,
  "forks": 0,
  "limit": "",
  "verbosity": 0,
  "extra_vars": "{\"_tenant_shortname\": \"TEN_GAS\", \"_customer_id\": \"EDS\", \"_epg_hpv\": \"false\", \"_epg_fw\": \"true\", \"_epg_tdi\": \"false\", \"_epg_f5\": \"true\", \"_epg_name\": \"EPG_DATA1\", \"_vlan_id\": \"465\", \"_epg_esx\": \"true\", \"_epg_bm\": \"false\", \"_epg_aix\": \"true\", \"_epg_l2\": \"true\", \"_vrf_name\": \"VRF_REGION1\", \"_bd_name\": \"BD_REGION1\"}",
  "job_tags": "",
  "force_handlers": false,
  "skip_tags": "",
  "start_at_task": "",
  "timeout": 0,
  "unified_job_template": 15,
  "launch_type": "manual",
  "status": "failed",
  "failed": true,
  "started": "2017-11-16T09:04:28.352705Z",
  "finished": "2017-11-16T09:04:53.932808Z",
  "elapsed": 25.58,
  "job_args": "[\"bwrap\", \"--unshare-pid\", \"--dev-bind\", \"/\", \"/\", \"--bind\", \"/tmp/ansible_tower_proot_9rcKNl/tmptqm0d6\", \"/etc/tower\", \"--bind\", \"/tmp/ansible_tower_proot_9rcKNl/tmpAsqbV0\", \"/tmp\", \"--bind\", \"/tmp/ansible_tower_proot_9rcKNl/tmp9AjVR9\", \"/var/lib/awx\", \"--bind\", \"/tmp/ansible_tower_proot_9rcKNl/tmpzoeRiK\", \"/var/lib/awx/job_status\", \"--bind\", \"/tmp/ansible_tower_proot_9rcKNl/tmp24TNn2\", \"/var/lib/awx/projects\", \"--bind\", \"/tmp/ansible_tower_proot_9rcKNl/tmp9A6ZXe\", \"/var/log\", \"--ro-bind\", \"/var/lib/awx/venv/ansible\", \"/var/lib/awx/venv/ansible\", \"--ro-bind\", \"/var/lib/awx/venv/tower\", \"/var/lib/awx/venv/tower\", \"--bind\", \"/tmp/ansible_tower_w4YqwA\", \"/tmp/ansible_tower_w4YqwA\", \"--bind\", \"/var/lib/awx/projects/haystacknetworks\", \"/var/lib/awx/projects/haystacknetworks\", \"--chdir\", \"/var/lib/awx/projects/haystacknetworks\", \"ansible-playbook\", \"-i\", \"/usr/lib/python2.7/dist-packages/awx/plugins/inventory/awxrest.py\", \"-u\", \"root\", \"-e\", \"{\\\"_epg_hpv\\\": \\\"false\\\", \\\"_epg_fw\\\": \\\"true\\\", \\\"_epg_f5\\\": \\\"true\\\", \\\"_epg_tdi\\\": \\\"false\\\", \\\"tower_job_launch_type\\\": \\\"manual\\\", \\\"_epg_esx\\\": \\\"true\\\", \\\"_epg_bm\\\": \\\"false\\\", \\\"_tenant_shortname\\\": \\\"TEN_GAS\\\", \\\"_customer_id\\\": \\\"EDS\\\", \\\"tower_user_id\\\": 3, \\\"tower_project_revision\\\": \\\"\\\", \\\"tower_user_name\\\": \\\"devops1\\\", \\\"_epg_name\\\": \\\"EPG_DATA1\\\", \\\"tower_job_template_id\\\": 15, \\\"_vlan_id\\\": \\\"465\\\", \\\"tower_job_template_name\\\": \\\"New_Customer_L2_Setup\\\", \\\"tower_job_id\\\": 301, \\\"_epg_aix\\\": \\\"true\\\", \\\"_epg_l2\\\": \\\"true\\\", \\\"_vrf_name\\\": \\\"VRF_REGION1\\\", \\\"_bd_name\\\": \\\"BD_REGION1\\\"}\", \"mtenant_tower.yml\"]",
  "job_cwd": "/var/lib/awx/projects/haystacknetworks",
  "job_env": {
    "ANSIBLE_NET_AUTHORIZE": "0",
    "ANSIBLE_RETRY_FILES_ENABLED": "False",
    "ANSIBLE_NET_PASSWORD": "**********",
    "DJANGO_LIVE_TEST_SERVER_ADDRESS": "localhost:9013-9199",
    "LC_CTYPE": "en_US.UTF-8",
    "CALLBACK_QUEUE": "callback_tasks",
    "_MP_FORK_LOGFILE_": "",
    "CELERY_LOG_REDIRECT": "1",
    "SUPERVISOR_GROUP_NAME": "tower-processes",
    "PATH": "/var/lib/awx/venv/ansible/bin:/var/lib/awx/venv/tower/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "HOME": "/var/lib/awx",
    "PS1": "(tower) ",
    "ANSIBLE_CALLBACK_PLUGINS": "/usr/lib/python2.7/dist-packages/awx/plugins/callback",
    "LANG": "en_US.UTF-8",
    "VIRTUAL_ENV": "/var/lib/awx/venv/ansible",
    "REST_API_TOKEN": "**********",
    "ANSIBLE_PARAMIKO_RECORD_HOST_KEYS": "False",
    "LANGUAGE": "en_US.UTF-8",
    "_MP_FORK_LOGFORMAT_": "[%(asctime)s: %(levelname)s/%(processName)s] %(message)s",
    "SHLVL": "0",
    "MAX_EVENT_RES": "700000",
    "TOWER_HOST": "https://192.168.242.130",
    "SUPERVISOR_ENABLED": "1",
    "CELERY_LOG_FILE": "",
    "DJANGO_PROJECT_DIR": "/usr/lib/python2.7/dist-packages",
    "ANSIBLE_HOST_KEY_CHECKING": "False",
    "CACHE": "localhost:11211",
    "JOB_ID": "301",
    "_MP_FORK_LOGLEVEL_": "10",
    "ANSIBLE_STDOUT_CALLBACK": "tower_display",
    "PYTHONPATH": "/usr/lib/python2.7/dist-packages/awx/lib:/var/lib/awx/venv/ansible/lib/python2.7/site-packages:",
    "PROJECT_REVISION": "",
    "ANSIBLE_USE_VENV": "True",
    "CELERY_LOADER": "djcelery.loaders.DjangoLoader",
    "SUPERVISOR_SERVER_URL": "unix:///var/run/supervisor.sock",
    "LC_ALL": "en_US.UTF-8",
    "INVENTORY_HOSTVARS": "True",
    "ANSIBLE_FORCE_COLOR": "True",
    "REST_API_URL": "http://127.0.0.1:80",
    "CELERY_LOG_REDIRECT_LEVEL": "WARNING",
    "SUPERVISOR_PROCESS_NAME": "awx-celeryd",
    "CELERY_LOG_LEVEL": "10",
    "CALLBACK_CONNECTION": "**********",
    "INVENTORY_ID": "4",
    "ANSIBLE_SSH_CONTROL_PATH": "/tmp/ansible_tower_w4YqwA/cp/%%h%%p%%r",
    "PWD": "/var/lib/awx",
    "ANSIBLE_NET_USERNAME": "toweradmin",
    "DJANGO_SETTINGS_MODULE": "awx.settings.production",
    "ANSIBLE_VENV_PATH": "/var/lib/awx/venv/ansible",
    "PROOT_TMP_DIR": "/tmp",
    "USER": "awx"
  },
  "job_explanation": "",
...

This is the basics of using the REST API with Ansible Tower, its powerful and can be used to provide simple dashboards for reporting or for repetitive tasks, as you are able to control what jobs the user can run, this enables the ability to create a simple service catalog for non technical users to use. For example, a sales person may generate new business with a customer who requires a simple template network to be created. The sales person can create this on completion of the sale as all that sales person needs to do is login, clock new customer network, fill in the fields and click submit, a few seconds later the customer is up and running – just like a basic public cloud service !

2 Replies to “Ansible Tower REST API Part 2”

  1. Pingback: Ansible Tower REST API Part 1 - Haystack Networks

  2. Stephen

    Many thanks for sharing Simon, has given me some great ideas for implementing a service catalog portal

Comments are closed.