DevOps Zone is brought to you in partnership with:

SysAdmin / Operations guy at DataSift managing the various elements of DataSift.com. Casual programmer in my spare time usually working with LAMP or Android (Java) but I also work with C++, Arduino and even embedded C on occasion (see blog for examples). In the past I've ran network cabling, managed the IT for a small business, been part of the SysAdmin team for a data center and managed a team of 10 SysAdmins at a managed hosting company that covered 4 data centers in 3 countries. Gareth is a DZone MVB and is not an employee of DZone and has posted 7 posts at DZone. You can read more from them at their website. View Full User Profile

Android Client for Chef: Cyllell

10.01.2012
| 4054 views |
  • submit to reddit

TL;DR; – Download it here.

When you have a server estate the size of DataSift you need a configuration management platform to keep track of what does what, how it is configured and to assist you in rolling out 10 new memcached servers with an identical configuration to the others in production at the drop of a hat. To do that we use Chef from OpsCode.

To interact with Chef you can either use the Web Interface or the CLI client called Knife.

Knife is a powerful command-line interface (CLI) that comes with Chef.
It is used by administrators to interact with the Chef Server API and the local Chef repository. It provides the capability to manipulate nodes, cookbooks, roles, databags, environments, etc., and can also be used to provision cloud resources and to bootstrap systems.


I love CLI’s and prefer them to GUI’s whenever I get the choice but I’m a sucker for writing an Android app if there’s an API available (much like with Rhybudd for Zenoss). So some time ago I set about creating an Android application that could emulate some of the features of Knife from the convenience of your phone.

The biggest hurdle was authenticating with the API as it uses MixLib::Authentication which according to the documentation provides a class-based header signing authentication object. This signed header object is then sent via HTTPS (usually self signed) and has to be within a strict time window.

Looking at the Wiki documentation regarding authenticated API requests it seems quite straight forward but when you discover that versions of Android below 2.2 don’t even have a Base64 encode function you know it’s going to be a bit of a slog.

Making the authenticated headers requires three key steps, first the cryptographic signing of the Headers with your private key;

PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(this.PrivateKey.getBytes(),0));
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey pk = kf.generatePrivate(spec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
cipher.init(Cipher.ENCRYPT_MODE, pk);
byte[] EncryptedStream = new byte[cipher.getOutputSize(dataToSign.length())];
try
{
 cipher.doFinal(dataToSign.getBytes(),0,dataToSign.length(), EncryptedStream,0);
}
catch (ShortBufferException e)
{
 // TODO Auto-generated catch block
 e.printStackTrace();
}
return Base64.encodeToString(EncryptedStream, Base64.NO_WRAP);

Secondly creating a variety of headers with other information;

Headers.add(new BasicNameValuePair("Accept","application/json"));
Headers.add(new BasicNameValuePair("Content-Type","application/json"));
Headers.add(new BasicNameValuePair("X-Ops-Sign","version=1.0"));
Headers.add(new BasicNameValuePair("X-Ops-Userid",this.ClientName));
Headers.add(new BasicNameValuePair("X-Ops-Timestamp",TimeStamp));
Headers.add(new BasicNameValuePair("X-Ops-Content-Hash",Disgesteriser.hash_string(Body)));
signed_canonicalize_request = SignHeaders("Method:GET"+
"\nHashed Path:" + Disgesteriser.hash_string(Path) +
"\nX-Ops-Content-Hash:"+Disgesteriser.hash_string(Body)+
"\nX-Ops-Timestamp:"+TimeStamp+
"\nX-Ops-UserId:"+this.ClientName);

Finally the cyrptographic signature needs to be added to the headers with the proviso that each line can’t be more than 60 characters;

while(rubyLength < 61 && charLocation < signed_canonicalize_request.length())
{
if(signed_canonicalize_request.charAt(charLocation) != '\n' && signed_canonicalize_request.charAt(charLocation) != '\r')
{
AuthString += signed_canonicalize_request.charAt(charLocation);
rubyLength++;
}
charLocation++;
}
Headers.add(new BasicNameValuePair("X-Ops-Authorization-"+Integer.toString(AuthorizationIteration),AuthString));

With all the authenticating work done getting the information is quite trivial from then on. As described on the OpsCode wiki the API endpoint for a list of cookbooks is simply /cookbooks.

String Path = "/cookbooks";
this.httpget = new HttpGet(this.ChefURL + Path);
List <NameValuePair> Headers = ChefAuth.GetHeaders(Path, "");
for(int i = 0; i < Headers.size(); i++)
{
     this.httpget.setHeader(Headers.get(i).getName(),Headers.get(i).getValue());
}
String jsonTempString = httpClient.execute(this.httpget, responseHandler);

The App is styled in the same manner as the OpsCode website;

A Read Only functionality Beta version is available on Google Play now;

Android app on Google Play

Any feedback can be sent to @NetworkString, Gareth@NetworksAreMadeOfString.co.uk or by sending ICMP packets padded with your suggestion to 2a01:348:18e:2::2

Published at DZone with permission of Gareth Llewellyn, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)