Daniel Watrous on Software Engineering

A Collection of Software Problems and Solutions

Posts tagged api

Software Engineering

Do Humans or Machines consume IaaS (REST API)

Infrastructure as a Service, like OpenStack and AWS, have made it possible to consume infrastructure on demand. It’s important to understand the ways in which both humans and machines interact with IaaS offerings in order to design optimal systems that leverage all possible automation opportunities. I drew the diagram below to help illustrate.

terraform-stackato-automation0001

Everything is an API

At the heart of IaaS are REST APIs that provide granular access to every resource type, such as compute, storage and network. These APIs provide clarity about which resources are being managed and accommodate the type of independent evolution of each resource offering that keeps responsibilities focused. APIs include things like Nova for compute on the OpenStack side and EC2 for compute on the AWS side. Other IaaS providers have similar APIs with similar delineations between resource types.

Consume the API RAW

Since REST today is typically done over HTTP and makes use of the HTTP methods and response codes, it is relatively straight forward to use a tool such as curl, fiddler or a REST client to craft individual calls to APIs. This works fine for humans who want to understand the APIs better, but it is slow and doesn’t scale well. Machines (by way of software, obviously) can make those same calls using HTTP request libraries (e.g. python requests, Go http package, Java URLConnection, etc.).

Abstractions

After a couple of hours sending raw requests against the IaaS REST APIs, you’ll be thrilled to know that there are well designed abstractions that provide a more friendly interface to IaaS. The openstack Command Line Interface (CLI) allows you to operate against openstack APIs by typing concise commands at a shell prompt. The Horizon web console provides a web based user experience to manage those same resources. In both cases, the CLI and Horizon console are simply translating your commands or clicks into sequences of REST API calls. This means the ultimate interaction with IaaS is still based on the REST API, but you, the human, have a more human like interface.

Since machines don’t like to type on a keyboard or click on web pages, it makes sense to have abstractions for them too. The machines I’m talking about are the ones we ask to automatically test our code, deploy our software, monitor and scale and heal our applications. For them, we have other abscrations, such including HEAT and terraform. This allows us to provide the machine with a plain text description (often in YAML or similar markup), of what we want our infrastructure to look like. These orchestration tools then analyze the current state of our infrastructure and decide whether any actions are necessary. They then translate those actions into REST API calls, just like the CLI and Horizon panel did for the human. So again, all interaction with IaaS happens by way of the REST API, but the machine too gets an abstraction that humans can craft and read.

Automation is the end goal

Whichever method you pursue, the end goal is to automate as many of the IaaS interactions as possible. The more the machine can do without human interaction, the more time humans have to design the software running on top of IaaS and the more resilient the end system will be to failures and fluctuations. If you find yourself using the human abstractions more than telling machines what to do by way of infrastructure definitions, you have a great opportunity to change and gain some efficiency.

Software Engineering

Managed Services in CloudFoundry

CloudFoundry defines a Service Broker API which can be implemented and added to a CloudFoundry installation to provide managed services for apps. In order to better understand the way managed services are created and integrated with CloudFoundry (and derivative technologies like Stackato and HP Helion Development Platform), I created an example service and implemented the Service Broker API to manage it. The code for both implementations are on github.

Deploy the services

For this exercise I deployed bosh-lite on my Windows 7 laptop. I then follow the documented procedure to push cf-echo-service and cf-service-broker-python to CloudFoundry. The desired end state is two running apps in CloudFoundry that can be used for service broker management and testing.

NOTE: Deploy the echo-service first. Then update the service-broker script so that it will use the cf hosted echo-service. The relevant lines in the service-broker.py script are shown.

# UPDATE THIS FOR YOUR ECHO SERVICE DEPLOYMENT
service_base = "echo-service.10.244.0.34.xip.io"

Security

In this setup, it is necessary for the service-broker app to communicate with the echo-service app within CloudFoundry. This requires adding a security group and applying it to running services. The first step is to create a JSON file with the definition of the new security group.

[
  {
    "protocol": "tcp",
    "destination": "10.244.0.34",
    "ports": "80"
  }
]

The security group can then be created using create-security-group. This command expects a security group name and a path to the JSON file created above. After creating the security group, it must be bound to running instances (optionally to staging instances too using bind-staging-security-group). If the service-broker was deployed prior to enabling this security group, the app will also need to be restarted.

vagrant@vagrant-ubuntu-trusty-64:~$ cf create-security-group port80 security.json
Creating security group port80 as admin
OK
vagrant@vagrant-ubuntu-trusty-64:~$ cf security-groups
Getting security groups as admin
OK
 
     Name              Organization   Space
#0   public_networks
#1   dns
#2   port80
vagrant@vagrant-ubuntu-trusty-64:~$ cf bind-running-security-group port80
Binding security group port80 to defaults for running as admin
OK
 
TIP: Changes will not apply to existing running applications until they are restarted.
vagrant@vagrant-ubuntu-trusty-64:~$ cf restart service-broker
Stopping app service-broker in org myorg / space mydept as admin...
OK
 
Starting app service-broker in org myorg / space mydept as admin...
 
0 of 1 instances running, 1 starting
1 of 1 instances running
 
App started
 
OK
Showing health and status for app service-broker in org myorg / space mydept as admin...
OK
 
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: service-broker.10.244.0.34.xip.io
last uploaded: Mon Nov 24 15:59:07 UTC 2014
 
     state     since                    cpu    memory        disk
#0   running   2014-11-24 08:13:40 PM   0.0%   56M of 256M   0 of 1G

NOTE: I deploy them to CloudFoundry for convenience and reliability, but this is not required. The service could be any service and the service broker API implementation can be deployed anywhere.

vagrant@vagrant-ubuntu-trusty-64:~$ cf apps
Getting apps in org myorg / space mydept as admin...
OK
 
name             requested state   instances   memory   disk   urls
echo-service     started           1/1         256M     1G     echo-service.10.244.0.34.xip.io
service-broker   started           1/1         256M     1G     service-broker.10.244.0.34.xip.io

Optional validation

It is possible to validate that the deployed apps work as expected. The curl commands below validate the echo service is working properly.

vagrant@vagrant-ubuntu-trusty-64:~$ curl -X PUT http://echo-service.10.244.0.34.xip.io/echo/51897770-560b-11e4-b75a-9ad2017223a9 -w "\n"
{"instance_id": "51897770-560b-11e4-b75a-9ad2017223a9", "state": "provision_success", "dashboard_url": "http://localhost:8090/dashboard/51897770-560b-11e4-b75a-9ad2017223a9"}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X PUT http://echo-service.10.244.0.34.xip.io/echo/51897770-560b-11e4-b75a-9ad2017223a9/myapp -w "\n"
{"state": "bind_success", "id": "51897770-560b-11e4-b75a-9ad2017223a9", "app": "myapp"}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X POST http://echo-service.10.244.0.34.xip.io/echo/51897770-560b-11e4-b75a-9ad2017223a9/myapp -H "Content-Type: application/json" -d '{"message": "Hello World"}' -w "\n"
{"response": "Hello World"}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X POST http://echo-service.10.244.0.34.xip.io/echo/51897770-560b-11e4-b75a-9ad2017223a9/myapp -H "Content-Type: application/json" -d '{"message": "Hello World 2"}' -w "\n"
{"response": "Hello World 2"}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X POST http://echo-service.10.244.0.34.xip.io/echo/51897770-560b-11e4-b75a-9ad2017223a9/myapp -H "Content-Type: application/json" -d '{"message": "Hello Worldz!"}' -w "\n"
{"response": "Hello Worldz!"}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X GET http://echo-service.10.244.0.34.xip.io/echo/dashboard/51897770-560b-11e4-b75a-9ad2017223a9
 
<table class="pure-table">
    <thead>
        <tr>
            <th>Instance</th>
            <th>Bindings</th>
            <th>Messages</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>51897770-560b-11e4-b75a-9ad2017223a9</td>
            <td>1</td>
            <td>3</td>
        </tr>
    </tbody>
</table>
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X DELETE http://echo-service.10.244.0.34.xip.io/echo/51897770-560b-11e4-b75a-9ad2017223a9/myapp -w "\n"
{"state": "unbind_success", "id": "51897770-560b-11e4-b75a-9ad2017223a9", "app": "myapp"}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X DELETE http://echo-service.10.244.0.34.xip.io/echo/51897770-560b-11e4-b75a-9ad2017223a9 -w "\n"
{"state": "deprovision_success", "bindings": 0, "id": "51897770-560b-11e4-b75a-9ad2017223a9", "messages": 3}

The service broker API implementation can also be verified using curl as shown below.

vagrant@vagrant-ubuntu-trusty-64:~$ curl -X GET http://service-broker.10.244.0.34.xip.io/v2/catalog -H "X-Broker-Api-Version: 2.3" -H "Authorization: Basic dXNlcjpwYXNz" -w "\n"
{"services": [{"name": "Echo Service", "dashboard_client": {"id": "client-id-1", "redirect_uri": "http://echo-service.10.244.0.34.xip.io/echo/dashboard", "secret": "secret-1"}, "description": "Echo back the value received", "id": "echo_service", "plans": [{"free": false, "description": "A large dedicated service with a big storage quota, lots of RAM, and many connections", "id": "big_0001", "name": "large"}], "bindable": true}, {"name": "Invert Service", "dashboard_client": null, "description": "Invert the value received", "id": "invert_service", "plans": [{"description": "A small shared service with a small storage quota and few connections", "id": "small_0001", "name": "small"}], "bindable": true}]}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X PUT http://service-broker.10.244.0.34.xip.io/v2/service_instances/mynewinstance -H "Content-type: application/json" -H "Authorization: Basic dXNlcjpwYXNz" -d '{"service_id": "echo_service", "plan_id": "small_0001", "organization_guid": "HP", "space_guid": "IT"}' -w "\n"
{"dashboard_url": "http://echo-service.10.244.0.34.xip.io/echo/dashboard/mynewinstance"}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X PUT http://service-broker.10.244.0.34.xip.io/v2/service_instances/mynewinstance/service_bindings/myappid  -H "Content-type: application/json" -H "Authorization: Basic dXNlcjpwYXNz" -d '{"service_id": "echo_service", "plan_id": "small_0001", "app_guid": "otherappid"}' -w "\n"
{"credentials": {"uri": "http://echo-service.10.244.0.34.xip.io/echo/mynewinstance/myappid"}}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X DELETE http://service-broker.10.244.0.34.xip.io/v2/service_instances/mynewinstance/service_bindings/myappid  -H "Authorization: Basic dXNlcjpwYXNz" -w "\n"
{}
vagrant@vagrant-ubuntu-trusty-64:~$ curl -X DELETE http://service-broker.10.244.0.34.xip.io/v2/service_instances/mynewinstance -H "Authorization: Basic dXNlcjpwYXNz" -w "\n"
{}

Manage the Service Broker

In CloudFoundry, the service broker can be added using the create-service-broker command with the cf cli.

vagrant@vagrant-ubuntu-trusty-64:~$ cf create-service-broker echo-broker user pass http://service-broker.10.244.0.34.xip.io
Creating service broker echo-broker as admin...
OK
vagrant@vagrant-ubuntu-trusty-64:~$ cf service-brokers
Getting service brokers as admin...
 
name          url
echo-broker   http://service-broker.10.244.0.34.xip.io

The service broker is added, but the service plans in the catalog do not allow public access by default, as can be seen with the call to service-access below. This also means that calls to marketplace show no available services.

vagrant@vagrant-ubuntu-trusty-64:~$ cf service-access
Getting service access as admin...
broker: echo-broker
   service          plan    access   orgs
   Echo Service     large   none
   Invert Service   small   none
vagrant@vagrant-ubuntu-trusty-64:~$ cf marketplace
Getting services from marketplace in org myorg / space mydept as admin...
OK
 
No service offerings found

The the service plan must be enabled to accommodate public consumption. This is done using the enable-service-access call and providing the name of the service. In this case, the “Echo Service” is desired. After enabling the service plan, the service-access listing shows it available and a call to marketplace shows it available to apps.

vagrant@vagrant-ubuntu-trusty-64:~$ cf enable-service-access "Echo Service"
Enabling access to all plans of service Echo Service for all orgs as admin...
OK
vagrant@vagrant-ubuntu-trusty-64:~$ cf service-access
Getting service access as admin...
broker: echo-broker
   service          plan    access   orgs
   Echo Service     large   all
   Invert Service   small   none
 
vagrant@vagrant-ubuntu-trusty-64:~$ cf marketplace
Getting services from marketplace in org myorg / space mydept as admin...
OK
 
service        plans   description
Echo Service   large   Echo back the value received

Manage Services

With the new service in the marketplace, it’s now possible to provision, bind and use instances of the new service by way of the cf cli. This happens in a few steps in CloudFoundry. First an instance of the service is provisioned. An app is then pushed to CloudFoundry. Finally the service and the app are associated through a binding. The call to create-service expects three arguments which can be found in the marketplace output above.

  • service name
  • service plan
  • a name for the new service instance
vagrant@vagrant-ubuntu-trusty-64:~$ cf create-service "Echo Service" large test-echo-service
Creating service test-echo-service in org myorg / space mydept as admin...
OK
vagrant@vagrant-ubuntu-trusty-64:~$ cf services
Getting services in org myorg / space mydept as admin...
OK
 
name                service        plan    bound apps
test-echo-service   Echo Service   large

Push an app

Next step is to create a simple app to which the service can be bound. It’s important to understand how the service details will be injected into the app environment. Service details will be contained in a JSON document under the key VCAP_SERVICES. The structure of the service details is shown below.

{
  "Echo Service": [
    {
      "name": "test-php-echo",
      "label": "Echo Service",
      "tags": [
 
      ],
      "plan": "large",
      "credentials": {
        "uri": "http://16.98.49.183:8090/echo/f2c43d9c-e912-4e56-b624-14e8522be912/4a421367-0e1a-4b56-a07f-9a6b404119a5"
      }
    }
  ]
}

For this example, the PHP script below extracts, decodes and isolates the service URI from the VCAP_SERVICES environment variable. The cURL library is used to send a call to that URI containing a basic message JSON document. The response is then printed using var_dump.

<?php
// extract injected service details from environment
$services_json = getenv("VCAP_SERVICES");
$services = json_decode($services_json);
$service_uri = $services->{'Echo Service'}[0]->{'credentials'}->{'uri'};
var_dump($service_uri);
 
// create a message to send
$message = '{"message": "Hello CloudFoundry!"}';
 
// setup and execute the cURL call to the echo service
$curl = curl_init($service_uri);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-type: application/json"));
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $message);
$response = curl_exec($curl);
 
// handle non-200 response
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ( $status != 200 ) {
    die("Error: call to URL $url failed with status $status, response $response, curl_error " . curl_error($curl) . ", curl_errno " . curl_errno($curl));
}
 
// clean up cURL connection
curl_close($curl);
 
// decode response and output
$response_as_json = json_decode($response);
var_dump($response_as_json);
?>

A simple manifest file is provided for the deployment.

applications:
- name: echo-php
  framework: php

The app can then be pushed to CloudFoundry. The resulting list of apps is shown.

vagrant@vagrant-ubuntu-trusty-64:~/echo-php$ cf apps
Getting apps in org myorg / space mydept as admin...
OK
 
name             requested state   instances   memory   disk   urls
echo-service     started           1/1         256M     1G     echo-service.10.244.0.34.xip.io, 16.85.146.179.xip.io
service-broker   started           1/1         256M     1G     service-broker.10.244.0.34.xip.io
echo-php         started           1/1         256M     1G     echo-php.10.244.0.34.xip.io

Bind the service to the app

With an existing service and an existing app, it’s possible to bind the service to the app. This is done using bind-service, as shown below. Note that it is necessary to restage the app after binding the service so that the environment variables will be properly injected.

vagrant@vagrant-ubuntu-trusty-64:~$ cf bind-service echo-php test-echo-service
Binding service test-echo-service to app echo-php in org myorg / space mydept as admin...
OK
TIP: Use 'cf restage' to ensure your env variable changes take effect
vagrant@vagrant-ubuntu-trusty-64:~$ cf restage echo-php
Restaging app echo-php in org myorg / space mydept as admin...
-----> Downloaded app package (4.0K)
-----> Downloaded app buildpack cache (4.0K)
-------> Buildpack version 1.0.2
Use locally cached dependencies where possible
 !     WARNING:        No composer.json found.
       Using index.php to declare PHP applications is considered legacy
       functionality and may lead to unexpected behavior.
       See https://devcenter.heroku.com/categories/php
-----> Setting up runtime environment...
       - PHP 5.5.12
       - Apache 2.4.9
       - Nginx 1.4.6
-----> Installing PHP extensions:
       - opcache (automatic; bundled, using 'ext-opcache.ini')
-----> Installing dependencies...
       Composer version ac497feabaa0d247c441178b7b4aaa4c61b07399 2014-06-10 14:13:12
       Warning: This development build of composer is over 30 days old. It is recommended to update it by running "/app/.heroku/php/bin/composer self-update" to get the latest version.
       Loading composer repositories with package information
       Installing dependencies
       Nothing to install or update
       Generating optimized autoload files
-----> Building runtime environment...
       NOTICE: No Procfile, defaulting to 'web: vendor/bin/heroku-php-apache2'
 
-----> Uploading droplet (64M)
 
0 of 1 instances running, 1 starting
1 of 1 instances running
 
App started
 
 
OK
Showing health and status for app echo-php in org myorg / space mydept as admin...
OK
 
requested state: started
instances: 1/1
usage: 256M x 1 instances
urls: echo-php.10.244.0.34.xip.io
last uploaded: Mon Nov 24 21:34:29 UTC 2014
 
     state     since                    cpu    memory          disk
#0   running   2014-11-24 09:39:27 PM   0.0%   88.4M of 256M   0 of 1G

The echo-php service can be called using cURL from the command line to verify proper connectivity between the echo php app and the new service.

vagrant@vagrant-ubuntu-trusty-64:$ curl http://echo-php.10.244.0.34.xip.io
string(117) "http://echo-service.10.244.0.34.xip.io/echo/8dea3b8e-4230-4509-8879-5f94b4812985/d2944b66-2d9e-46fe-8e3d-d9b102cb5bca"
object(stdClass)#4 (1) {
  ["response"]=>
  string(19) "Hello CloudFoundry!"
}

The existing service can be bound to additional app instances. It is also trivial to create new service instances. If the service is no longer needed by the application, it is straightforward to unbind and deprovision.

Software Engineering

OpenStack REST API

There are some high quality resources that already cover the OpenStack API, so this is a YEA (yet another example) post. See the resources section below for some helpful links.

OpenStack APIs provide access to all OpenStack components, such as nova (compute), glance (VM images), swift (object storage), cinder (block storage), keystone (authentication) and neutron (networking). Authentication tokens are valid for a fixed duration, after which they expire and must be replaced. Each service requires it’s own token. Services that are hosted on the same logical server are typically accessible over different ports.

OpenStack APIs are RESTful, which means there are many ways to use them. In this post I’ll demonstrate three approaches that should provide clarity into their structure.

  • Command Line Interface (CLI)
  • cURL
  • REST Client

In this post I don’t cover programming against the REST APIs, but instead focus just on how they work. This work builds on my OpenStack development post.

Command Line Interface (CLI)

Command Line Interfaces used to manage OpenStack components make use of the REST APIs behind the scenes: a rather smart design choice on the part of the OpenStack community. This brings consistency to OpenStack management efforts and discourages disparity between standard tooling (CLI) and custom tooling (direct API access).

Credentials are required to access the REST APIs. For the command line client, these credentials are stored as environment variables. If you’re using DevStack, you can use the openrc script to automatically setup your environment.

source openrc admin admin

Some clients support a debug option that will output full details about the request and response cycle. Raw request and response details can be helpful when learning the APIs or creating programmatic access libraries that wrap the APIs. Here’s an example that will list the flavors available.

$ nova --debug flavor-list
REQ: curl -i 'http://openstack.danielwatrous.com:5000/v2.0/tokens' -X POST -H "Accept: application/json" -H "Content-Type: application/json" -H "User-Agent: python-novaclient" -d '{"auth": {"tenantName": "admin", "passwordCredentials": {"username": "admin", "password": "{SHA1}95397c42a173838417806ce19d78f133ae6baa24"}}}'
INFO (connectionpool:258) Starting new HTTP connection (1): proxy.company.com
DEBUG (connectionpool:375) Setting read timeout to 600.0
DEBUG (connectionpool:415) "POST http://openstack.danielwatrous.com:5000/v2.0/tokens HTTP/1.1" 200 6823
RESP: [200] CaseInsensitiveDict({'content-length': '6823', 'proxy-connection': 'Keep-Alive', 'vary': 'X-Auth-Token', 'server': 'Apache/2.4.7 (Ubuntu)', 'connection': 'Keep-Alive', 'date': 'Thu, 21 Aug 2014 19:09:21 GMT', 'content-type': 'application/json'})
RESP BODY: {"access": {"token": {"issued_at": "2014-08-21T19:09:21.692110", "expires": "2014-08-21T20:09:21Z", "id": "{SHA1}99ff604f28f5706bfd82a00c21e099cba7fafab2", "tenant": {"enabled": true, "description": null, "name": "admin", "id": "32c13e88d51e49179c28520f688fa74d"}}, "serviceCatalog": [{"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d", "internalURL": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d", "id": "03d570ce41c04daeb7ffa274c20435f0"}], "type": "compute", "name": "nova"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:8776/v2/32c13e88d51e49179c28520f688fa74d", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:8776/v2/32c13e88d51e49179c28520f688fa74d", "internalURL": "http://openstack.danielwatrous.com:8776/v2/32c13e88d51e49179c28520f688fa74d", "id": "20d2caebf4814e1bb2c05f30a4802a2c"}], "type": "volumev2", "name": "cinderv2"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:8774/v3", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:8774/v3", "internalURL": "http://openstack.danielwatrous.com:8774/v3", "id": "47f43a622264422f8980f3b0fbac5f00"}], "type": "computev3", "name": "novav3"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:3333", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:3333", "internalURL": "http://openstack.danielwatrous.com:3333", "id": "149e00e61cc543cf94ae6162f79d9f00"}], "type": "s3", "name": "s3"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:9292", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:9292", "internalURL": "http://openstack.danielwatrous.com:9292", "id": "1b7a45b6d1c840978491250fd1a67204"}], "type": "image", "name": "glance"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:8000/v1", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:8000/v1", "internalURL": "http://openstack.danielwatrous.com:8000/v1", "id": "0b8abc323d884a0aa657bcb2f0274ee5"}], "type": "cloudformation", "name": "heat-cfn"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:8776/v1/32c13e88d51e49179c28520f688fa74d", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:8776/v1/32c13e88d51e49179c28520f688fa74d", "internalURL": "http://openstack.danielwatrous.com:8776/v1/32c13e88d51e49179c28520f688fa74d", "id": "63675bf8e9a04d199cffafb7b8354b05"}], "type": "volume", "name": "cinder"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:8773/services/Admin", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:8773/services/Cloud", "internalURL": "http://openstack.danielwatrous.com:8773/services/Cloud", "id": "520de40a96ea47c4a08c3ae5e0a8243c"}], "type": "ec2", "name": "ec2"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:8004/v1/32c13e88d51e49179c28520f688fa74d", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:8004/v1/32c13e88d51e49179c28520f688fa74d", "internalURL": "http://openstack.danielwatrous.com:8004/v1/32c13e88d51e49179c28520f688fa74d", "id": "0dcff3f004ff4c9b9b96d012e47d2edb"}], "type": "orchestration", "name": "heat"}, {"endpoints_links": [], "endpoints": [{"adminURL": "http://openstack.danielwatrous.com:35357/v2.0", "region": "RegionOne", "publicURL": "http://openstack.danielwatrous.com:5000/v2.0", "internalURL": "http://openstack.danielwatrous.com:5000/v2.0", "id": "6f43e35702844e149dde900124c352bf"}], "type": "identity", "name": "keystone"}], "user": {"username": "admin", "roles_links": [], "id": "b9936b16c5d343588f5a19d31a55c1ea", "roles": [{"name": "_member_"}, {"name": "heat_stack_owner"}, {"name": "admin"}], "name": "admin"}, "metadata": {"is_admin": 0, "roles": ["9fe2ff9ee4384b1894a90878d3e92bab", "c28444beb7e64b4ea2ea223a6efcba6a", "3ea10423929b47779f977e11015fe480"]}}}
 
REQ: curl -i 'http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/detail' -X GET -H "Accept: application/json" -H "User-Agent: python-novaclient" -H "X-Auth-Project-Id: admin" -H "X-Auth-Token: {SHA1}99ff604f28f5706bfd82a00c21e099cba7fafab2"
INFO (connectionpool:258) Starting new HTTP connection (1): proxy.company.com
DEBUG (connectionpool:375) Setting read timeout to 600.0
DEBUG (connectionpool:415) "GET http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/detail HTTP/1.1" 200 3337
RESP: [200] CaseInsensitiveDict({'content-length': '3337', 'proxy-connection': 'Keep-Alive', 'x-compute-request-id': 'req-802ef8c9-d4a3-41e5-a93d-7ab2120089db', 'connection': 'Keep-Alive', 'date': 'Thu, 21 Aug 2014 19:09:24 GMT', 'content-type': 'application/json', 'age': '0'})
RESP BODY: {"flavors": [{"name": "m1.tiny", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/1", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/1", "rel": "bookmark"}], "ram": 512, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 1, "id": "1"}, {"name": "m1.small", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/2", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/2", "rel": "bookmark"}], "ram": 2048, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 20, "id": "2"}, {"name": "m1.medium", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/3", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/3", "rel": "bookmark"}], "ram": 4096, "OS-FLV-DISABLED:disabled": false, "vcpus": 2, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 40, "id": "3"}, {"name": "m1.large", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/4", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/4", "rel": "bookmark"}], "ram": 8192, "OS-FLV-DISABLED:disabled": false, "vcpus": 4, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 80, "id": "4"}, {"name": "m1.nano", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/42", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/42", "rel": "bookmark"}], "ram": 64, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 0, "id": "42"}, {"name": "m1.heat", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/451", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/451", "rel": "bookmark"}], "ram": 512, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 0, "id": "451"}, {"name": "m1.xlarge", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/5", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/5", "rel": "bookmark"}], "ram": 16384, "OS-FLV-DISABLED:disabled": false, "vcpus": 8, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 160, "id": "5"}, {"name": "m1.micro", "links": [{"href": "http://openstack.danielwatrous.com:8774/v2/32c13e88d51e49179c28520f688fa74d/flavors/84", "rel": "self"}, {"href": "http://openstack.danielwatrous.com:8774/32c13e88d51e49179c28520f688fa74d/flavors/84", "rel": "bookmark"}], "ram": 128, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 0, "id": "84"}]}
 
+-----+-----------+-----------+------+-----------+---------+-------+-------------+-----------+
| ID  | Name      | Memory_MB | Disk | Ephemeral | Swap_MB | VCPUs | RXTX_Factor | Is_Public |
+-----+-----------+-----------+------+-----------+---------+-------+-------------+-----------+
| 1   | m1.tiny   | 512       | 1    | 0         |         | 1     | 1.0         | True      |
| 2   | m1.small  | 2048      | 20   | 0         |         | 1     | 1.0         | True      |
| 3   | m1.medium | 4096      | 40   | 0         |         | 2     | 1.0         | True      |
| 4   | m1.large  | 8192      | 80   | 0         |         | 4     | 1.0         | True      |
| 42  | m1.nano   | 64        | 0    | 0         |         | 1     | 1.0         | True      |
| 451 | m1.heat   | 512       | 0    | 0         |         | 1     | 1.0         | True      |
| 5   | m1.xlarge | 16384     | 160  | 0         |         | 8     | 1.0         | True      |
| 84  | m1.micro  | 128       | 0    | 0         |         | 1     | 1.0         | True      |
+-----+-----------+-----------+------+-----------+---------+-------+-------------+-----------+

The first two sections are calls the REST APIs, first for the keystone service to Authenticate and receive a token. Responses come as JSON due to the Accept header of application/json. If you look closely, you’ll see that the response actually included an access token and entry point URLs for each of the services that are integrated with keystone. These make up the ServiceCatalog and in this case there are ten.

The second section is the actual call to the nova API. In this case it returns a list of eight flavors. The final section is a tabular view of the JSON response created by the nova command line client.

cURL

If you look closely at the debug output of the examples above, you’ll see that the command line clients use cURL to make HTTP requests. We can already see what the authentication call looks like. Calls directly to cURL look similar. For example, here I call the keystone service to get a list of tenants.

$ curl -i -X GET http://openstack.danielwatrous.com:35357/v2.0/tenants -H "User-Agent: linux-command-line" -H "X-Auth-Token: TOKEN"
HTTP/1.1 200 OK
Date: Thu, 21 Aug 2014 20:05:39 GMT
Server: Apache/2.4.7 (Ubuntu)
Vary: X-Auth-Token
Content-Length: 546
Content-Type: application/json
Proxy-Connection: Keep-Alive
Connection: Keep-Alive
 
{"tenants_links": [], "tenants": [{"description": null, "enabled": true, "id": "1b7f733fa1394b9fb96838d3d7c6feea", "name": "service"}, {"description": null, "enabled": true, "id": "298cfcec9e9e49858e9b8e83d6b7d14e", "name": "demo"}, {"description": null, "enabled": true, "id": "32c13e88d51e49179c28520f688fa74d", "name": "admin"}, {"description": null, "enabled": true, "id": "8536da0aee8149d48e1fe6078dade4bf", "name": "alt_demo"}, {"description": null, "enabled": true, "id": "e66e6a80a6014dd28c7b4c1fcad19448", "name": "invisible_to_admin"}]}

REST Client

On Windows, the tool Fiddler can be used to create REST calls. When fiddler is first started, you may need to turn off capturing of traffic. You can do this from the File menu or by pressing F12. In the right side of the window, choose the composer tab. There you can provide the URL, headers and other HTTP request details. Below you can see a call to Keystone for tokens.

openstack-rest-api-fiddler-1

The response can be viewed by selecting the resulting request in the left pane and choosing the Inspectors tab in the right pane. The results can be viewed raw, as shown here.

openstack-rest-api-fiddler-2

Fiddler also provides various parsers, including JSON, to make the content easier to visualize.

openstack-rest-api-fiddler-3

Resources

The quality of the documentation available for OpenStack APIs is really amazing. Here are a couple of starting points for you.

http://developer.openstack.org/
http://docs.openstack.org/api/quick-start/content/

Software Engineering

RESTful Java Servlet

For a recent project I found that a RESTful interface would be appropriate. My first inclination was to use Jersey (or one of the JAX-RS implementations available). The environment where this new REST API would deploy is still using Java 1.5. This became a major roadblock when I was found that none of the JAX-RS implementations provide support for the Java 1.5 virtual machine. This is not surprising since it’s few YEARS past EOSL (end of support life) for Java 1.5, but disappointing still the same.

After spending a day or so with the available frameworks, trying to get one of them to work in Java 1.5, I grew nervous that even if I did succeed, that puts my implementation squarely in a non-support region, even on the mailing lists. So I decided to see how close I could come to a simple, reliable RESTful interface using a plain old Java servlet (POJS?).

While I’m disappointed that I’m unable to leverage an existing, mature framework, I was happy with the outcome. Here are the details

HTTP Methods built in

To begin with, the servlet specification defines methods for the HTTP actions: GET, POST, PUT, DELETE, OPTIONS and HEAD.

Each of these methods receives a HttpServletRequest and HttpServletResponse object, which makes it easy to access the payload of the request and construct a response. Manipulation of return headers, payload content, response codes (200, 404, etc.) are all available directly through the HttpServletResponse object.

The only imports required to accomplish this were available in the standard Java environment, which relieved me of the need to worry about interoperability.

1
2
3
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

URI mapping

The next challenge was how to map RESTful URIs on to the methods in my servlet. At first glance, the conventional mapping done in the web.xml file wouldn’t be sufficient. The problem was with the extra parameters that were embedded in the URI. This goes just beyond what the mapping delineates in web.xml.

What I found was that a wildcard mapping and use of HttpServletRequest.getPathInfo() gave me all the information that I needed to complete the mapping. This is what my web.xml file looks like

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
	<display-name>testrestservlet</display-name>
	<servlet>
		<description></description>
		<display-name>TestRestServlet</display-name>
		<servlet-name>TestRestServlet</servlet-name>
		<servlet-class>com.danielwatrous.testrestservlet.TestRestServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>TestRestServlet</servlet-name>
		<url-pattern>/api/v1/*</url-pattern>
	</servlet-mapping>
</web-app>

The call I mentioned above, getPathInfo(), returns a string with everything that is in place of the asterisk (*) in my url-pattern above. This means that a URI of the form http://server/testrestservlet/api/vi/resource/id would return a string of “/resource/id” when getPathInfo() is called.

Avoid String Handling

I wanted to avoid messy string manipulation or analysis to parse out the details of this resource identification information. This ruled out any string splitting and checking for indexOf or startsWith.

Instead I wanted to be deliberate and reduce the possibility that a mis-mapping would slip through or that a future developer would misunderstand the exact nature of what to expect. For this reason I chose to use regular expressions. This provides a clear pattern of what my URI should look like, and it will be obvious to other developers. It also reduces the chances of a bad URI resulting in an inconsistent result.

I always use Kodos to develop regular expressions. After I got the regular expressions worked out for each class, I created an inner class inside the servlet that would help me map the URI to a specific request and give me access to the parameters embedded in the request URI. Here’s what that inner class looks like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  private class RestRequest {
    // Accommodate two requests, one for all resources, another for a specific resource
    private Pattern regExAllPattern = Pattern.compile("/resource");
    private Pattern regExIdPattern = Pattern.compile("/resource/([0-9]*)");
 
    private Integer id;
 
    public RestRequest(String pathInfo) throws ServletException {
      // regex parse pathInfo
      Matcher matcher;
 
      // Check for ID case first, since the All pattern would also match
      matcher = regExIdPattern.matcher(pathInfo);
      if (matcher.find()) {
        id = Integer.parseInt(matcher.group(1));
        return;
      }
 
      matcher = regExAllPattern.matcher(pathInfo);
      if (matcher.find()) return;
 
      throw new ServletException("Invalid URI");
    }
 
    public Integer getId() {
      return id;
    }
 
    public void setId(Integer id) {
      this.id = id;
    }
  }

Now I’m set to override the methods for each of my HTTP actions.

The Servlet

At this point, creating the servlet is trivial. The mapping provides all path (resource) information to the servlet and our inner class is responsible to determining whether the URI makes sense for our application and to make that information available in a sensible way for processing of the request in the action methods. Here’s what I came up with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.danielwatrous.testrestservlet;
 
import java.io.IOException;
import java.io.PrintWriter;
 
import java.util.regex.Pattern;
import java.util.regex.Matcher;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
public class TestRestServlet extends HttpServlet {
 
  private class RestRequest {
    // Accommodate two requests, one for all resources, another for a specific resource
    private Pattern regExAllPattern = Pattern.compile("/resource");
    private Pattern regExIdPattern = Pattern.compile("/resource/([0-9]*)");
 
    private Integer id;
 
    public RestRequest(String pathInfo) throws ServletException {
      // regex parse pathInfo
      Matcher matcher;
 
      // Check for ID case first, since the All pattern would also match
      matcher = regExIdPattern.matcher(pathInfo);
      if (matcher.find()) {
        id = Integer.parseInt(matcher.group(1));
        return;
      }
 
      matcher = regExAllPattern.matcher(pathInfo);
      if (matcher.find()) return;
 
      throw new ServletException("Invalid URI");
    }
 
    public Integer getId() {
      return id;
    }
 
    public void setId(Integer id) {
      this.id = id;
    }
  }
 
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    PrintWriter out = response.getWriter();
 
    out.println("GET request handling");
    out.println(request.getPathInfo());
    out.println(request.getParameterMap());
    try {
      RestRequest resourceValues = new RestRequest(request.getPathInfo());
      out.println(resourceValues.getId());
    } catch (ServletException e) {
      response.setStatus(400);
      response.resetBuffer();
      e.printStackTrace();
      out.println(e.toString());
    }
    out.close();
  }
 
  // implement remaining HTTP actions here
  ... 
 
}

Conclusion

This turned out to be a clear enough way to capture REST like URIs and map those onto a servlet for processing. Regular expressions provide for reliable and clear access to resource details and should be easy for future developers to quickly understand and extend.

In my next article, I’ll show you how I extend this servlet using the Jackson JSON library and a helper object to serialize and deserialize JSON payloads in RESTful requests.

Software Engineering

Software licensing: Jersey framework for REST

I did some investigation into building the RESTful API for the software licensing system using wicket. At first I was encouraged that URL management was so easy (nevermind that really old article I just linked to).

Wicket Not Ideal for RESTful APIs

In the end I decided that wicket was not a good choice for the RESTful API. The crux came in treating HTTP methods differently. In other words, for a given resource URI, I want to do something different for GET, POST, PUT and DELETE. That’s not one of wicket’s strengths, so I moved on.

The first framework I looked at had a really slick look. I know that’s not necessarily an indication of technical superiority, but the simplicity of the homepage made me believe that Restlet would be a good choice. But before making my choice, I looked for references and found this stackoverflow discussion.

Jersey Wins

I had seen Jersey before reading that, but was turned off thinking I had to dive deep into glassfish and other technologies. The stackoverflow discussion convinced me to give Jersey a try and I’m so glad that I did. In about a half an hour I had my initial API calls framed and responding to web calls in Google App Engine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.danielwatrous.softwarelicensing;
 
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
 
//The Java class will be hosted at the URI path "/sitelicenses"
@Path("/sitelicenses")
public class SiteLicenses {
 
	@GET
	@Produces("text/plain")
	public String getAllSiteLicenses() {
		// Return some cliched textual content
		return "Hello World GET";
	}
 
	@POST
	@Produces("text/plain")
	public String createNewSiteLicense() {
		return "Hello World POST";
	}
 
	// @PUT
	// @DELETE
}

I followed this extremely concise Jersey + GAE tutorial to get things going. There were more jar files required that are shown there. Otherwise I can’t believe how easy it was to get my RESTful API framed in and working.

TIP:

About a year ago I found a great tool to explore REST and test RESTful services. It’s a Chrome plugin named Simple REST Client. This makes it possible to send requests to a URI using any of the HTTP methods and see the full response, including headers. Here’s a screenshot of my first API response:

Software Engineering

Software licensing: RESTful API, resources vs. parameters

I’m starting to build out the RESTful API for the software licensing system. The first api call to implement is this

http://mysite/api/sitelicenses/?apikey=KEYVAL

That came from the initial design documents and implicitly suggests that there is a difference between the resource and parameters passed in when asking for the resource.

This distinction is important and came up today when I was thinking about pagination. As the API is defined currently, the above URI would return all site licenses associated with an apikey. The first thought that came to mind was that the number of licenses could be very large and so there needs to be some way to paginate the results.

Difference between resource and parameters

The pagination itself shouldn’t be too difficult. However, I wanted to be sure that the API calls made sense and would be intuitive. That brought me to this discussion about pagination in a REST web application. What I take away from that post is that it’s fine to mix query string parameters with RESTful identification of a web resource. That wasn’t clear to me before.

One helpful tip given in one of the comments was to look at the Twitter API. I did have a look and I do agree that it’s very clean. They have a clean and clear style of documenting the API and the structure of their RESTful calls, which includes both identification of resources and parameters to paginate, limit or otherwise clarify aspects of that resource.

Structuring my API so that it clearly identifies resources and accepts parameters in a query string in the URI is how I’ll approach development for the current software licensing plugin going forward.

Software Engineering

WordPress plugin licensing: API and class diagram

In the first article in this series, I sketched out some of the basic design ideas for the wordpress plugin licensing system.

The next step is to come up with some more concrete details about how the application will work, what information it will manage and what the interfaces will look like when accessing it. I really like using the UML as I flesh out a design. In particular I find the class diagram and activity diagram useful.

I frequently make use of stereotypes to extend the meaning of my classes. I’m not sure if this is exactly how the UML was intended to be used, but it’s really helpful. At times I also use simple tables, hand drawings, etc. if it helps capture an important point.

Here are the first design documents showing the classes I have in mind for the application and the RESTful API that will provide access to them.

Classes and API

Design choices

My initial design included normalized classes for Licensee, Product and LicenseTemplate. This would prevent duplication of licensee data across multiple licenses and would facilitate reporting by liensee and product. I also had APICall associated with AdministratorAccount since each API call could correspond to various objects in the system.

As I fleshed out the API, a few drawbacks came to mind regarding the normalization I mentioned above. First was the additional complexity in managing all of the normalized classes and associating parent and child objects. Since I am not targeting an RDBMS, the traditional SQL joins and other query approaches don’t benefit me with properly normalized design. I can make the application much more performant and simple by keeping everything in a single SiteLicenses container as shown.

Next design steps

You can see from the diagram above that I haven’t begun assigning responsibilities yet. However, it’s easy to see already where some of the key responsibilities, like isPremiumAccount(), getSiteLicenses(), revokeLicense(), getAPICallsMinutes(Integer minutesBack), etc. will end up.