Keystone トークン機能

概要

Keystone のトークンは、2つ以上の技術の層で構成される階層構造となっている。
ペイロードは、いくつかのトランスポート形式でラップされており、一意性、ID、および認証コンテキストなどの属性を提供する。
トランスポートフォーマットでは、伝送と検証に必要なパッケージを提供する。

Keystone で使用可能な認証ドライバとして、UUID、PKI、PKIZ、Fernet がある。(2018年11月20日 時点)。
それぞれのメリット・デメリットについて以下に示す。

Keystone 認証ドライバ メリット デメリット 備考
UUID - シンプルで最小のトークン形式
シンプルな OpenStack の環境をつくる場合には良い
- 永続的にトークンを保持する必要がある
- トークンテーブルが大きくなるとパフォーマンスに影響が出る
- トークン検証にはメタデータを保持した keystone 等の認証サービスへの接続が必須
- マルチ OpenStack 環境には不向き
Newton 以降、Keystone のデフォルトのトークンドライバが “UUID” から “fernet” に変更となる
- Rocky で削除予定
PKI トークン検証として keystone への問い合わせは不要 標準的な HTTP ヘッダーサイズより大きい
- 複雑な設定
- マルチ OpenStack 環境への適用は不可
Ocata 以降、非推奨となる
PKIZ トークン検証として keystone への問い合わせは不要
- 圧縮により PKI のトークンサイズよりは小さくなる
標準的な HTTP ヘッダーサイズより大きい
- 複雑な設定
- マルチ OpenStack 環境への適用は不可
Fernet 永続的にトークンを保持しないため、定期的にトークンを削除する cron ジョブは不要
- トークンサイズサイズが小さい
- マルチ DC 環境に最適
失効イベント数に比例してトークン検証のレスポンス時間が増加 Ocata 以降、デフォルトで使用される

UUID

ペイロード: UUID4

Keystone がサポートする最もシンプルなトークン形式で、UUID トークンのデータは、一意のランダムに生成された UUID4 値のみで構成される。

In [1]: import uuid

In [2]: payload = uuid.uuid4()

In [3]: payload
Out[3]: UUID('0b1e0002-31c8-482a-837f-e2842b2c3d92')

In [4]: print(payload)
0b1e0002-31c8-482a-837f-e2842b2c3d92

書式: 16進数

これらのトークンは、その16進表記でパッケージ化される。

In [5]: token = payload.hex

In [6]: token
Out[6]: '0b1e000231c8482a837fe2842b2c3d92'

In [7]: len(token)
Out[7]: 32

トークンは、0b1e000231c8482a837fe2842b2c3d92 といった形式となる。
トークン自体に ID や認証コンテキストは含まないため、オンラインで Keystone を使用した検証を行う必要がある。
Pyystoneでは、発行された UUID トークンを ID および認証コンテキストとマッピングを行う。

トークン生成フロー

TBD.

トークン検証フロー

トークン失効フロー

PKI/PKIZ

PKIトークンには、ユーザのカタログ情報が含まれており、クラウドの規模に比例して大きくなる可能性がある。
PKI と PKIZトークンはほぼ同じだが、PKIZ トークンは圧縮を行う点が大きく異なる。

ペイロード: JSON

PKI/PKIZ トークンは、トークンのペイロードとして、オンラインでのトークン検証結果として生成される JSON 形式のデータが使用される。

{
    "token": {
        "audit_ids": [
            "YyobSaHcTNCu7seusdTtpQ"
        ],
        "catalog": [
            {
                "endpoints": [
                    {
                        "id": "9a29eaf20f7942b6b9c96cfb0aa02a3e",
                        "interface": "admin",
                        "region": null,
                        "region_id": null,
                        "url": "http://104.239.163.215:35357/v3"
                    },
                    {
                        "id": "d3233afd2b6041d4a39f8ac1233757fd",
                        "interface": "public",
                        "region": null,
                        "region_id": null,
                        "url": "http://104.239.163.215:35357/v3"
                    }
                ],
                "id": "1b796e214f8140118108a7e4e4ca6e16",
                "name": "Keystone",
                "type": "identity"
            }
        ],
        "expires_at": "2015-02-26T05:48:26.094098Z",
        "extras": {},
        "issued_at": "2015-02-26T05:33:26.094127Z",
        "methods": [
            "password"
        ],
        "project": {
            "domain": {
                "id": "default",
                "name": "Default"
            },
            "id": "59002ce739f143bb8b2cc33caf98fcf9",
            "name": "admin"
        },
        "roles": [
            {
                "id": "360b177d8c2347ff95e0ac1615ba8fb6",
                "name": "admin"
            }
        ],
        "user": {
            "domain": {
                "id": "default",
                "name": "Default"
            },
            "id": "85a9af145ddb4d19a9544dfbeac5d1f0",
            "name": "admin"
        }
    }
}

書式: CMS + [zlib] + base64

JSON ペイロードは、最初に非対称キーを使用して署名され、暗号メッセージ構文でラップされる。

PKIZ トークンの場合、署名されたペイロードは zlib を使用して圧縮され、その後、PKI トークンは base64でエンコードされ、次に任意の置換方式を使用して URL セーフにされる。
代わりに、PDKZ トークンは、従来の置換スキームを使用して base64-URL エンコードされる。

最後に、PKI トークンには固定プレフィックスがありませんが、PKIZ トークンには明示的に PKIZ_ というプレフィックスが付く。
最小の PKI トークンは以下のようになる。

MIIE-gYJKoZIhvcNAQcCoIIE7zCCBOsCAQExDTALBglghkgBZQMEAgEwggNMBgkqhkiG9w0BBwGgggM9BIIDOXsidG9rZW4iOnsibWV0aG9kcyI6WyJwYXNzd29yZCJd
LCJyb2xlcyI6W3siaWQiOiIzNjBiMTc3ZDhjMjM0N2ZmOTVlMGFjMTYxNWJhOGZiNiIsIm5hbWUiOiJhZG1pbiJ9XSwiZXhwaXJlc19hdCI6IjIwMTUtMDItMjZUMDU6
NDg6MjYuMDk0MDk4WiIsInByb2plY3QiOnsiZG9tYWluIjp7ImlkIjoiZGVmYXVsdCIsIm5hbWUiOiJEZWZhdWx0In0sImlkIjoiNTkwMDJjZTczOWYxNDNiYjhiMmNj
MzNjYWY5OGZjZjkiLCJuYW1lIjoiYWRtaW4ifSwiY2F0YWxvZyI6W3siZW5kcG9pbnRzIjpbeyJyZWdpb25faWQiOm51bGwsInVybCI6Imh0dHA6Ly8xMDQuMjM5LjE2
My4yMTU6MzUzNTcvdjMiLCJyZWdpb24iOm51bGwsImludGVyZmFjZSI6ImFkbWluIiwiaWQiOiI5YTI5ZWFmMjBmNzk0MmI2YjljOTZjZmIwYWEwMmEzZSJ9LHsicmVn
aW9uX2lkIjpudWxsLCJ1cmwiOiJodHRwOi8vMTA0LjIzOS4xNjMuMjE1OjM1MzU3L3YzIiwicmVnaW9uIjpudWxsLCJpbnRlcmZhY2UiOiJwdWJsaWMiLCJpZCI6ImQz
MjMzYWZkMmI2MDQxZDRhMzlmOGFjMTIzMzc1N2ZkIn1dLCJ0eXBlIjoiaWRlbnRpdHkiLCJpZCI6IjFiNzk2ZTIxNGY4MTQwMTE4MTA4YTdlNGU0Y2E2ZTE2IiwibmFt
ZSI6IktleXN0b25lIn1dLCJleHRyYXMiOnt9LCJ1c2VyIjp7ImRvbWFpbiI6eyJpZCI6ImRlZmF1bHQiLCJuYW1lIjoiRGVmYXVsdCJ9LCJpZCI6Ijg1YTlhZjE0NWRk
YjRkMTlhOTU0NGRmYmVhYzVkMWYwIiwibmFtZSI6ImFkbWluIn0sImF1ZGl0X2lkcyI6WyJZeW9iU2FIY1ROQ3U3c2V1c2RUdHBRIl0sImlzc3VlZF9hdCI6IjIwMTUt
MDItMjZUMDU6MzM6MjYuMDk0MTI3WiJ9fTGCAYUwggGBAgEBMFwwVzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVVuc2V0MQ4wDAYDVQQHDAVVbnNldDEOMAwGA1UECgwF
VW5zZXQxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbQIBATALBglghkgBZQMEAgEwDQYJKoZIhvcNAQEBBQAEggEAYJR+ETbjA4RpgToeRm0qh-zxRWyBL4RdN99hLHV6
foIpcr6uXMN-DaUJvGygPDi1wi-HAbpErJAe9iRHk4+8BUnX--jQRTaYhkg237eyjpYHU8Hgt8Ydn7Wdnn0hriXKt+RZBG-ZEnnP-MZ9V9GGJz-BoAMHx42uF5j6mlfV
vUxtJGSaZ2wPROkLIHAjrX-8zEo8YhtGQHi-rFvXOoP+w8TVb907R2WNsGs3LbFKRmDv-yev6pMnz+gQu8uImf2idd18hyEYdw8M9bgZc2YsGBiPSeIm-VhzH9qTX0e7
fK-chhAE+saIEbl5Mw0PzybhTyKHRzqtsW4HWFOlbE0yOA==

今回のケースでは 1,712 バイトとなっている。
一方、最小の PKIZ トークンは次のようになる。

PKIZ_eJxtVcmSozgUvPMVc6_oKMBgm0Mf2IzBCIpVlm4sNiAEtssLy9eP7K6Jqo4YboCUysyX7-nXL_ZopmV7_-gger784oBtm-8VcnYnbNePwlODQj-xb6tZ1zX_qqu
BORqx6moVreq20nAATLUyh6rygFa1F65uG0sZeE0brKqqgKLZtuHvr01pKZ8YSo3fX5scpnxmKW0x2Us4OQPae3MpKhPWnZJzdWfKxZG-fi6uTQaDxm9s2TPAgEgwe10
i-9DkPWLOfkwpIJWMYq32LId4c7LgfN2-2p1c5zBhG50aW8I5bxxlHw0N3tdDtndoISh1qdtLm9gDiJMbMOwbIDgBBlpyIEZLQII7mNuJnTrDhgH2GmN1pmgRvCRgS7k
hSO82Oa_sjrY2ObFvaYf26ZUr_2ZgYojrEo683fPX78WmhOaw82MgITHtPCvhgWjzvpW2HLBwh4nX-kYgYENtmCd3BAX63IhgeMuYkUcmB4kbHsHxgb-8wlBuC0s5c3k
fzoxafpicCcPynIvy8WVkJwu5NTA56ZQ_9Xc1X27VpTutR2AwyQTILjFFDkzSxIxZgjmZvbh4lAQ8WXyBSd9AHb2XVjrhbkNw9ATctDnzhbOb4at0Tu2RkIC4HX3DHDF
BPIYhRXG1AHNKEUEy6hAPIJhw5Cju9toUXdpzGVTue_Fp1vnOzLuy04WiG56Ap3IbDn6zfoBY5V1iz34kjR4BjL4p-AQI4JkDd4HmJ4sn2hPsB9CZ-UOLDtdIfFVoKKF
zzeBL4hm_fAELDhgVQy07TwwpjkMmg9a-0cqsTIJnPdPXDqBDC7sXSraRP-y1V4UyJo8dcObKbfuNSBIex7YErISFqlpgI-CxUdYotmcQOy0mxeiJKYuwR5-s825z416
Otjd62Hs8KyH9OoketuGE9oAl8aa8fBHT6U8Sw0cONyzu9pKV_sz90cLodxsh3wZ_BSn8imupO8o3S6_GsSkxhjyaW55jNAVECtm37AUmlQQgK6eFJCAC-T-aP-v-J-I
bAVuUf1aP--rxNklGMekrIRM290g8NxnFt6yjJOmd3qavvpiLRUrx5u_O5H62JjDMH52JJMja-hhbuooSNoEsjU0iDWyGIZ1NF6itpQqJyWk10NMUjAZR2YjyUrYKaGl
6Z6bxIJAGQ0VGGgRbQ03TvPdoaZg-UIfXZr0aNlwK5Rnvg9EyVPgHAABjUS7KSaYHa3MrrJG6nffIA1tT_2c2ckbwc6CamhaoZlWZ6s5fHiM7FSN_F4LPwIZ62eK-Ck7
bCCpG5gpWk55VZuJb-wZ30-Uwfh6c4_0Srgp12Ak0si9usTwdmuUcuHlIuqUjXarRXcN-_THIn6tdAN-nPSg57PGwD4Wt2Avm6qpmghnW1w0ZrGUX7cQ3MprKmr7nWFm
kufamysNiZfWSqNPDabMl54Q7ykPw2Gzxx1G8gzcNvGvRvTCjTLAqtQ1dZ7xM-zxbbam8Vha3SgGNhxL8-bESItc8SiF3PhHSXD4Mfztp16N2Em_F8CYqviBlaj917zP
Uwf2h-1nsiVSIpWGKeu-Gdtc6rtfD2eRWEbn5VNhNU-wivHb8i14U1yo6RNH7qf0Y4ValpVTG9nR4NMHv39zrQjM94_ty-xc2_Erg

PKI と比較して、1,637 バイトと若干小さくなっている。
今回の場合、トークンには最小限のカタログ情報だけがエンコードされているが、カタログ情報の大きい PKIZ トークンでは、PKI と比較して圧縮率がはるかに高くなる。

トークン生成フロー

TBD.

トークン検証フロー

トークン失効フロー

Fernet

Fernet トークンはトークン内にアカウント情報(ユーザID、プロジェクトID)や有効期限を保持し、共通鍵で暗号化して格納している。
そのため Keystone のデータベース問い合わせすることなくトークンの検証が可能となっており、データベース負荷を大幅に低減することが可能となる。

Fornet トークンでは、memcached の使用が推奨されている。
トークンを定期的に削除する cron ジョブは不要となる。
Newton 以降、Keystone のデフォルトのトークンドライバが “UUID” から “fernet” に変更となる。

ペイロード: Fernet トークン

Fernet トークンは、最小の識別情報と動的な認証コンテキストを優先する非常に軽いトークン。

書式: Fernet

Keystone の Fernet トークン形式は、暗号化認証の機構である Fernet がベースとなっている。
Fernet は、もともと Heroku が API トークンで使用するために作成された安全なメッセージング形式で、共通鍵暗号化を実装したもの。

共通鍵暗号とは、暗号用と復号用に同じ鍵を用いる方式で、ファイル暗号などによく使われ、処理が早いのが特徴。 共通鍵暗号では、暗号化されたデータを復号化する前に鍵を渡す必要がある。

Fernet 認証方式では、共通鍵の一覧を取得し、一覧内にある最初の鍵を使用してすべての暗号化を実行し、その一覧からすべての鍵を使用して復号化を試みるといった複数の鍵にも対応している。

Fernet 鍵は base64 エンコーディング(base64.urlsafe_b64encode)で、次の項目の組み合わせで構成されている。

項目 データ長 備考
Fernet 形式のバージョン番号 (Version) 8ビット 0x80
現在のタイムスタンプ (Timestamp) 64ビット int(time.time())
初期化ベクトル (IV) 128ビット os.urandom(16) - 署名鍵はfernet鍵の最初の16バイト
- 暗号化キーは fernet キーの最後の16バイト
暗号文 (Ciphertext) 可変長(128 ビットの倍数) 以下のデータにより構成されている。
- バージョン:
  0(スコープなしのペイロード)
  1(ドメインスコープのペイロード)
  2(プロジェクトスコープのペイロード)
  3(トラストスコープのペイロード)
- ユーザID: バイト形式
- メソッド: 整数形式
- プロジェクトID: バイト形式
- 有効期限: タイムスタンプの整数形式
- 監査ID: バイト形式
HMAC 256 ビット

Fernet は、トークンの作成時間をトークンから把握することもできる。

>>> token = b'gAAAAABb93HyEo0JIFZlTfKHlyRFTiJPqlBK75MEt_858fnATWN3mRNomlNQr-ZjHwnmlzcXKKZYpuGSmc8UgMwwEhCvWk5PsCiAxV-GsVDhpYcduZVK6ugtLTVkGgZZiEBC3-77Jkpi8VA2qouzyWzDbBgjMO98YuQkjEH6kPAKApGYrSGnFEw='
>>> 
>>> import struct, base64, datetime
>>> timestamp = struct.unpack('>Q', base64.urlsafe_b64decode(token)[1:9])[0]
>>> 
>>> timestamp
1542943218
>>> datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
'2018-11-23 12:20:18'

トークン生成フロー

Fernet トークンのバージョン

Fernet 形式のバージョン。現在は “0x80” で固定。

>>> version=b'\x80'

現在時刻のタイムスタンプ

現在時刻を取得する。

>>> import time
>>> time.time()
1542943218.944182

現在時刻を int 型へ変換する。

>>> current_time=int(time.time())
>>> current_time
1542943218

IV

初期化ベクトルを作成する。

>>> import os
>>> iv = os.urandom(16)
>>> iv
b'\x12\x8d\t VeM\xf2\x87\x97$EN"O\xaa'

暗号文

>>> from cryptography.hazmat.primitives import hashes, padding
>>> from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
>>> from cryptography.hazmat.backends import default_backend
>>> 
>>> padder = padding.PKCS7(algorithms.AES.block_size).padder()

padder にブロックサイズ 128 が設定される。

>>> padder.__dict__
{'_buffer': b'', 'block_size': 128}

プロジェクトスコープ付きペイロードを定義する。

>>> project_scoped_payload = b'\x96\x02\xb0\x134\xf3\xed~\xb2H;\x91\xb8\x19+\xa0C\xb5\x80\x02\xb0B=E\xcd\xde\xc8Ap\xbe6^\x0b1\xa1\xb1_\xcbA\xd5\x87P\x02\xb4C\xd9\x91\xb0}oA\xd3fCu\x95z\\\xbd\xd8{\x89\xbc'

ブロックサイズに合わせてパディングを適用する。

>>> padded_data = padder.update(project_scoped_payload) + padder.finalize()
>>> padded_data
b'\x96\x02\xb0\x134\xf3\xed~\xb2H;\x91\xb8\x19+\xa0C\xb5\x80\x02\xb0B=E\xcd\xde\xc8Ap\xbe6^\x0b1\xa1\xb1_\xcbA\xd5\x87P\x02\xb4C\xd9\x91\xb0}oA\xd3fCu\x95z\\\xbd\xd8{\x89\xbc\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'

32バイトのランダム文字列バイナリ形式の fernet 鍵を定義する。

>>> b_key = os.urandom(32)

暗号化キーは fernet キーの最後の16バイトとなる。

>>> backend = default_backend()
>>> e_key = b_key[16:]

CBC モードで ABC 暗号化を行う。

>>> encryptor = Cipher(algorithms.AES(e_key), modes.CBC(iv), backend).encryptor()

パディングされた Keystone ペイロードを暗号化する。

>>> ciphertext = encryptor.update(padded_data) + encryptor.finalize()
>>> ciphertext
b'PJ\xef\x93\x04\xb7\xff9\xf1\xf9\xc0Mcw\x99\x13h\x9aSP\xaf\xe6c\x1f\t\xe6\x977\x17(\xa6X\xa6\xe1\x92\x99\xcf\x14\x80\xcc0\x12\x10\xafZNO\xb0(\x80\xc5_\x86\xb1P\xe1\xa5\x87\x1d\xb9\x95J\xea\xe8--5d\x1a\x06Y\x88@B\xdf\xee\xfbJb'

HMAC

署名鍵付き 256 ビット SHA256 HMAC。 上記のすべての項目(バージョン、タイムスタンプ、IV、暗号文)を結合したもの。

署名鍵はfernet鍵の最初の16バイトとなる。

>>> s_key = b_key[:16]

HMAC の作成では SHA256 ハッシュを使用する。

>>> from cryptography.hazmat.primitives.hmac import HMAC
>>> h = HMAC(s_key, hashes.SHA256(), backend)

HMAC は Fernet Version、Timestamp、IV、Ciphertext の組み合わせで構成される。

>>> import struct
>>> 
>>> basic_parts = (version+ struct.pack(">Q", current_time) + iv + ciphertext)
>>> basic_parts
b'\x80\x00\x00\x00\x00[\xf7q\xf2\x12\x8d\t VeM\xf2\x87\x97$EN"O\xaaPJ\xef\x93\x04\xb7\xff9\xf1\xf9\xc0Mcw\x99\x13h\x9aSP\xaf\xe6c\x1f\t\xe6\x977\x17(\xa6X\xa6\xe1\x92\x99\xcf\x14\x80\xcc0\x12\x10\xafZNO\xb0(\x80\xc5_\x86\xb1P\xe1\xa5\x87\x1d\xb9\x95J\xea\xe8--5d\x1a\x06Y\x88@B\xdf\xee\xfbJb'

“>” はビッグエンディアン、“Q” は符号なし long long 型を表す。

Fernet Version、Timestamp、IV、Ciphertext から HMAC を生成する。

>>> h.update(basic_parts)
>>> hmac = h.finalize()
>>> hmac
b'\xf1P6\xaa\x8b\xb3\xc9l\xc3l\x18#0\xef|b\xe4$\x8cA\xfa\x90\xf0\n\x02\x91\x98\xad!\xa7\x14L'

Fernet トークンは、Version、Timestamp、IV、Ciphertext、HMAC の Base64 URL-Safe エンコーディングとなる。

>>> import base64
>>> 
>>> fernet = base64.urlsafe_b64encode(basic_parts + hmac).decode('utf-8')
>>> fernet
'gAAAAABb93HyEo0JIFZlTfKHlyRFTiJPqlBK75MEt_858fnATWN3mRNomlNQr-ZjHwnmlzcXKKZYpuGSmc8UgMwwEhCvWk5PsCiAxV-GsVDhpYcduZVK6ugtLTVkGgZZiEBC3-77Jkpi8VA2qouzyWzDbBgjMO98YuQkjEH6kPAKApGYrSGnFEw='

トークン検証フロー

トークン失効フロー

Keystone における Fernet トークン設定

Fernet トークンを使用するには、keystone.conf の [fernet_token] セクション項目、および [token] セクションの provider 項目を設定する。

[token]
provider = keystone.token.providers.fernet.Provider

[fernet_tokens]
# Fernet 鍵を保存する鍵リポジトリの場所
key_repository = /etc/keystone/fernet-keys/

# 鍵リポジトリ上の最大の鍵の数(初期値: 3)
# - コントローラノードの台数以下を設定する。
max_active_keys = 5

Fernet 鍵は、デフォルトで “/etc/keystone/fernet-keys/” にある鍵リポジトリに保存されている。

Fernet 鍵ファイルは 0 から始まる integer 形式の名前となる。

$ ls /etc/keystone/fernet-keys
0 1 2 3 4

以下の 3 つのタイプの鍵ファイルが存在する。

  • プライマリ鍵 - プライマリ鍵は、fernet トークンの暗号化および復号化に使用される。最も大きいインデックス名となっているものがプライマリ鍵ファイルとなる。
  • セカンダリ鍵 - セカンダリ鍵は、fernet トークンの復号にのみ使用される。 セカンダリ鍵ファイルの名前は、最大のインデックスより小さく、最小のインデックス(0)よりも大きい値となる。
  • ステージ鍵 - ステージング鍵は、fernet トークンの復号にのみ使用されるセカンダリ鍵と似ているが、最小のインデックス番号(0)となっているものがステージング鍵となる。

Fernet 鍵は、以下のフローで遷移する。

  • 新規作成 → ステージ鍵 → プライマリ鍵 → セカンダリ鍵 → 削除
Fernet 鍵 インデックス番号
(max_active_keys=3 の場合)
暗号化 複合化
プライマリ鍵 2
セカンダリ鍵 1 -
ステージ鍵 0 -

要約すると、fernet トークンはプライマリ鍵を使用して暗号化され、鍵リポジトリからの fernet キーの一覧を使用して復号化される。

Fernet 鍵のローテーション手順

keystone-manage コマンドを使用し、鍵リポジトリに2つの鍵を新規作成する fernet-setup を行う。

セカンダリ鍵はなく、プライマリ鍵 1 と、ステージ鍵 0 とで構成される。

$ keystone-manage fernet_setup
2507 INFO keystone.token.providers.fernet.utils [-] [fernet_tokens] key_repository does not appear to exist; attempting to create it
2507 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2507 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 1 key files: ['/etc/keystone/fernet-keys/0']
2507 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 0
2507 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 1
2507 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 1
2507 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2507 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: []
$ ls /etc/keystone/fernet-keys/
0  1

ローテーションの状態を確認しながら、ローテーションを行う。

$ keystone-manage fernet_rotate
2528 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 2 key files: ['/etc/keystone/fernet-keys/0', '/etc/keystone/fernet-keys/1']
2528 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 1
2528 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 2
2528 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 2
2528 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2528 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: []
$ ls /etc/keystone/fernet-keys/
0  1  2
  • ローテーションした後、2 がプライマリ鍵となり、0 および 1 は復号化で使用される。
  • 前のプライマリ鍵 1 は、同じ名前のままセカンダリ鍵となる。
  • 新しいステージ鍵 0 が導入される。

再度 Fernet 鍵のローテーションを行う。

$ keystone-manage fernet_rotate
2698 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 3 key files: ['/etc/keystone/fernet-keys/0', '/etc/keystone/fernet-keys/1', '/etc/keystone/fernet-keys/2']
2698 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 2
2698 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 3
2698 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 3
2698 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2698 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: [1]
$ ls /etc/keystone/fernet-keys/
0  2  3

今度は、最大のインデックスは 3 となり、ステージ鍵 0 は、鍵 3 のプライマリ鍵となる。
前のプライマリ鍵 2 は、同じファイル名のままセカンダリ鍵となり、前のセカンダリ鍵は、同じファイル名のままセカンダリ鍵として残る。そして、新たにステージ鍵 0 が導入される。
max_active_keys を 3 に設定した場合、セカンダリ鍵 1 は今回の鍵のローテーション中に削除される。

想定通り、プライマリ鍵が 3 となり、鍵 1 が削除されている。

今回の場合、max_active_keys=3 で設定しているため、有効な鍵は 3 つとなっており、最も小さいインデックスの鍵が削除される。
鍵 2 と鍵 3 がプライマリ鍵のときに暗号化されたものは、引き続き検証可能な状態だが、削除済みである鍵 1 で暗号化されたものは検証することができない。
鍵のローテーションを頻繁に行ったり、max_active_keys の値が小さすぎたりした場合、有効なトークンが早期に消滅することがあり得るため、注意が必要となる。
たとえば、プライマリ鍵を 30 分ごとにローテーションするようにし、Keystone トークンの有効期間を 6 時間に設定している場合、max_active_keys には 12 以上の値を設定する必要がある。

max_active_keys 設定値
>= Keystone トークン有効時間 / ローテーション間隔
= 6 [時間 ] / 30 [分]
= 360 [分] / 30 [分]
= 12

性能比較

トークンの作成速度は、PKI/PKIZ が一番遅く、次に UUID が続き、Fernet が一番早い。(2015年6月時点)

UUID は、Keystone で使用可能な最もシンプルなトークンフォーマットで、ランダムに生成された 32 文字の文字列を使用して Keystone に接続し、トークン検証を行う。
この時、Keystone でトークンが有効か無効化かを判断するために、トークンとユーザー ID、認証メタデータのマッピング情報を保持している必要がある。

PKI トークンは、所有者の ID とトークン認証コンテキストをトークンと共にパックし、CMS を使用してパッケージ全体をラップする。

PKIZ トークンは、端的に言うと PKI トークンを圧縮を追加したもの。

近年では、A​​ES-CBC を使用した暗号化、および SHA256 を使用した署名を行う Fernet トークンを使用することができるようになっている。

Fernet トークンには、所有者 ID や認証コンテキストに関するメタデータも含まれている。

Keystone で行う処理の内、性能が大きく影響するものとしては、主にトークン作成およびトークン検証が挙げられる。

その他の処理については、頻繁に行われるものでもなく、性能はさほど問題にはならない。

Keystone トークンを複数の地域にまたがった同期機能は、機能要求としてあるのが一般的となっている。
これは、2 つ以上の地域にそれぞれ Keystone インスタンスを立て、ある地域で生成されたトークンを他の地域で検証し、正常に使用できるようにするといったケースである。

UUID トークンを使用する場合には、可能な限り迅速にすべての地域にトークンを同期させる必要があり、複雑となる傾向がある。
この場合、クライアントからのトークンをすべての場所に複製するまで保留するか、ローカルで作成して非同期で複製するかの 2 つの方法が考えられる。
しかし、1つ目の方法では、トークンの作成時に応答時間が長くなり、2つ目の方法では、トークン発行が競合状態になる可能性がある。
つまり、どちらの方法を採択するにせよ応答時間の短縮が必須となってくる。

一方、Fernet トークンは永続化する必要がないため、token テーブルは空となり、すべての地域と同期を取る必要がない。

> SELECT * FROM `token`;
Empty set (0.00 sec)

世界中に点在するう5つのデータセンター(ワシントンDC、シカゴ [イリノイ州]、ダラス [テキサス州]、香港、シドニー [オーストラリア])で Fernet 検証結果では、応答時間は1つの地域で行なった場合と変わらないにも関わらず、すべての地域で即座にトークンが使用可能となる。
待機時間や同期にかかる時間は一切なく、応答時間には同期レプリケーションのオーバーヘッドが発生しないため、すべての地域でトークンの暗号鍵をすぐに確認することができるようになる。
※ 今回、検証ケースを絞るため Fernet トークンは常に255バイト未満で設定。

Keystone + グローバルネットワークでクラスタ化された Galera Cluster 構成

これらのメトリックは、公開されたインターネット上でレプリケーションを実行するグローバルに分散された5台のGalera クラスタノードの場合の検証結果を示している。

トークン作成の性能

トークン 応答時間 リクエスト数/秒
UUID 342.4 166.9
PKI 351.4 110.2
PKIZ 339.7 120.7
Fernet 50.8 237.1

トークン検証の性能

トークン 応答時間 リクエスト数/秒
UUID 6.02 1715.7
PKI 6.25 1717.2
PKIZ 6.15 1676.4
Fernet 5.55 1957.8

※1 ApacheBench には最大要求サイズがあり、PKI または PKIZ トークンの ( X-Auth-Token + X-Subject-Token) がこれを超える。そのため、Stack Overflow での提案に沿って ApacheBench 最大要求サイズを広げた後、再コンパイルすることでこの制限を回避している。

参考URL

[徹底解説] Linux パイプのしくみ

パイプは、1970年初頭に Third Edition Unix で実装された、Unix の IPC (Interprocess Communication / プロセス間通信) 。
パイプは、関連プロセス間でデータの受け渡しを行い、異なるプログラムを実行する2プロセスにおいて、一方の出力をもう一方の入力とすることができる。

概要

パイプは以下のように使用される。

# ls / | wc -l
22

この例の場合、以下のように順に処理されている。

  1. ls / コマンドで対象ファイルの一覧を表示する。

    bin   dev  home  lib64  mnt  proc  run   srv  tmp  vagrant  work
    boot etc lib media opt root sbin sys usr var
  2. 前の出力結果を入力値として、wc -l を実行して出力ライン数をカウント・表示する。

    22

パイプの実装

カーネルにおけてパイプは以下のように実装されている。

  • パイプはファイルシステム上に実態を持たないが、 inode 構造体などの管理構造を維持する必要があるため、pipefs という仮想ファイルシステム (VFS) を導入している
  • pipefs ファイルシステムは、システムのディレクトリツリーにマウントポイントを持っていないため、ユーザから見ることはできない
  • pipefs のエントリポイントは、シェルや他のプログラムでパイプを実装するために使用される pipe(2) システムコール
  • pipe(2) は新しいパイプを作成し、ファイルディスクリプターを二つ返す(ディスクリプターのうち、一方はパイプの読み出し側を、もう一方は 書き込み側を参照している)

パイプ I/O、バッファリング、容量

Linux ではパイプの容量が限られており、パイプの容量がフルになると write(2)がブロックされる(O_NONBLOCK フラグがセットされている場合は失敗する)。
Linux 2.6.35 以降、デフォルトのパイプ容量は 65,536 バイト、それ以前のバージョンでは、パイプの容量はシステムのページサイズと同一(例えば i386 の場合は 4,096 バイト)となっている。

プロセスが空のパイプからの読み取りを試みた際、read(2) はデータがパイプ内で利用可能になるまでブロックする。
パイプの書き込み側を指すファイルディスクリプタがすべて閉じられた場合、パイプから読み取りを試みると EOF が返される(read(2) が 0 を返す)。

プロセスがフルになったパイプに書き込もうとすると、write 呼び出しが成功するのに十分なデータがパイプから読み取られるまで、write(2) はブロックされる。
パイプの読み込み側のファイルディスクリプタがすべて閉じられると、パイプに書き込んだ際に SIGPIPE シグナルが送られる。
呼び出し元のプロセスがこのこのシグナルを無視している場合、write(2) はエラー EPIPE で失敗する。

つまり、プロセスAからの書き込みと、プロセスBからの読み込みが同じ速度で行われている場合は、最大のパフォーマンスを出すことができるが、速度が乖離している場合には、パフォーマンス劣化を引き起こす場合があるので、注意が必要となる。

シェルでパイプを行う方法

ここからは、Linux においてプロセスがどのように生成されるのかについて、理解していることを前提として説明をしていく。
ご存知でない方は “[徹底解説] Linux プロセス生成のしくみ” を参照のこと。

シェルは、「リダイレクションの実装方法」と非常によく似た方法でパイプを実装します。
基本的には、親プロセスは、一緒にパイプ処理される2つのプロセスごとに、 pipe(2) を1回呼び出します。
上記の例では、bash は2つのパイプを作成するために pipe(2) を2回呼び出す必要があります。次に、bash はプロセスごとに1回ずつフォークします(この例では3回)。
各子は1つのコマンドを実行します。
しかし、子供がコマンドを実行する前に、stdin または stdout(またはその両方)を上書きします。上記の例では、次のように動作します。

  • bash は2つのパイプを作成します.1つはソートするパイプ、もう1つはソートするパイプです
  • bash は自分自身を3回フォークします(各コマンドごとに1つの親プロセスと3つの子プロセス)
  • 子1 (ls) は、標準出力ファイル記述子をパイプ A の書き込み終了に設定します
  • 子2 (sort) は、パイプ A の読み込み側のファイルディスクリプタを stdin に設定します(ls からの入力を読み込むため)
  • 子2 (sort) は、標準出力ファイル記述子をパイプBの書込み終了に設定します
  • 子3 (less) は、パイプ B の読み込み側のファイルディスクリプタを stdin に設定します(sort からの入力を読み込むため)
  • それぞれの子はコマンドを実行する

カーネルはプロセスを自動的にスケジューリングして、大まかに並行して実行します。
子2がそれを読み取る前に子1がパイプAにあまりにも多くの書き込みを行った場合、子2はパイプから読み出すまでの間、しばらくブロックします。
これは通常、あるプロセスが他のプロセスがデータの処理を開始するのを待つ必要がないため、非常に高いレベルの効率を可能にします。
もう1つの理由は、パイプのサイズが限られていることです(通常、メモリの1ページのサイズ)。

パイプサンプルコード

ここでは、bash のようなプログラムがパイプを実装する方法の C の例を示します。
私の例はかなりシンプルで、2つの引数を受け取ります。ディレクトリと検索する文字列です。
ls -la を実行してディレクトリの内容を取得し、grep にパイプして文字列を検索します。

#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>

#define READ_END 0
#define WRITE_END 1

int main(int argc, char *argv[])
{
int pid, pid_ls, pid_grep;
int pipefd[2];

// Syntax: test . filename
if (argc < 3) {
fprintf(stderr, "Please specify the directory to search and the filename to search for\n");
return -1;
}

fprintf(stdout, "parent: Grepping %s for %s\n", argv[1], argv[2]);

// Create an unnamed pipe
if (pipe(pipefd) == -1) {
fprintf(stderr, "parent: Failed to create pipe\n");
return -1;
}

// Fork a process to run grep
pid_grep = fork();

if (pid_grep == -1) {
fprintf(stderr, "parent: Could not fork process to run grep\n");
return -1;
} else if (pid_grep == 0) {
fprintf(stdout, "child: grep child will now run\n");

// Set fd[0] (stdin) to the read end of the pipe
if (dup2(pipefd[READ_END], STDIN_FILENO) == -1) {
fprintf(stderr, "child: grep dup2 failed\n");
return -1;
}

// Close the pipe now that we've duplicated it
close(pipefd[READ_END]);
close(pipefd[WRITE_END]);

// Setup the arguments/environment to call
char *new_argv[] = { "/bin/grep", argv[2], 0 };
char *envp[] = { "HOME=/", "PATH=/bin:/usr/bin", "USER=brandon", 0 };

// Call execve(2) which will replace the executable image of this
// process
execve(new_argv[0], &new_argv[0], envp);

// Execution will never continue in this process unless execve returns
// because of an error
fprintf(stderr, "child: Oops, grep failed!\n");
return -1;
}

// Fork a process to run ls
pid_ls = fork();

if (pid_ls == -1) {
fprintf(stderr, "parent: Could not fork process to run ls\n");
return -1;
} else if (pid_ls == 0) {
fprintf(stdout, "child: ls child will now run\n");
fprintf(stdout, "---------------------\n");

// Set fd[1] (stdout) to the write end of the pipe
if (dup2(pipefd[WRITE_END], STDOUT_FILENO) == -1) {
fprintf(stderr, "ls dup2 failed\n");
return -1;
}

// Close the pipe now that we've duplicated it
close(pipefd[READ_END]);
close(pipefd[WRITE_END]);

// Setup the arguments/environment to call
char *new_argv[] = { "/bin/ls", "-la", argv[1], 0 };
char *envp[] = { "HOME=/", "PATH=/bin:/usr/bin", "USER=brandon", 0 };

// Call execve(2) which will replace the executable image of this
// process
execve(new_argv[0], &new_argv[0], envp);

// Execution will never continue in this process unless execve returns
// because of an error
fprintf(stderr, "child: Oops, ls failed!\n");
return -1;
}

// Parent doesn't need the pipes
close(pipefd[READ_END]);
close(pipefd[WRITE_END]);

fprintf(stdout, "parent: Parent will now wait for children to finish execution\n");

// Wait for all children to finish
while (wait(NULL) > 0);

fprintf(stdout, "---------------------\n");
fprintf(stdout, "parent: Children has finished execution, parent is done\n");

return 0;
}

私はそれを徹底的にコメントしたので、うまくいけば意味がある。

名前付きパイプと無名パイプ

上記の例では、名前のない/匿名のパイプを使用しています。
これらのパイプは一時的なもので、プログラムが終了するか、ファイル記述子がすべて閉じられると破棄されます。
それらは最も一般的なタイプのパイプです。

名前付きパイプは、FIFO (first in, first out) としても知られ、ハードディスク上の名前付きファイルとして作成されます。
無関係な複数のプログラムを開いて使用することができます。
非常にシンプルなクライアント/サーバー型設計のために、複数のライターを1つのリーダーで簡単に作成できます。
たとえば、Nagios はこれを行います。
マスタープロセスは名前付きパイプを読み込み、すべての子プロセスは名前付きパイプにコマンドを書き込みます。

名前付きパイプは、mkfifo コマンドまたは syscall を使用して作成しています。
例:

$ mkfifo ~/test_pipe

彼らの作成以外にも、名前のないパイプとほとんど同じ働きをします。
作成したら、 open(2) を使って開くことができます。
O_RDONLY を使って読み込み終了を開くか、O_WRONLY を使って書き込み終了をオープンする必要があります。
ほとんどのオペレーティングシステムでは単方向パイプが実装されているため、読み取り/書き込みモードで開くことはできません。

FIFO は、複数のプロセスを持つシステムのために、単方向 IPC 技術としてしばしば使用されます。
マルチスレッドアプリケーションは、共有メモリセグメントなどの他の IPC 技術と同様に、名前付きパイプまたは名前のないパイプを使用することもできます。

FIFO は inode として作成され、i_pipe プロパティは実際のパイプへの参照として設定されます。
名前がファイルシステム上に存在するかもしれませんが、inode が読み込まれると、パイプは無名のパイプのように振る舞い、メモリ内で動作するため、パイプは基盤となるデバイスへの I/O を引き起こしません。

参考 URL

[徹底解説] Linux リダイレクトのしくみ

フードの中で bash リダイレクションがどのように機能するのか疑問に思ったことはありますか?
リダイレクト自体はかなり簡単です。

bash を使うと、ファイルをプロセスの stdin にリダイレクトすることも、プロセスの stdout/stderr をファイルや他のファイル記述子にリダイレクトすることもできます( stderr を stdout にリダイレクトするなど、ファイル記述子なので)。

リダイレクトは次のようになります。

$ ls -la > output.txt

上記のコマンドは、stdout を ls コマンドから output.txt ファイルにリダイレクトします。
stderr をリダイレクトする方法は次のとおりです。

$ ls -la 2 > errors.txt

これにより、stderr から errors.txt にすべてが送信されます(許可が拒否されたメッセージなど)。
そのため、たとえば次のような場合は、

$ ls /root > /dev/null

ルート以外のユーザーは、出力を /dev/null にリダイレクトしたにもかかわらずエラーメッセージが表示されます。
通常、エラーは stderr に書き込まれます。

最後に、stdin を出力とするファイルの入力リダイレクションを行うこともできます。

$ bash < commands.txt

上記のコマンドは、commands.txt の内容を読み込み、bash インタプリタを使用してそれらを実行します。

リダイレクトの実装

とにかく、この記事のポイントは、そのような機能がどのように実装されているかです。
プロセスフォークを理解すれば、非常に簡単になることが分かります。

プロセスが別のプロセス(例えば bash を実行しているls)を実行したいとき、それは一般的に次のように動作します:

  1. メインプロセス(bash など)は、fork という glibc ラッパーを使って fork します(sidenode: fork では、実際には fork(2) システムコールではなく glibc ラッパーを呼び出しています)。glibc ラッパーは、clone(2) がより強力であるため、fork(2) のシステムコールではなく、システムコール(syscall)
  2. fork されたプロセスは、出力リダイレクトがコマンドラインで入力されたことを確認し、open(2) のシステムコールまたは同等のものを使用して指定されたファイルを開きます。
  3. fork されたプロセスは dup2 を呼び出して、新しく開かれたファイル記述子を stdin/stdout/stderr にコピーします。
  4. fork されたプロセスは元のファイルハンドラを閉じて、リソースリークを防ぎます。
  5. fork されたプロセスは execve(2) のシステムコールまたは同様のものを呼び出して、実行可能なイメージを実行するプロセスのイメージ(たとえば ls)に置き換えるか、

同じことをするコード例を次に示します。

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
char *argv[] = { "/bin/ls", "-la", 0 };
char *envp[] =
{
"HOME=/",
"PATH=/bin:/usr/bin",
"USER=brandon",
0
};
int fd = open("/home/brandon/ls.log", O_WRONLY|O_CREAT|O_TRUNC, 0666);
dup2(fd, 1); // stdout is file descriptor 1
close(fd);
execve(argv[0], &argv[0], envp);
fprintf(stderr, "Oops!\n");
return -1;
}

上記のコードは stdout を ls.log に設定し、 ls -la を実行します。
execve が失敗しない限り、fprintf 以下は実行されませんのでご注意ください。
それで、リダイレクトが bash でどのように実装されるのです!

2247  execve("./redirect", ["./redirect"], [/* 18 vars */]) = 0
2247  open("/home/vagrant/ls.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
2247  dup2(3, 1)                        = 1
2247  execve("/bin/ls", ["/bin/ls", "-la"], [/* 3 vars */]) = 0
2247  write(1, "total 64\ndrwxrwxrwx 2 root    ro"..., 620) = 620
2247  exit_group(0)                     = ?
2247 +++ exited with 0 +++

/usr/include/unistd.h

/* Standard file descriptors.  */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */

# who
root pts/0 2018-10-07 20:10 (10.16.0.1)
root pts/1 2018-10-08 07:54 (10.16.0.1)
# tty
/dev/pts/0

[徹底解説] Linux プロセス生成のしくみ

Linux においてプロセスは、fork(2) および clone(2) システムコールを使用することにより生成された後、exec(2) システムコールを使用して新規プログラムが実行される。

まずは、実際のプロセス生成を模擬した以下のサンプルプログラムを実行し、プロセス生成のしくみについて理解を深めていく。

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

int main(void)
{
int pid = fork();

if (pid == -1) {
fprintf(stderr, "プロセスの複製に失敗しました。\n");
return -1;
} else if (pid == 0) { // 子プロセスの場合
fprintf(stdout, "ls コマンド実行のための子プロセス生成 (PID: %d)\n", getpid());

// システムコールするため、引数および環境変数を設定
char *argv[] = { "/bin/ls", "-la", 0 };
char *envp[] = { "HOME=/", "PATH=/bin:/usr/bin", "USER=brandon", 0 };

// execve(2) システムコールにより、プロセスを実行可能イメージに置き換える
execve(argv[0], &argv[0], envp);

// execve(2) からのリターンがない場合は異常終了
fprintf(stderr, "エラーが発生しました。\n");
return -1;
} else if (pid > 0) { // 親プロセスの場合
int status;

fprintf(stdout, "親プロセスは子プロセスの実行完了するまで待機 (PID: %d)\n", getpid());
wait(&status);
fprintf(stdout, "子プロセスの実行完了 (リターンコード: %i)、および親プロセスの終了\n", status);
}
return 0;
}

サンプルコードを実行すると以下のような出力を得られる。
この出力結果より、まず、子プロセスが生成され、親プロセスは子プロセス完了を待機し、子プロセスの処理終了を受けて親プロセスが終了していることがわかる。

$ gcc -o fork_exec fork_exec.c
$ strace -o /tmp/fork_exec.log -f ./fork_exec
親プロセスは子プロセスの実行完了するまで待機 (PID: 18040)
ls コマンド実行のための子プロセス生成 (PID: 18041)
total 16
drwxr-xr-x 2 root root 40 Oct 7 21:30 .
drwxr-xr-x 3 root root 19 Oct 7 21:23 ..
-rwxr-xr-x 1 root root 8792 Oct 7 21:25 fork_exec
-rw-r--r-- 1 root root 1181 Oct 7 21:25 fork_exec.c
子プロセスの実行完了 (リターンコード: 0)、および親プロセスの終了

strace の出力ファイルを参照すると、より詳細な内容を把握することができる。

  1. 親プロセスから子プロセスをクローン

    18040 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb6cbfeaa10) = 18041
  2. 親プロセスは子プロセスの実行が完了するまで待機

    // "親プロセスは子プロセスの実行完了するまで待機 (PID: 18040)"
    18040 wait4(-1, <unfinished ...>
  3. 子プロセスが execve() によって実行可能状態に遷移

    // "ls コマンド実行のための子プロセス生成 (PID: 18041)"
    18041 execve("/bin/ls", ["/bin/ls", "-la"], [/* 3 vars */]) = 0
  4. ls コマンドの実行・表示

    // "total 16"
    // "drwxr-xr-x 2 root root 40 Oct 7 21:30 ."
    // "drwxr-xr-x 3 root root 19 Oct 7 21:23 .."
    // "-rwxr-xr-x 1 root root 8792 Oct 7 21:25 fork_exec"
    // "-rw-r--r-- 1 root root 1181 Oct 7 21:25 fork_exec.c"
  5. 子プロセスの終了 (リターンコード: 0)

    18041 exit_group(0)                     = ?
    18041 +++ exited with 0 +++
  6. 親プロセスで子プロセスの終了を受理・処理の再開

    18040 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 18041
    ...
    18040 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=18041, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
    ...
    // "子プロセスの実行完了 (リターンコード: 0)、および親プロセスの終了"
  7. 親プロセスの終了 (リターンコード: 0)

    18040 exit_group(0)                     = ?
    18040 +++ exited with 0 +++

OpenStack API 実装例

curl 使用

まずは OpenStack の環境変数を読み込む。

source ~/overcloudrc

次にユーザ名とパスワード、ドメイン名を指定し、トークンを取得する。
keystone で v2 を使用しているか v3 を使用しているかによって トークンの表示位置が変更するので注意が必要。
今回は、keysotne v3 を前提にしている。

続きを読む