二日酔いのアナタに目の覚めるようなpythonワンライナーをお届け

CloudStackな皆さん、はじめまして。
昨日の忘年会はいかがでしたか?賑やかなタイムラインを横目に、風邪ひいてる僕はずっとコレを書いてましたよ。
それではAdventCalendarの番外編ということで、今日はCloudStack素人な僕がAPIを叩くPythonスクリプトをお届けします。
いやぁCloudStackのドキュメントは充実してていいですね。

APIを叩くためのポイントは以下の通り

■なにはなくともAPI鍵と秘密鍵

管理者からもらいましょう。
僕の場合は @hirolovesbeerさんに土下座して拝領いたしました。

■リクエストURLとコマンド文字列。そして署名。

APIリクエストは、

[書式]
ベースURL/APIパス?コマンド文字列&署名

形式で送信される。

  • ベースURL
[書式]
http://$CS_HOST_URL
[書式]
/client/api
  • コマンド文字列

フィールド=値 で構成される。値はケースセンシティブ。

command=listVirtualMachines&response=json&apiKey=$CS_API_KEY
  • 署名

ユーザーの秘密鍵とHMAC SHA-1ハッシュアルゴリズムにより生成された、ベースURL用の署名ハッシュ。

signature=$SIGNATURE

■署名方法

署名方法がちょっと難解なので自分なりにまとめてみました。

1. コマンド文字列を生成する
2. コマンド文字列内の(「&」で区切られた)各「フィールド=値」ペアを、URLエンコードする。

注) スペースは、すべて「+」ではなく「%20」でエンコードすること。

3. コマンド文字列をすべて小文字にする。
4. 各「フィールド=値」ペアをフィールドのアルファベット順に並べ替える。

1-4を実施するとコマンド文字列(例)は以下のようになる。

apikey=QcEhNNSVaCRO9S8qa2rXU6evZA78FyAuQk0YKKoCzoiMi_ySgMHiDhW8BN0T-vXQIbU0a58XxIJOtQmKaMvIhw&command=deployvirtualmachine&diskofferingid=1&serviceofferingid=1&templateid=2&zoneid=4
5. 並べ替えたコマンド文字列を、ユーザーの秘密キーと共にHMAC SHA-1アルゴリズムにかけ、生成されたダイジェストメッセージのバイト列を得る。
6. 5.で生成したバイト列をBase64エンコードし、さらにsignature=値の形式でurlencodeする。

5-6を実施すると以下のような署名が得られる。

signature=WoxW36LDDHwtASPOPs737jsgzho%3D

1-6を実施して生成されたURLを「ベースURL+APIパス+コマンド文字列+署名」形式に再構成すると、最終的なURLは以下のようになる。

http://localhost:8080/client/api?command=deployVirtualMachine&serviceOfferingId=1&diskOfferingId=1&templateId=2&zoneId=4&apikey=QcEhNNSVaCRO9S8qa2rXU6evZA78FyAuQk0YKKoCzoiMi_ySgMHiDhW8BN0T-vXQIbU0a58XxIJOtQmKaMvIhw&signature=WoxW36LDDHwtASPOPs737jsgzho%3D

■書いてみる

まずはふつうに書いてみる。

  • ふつうのコード
  1 #!/usr/bin/env python
  2 # -*- coding: utf-8 -*-
  3 
  4 import os
  5 import base64
  6 import urllib
  7 import json
  8 
  9 import hmac
 10 import sha
 11 
 12 ##
 13 ## 環境変数からCloudStackAPI接続情報を取得
 14 ##
 15 url = os.environ["CS_HOST_URL"]
 16 api_key = os.environ["CS_API_KEY"]
 17 secret_key = os.environ["CS_SECRET_KEY"]
 18 
 19 
 20 def create_request():
 21     data = {}
 22     data["response"] = "json"
 23     data["command"] = "listVirtualMachines"
 24     data["apikey"] = os.environ["CS_API_KEY"]
 25     # ひとまずurlencodeする
 26     # [注意] "+"は"%20"に変換した文字列encoded_dataを作成
 27     encoded_data = urllib.urlencode(data).replace("+","%20").lower()
 28     # keyのアルファベット順に並び替えた辞書型sorted_dataを作成
 29     sorted_data = dict([buff.split("=") for buff in encoded_data.split("&")]    )   
 30     # 再度urlencodeしたencoded_sdを作成
 31     encoded_sd = urllib.urlencode(sorted_data)
 32     # encoded_sdのdigestを取得してbase64encodeし、signatureを作成する
 33     digest = hmac.new(os.environ["CS_SECRET_KEY"], encoded_sd, sha)
 34     signature = urllib.quote(base64.b64encode(digest.digest()))
 35     # 署名を追加したコマンド文字列を返す
 36     return "%s&signature=%s" % (urllib.urlencode(data), signature)
 37 
 38 
 39 if __name__=="__main__":
 40     request = create_request()
 41     connection = urllib.urlopen("%s?%s" % (os.environ["CS_HOST_URL"], reques    t))
 42     print json.dumps(json.loads(connection.read()),indent=2)
 43     connection.close()
 44 
 45 
 46 ##
 47 ## [EOF]
 48 ##
  • ふつうの結果

なんとなく取れたっぽい。
くそーなんか取得できる情報の項目が実に洗練されてるじゃないか...どれと比較してとは言わないけれど。

{
  "listvirtualmachinesresponse": {
    "count": 1, 
    "virtualmachine": [
      {
        "domain": "ROOT", 
        "domainid": "3c2f6a65-0917-4acf-858c-9bdfea756595", 
        "haenable": false, 
        "isoid": "815be793-e2bd-4075-8d95-4655f3c4ec0d", 
        "templatename": "CentOS-6.3-livecd.iso", 
        "securitygroup": [
          {
            "description": "Default Security Group", 
            "id": "15dc21dc-8ee3-4285-8e3f-90535b8d0635", 
            "name": "default"
          }
        ], 
        "zoneid": "e1ae8354-07f2-4c7e-8156-b01950869174", 
        "cpunumber": 1, 
        "passwordenabled": false, 
        "instancename": "i-2-5-VM", 
        "id": "01d4453e-a18b-45aa-9fdb-2758456e78f3", 
        "cpuused": "0.02%", 
        "isoname": "CentOS-6.3-livecd.iso", 
        "hostname": "sv.example.com", 
        "state": "Running", 
        "guestosid": "3a9f75cf-476b-4b56-88a7-c3598606dab9", 
        "networkkbswrite": 0, 
        "cpuspeed": 1000, 
        "serviceofferingid": "3142ada2-ae65-4f58-bf52-3adf56f2c741", 
        "zonename": "zone1", 
        "displayname": "livecd", 
        "tags": [], 
        "nic": [
          {
            "networkid": "c9922da5-d1bd-40f0-880e-d2954700d8b0", 
            "macaddress": "06:f4:c2:00:00:35", 
            "isolationuri": "ec2://untagged", 
            "type": "Shared", 
            "gateway": "10.10.0.1", 
            "traffictype": "Guest", 
            "broadcasturi": "vlan://untagged", 
            "netmask": "255.255.0.0", 
            "ipaddress": "10.10.0.153", 
            "id": "dc8f41c0-e84f-4bbd-8b4f-dd57146c3fdb", 
            "isdefault": true
          }
        ], 
        "memory": 1024, 
        "templateid": "815be793-e2bd-4075-8d95-4655f3c4ec0d", 
        "account": "admin", 
        "hostid": "442d66a0-3a09-4d9d-8b15-a7a3a63f92b8", 
        "name": "livecd", 
        "networkkbsread": 23577, 
        "created": "2012-12-05T21:37:56+0900", 
        "hypervisor": "KVM", 
        "rootdevicetype": "NetworkFilesystem", 
        "rootdeviceid": 0, 
        "serviceofferingname": "Medium Instance", 
        "templatedisplaytext": "CentOS-6.3-livecd.iso"
      }
    ]
  }
}

■それではワンライナーで書いてみます

これで仕組みもわかりました。もうね、ちゃちゃっとワンライナーでも書けますよ。とりあえず、さきほどのふつうのコードを何も考えずに直してみます。
";"を使わずタプルの評価をorで繋ぐ感じで書いてみました。

python -c "import os,sys,base64,urllib,json,hmac,sh;(globals().__setitem__('data',{'response':'json','command':'listVirtualMachines','apikey':os.environ['CS_API_KEY']}))or(globals().__setitem__('encoded_data',urllib.urlencode(data).replace('+','%20').lower()))or(globals().__setitem__('sorted_data',dict([buff.split('=') for buff in encoded_data.split('&')])))or(globals().__setitem__('encoded_sd',urllib.urlencode(sorted_data)))or(globals().__setitem__('digest',hmac.new(os.environ['CS_SECRET_KEY'], encoded_sd, sha)))or(globals().__setitem__('signature',urllib.quote(base64.b64encode(digest.digest()))))or(globals().__setitem__('connection',urllib.urlopen('%s?%s&signature=%s'%(os.environ['CS_HOST_URL'],urllib.urlencode(data),signature))))or(sys.stdout.write(json.dumps(json.loads(connection.read()),indent=2)))"

動作してるっぽいです。
やっぱり署名のところが厄介かな。うーもうこの時間だとlabmdaとか使う気力ない。
初めてのCloudStackAPIでしたが、なんとかなったのはやはりドキュメント類が充実していたからだと思います。すばらしい。

今日はこのあたりで勘弁しといてやるかな。
このワンライナーはふつうに書いただけなので、もっともっと整理できます。時間があったら書き直して見よう。