読者です 読者をやめる 読者になる 読者になる

続:クリスマスはOpenStackをPythonワンライナーで決める!

Python OpenStack

続:クリスマスはOpenStackをPythonワンライナーで決める!

みなさんメリークリスマス。

OpenStackのAPIを叩いて仕事をさせる方法

OpenStackの各コンポーネントは、REST-APIを持っています。 このAPIを適切に利用することにより、novaコマンドなどを利用しなくてもOpenStackに仕事をさせることが可能です。

忘れもしない昨年のクリスマスに書いたクリスマスはOpenStackをPythonワンライナーで決める!ワンライナーを例にして、OpenStackをREST-APIで操作する流れを解説したいと思います。そもそも去年書いたときは動作解説も無しだったとは.....

ワンライナーのコード部分は新たに書こうかとも思いましたが、なんか最近会議ばっかりで、すっかり腕がナマってしまったのであきらめました:)

環境

all-in-one環境を用意してテストしてみました。


------+------------------------ 192.168.0.0/24 (br-ex)
      |
      |
      |.1
+-----------+    +-----------+
| OpenStack |    | Client PC |
+-----+-----+    +-----+-----+
      |.10             |.1
      |                |
      |                |
------+----------------+------- 172.16.0.0/24 (eth1)

1. アクセス用トークンとエンドポイントURLを取得する

まずは、keystoneからトークンを取得する必要があります。このトークンを利用して認可されたエンドポイントにREST-APIでアクセスします。


+-----------+         +-----------+
| client-PC |         | keystone  |
+-----+-----+         +-----+-----+
      |(0)                  |
      |---------(1)-------->|
      |<--------(2)---------|

(0) アクセス用環境変数を設定する

ワンライナーで書いたコード用に環境変数を登録しておく。

export OS_USERNAME=demo
export OS_TENANT_NAME=demo
export OS_PASSWORD=3150a051ead74394
export OS_AUTH_URL=http://172.16.0.10:35357/v2.0

(1) ユーザID,パスワード,テナント名をリクエスト情報に設定てトークンを取得するためのリクエストをkeystoneに送る

リクエスト

{
    "auth":{
        "tenantName":"demo",
        "passwordCredentials":{
            "username": "demo",
            "password": "3150a051ead74394"
        }
    }
}

(2) レスポンスの中からこのトークンと認可された操作対象コンポーネントAPIのエンドポイントURLを取得する

取得したエンドポイントの中で、操作したいコンポーネントのURLに対して操作リクエストを送る。 ただし、移行はリクエストヘッダに"X-Auth-Token: トークンの値"を付加することによって認可を受けているリクエストであることを証明する必要がある。

レスポンス

{
    "access": {
        "token": {
            "issued_at": "2013-12-25T12:00:57.818128",
            "expires": "2013-12-26T12:00:57Z",
            "id": "ココにトークンが入る",
            "tenant": {
              "enabled": true,
              "description": "default tenant",
              "name": "demo",
              "id": "8b6834b5bb184a809160db3616a4d7f0"
            }
        },
        "serviceCatalog": [{
            "endpoints_links": [],
            "endpoints": [{"adminURL": "http://172.16.0.10:8774/v2/8b6834b5bb184a809160db3616a4d7f0", "region": "RegionOne", "publicURL": "http://172.16.0.10:8774/v2/8b6834b5bb184a809160db3616a4d7f0", "internalURL": "http://172.16.0.10:8774/v2/8b6834b5bb184a809160db3616a4d7f0", "id": "2821d2cff5d141a8b529ed593d2017bd"}],
            "type": "compute",
            "name": "nova"
            },{
            "endpoints_links": [],
            "endpoints": [{"adminURL": "http://172.16.0.10:9696/", "region": "RegionOne", "publicURL": "http://172.16.0.10:9696/", "internalURL": "http://172.16.0.10:9696/", "id": "55e53543410449d0b88b3005128373dc"}], 
            "type": "network",
            "name": "neutron"
            },{
            "endpoints_links": [],
            "endpoints": [{"adminURL": "http://172.16.0.10:9292", "region": "RegionOne", "publicURL": "http://172.16.0.10:9292", "internalURL": "http://172.16.0.10:9292", "id": "3b85971ffa9c465d848a4ab36ca1c634"}],
            "type": "image",
            "name": "glance"
            },{
            "endpoints_links": [],
            "endpoints": [{"adminURL": "http://172.16.0.10:8776/v1/8b6834b5bb184a809160db3616a4d7f0", "region": "RegionOne", "publicURL": "http://172.16.0.10:8776/v1/8b6834b5bb184a809160db3616a4d7f0", "internalURL": "http://172.16.0.10:8776/v1/8b6834b5bb184a809160db3616a4d7f0", "id": "69ddc96be8cd4f4b861cdc83d94d1c2f"}],
            "type": "volume",
            "name": "cinder"
            },{
            "endpoints_links": [],
            "endpoints": [{"adminURL": "http://172.16.0.10:8773/services/Admin", "region": "RegionOne", "publicURL": "http://172.16.0.10:8773/services/Cloud", "internalURL": "http://172.16.0.10:8773/services/Cloud", "id": "54c30c98b21d4c6ba0aa954a89238e1b"}],
            "type": "ec2",
            "name": "nova_ec2"
            },{
            "endpoints_links": [],
            "endpoints": [{"adminURL": "http://172.16.0.10:35357/v2.0", "region": "RegionOne", "publicURL": "http://172.16.0.10:5000/v2.0", "internalURL": "http://172.16.0.10:5000/v2.0", "id": "0d9415c8c285442394cd2eccbc8d8563"}],
            "type": "identity",
            "name": "keystone"
            }],
        "user": {
            "username": "demo",
            "roles_links": [],
            "id": "87c6f92c39c24d9bae0af37b27c77c51",
            "roles":[{"name": "_member_"}],
            "name": "demo"
        },
        "metadata": {
            "is_admin": 0,
            "roles": ["9fe2ff9ee4384b1894a90878d3e92bab"]
        }
    }
}

普通に書いたコード

#!/usr/bin/env python

from getpass import getpass
from httplib import HTTPConnection
import json

HOST = "172.16.0.10"
PORT = "35357"

user = raw_input("user: ")
password = getpass("password: ")
tenant = raw_input("tenant: ")

def token(tenant, user, password, session):
    token_path = "/v2.0/tokens"
    header = { "Content-Type": "application/json" }
    request='''{
    "auth":{
        "tenantName":"%s",
        "passwordCredentials":{
            "username": "%s",
            "password": "%s"
        }
    }
}''' % (tenant, user, password)
    session.request("POST", token_path, request, header)
    return json.load(session.getresponse())

if __name__ == "__main__":
   session = HTTPConnection("%s:%s" % (HOST, PORT))

   auth_result = token(tenant, user, password, session)

   session.close()

   print json.dumps(auth_result)

##
## [EOF]
##

ワンライナー

特に工夫もなく普通に書いてみた

python -c 'import re,os;from sys import stdout as s;from httplib import HTTPConnection as c;import json as j;(globals().__setitem__("e",os.environ))or(globals().__setitem__("r",r"^(http|https)://(.*)(/.*)$"))or(globals().__setitem__("f0",lambda a,b:re.match(a,b).groups()))or(globals().__setitem__("f1",lambda b:c(b)))or(globals().__setitem__("p",f1(f0(r,e["OS_AUTH_URL"])[1])))or(p.request("POST",f0(r,e["OS_AUTH_URL"])[2]+"/tokens","{\"auth\":{\"tenantName\":\"%s\",\"passwordCredentials\":{\"username\":\"%s\",\"password\":\"%s\"}}}"%(e["OS_TENANT_NAME"],e["OS_USERNAME"],e["OS_PASSWORD"]),{"Content-Type":"application/json"}))or(s.write(j.dumps(j.load(p.getresponse()),indent=2)))or(p.close())'

2.仮想マシンのリストをnova-apiから取得してみる

前項「アクセス用トークンとエンドポイントURLを取得する」の方法で取得したトークンを使用して、novaが管理している仮想マシンのリストを取得してみます。 他の操作(nova以外)でも基本的には受け取ったトークンを利用してエンドポイントURLにREST-APIでアクセスするという操作は変わりません。


+-----------+         +-----------+
| client-PC |         |   nova    |
+-----+-----+         +-----+-----+
      |(0)                  |
      |---------(1)-------->|
      |<--------(2)---------|
      |---------(3)-------->|
      |<--------(4)---------|

(0) アクセス用環境変数を設定する

前項と同様

(1) ユーザID,パスワード,テナント名をリクエスト情報に設定してトークンを取得するためのリクエストをkeystoneに送る

前項と同様

(2) レスポンスの中からこのトークンと認可された操作対象コンポーネントAPIのエンドポイントURLを取得する

前項と同様

(3) エンドポイントURLの中から"compute"用のエンドポイントURLに向けてリクエストを送信

今回のリスト取得対象となるのは権限を持っているテナントに所属している仮想マシンのみです。

リクエストヘッダ

{
        "Content-Type": "application/json",
        "X-Auth-Token": "取得したトークンをセット"
}

(4) 仮想マシンリストをレスポンスとして受け取る

demoユーザの管理下にある仮想マシン 2台(server1/server2)が取得できました。

{
  "servers": [
    {
      "id": "31e65072-defa-419f-a019-6a6a316fc2ee", 
      "links": [
        {
          "href": "http://172.16.0.10:8774/v2/8b6834b5bb184a809160db3616a4d7f0/servers/31e65072-defa-419f-a019-6a6a316fc2ee", 
          "rel": "self"
        }, 
        {
          "href": "http://172.16.0.10:8774/8b6834b5bb184a809160db3616a4d7f0/servers/31e65072-defa-419f-a019-6a6a316fc2ee", 
          "rel": "bookmark"
        }
      ], 
      "name": "server2"
    }, 
    {
      "id": "fad29e21-831c-41e1-b4c7-6df6b8362f6f", 
      "links": [
        {
          "href": "http://172.16.0.10:8774/v2/8b6834b5bb184a809160db3616a4d7f0/servers/fad29e21-831c-41e1-b4c7-6df6b8362f6f", 
          "rel": "self"
        }, 
        {
          "href": "http://172.16.0.10:8774/8b6834b5bb184a809160db3616a4d7f0/servers/fad29e21-831c-41e1-b4c7-6df6b8362f6f", 
          "rel": "bookmark"
        }
      ], 
      "name": "server1"
    }
  ]
}

ワンライナー

setitemとかlambdaを使ってそれらしく書いてみた。タプルの戻り値の副作用を使って or でパイプライン的に処理しています。

$ python -c 'import re,os;from sys import stdout as s;from httplib import HTTPConnection as c;import json as j;(globals().__setitem__("e",os.environ))or(globals().__setitem__("r0",r"^(http|https)://(.*)(/.*)$"))or(globals().__setitem__("r1",r"^(http|https)://(.*:\d+)(/.*)$"))or(globals().__setitem__("f0",lambda a,b:re.match(a,b).groups()))or(globals().__setitem__("f1",lambda b:c(b)))or(globals().__setitem__("p",f1(f0(r0,e["OS_AUTH_URL"])[1])))or(p.request("POST",f0(r0,e["OS_AUTH_URL"])[2]+"/tokens","{\"auth\":{\"tenantName\":\"%s\",\"passwordCredentials\":{\"username\":\"%s\",\"password\":\"%s\"}}}"%(e["OS_TENANT_NAME"],e["OS_USERNAME"],e["OS_PASSWORD"]),{"Content-Type":"application/json"}))or(globals().__setitem__("z",j.load(p.getresponse())))or(globals().__setitem__("t",z["access"]["token"]["id"]))or(globals().__setitem__("u",f0(r1,[z["endpoints"][0]["publicURL"] for z in z["access"]["serviceCatalog"] if z["type"]=="compute"][0])))or(p.close())or(globals().__setitem__("p",f1(u[1])))or(p.request("GET",u[2]+"/servers","",{"Content-Type":"application/json","X-Auth-Token":t}))or(s.write(j.dumps(j.load(p.getresponse()),indent=2)))or(p.close())'

テナント(プロジェクト)情報を確認する(おまけ)

admin_tokenの使いどころ

一般ユーザがAPI経由でOpenStackを操作するためのtokenを取得するためには、対象ユーザが所属するテナント情報が必要となります。そのテナント情報は、、、もちろんあらかじめ管理者から知らされている訳です。 では、管理者が自身の管理下にあるテナントの情報を取得するためには、どうしたら良いのでしょうか?

/etc/keystone.conf内で設定しているadmin_tokenの使いどころはココです!
以下のワンライナーではkeystone.conf内で定義したadmin_tokenを環境変数のADMIN_TOKENの値として設定しています。

リクエストヘッダ

{
     "Content-Type": "application/json",
     "X-Auth-Token": "トークン"",
}

レスポンス

{
    "tenants": 
        [
            {
                "enabled": true,
                "description": "Tenant for the openstack services",
                "name": "services",
                "id": "0b30ec27e81345a4b34d95e10fe8cbc7"
            }, 
            {
                "enabled": true,
                "description": "alt tenant",
                "name": "alt_demo",
                "id": "213da8890d644b3d9420ede8c4fb0a38"
            },
            {
                "enabled": true,
                "description": "demo1",
                "name": "demo1",
                "id": "554bed77ffbb4c14b678ff3c27d76566"
            },
            {
                "enabled": true,
                "description": "default tenant",
                "name": "demo",
                "id": "8b6834b5bb184a809160db3616a4d7f0"
            },
            {
                "enabled": true,
                "description": "admin tenant",
                "name": "admin",
                "id": "ccd340020fbd4ee79cfd376fc447212a"
            }
        ],
    "tenants_links": []
}

普通に書いたコード

  1. Header情報にトークンをセットする
  2. http://172.16.0.10/v2.0/tenantsにGETリクエストを投げる
  3. レスポンスはJSON形式で返される
#!/usr/bin/env python

from getpass import getpass
from httplib import HTTPConnection
import json

HOST = "172.16.0.10"
PORT = "35357"

token = "044b77537ed54c2dba5d71906d2458ec"

def tenants(id, session):
    tenant_path = "/v2.0/tenants"
    header = {
        "Content-Type": "application/json",
        "X-Auth-Token": token,
        }
    print json.dumps(header)
    session.request("GET", tenant_path, "", header)
    return json.load(session.getresponse())
    

if __name__ == "__main__":
   session = HTTPConnection("%s:%s" % (HOST, PORT))

   tenant_list = tenants(id, session)
   
   session.close()
   print json.dumps(tenant_list)

##
## [EOF]
##

まとめ

APIへのリクエストをまとめて管理してくれるリクエストブローカー的な仕組みがあるとコードが利用するためのコードを書きやすいんだけどな。 それぞれのコンポーネントAPIがエンドポイントを持つというのは、ひと手間かかりますね。