TSC(Time Stamp Counter)の取得と設定 - 備忘録

TSC(Time Stamp Counter)の取得と設定 - 備忘録

仕事でちょっと調べることになったのでメモしておく。

モデル固有レジスタ(MSR)

IA-32 インテル® アーキテクチャ ソフトウェア・デベロッパーズ・ マニュアル 下巻:システム・プログラミング・ガイドには以下のように記載されています。(P.832)

レジスタアドレス
(10進)
レジスタ
(フィールド)
モデル利用 共有 ビット説明
0x10H (16) IA32_TIME_STAMP_ COUNTER (63:0) 0, 1, 2, 3 独自 タイムスタンプ・カウンタ。15.7. 節「タイムスタンプ・カウンタ」を参照のこと。タイムスタンプ・カウント値。(R/W) 現在のタイムスタンプ・カウント値を返す。 全64ビットが読み取り可能だが、書き込みできるのは下位32ビットだけである。下位32ビットに書き込みを行なうときは、上位32ビットがクリアされる。

msr-toolsを取得してコンパイルする

  1. kernel.orgからmsr-toolsを取得する。
  2. コンパイルする
    # tar zxf msr-tools-1.1.2.tar.gz && cd msr-tools-1.1.2
    # make
    gcc -Wall -g -O2 -fomit-frame-pointer -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64  -o wrmsr wrmsr.c
    gcc -Wall -g -O2 -fomit-frame-pointer -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64  -o rdmsr rdmsr.c
  3. rdmsr,wrmsrコマンドのできあがり
    # ls -lart
    total 68
    -rwxrwxr-x. 1 1026 1026   231 Oct 10  2001 MAKEDEV-cpuid-msr
    -rw-rw-r--. 1 1026 1026  2719 Jul 21  2004 wrmsr.c
    -rw-rw-r--. 1 1026 1026   104 Jul 21  2004 version.h
    -rw-rw-r--. 1 1026 1026  7105 Jul 21  2004 rdmsr.c
    -rw-rw-r--. 1 1026 1026  1030 Jul 21  2004 Makefile
    dr-xr-x---. 4 root root  4096 Jan  5 03:23 ..
    -rwxr-xr-x. 1 root root 14013 Jan  5 03:24 wrmsr   <--コレと...
    drwxrwxr-x. 2 1026 1026  4096 Jan  5 03:24 .
    -rwxr-xr-x. 1 root root 19370 Jan  5 03:24 rdmsr   <--コレね

MSRからTSCの値を取得する

rdmsrコマンドでMSRからTSCの値を取得する

  • この例ではCPU0(-p0)からTSC(0x10h)の値を取得する
    # ./rdmsr -u -p0 0x10h
    10781437437367

MSRにTSCの値を設定する

wrmsコマンドでMSRにTSCの値をセットする

  • この例ではTSCを0に設定する
# ./wrmsr -p0 0x10h 0

じゃ、休み明けから再現テストしようかな。

参考資料

OpenStack ~ Quick Start ~を最新(2013/12/29現在)のopenstack-packstackに対応させる

RDO & Packstackで本当にQuickStartする

このBlogは日本OpenStackユーザ会が主催しているAdvent Calendar 2013の12/29版として、前日の @kkitase さんの12/28版の後を受けて書いています。

インストールにはPackstackを利用しているのですが、結構な頻度でバージョンアップしていて、構築に利用するanswer-fileがすぐに古くなってしまいます。

# Package Name Date
0 openstack-packstack-2013.2.1-0.1.dev642.el6.noarch.rpm 16-Jul-2013 15:20
1 openstack-packstack-2013.2.1-0.1.dev691.el6.noarch.rpm 01-Aug-2013 12:34
2 openstack-packstack-2013.2.1-0.2.dev702.el6.noarch.rpm 13-Aug-2013 17:21
3 openstack-packstack-2013.2.1-0.4.dev740.el6.noarch.rpm 02-Sep-2013 15:36
4 openstack-packstack-2013.2.1-0.6.dev749.el6.noarch.rpm 09-Sep-2013 10:12
5 openstack-packstack-2013.2.1-0.7.dev752.el6.noarch.rpm 09-Sep-2013 18:09
6 openstack-packstack-2013.2.1-0.9.dev756.el6.noarch.rpm 10-Sep-2013 17:06
7 openstack-packstack-2013.2.1-0.10.dev763.el6.noarch.rpm 24-Sep-2013 17:50
8 openstack-packstack-2013.2.1-0.11.dev806.el6.noarch.rpm 17-Oct-2013 15:03
9 openstack-packstack-2013.2.1-0.13.dev840.el6.noarch.rpm 04-Nov-2013 16:25
10 openstack-packstack-2013.2.1-0.15.dev847.el6.noarch.rpm 14-Nov-2013 15:33
11 openstack-packstack-2013.2.1-0.16.dev870.el6.noarch.rpm 03-Dec-2013 09:11
12 openstack-packstack-2013.2.1-0.17.dev876.el6.noarch.rpm 03-Dec-2013 15:47
13 openstack-packstack-2013.2.1-0.25.dev936.el6.noarch.rpm 20-Dec-2013 13:44

そこで、OpenStack QuickStartを改訂(v1.8)して、Packstackとanswer-fileのバージョンを現時点での最新版に決め打ちしてインストール作業を行うことにより、バージョンの差異によるインストールの失敗が発生しないように修正しました。

answer-file

github上では、バージョン番号でtagを打ちました

2013/12/29現在では、以下の2つのバージョンに対応しています。

$ git tag
openstack-packstack-2013.2.1-0.17.dev876
openstack-packstack-2013.2.1-0.25.dev936

パスワードとトークンは変更してね

answer-file(all-in-one.conf, minimal.conf)のデフォルトパスワードは"changeme"にしてあります。
※packstackコマンド実行前に必ず変更するようにしてください。

CONFIG_MYSQL_PW=changeme
CONFIG_QPID_NSS_CERTDB_PW=changeme
CONFIG_QPID_AUTH_PASSWORD=changeme
CONFIG_KEYSTONE_DB_PW=changeme
CONFIG_KEYSTONE_ADMIN_TOKEN=changeme
CONFIG_KEYSTONE_ADMIN_PW=changeme
CONFIG_KEYSTONE_DEMO_PW=changeme
CONFIG_GLANCE_DB_PW=changeme
CONFIG_GLANCE_KS_PW=changeme
CONFIG_CINDER_DB_PW=changeme
CONFIG_CINDER_KS_PW=changeme
CONFIG_NOVA_DB_PW=changeme
CONFIG_NOVA_KS_PW=changeme
CONFIG_NEUTRON_KS_PW=changeme
CONFIG_NEUTRON_DB_PW=changeme
CONFIG_NEUTRON_METADATA_PW=changeme
CONFIG_SWIFT_KS_PW=changeme
CONFIG_SWIFT_HASH=changeme
CONFIG_HEAT_DB_PW=changeme
CONFIG_HEAT_KS_PW=changeme
CONFIG_CEILOMETER_SECRET=changeme
CONFIG_CEILOMETER_KS_PW=changeme
CONFIG_NAGIOS_PW=changeme

all-in-oneホストのIPアドレスの変更をする場合

all-in-oneホストのIPアドレス

以下のパラメータにall-in-oneホストのIPアドレスを指定してください。
VMware Fusionを利用したりしていて、各セグメントのホストアドレスの先頭がHostOSで利用されてしまうため、OpenStack QuickStartに記載されている例とは別のIPアドレスをall-in-oneホストに設定するような場合は変更が必要となります。

CONFIG_MYSQL_HOST=
CONFIG_QPID_HOST=
CONFIG_KEYSTONE_HOST=
CONFIG_GLANCE_HOST=
CONFIG_CINDER_HOST=
CONFIG_NOVA_API_HOST=
CONFIG_NOVA_CERT_HOST=
CONFIG_NOVA_VNCPROXY_HOST=
CONFIG_NOVA_COMPUTE_HOSTS=
CONFIG_NOVA_CONDUCTOR_HOST=
CONFIG_NOVA_SCHED_HOST=
CONFIG_NOVA_NETWORK_HOSTS=
CONFIG_NEUTRON_SERVER_HOST=
CONFIG_NEUTRON_L3_HOSTS=
CONFIG_NEUTRON_DHCP_HOSTS=
CONFIG_NEUTRON_METADATA_HOSTS=
CONFIG_OSCLIENT_HOST=
CONFIG_HORIZON_HOST=
CONFIG_SWIFT_PROXY_HOSTS=
CONFIG_SWIFT_STORAGE_HOSTS=
CONFIG_HEAT_HOST=
CONFIG_HEAT_CLOUDWATCH_HOST=
CONFIG_HEAT_CFN_HOST=
CONFIG_CEILOMETER_HOST=
CONFIG_NAGIOS_HOST=

FloatingIPアドレス

前項と同様、FloatingIPアドレスを変更する場合には、以下のパラメータを変更してください。

CONFIG_NOVA_NETWORK_FLOATRANGE=192.168.0.0/24  <-- ココを適切に変更
CONFIG_PROVISION_DEMO_FLOATRANGE=192.168.0.0/24  <-- ココを適切に変更

まとめ

PackstackでのOpenStack構築は、これで恐らく問題なくQuickStartできるハズです。 みなさんも年末年始、是非お手元でOpenStackを動かしてみてください。 それではみなさん良いお年を:-)

Special Thanks

  • 初版から付き合ってくれている横田愛美さん、いつもありがとう
  • Okinawa Open Days の開催期間中、夜遅くまでデバッグに付き合ってくれた @ii__ya さん @MTomokim さん
  • テストしてくれた @ritchey98 さん @kkitase さん @giraffeforestg さん
  • RDO & Packstackの開発に関わっている全てのみなさん

みなさんのおかげでQuickStartできそうです。ありがとうございます。

感謝 m(__)m

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

続:クリスマスは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がエンドポイントを持つというのは、ひと手間かかりますね。

RDO & packstack & VirtualBox でOpenStackを簡単インストール

RDO & packstack & VirtualBox でOpenStackを簡単インストール
とある場所で話すことになったので書いてみた。
おれ、次はちゃんと2週間前くらいから書き始めるんだ...
OpenStack QuickStart - havana

AnsibleのJUNOS設定モジュール書いてみた(VLAN編)

Ansibleのお勉強のためにJUNOS向けにNETCONFでVLANを作成・削除するモジュールを書いてみた。
動いてるけど、VC環境だとcommit syncするときにタイムアウトするかも。
Ex3300とEx2200で動作確認した。意外にちゃんとうごいて冪等性も担保できてる。

引数

  • state: 作成する時(present),削除する時(absent)
  • node: 操作対象ネットワーク機器のノード名またはIPアドレス
  • port: 操作対象ネットワーク機器がNETCONF用に口をあけているポート(デフォルト:830)
  • user: 操作対象ネットワーク機器のログインアカウント
  • password: 操作対象ネットワーク機器のパスワード
  • vlan_name: VLAN名
  • vlan_id: VLANID
  • vlan_desc: VLANのDescription

テストしてみる

VLANを作成する
$ ~/ansible/hacking/test-module -m ./junos_vlan -a "node='switch00' port=830 user='foo' password='bar' state='present' vlan_name='VLAN3000' vlan_id=3000 vlan_desc='test vlan'"
VLANを削除する

もちろん対象VLANが存在しなければ何もしない。

$ ~/ansible/hacking/test-module -m ./junos_vlan -a "node='switch00' port=830 user='foo' password='bar' state='absent' vlan_name='VLAN3000' vlan_id=3000 vlan_desc='test vlan'"

モジュール

#!/usr/bin/env python
# -*- coding: utf-8 -*-

DOCUMENTATION = '''
---
module: junos_vlan
author: Hideki Saito
short_description: Manage VLAN resources
requirements:
    - ncclient v0.3.2
description:
    - Manage VLAN resources on JUNOS devices.  This module requires the
      ncclient.
      [link]
      http://ncclient.grnet.gr/
options:
    node:
        desctiption
            - the hostname or IP address to connect
        requred: true
    port:
        description
            - the port number to connect
    user:
        description
            - junos user id
        required: true
    password:
        description
            - junos user password
        requre: true
    vlan_name:
        description:
            - name of vlan
        required: false
    vlan_id:
        description:
            - the vlan id
        required: true
    vlan_desc:
        description:
            - a descriptive name for the vlan
        required: false
    state:
        description:
            - describe the desired state of the vlan related to the config
        required: false
        default: 'present'
        choices: [ 'present', 'absent' ]
    logging:
        description:
            - enables or disables the syslog facility for this module
        required: false
        choices: [ 'true', 'false', 'yes', 'no' ]
notes:
    - The ncclient module must be installed.
    - See http://ncclient.grnet.gr/ for details
'''

import json
from jinja2 import Template
from ncclient import manager
from ncclient.operations.errors import TimeoutExpiredError
from xml.etree import ElementTree


create_vlan = """
<config>
  <configuration>
    <vlans>
      <vlan operation="create">
        <name>{{ vlan_name }}</name>
        <description>{{ vlan_desc }}</description>
        <vlan-id>{{ vlan_id }}</vlan-id>
      </vlan>
    </vlans>
  </configuration>
</config>
"""

delete_vlan = """
<config>
  <configuration>
    <vlans>
      <vlan operation="delete">
        <name>{{ vlan_name }}</name>
        <vlan-id>{{ vlan_id }}</vlan-id>
      </vlan>
    </vlans>
  </configuration>
</config>
"""

set_description = """
<config>
  <configuration>
    <vlans>
      <vlan>
        <name>{{ vlan_name }}</name>
        <description>{{ vlan_desc }}</description>
      </vlan>
    </vlans>
  </configuration>
</config>
"""


class JunosVlan(object):
    def __init__(self, module):
        self.module = module
        self.node = module.params['node']
        self.port = module.params['port']
        self.user = module.params['user']
        self.password = module.params['password']
        self.vlan_name = module.params['vlan_name']
        self.vlan_id = module.params['vlan_id']
        self.vlan_desc = module.params['vlan_desc']
        self.state = module.params['state']
        with manager.connect(host=self.node,
                             port=self.port,
                             username=self.user,
                             password=self.password,
                             hostkey_verify=False) as m:
            self.config = m.get_config(source='candidate').data_xml

    def push(self, config):
        try:
            with manager.connect(host=self.node,
                                 port=self.port,
                                 username=self.user,
                                 password=self.password,
                                 unknown_host_cb=_always_unknown_true) as mgr:
                mgr.edit_config(target="candidate",
                                config=config,
                                test_option="test-then-set")
                mgr.commit()
                rc = 0
                out = 'commit succeeded'
                err = ''
        except TimeoutExpiredError as e:
            rc = 1
            out = 'commit failure'
            err = 'operation timeout'
        return (rc, out, err)

    def vlans(self):
        elm = ElementTree.fromstring(self.config)
        elm_vlans = elm.find('.//vlans')
        vlan_info = dict()
        for elm_vlan in elm_vlans.findall('.//vlan'):
            vlan_id = elm_vlan.findtext('.//vlan-id')
            name = elm_vlan.findtext('.//name')
            description = elm_vlan.findtext('.//description')
            vlan_info[name] = dict(vlan_id=vlan_id, description=description)
        return vlan_info.keys()

def _always_unknown_true(host, fingerprint):
    return True


def main():

    module = AnsibleModule(
        argument_spec = dict(
            node=dict(default=None, requred=True, type='str'),
            port=dict(default=830, requred=True, type='int'),
            user=dict(default=None, requred=True, type='str'),
            password=dict(default=None, requred=True, type='str'),
            vlan_name=dict(default=None, required=True, type='str'),
            vlan_id=dict(default=None, type='int'),
            vlan_desc=dict(default=None, type='str'),
            state=dict(default='present',
                       choices=['present', 'absent']),
        ),
        supports_check_mode = True
    )

    obj = JunosVlan(module)
    rc = None
    result = dict()
    changed = False

    if obj.state == 'absent':
        if obj.vlan_name in obj.vlans():
            template = Template(delete_vlan)
            config = template.render(vlan_name=obj.vlan_name,
                                     vlan_id=obj.vlan_id)
            if module.check_mode:
                module.exit_json(changed=True)
            (rc, out, err) = obj.push(config)
            result['results'] = out

    elif obj.state == 'present':
        if obj.vlan_name in obj.vlans():
            template = Template(set_description)
            config = template.render(vlan_name=obj.vlan_name,
                                     vlan_desc=obj.vlan_desc)
        else:
            template = Template(create_vlan)
            config = template.render(vlan_name=obj.vlan_name,
                                     vlan_id=obj.vlan_id,
                                     vlan_desc=obj.vlan_desc)
        if module.check_mode:
            module.exit_json(changed=True)
        (rc, out, err) = obj.push(config)
        result['results'] = out

        if rc is not None and rc != 0:
            module.fail_json(msg=err, rc=rc)

    if rc is None:
        result['changed'] = False
    else:
        result['changed'] = True

    module.exit_json(**result)


# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

##
## [EOF]
##