zaki work log

作業ログやら生活ログやらなんやら

[Ansible] to_datetime フィルタを使って文字列をdatetimeオブジェクトに変換

AnsibleでWed Feb 10 09:17:08 JST 2021という表記の文字列をdatetimeオブジェクトに変換するには、to_datetimeフィルターを使う。
使用例はフィルターのページの「Handling dates and times」に使い方が載っているのでここに書かれている通り。

内部実装はPythonのstrptime()が使用されているので、フォーマットの指定はPythonのドキュメントを見るとよい。

def to_datetime(string, format="%Y-%m-%d %H:%M:%S"):
    return datetime.datetime.strptime(string, format)

datetimeオブジェクトにすれば各メソッドが使用できるため、UNIX Epochにして比較するなど簡単になる。(←C/Perl育ちの秒で管理したい人間の発想)

環境

サンプルコード

    - name: to_datetime
      vars:
        str_sample: "2016-08-14 20:00:12"
        str_year: "2021"
        str_ymdhms: 2021.02.10 09-15-20
        str_jst: Wed Feb 10 09:17:08 JST 2021
        str_offset: Wed Feb 10 09:17:08 +0900 2021
      debug:
        msg:
          - "{{ str_sample | to_datetime }}"
          - "{{ str_year | to_datetime('%Y') }}"
          - "{{ str_ymdhms | to_datetime('%Y.%m.%d %H-%M-%S') }}"
          - "{{ str_jst | to_datetime('%a %b %d %H:%M:%S %Z %Y') }}"
          - "{{ str_offset | to_datetime('%a %b %d %H:%M:%S %z %Y') }}"
          - "{{ (str_jst | to_datetime('%a %b %d %H:%M:%S %Z %Y')).timestamp() }}"  # 型はfloat ('1612916228.0')
          - "{{ lookup('pipe', 'date +%s') }}"

実行結果

ok: [localhost] => 
  msg:
  - '2016-08-14 20:00:12'
  - '2021-01-01 00:00:00'
  - '2021-02-10 09:15:20'
  - '2021-02-10 09:17:08'
  - '2021-02-10 09:17:08+09:00'
  - '1612916228.0'
  - '1613054485'

書式について

デフォルトの書式

ドキュメントの以下コメント、

Default date format is %Y-%m-%d %H:%M:%S but you can pass your own format

および、実装もそうなってる通り、フィルター使用時に書式を何も指定しなければ%Y-%m-%d %H:%M:%Sとなる。
任意のフォーマット文字列を指定することも可能。

タイムゾーン

タイムゾーンについては実行環境のロケールPythonバージョンに依存するので注意。
(書式(抜粋)についても下記記事参照)

zaki-hmkc.hatenablog.com

  • %z

    • サンプルコードのstr_offsetにおける+0900の部分を解釈するが、Python 2系で動くAnsibleでは使用できない。
    • エラー:

      the field 'args' has an invalid value ({u'msg': [u"{{ str_offset | to_datetime('%a %b %d %H:%M:%S %z %Y') }}"]}), and could not be converted to an dict.The error was: 'z' is a bad directive in format '%a %b %d %H:%M:%S %z %Y'

  • %Z

    • サンプルコードのstr_jstにおけるJSTの部分を解釈するが、実行OSのタイムゾーン設定がJSTでないと使用できない。
    • エラー:

      the field 'args' has an invalid value ({u'msg': [u"{{ str_jst | to_datetime('%a %b %d %H:%M:%S %Z %Y') }}"]}), and could not be converted to an dict.The error was: time data 'Wed Feb 10 09:17:08 JST 2021' does not match format '%a %b %d %H:%M:%S %Z %Y'

[Python] datetime.strptime()を使った文字列からdatetimeオブジェクトへの変換とタイムゾーン

Pythonでは日付を表す文字列からdatetime型のオブジェクトへの変換にstrptime()を使用できる。
例えば'Thu Feb 11 17:01:34 2021'という文字列があった場合、'%a %b %d %H:%M:%S %Y'というフォーマット文字列を用意することでオブジェクトにできる。
なお、Ansibleの場合はto_datetimeフィルターでこの機能を使うこともできる。

docs.python.org

ここで、タイムゾーンの表記と処理で少しハマったので備忘録。

UTCオフセットによる表記 (例 +0900)

'Thu Feb 11 17:21:52 +0900 2021'

この表記の+0900の部分は、小文字の%zを使えば取得できる。

import datetime

date_str_tz = 'Thu Feb 11 17:21:52 +0900 2021'
format_str_tz = '%a %b %d %H:%M:%S %z %Y'
dt_tz = datetime.datetime.strptime(date_str_tz, format_str_tz)
print(dt_tz)
print(dt_tz.tzinfo)

このコードを実行すると、結果は以下の通りでawareなオブジェクトとなる。

2021-02-11 17:21:52+09:00
UTC+09:00

ただしこのコードは、Python 3.6.9 では動作するが、Python 2.7.18 では%zが対応しておらず以下のエラーで機能しない。

Traceback (most recent call last):
  File "strptime.py", line 18, in <module>
    dt_tz = datetime.datetime.strptime(date_str_tz, format_str_tz)
  File "/usr/local/lib/python2.7/_strptime.py", line 324, in _strptime
    (bad_directive, format))
ValueError: 'z' is a bad directive in format '%a %b %d %H:%M:%S %z %Y'

タイムゾーン名表記 (例 JST)

'Thu Feb 11 17:07:53 JST 2021'

この表記のJSTの部分は、大文字の%Zを使えば取得できる。

date_str_tz = 'Thu Feb 11 17:07:53 JST 2021'
format_str_tz = '%a %b %d %H:%M:%S %Z %Y'
dt_tz = datetime.datetime.strptime(date_str_tz, format_str_tz)
print(dt_tz)
print(dt_tz.tzinfo)  # None

実行すると以下の通り。
%zの場合と異なりtzinfoはNoneとなっている。

2021-02-11 17:07:53
None

ただしこの%Zを使ったタイムゾーン名の書式指定は、実行環境であるOSのタイムゾーン設定に依存しており、GMTUTC以外のタイムゾーンについてはOSの設定と合致していないとエラーになってしまう。
例えばpython:3Dockerイメージのように、OSのタイムゾーンが設定されておらずUTCのまま(シェル上でdate実行などで確認できる)の場合は、以下のようにエラーとなる。

Traceback (most recent call last):
  File "/tmp/strptime.py", line 12, in <module>
    dt_tz = datetime.datetime.strptime(date_str_tz, format_str_tz)
  File "/usr/local/lib/python3.9/_strptime.py", line 568, in _strptime_datetime
    tt, fraction, gmtoff_fraction = _strptime(data_string, format)
  File "/usr/local/lib/python3.9/_strptime.py", line 349, in _strptime
    raise ValueError("time data %r does not match format %r" %
ValueError: time data 'Thu Feb 11 17:07:53 JST 2021' does not match format '%a %b %d %H:%M:%S %Z %Y'

該当ドキュメントは以下の通り。(日本語版でも訳されてないけどなぜかどの言語もJSTと日本に住んでる人に向けた例文になっているw)

  • %Z

    In strftime(), %Z is replaced by an empty string if tzname() returns None; otherwise %Z is replaced by the returned value, which must be a string. strptime() only accepts certain values for %Z:

    1. any value in time.tzname for your machine's locale
    2. the hard-coded values UTC and GMT

    So someone living in Japan may have JST, UTC, and GMT as valid values, but probably not EST. It will raise ValueError for invalid values.

"Technical Detail" / datetime — Basic date and time types — Python 3.9.1 documentation https://docs.python.org/3/library/datetime.html#technical-detail

例えばESTというタイムゾーン名(東部標準時)の場合は、OSのタイムゾーン設定もESTになっている必要がある。

$ docker run --rm -v "$PWD":/mnt -e TZ=America/New_York -it python:3 bash
root@4e1469a8fd52:/# date
Thu Feb 11 08:09:23 EST 2021
root@4e1469a8fd52:/# python
Python 3.9.1 (default, Feb  9 2021, 07:42:03) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> date_str_tz = 'Thu Feb 11 17:07:53 JST 2021'
>>> format_str_tz = '%a %b %d %H:%M:%S %Z %Y'
>>> dt_tz = datetime.datetime.strptime(date_str_tz, format_str_tz)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.9/_strptime.py", line 568, in _strptime_datetime
    tt, fraction, gmtoff_fraction = _strptime(data_string, format)
  File "/usr/local/lib/python3.9/_strptime.py", line 349, in _strptime
    raise ValueError("time data %r does not match format %r" %
ValueError: time data 'Thu Feb 11 17:07:53 JST 2021' does not match format '%a %b %d %H:%M:%S %Z %Y'
>>> print(dt_tz)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'dt_tz' is not defined

上記の通り、OS設定がESTになっていると,JSTの文字列は解析失敗している。
この状態からESTである文字列を処理すると以下の通り。

>>> date_str_tz = 'Thu Feb 11 17:07:53 EST 2021'
>>> dt_tz = datetime.datetime.strptime(date_str_tz, format_str_tz)
>>> print(dt_tz)
2021-02-11 17:07:53
>>> print(dt_tz.tzinfo)
None

この通り、ESTというタイムゾーン名を処理できている。

なお、タイムゾーン名がGMTあるいはUTCであれば、OSのタイムゾーンには関係せず、Python 2.7.18 でも動作確認。

$ docker run --rm -v "$PWD":/mnt -it python:2 bash
root@2bd09566c12f:/# python
Python 2.7.18 (default, Apr 20 2020, 19:27:10) 
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> date_str_tz = 'Thu Feb 11 17:07:53 UTC 2021'
>>> format_str_tz = '%a %b %d %H:%M:%S %Z %Y'
>>> dt_tz = datetime.datetime.strptime(date_str_tz, format_str_tz)
>>> dt_tz
datetime.datetime(2021, 2, 11, 17, 7, 53)
>>> dt_tz.tzinfo
>>> print(dt_tz.tzinfo)
None

Dockerのpythonコンテナでタイムゾーン設定するには

-e TZ=Asia/Tokyoを付与する。
※ これはpythonコンテナイメージ(baseがdebian)の場合。CentOS系も同様。ただしAlpineはまた別

[zaki@cloud-dev ~]$ docker run --rm -v "$PWD":/mnt -e TZ=Asia/Tokyo -it python:3 bash
root@d8719dc21019:/# date
Thu Feb 11 22:47:17 JST 2021

(おまけ) strptime()のフォーマット文字列抜粋

頻出

指定子 意味
%Y 西暦 (4桁)
%m 月 (01オリジン)
%d
%H
%M
%S

マイナー(個人の感想)な表記

指定子 意味
%a 曜日の英語表記(Mon.とかfriとか)
%b 月の英語表記(Febとかmayとか)
%f マイクロ秒(秒の小数点未満6桁)
%I 12時間表記のhour (↓とセットで使う)
%p AM/PM (↑とセットで使う)
%Z タイムゾーン名 (UTCとかGMTとかJSTとか。本記事参照)
%z UTCオフセット

ドキュメントには「ロケールの曜日名」とか書かれているが、試した限りja_JP.UTF-8設定の環境でも「」とかは認識しなかった。

参考情報

docs.python.org

qiita.com

dev.classmethod.jp

note.nkmk.me

qiita.com

[Ansible] jc parserのFilterを使った構造化データ化 (community.generalのフィルタ)

jc parserをAnsibleで使ってみる。

github.com

2021.07.06追記: community.generalコレクションのフィルタのドキュメントがいつのまにか追加されてた -> community.general Filter Guide — Ansible Documentation

jc parserについて

jc parserは、Ansibleで使用できるFilterの一つ。
jcで用意されているparserを指定することで、様々なLinuxコマンドの出力などを自動でparseし、構造化データに変換することができる。

Ansible 2.10だとcommunity.general collectionにFilterとして含まれる。

Community.General Plugin Index

ただ、Ansible DocumentationサイトではFilterのリファレンス(Filterのリスト)が無いため、2021.02.09時点でドキュメントはちょっと見当たらず。。 → 2021.07.06 現在、フィルターガイドというドキュメントが(6月頃に?)新設されていて、community.general コレクションで提供されるフィルタープラグインはここにまとめられている。

docs.ansible.com

ソースはplugins/filter/jc.pyで確認でき、コメントを見ればだいたい使い方はわかるはず。

また、jcそのものについては、jc parser本体のリポジトリに詳細がある

github.com

使えるparserの種類

これはjcのGitHubのリポジトリのREADMEのParsersの章を見るのが早い。
ここの一覧はCLIツールとしてのjcで使用するparserのリストになっているが、Ansibleでこれらのparser名をFilterの引数に指定して使う。

簡単な使い方

環境

確認した環境は以下の通り。

  • Ansible: 2.10.5
  • community.general: 1.3.5
  • jc: 1.14.1

準備

jcパッケージが必要なのでpipでインストールしておく。

$ pip install jc

ps

例えばps auxの結果を構造化データに変換したい場合。

    - name: jc parser
      vars:
        result: "{{ lookup('pipe', 'ps aux') }}"
      debug:
        msg: "{{ result | community.general.jc('ps') }}"

この内容のplaybookを実行すると、実行結果は以下の通り。

TASK [jc parser] **************************************************************
ok: [localhost] => {
    "msg": [
        {
            "command": "/usr/lib/systemd/systemd --switched-root --system --deserialize 22",
            "cpu_percent": 0.0,
            "mem_percent": 0.0,
            "pid": 1,
            "rss": 6048,
            "start": "2020",
            "stat": "Ss",
            "time": "9:40",
            "tty": null,
            "user": "root",
            "vsz": 194036
        },
        {
            "command": "[kthreadd]",
            "cpu_percent": 0.0,
            "mem_percent": 0.0,
            "pid": 2,
            "rss": 0,
            "start": "2020",
            "stat": "S",
            "time": "0:00",
            "tty": null,
            "user": "root",
            "vsz": 0
        },

        [...]

ちなみに、ps -efの結果でもちゃんとparseされる。
ただし、ps auxf見れる階層情報についてはparse対象外のため、プロセス名(command)の部分に階層表示のための|\\_が入り込む。

free

    - name: jc parser free
      vars:
        free: "{{ lookup('pipe', 'free') }}"
      debug:
        msg: "{{ free | community.general.jc('free') }}"
TASK [jc parser free] *********************************************************
ok: [localhost] => {
    "msg": [
        {
            "available": 3237832,
            "buff_cache": 3034204,
            "free": 528604,
            "shared": 27256,
            "total": 7990060,
            "type": "Mem",
            "used": 4427252
        },
        {
            "free": 3655420,
            "total": 3670012,
            "type": "Swap",
            "used": 14592
        }
    ]
}

freeは人の目に優しいhuman optionを付けてると(おそらく数値のみを拾ってるっぽくて)parseできない。
(オプション確認してhumanhだとわかったけど、ずっとhugehだと思ってた…)

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           7.6G        4.2G        584M         26M        2.9G        3.2G
Swap:          3.5G         14M        3.5G

こうなる状態で、以下のplaybookを実行。

    - name: jc parser free
      vars:
        free: "{{ lookup('pipe', 'free -h') }}"
      debug:
        msg: "{{ free | community.general.jc('free') }}"

結果は以下の通り。

TASK [jc parser free] *********************************************************
ok: [localhost] => {
    "msg": [
        {
            "available": null,
            "buff_cache": null,
            "free": null,
            "shared": null,
            "total": null,
            "type": "Mem",
            "used": null
        },
        {
            "free": null,
            "total": null,
            "type": "Swap",
            "used": null
        }
    ]
}

他にもiptablesrouteなど、OSの設定系を構造化データとして取得することで加工・再利用しやすくなると思う。
ssはあるのに、ipは一覧に無さげなのは何故だろう。(なのでTTP使ったparseを試した)

2021.07.03 追記:

なるほど、ip -j afrom_jsonフィルタに突っ込んだりすれば簡単にオブジェクトに出来るから、jc parserが無くても大丈夫ですね。情報ありがとうございます!

ini

個人的に面白いと思ったのがiniのフィルタ。

[ansible]
command = ansible
filetype = yaml
name = playbook

[kubernetes]
command = kubectl
filetype = yaml
name = manifest

こんな形式のテキストデータに対して、

    - name: jc parser ini
      vars:
        ini: "{{ lookup('file', 'jc-sample.ini') }}"
      debug:
        msg: "{{ ini | community.general.jc('ini') }}"

iniを指定してフィルタをかますと、

ok: [localhost] => {
    "msg": {
        "ansible": {
            "command": "ansible",
            "filetype": "yaml",
            "name": "playbook"
        },
        "kubernetes": {
            "command": "kubectl",
            "filetype": "yaml",
            "name": "manifest"
        }
    }
}

このように、セクション名をキーにとしたディクショナリ型に変換してくれる。

kv (key / value)

セクション名の無い単なるkey=value形式のファイルの場合。

command = ansible
filetype = yaml
name = playbook

kvというparserもあるが、実はiniでもparseできる。
デリミタは=でなく:でもOK

command: ansible
filetype: yaml
name: playbook

ソース確認するとkvの中身は実はiniをコールしているだけ。

サンプル

github.com

まとめ

jc parserに対応している書式のテキストデータを扱う場合は簡単に処理できるのでオススメです。

[Ansible] cli_parseとTTP(Template Text Parser)を使った任意のテキストのparse処理おためし入門

Ansibleのcli_parseモジュールで、PythonのTTP(Template Text Parser)とテンプレートファイルを使ったテキスト解析を試してみた。

ttp.readthedocs.io

cli_parseは、2.10時点で標準のansible.netcommon.cli_parseと、Ansible 3(3.0.0b1)に新たに標準に含まれているansible.utils.cli_parseがある。
現状では中身は同じだが、ansible.utilsコレクション側でメンテされる(こっちに移管される?)のかな?

今回はひとまず標準で使用可能なansible.netcommon.cli_parseを使ってみる。
ansible.utils.cli_parseの場合はnetcommonの場所を読み替えればOK。(のハズ)

環境

  • Ansible: 2.10.5
  • ansible.netcommon 1.4.1
  • ttp: 0.6.0

お題

ip aの結果をparseしてみる。

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:22:86:2f brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.18/24 brd 192.168.0.255 scope global noprefixroute ens192
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe22:862f/64 scope link 
       valid_lft forever preferred_lft forever
3: br-7a62608ea9c3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:78:cd:77:68 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-7a62608ea9c3
       valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:12:db:d5:fd brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:12ff:fedb:d5fd/64 scope link 
       valid_lft forever preferred_lft forever
5: br-ec4e8609788c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:a0:58:03:b3 brd ff:ff:ff:ff:ff:ff
    inet 172.26.0.1/16 brd 172.26.255.255 scope global br-ec4e8609788c
       valid_lft forever preferred_lft forever
6: br-f28d1c1cb87b: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:03:19:f1:f8 brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.1/16 brd 172.19.255.255 scope global br-f28d1c1cb87b
       valid_lft forever preferred_lft forever
7: br-11a5b9bc6a2c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:aa:9c:c7:a8 brd ff:ff:ff:ff:ff:ff
    inet 172.20.0.1/16 brd 172.20.255.255 scope global br-11a5b9bc6a2c
       valid_lft forever preferred_lft forever
    inet6 fc00:f853:ccd:e793::1/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aaff:fe9c:c7a8/64 scope link 
       valid_lft forever preferred_lft forever
    inet6 fe80::1/64 scope link 
       valid_lft forever preferred_lft forever
8: br-1e7a6f36bebc: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:5f:6e:38:28 brd ff:ff:ff:ff:ff:ff
    inet 172.21.0.1/16 brd 172.21.255.255 scope global br-1e7a6f36bebc
       valid_lft forever preferred_lft forever
9: br-4221181b1c51: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:de:a0:d1:67 brd ff:ff:ff:ff:ff:ff
    inet 172.22.0.1/16 brd 172.22.255.255 scope global br-4221181b1c51
       valid_lft forever preferred_lft forever
44: br-d51584ee1064: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:83:4a:1d:18 brd ff:ff:ff:ff:ff:ff
    inet 172.23.0.1/16 brd 172.23.255.255 scope global br-d51584ee1064
       valid_lft forever preferred_lft forever
    inet6 fe80::42:83ff:fe4a:1d18/64 scope link 
       valid_lft forever preferred_lft forever
71: br-378c264dc6ee: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:21:be:06:4f brd ff:ff:ff:ff:ff:ff
    inet 172.24.0.1/16 brd 172.24.255.255 scope global br-378c264dc6ee
       valid_lft forever preferred_lft forever
    inet6 fe80::42:21ff:febe:64f/64 scope link 
       valid_lft forever preferred_lft forever
117: vethc853031@if116: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether ea:d7:90:4c:62:be brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::e8d7:90ff:fe4c:62be/64 scope link 
       valid_lft forever preferred_lft forever

準備

ttp

TTPの利用にはPythonモジュールのttpが必要。
無いとエラーになる。

TASK [cli parse] **************************************************************
fatal: [localhost]: FAILED! => changed=false 
  msg: Failed to import the required Python library (ttp) on cloud-dev's Python /home/zaki/src/ansible-sample/venv/a2.10/bin/python3. Please read the module documentation and install it in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter

$ pip install ttpする。

ansible.utils

ansible.netcommon.cli_parseでなくansible.utils.cli_parseを使用する場合は追加でコレクションをインストールする。

$ ansible-galaxy collection install ansible.utils

pip install ansibleでインストールしたAnsible環境であればansible.netcommon.cli_parseは標準で利用可能。

cli_parse

入力テキスト

cli_parseでテキストをparseする上で、よくあるケースとしては「ネットワークコマンドの実行結果のparse」で、これはcommandパラメタでコマンドを指定することで実現できる。
今回のip aについても同様にcommandで指定する。

    - name: cli parse
      ansible.netcommon.cli_parse:
        command: ip a

なお、すでにコマンド結果をテキストで保持しており、その変数を対象にparseしたい場合はtextで指定することもできる。

      ansible.netcommon.cli_parse:
        text: "{{ ip_a_result }}"

TTPの指定

parseのエンジンにTTPを指定する。
それ加えて、parse用のテンプレートファイルを指定する。

        parser:
          name: ansible.netcommon.ttp
          template_path: "./ttp-template/ip-a.ttp"

ここでは./ttp-template/ip-a.ttpにあるファイルを指定。

TTPテンプレートファイル

ip aの結果に対して何をピックアップしたいかをテンプレートファイルに定義する。

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} {{ ignore(".*") }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}

このテンプレートを使うことで、ip aの結果の以下の部分をparseすることができる。

2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    inet 192.168.0.18/24 brd 192.168.0.255 ...

parseした結果は、{{ hogehoge }}で定義した箇所をhogehoge: 元テキストの値という構造化データとして扱うことができる。

ignore()

ttp.readthedocs.io

テンプレートファイルの定義は先頭一致ではなく行全体をマッチングするため、取り込みたい部分以降についても記載が必要。
その際、特に処理が不要であればignoreを指定すれば無視される。

ignoreはデフォルトで正規表現\Sにマッチでスペースは含まれないため、「行の残り全てを無視」であれば、正規表現.*を指定する。

サンプル実行

    - name: cli parse
      ansible.netcommon.cli_parse:
        command: ip a
        parser:
          name: ansible.netcommon.ttp
          template_path: "./ttp-template/ip-a.ttp"
      register: result
    - debug: print result
      debug:
        msg: "{{ result.parsed }}"

registerを使って結果を参照している。
これを実行すると以下の通り。

ok: [localhost] => 
  msg:
  - - - interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        interface: br-7a62608ea9c3
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '3'
      - addr: 172.17.0.1/16
        broadcast: 172.17.255.255
        interface: docker0
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '4'
      - addr: 172.26.0.1/16
        broadcast: 172.26.255.255
        interface: br-ec4e8609788c
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '5'
      - addr: 172.19.0.1/16
        broadcast: 172.19.255.255
        interface: br-f28d1c1cb87b
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '6'
      - addr: 172.20.0.1/16
        broadcast: 172.20.255.255
        interface: br-11a5b9bc6a2c
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '7'
      - addr: 172.21.0.1/16
        broadcast: 172.21.255.255
        interface: br-1e7a6f36bebc
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '8'
      - addr: 172.22.0.1/16
        broadcast: 172.22.255.255
        interface: br-4221181b1c51
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '9'
      - addr: 172.23.0.1/16
        broadcast: 172.23.255.255
        interface: br-d51584ee1064
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '44'
      - addr: 172.24.0.1/16
        broadcast: 172.24.255.255
        interface: br-378c264dc6ee
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '71'
      - interface: vethc853031@if116
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '117'

何となくいい感じに構造化データにできた。

テンプレートの書き方いろいろ

行内の項目が有ったり無かったり

例えばinetの行、ループバック(lo)の場合は以下の通り。

    inet 127.0.0.1/8 scope host lo

対して、ループバック以外のインタフェースについては以下の通り。

    inet 192.168.0.18/24 brd 192.168.0.255 scope global noprefixroute ens192

この通り、brd ブロードキャストアドレスの項目が有ったり無かったりする。

この場合は、無理に1行でテンプレートを定義せずに(というかわからなかった)、2行に分けると簡単に定義できる。

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} {{ ignore(".*") }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

このテンプレートを使用すれば、parse結果は以下の通り。

ok: [localhost] => 
  msg:
  - - - addr: 127.0.0.1/8
        interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        interface: br-7a62608ea9c3
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '3'

        [...snip...]

この通り、ブロードキャストアドレスの記載の無いループバックのインタフェースも、inetの行のIPアドレスを取得できている。

重要なのはbrd ブロードキャストアドレスのテンプレートから先に記述すること。
ブロードキャストアドレスの定義がない行を先に書いてしまうと、全ての項目がこちらにマッチしてしまい、ブロードキャストアドレスのテンプレートまで処理が到達しない。

ただし、先頭行はこの方法は使えないので別の対処が必要。

先頭行の複数定義

テンプレートの先頭行を、

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}

と記述すると、ループバックとens192はマッチするが、残りのインタフェースはqlenの記載がないためそもそも処理対象にならない。
実行すると以下の通り。

ok: [localhost] => 
  msg:
  - - - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP

そしてこの場合、前述と同じように以下のように2行安直に書いても動作しない。(2行目の記述が機能しない)

※ ダメな例
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

これは、TTPのテンプレートにおける1行目の記述は「グループの開始」を表す意味も持っており、デフォルトでは1行目のみがその効果を持つ。
そこで、グループの開始の定義を複数行で行いたい場合は_start_識別子を使う。

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

このように2行目の記述した定義に| _start_を追加(これは試した限り行内のどの変数に入れても大丈夫そう。ドキュメントへの記載は見当たらず)する。
このテンプレートで実行すると以下の通り、qlenの有るインタフェースも無いインタフェースもparseできている。

ok: [localhost] => 
  msg:
  - - - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        group: default
        interface: br-7a62608ea9c3
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '3'
        qdisc: noqueue
        state: DOWN
      - addr: 172.17.0.1/16
        broadcast: 172.17.255.255
        group: default
        interface: docker0
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '4'
        qdisc: noqueue
        state: UP

        [...snip...]

<>

{{ mode }}で取っている mode: <BROADCAST,MULTICAST,UP,LOWER_UP> の部分、普通に考えて<>を取り除きたいけど、単純に

{{ number }}: {{ interface }}: <{{ mode }}> mtu {{ mtu }} ...

のようにテンプレート内に<>を直接記述すると、

fatal: [localhost]: FAILED! => changed=false 
  msg: 'Template Text Parser returned an error while parsing. Error: not well-formed (invalid token): line 2, column 32'

とTTPの書式のエラーになってしまう。
これは、TTPのテンプレート内ではXML書式として<>がメタ文字として扱われるためで、たとえば1テンプレートファイルで複数の書式を記述したい場合に

<group name="interfaces">
{{ number }}: {{ interface }}: {{ mode }} ...
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}
</group>

このような使い方をするため、TTPの処理で<>が期待する使われ方をしていないというエラーになっている。

回避策としてはHTMLの実体参照で記載する(これ見つけるのに半日かかった)。。。んだけど、これバグじゃね?って感じが少しするが、とりあえず現バージョン(ansible.netcommon 1.4.1 + ttp 0.6.0)だと以下の通りの動作。

ただし、HTML実体参照で動くというのもドキュメントには記載を見つけられなかったので、正しい使い方なのか未対応の処理を貫通した動作なのかは不明。

<group>で囲む

全体を<group name="...">などで囲む。

ttp.readthedocs.io

その上で、以下のように&lt;{{ mode }}&gt;と記載する。

<group name="interfaces">
{{ number }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}
</group>

そうすると、結果の構造がnameで指定したキーの辞書型になるが、HTMLの実体参照を使って&lt;&gt;を指定したものが<>にマッチして正しく動く。

結果は以下の通り。

ok: [localhost] => 
  msg:
  - - interfaces:
      - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode: LOOPBACK,UP,LOWER_UP
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode: BROADCAST,MULTICAST,UP,LOWER_UP
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        group: default
        interface: br-7a62608ea9c3
        mode: NO-CARRIER,BROADCAST,MULTICAST,UP
        mtu: '1500'
        number: '3'
        qdisc: noqueue
        state: DOWN

        [...snip...]

直接HTML実体参照を記述

以下のように<group>などで囲まずに直接HTML実体参照を記述すると、

※ ダメな例
{{ number }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

<>を直接使用した場合と同じくinvalid tokenのエラーとなる。
ここでちょっと小細工して、&をさらに実体参照で記述して、

{{ number }}: {{ interface }}: &amp;lt;{{ mode }}&gt; ...

と記述すると正しく動作する。
TTPの処理のソース見ればわかるかもしれないけどそこまではチェックしてないが、どうやら&を見つけたところから処理が変わってると思われ。
ちなみに&gt;の方はそのままでも動作するし、試した感じでは以下のように>と直接書いても大丈夫だった。

{{ number }}: {{ interface }}: &amp;lt;{{ mode }}> ...

紛らわしいので、現状では<group>で囲んだ方が良さそう。

部分的にsplit

<>を除いたらこの内部の,区切りの文字列をバラシてリストにしたいですが、はい、splitも利用可。

<group name="interfaces">
{{ number }}: {{ interface }}: &lt;{{ mode | split(",") }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: &lt;{{ mode | split(",") }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}
</group>

これ実行すれば以下の通り。

ok: [localhost] => 
  msg:
  - - interfaces:
      - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode:
        - LOOPBACK
        - UP
        - LOWER_UP
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode:
        - BROADCAST
        - MULTICAST
        - UP
        - LOWER_UP
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        group: default
        interface: br-7a62608ea9c3
        mode:
        - NO-CARRIER
        - BROADCAST
        - MULTICAST
        - UP
        mtu: '1500'
        number: '3'
        qdisc: noqueue
        state: DOWN

        [...snip...]

splitはドキュメントに記載がない。
joinはあるのに何故だろう。。

まとめ

ということで、自前のテンプレートを用意することで、Ansibleのcli_parseとTTPを使って任意の書式の解析と構造化データとしての取り込みはできそう。
なお、cli_parseはTTP以外にもtextfsmなどほかのparserを使用することができるので、そのうち試したい。

関連ドキュメント

netcommon.cli_parseのドキュメントはこちら。

docs.ansible.com

コレクションのGitHubプロジェクトのドキュメントはこちら。

github.com

github.com

構造化でデータのparseについて

docs.ansible.com

TTP(Template Text Parser)

ttp.readthedocs.io

てくなべcli_parse

tekunabe.hatenablog.jp

サンプルコード

github.com

Ansibleでsplitとjoinを使った文字列・配列操作

文字列を指定文字で分割して配列にバラすsplitと、配列を指定文字で結合して文字列に合成するjoinは、文字列と配列操作において大変有用。
主要なプログラミング言語には大抵この関数/メソッドは用意されており(※俺調べ)、フィルタ系の処理を書くときには重宝する。

qiita.com

Ansibleでこれらを活用するシチュエーションがどれくらいあるかはともかく、Ansibleでもsplitjoinを使うことができるのでその方法についてまとめ。
なお、環境はAnsible 2.10.5 時点。

split

Ansibleとしてはちょっとトリッキーかもしれないが、string型の変数に対してPythonstr#split()が使用できる。
時期バージョンではフィルターとしてsplitが使えるようになる模様

docs.python.org

  - name: split
    vars:
      string: 'tokyo, yokohama, fukuoka'
    debug:
      msg: "{{ string.split(',') }}"

実行結果

ok: [localhost] => 
  msg:
  - tokyo
  - ' yokohama'
  - ' fukuoka'

このとおり、,で分割して配列化できている。
が、,の後ろのスペースを除去できてないので配列要素にスペースが入っており綺麗な状態になっていない。

,の後ろの必ずスペース1文字しかないという書式であれば正規表現を使うまでもないが、そこが不明瞭であれば、やっぱり m/,\s*/ を使いたいところ。

Pythonstr#split()正規表現を直接指定できずreと連携する必要があるため、Ansibleから使うのはちょっと厳しい(やりかたがわからなかった)
なので一旦正規表現を使った文字列置換を一度行い、その後にsplitをすれば大体良い感じにできる。
(これは状況に因るので要件によって適切な方法を検討すること)

  - name: split
    vars:
      string: 'tokyo, yokohama, fukuoka,sapporo,  sendai, tokorozawa'
    debug:
      msg: "{{ (string | regex_replace(',\\s*', ',')).split(',') }}"

この例のように、対象文字列stringのデリミタで、,の後にスペースがあったりなかったり、あっても2つ以上のスペースだったり、、、という場合、regex_replaceを使って「綺麗に」してあげれば、実行結果は以下の通り。

ok: [localhost] => 
  msg:
  - tokyo
  - yokohama
  - fukuoka
  - sapporo
  - sendai
  - tokorozawa

余計なスペースが入っておらず、綺麗になった。

フィルターとしてのsplitについての情報はこちら。

github.com

join

Jinja2テンプレートのフィルタとして用意されているjoin()を使用できる。

jinja.palletsprojects.com

    - name: join sample
      vars:
        sample_items:
          - foo
          - bar
          - baz
      debug:
        msg: 
          - "{{ sample_items }}"
          - "{{ sample_items | join('<>') }}"

['foo', 'bar', 'baz']という配列に対して、joinを使って<>で連結」という内容。
実行すると以下の通り。

ok: [localhost] => 
  msg:
  - - foo
    - bar
    - baz
  - foo<>bar<>baz

まとめ

splitはファイルパスやURLの文字列を操作するためのsplit系フィルタは用意されてるが、任意の文字列に対する汎用処理を行うフィルタはない。ただし、Pythonのメソッドを使用することが可能。
joinはJinaj2のフィルタを使えばOK


splitjoinは(ログファイル解析などで)めっちゃ応用が利くので、インフラの人も正規表現とセットで覚えるべき。

[Ansible] intフィルターで16進数表記の文字列を基数変換

AnsibleではJinja2のint()フィルターで文字列を整数へ変換できるが、0xdeadbeafみたいな16進数表記の文字列をint(base=16)のようにパラメタを指定することで基数変換することもできる。

16進表記の文字列を比較する処理を使う機会があったのでまとめ。

確認した環境は以下の通り。

(a2.10) [zaki@cloud-dev filter (master)]$ ansible --version
ansible 2.10.5
  config file = /home/zaki/src/ansible-sample/filter/ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

int(base=16)

16進数表記の文字列をintegerに変換。

  - name: hex to dec
    vars:
      hex_values:
      - '0xa'
      - '0x0a'
      - '0xff'
      - '0xffff'
      - '0xdeadbeaf'
      - '0b0'  # 0x0b0 
      - 'b'    # 0x0b
      - 'ff'   # 0xff
      - 'z'    # 0
      - '0o11' # 0
    debug:
      msg: "{{ item | int(base=16) }}"
    with_items: "{{ hex_values }}"

実行結果は以下の通り。

TASK [hex to dec] *************************************************************
ok: [localhost] => (item=0xa) => 
  msg: '10'
ok: [localhost] => (item=0x0a) => 
  msg: '10'
ok: [localhost] => (item=0xff) => 
  msg: '255'
ok: [localhost] => (item=0xffff) => 
  msg: '65535'
ok: [localhost] => (item=0xdeadbeaf) => 
  msg: '3735928495'
ok: [localhost] => (item=0b0) => 
  msg: '176'
ok: [localhost] => (item=b) => 
  msg: '11'
ok: [localhost] => (item=ff) => 
  msg: '255'
ok: [localhost] => (item=z) => 
  msg: '0'
ok: [localhost] => (item=0o11) => 
  msg: '0'

16進表記になっていない文字列は0になっている。
2進数表記の0b00b01010101のような文字列を変換すると、2進数でなく0xがあるとみなされた16進数として処理されるので注意。

int(baes=8)

8進数表記の文字列をintegerに変換。

  - name: oct to dec
    vars:
      oct_values:
      - '0o11'
      - '77'   # 0o77 -> 63
      - 'z'    # 0
      - '0x0a' # 0
    debug:
      msg: "{{ item | int(base=8) }}"
    with_items: "{{ oct_values }}"

実行結果は以下の通り。

TASK [oct to dec] *************************************************************
ok: [localhost] => (item=0o11) => 
  msg: '9'
ok: [localhost] => (item=77) => 
  msg: '63'
ok: [localhost] => (item=z) => 
  msg: '0'
ok: [localhost] => (item=0x0a) => 
  msg: '0'

そのまま。

int(base=2)

2進数表記の文字列をintegerに変換。

  - name: bin to dec
    vars:
      bin_values:
      - '0b11'
      - '101'  # 0b101 -> 5
      - 'z'    # 0
      - '0x0a' # 0
    debug:
      msg: "{{ item | int(base=2) }}"
    with_items: "{{ bin_values }}"

実行結果は以下の通り。

TASK [bin to dec] *************************************************************
ok: [localhost] => (item=0b11) => 
  msg: '3'
ok: [localhost] => (item=101) => 
  msg: '5'
ok: [localhost] => (item=z) => 
  msg: '0'
ok: [localhost] => (item=0x0a) => 
  msg: '0'

参考

jinja.palletsprojects.com

int(value, default=0, base=10)

You can also override the default base (10) in the second parameter, which handles input with prefixes such as 0b, 0o and 0x for bases 2, 8 and 16 respectively.

サンプル

github.com


(´-`).。oO(これintegerにしてるだけだし基数変換って言って良いのかな…

[Oracle Cloud] CLIツールのセットアップメモ

Oracle CloudをCLIで操作するためのツールのインストールとセットアップについて。
以前何事もなくセットアップしていたんだけど、新しい環境に入れようとしたらいろいろ(鍵設定周りで)詰まったのでメモ。
(全自動でAPIキー設定出来る方法があるかもしれないけど分からなかったので、公開鍵を手動で登録してる)

環境

[zaki@manager-dev ~]$ oci --version
2.20.0
[zaki@manager-dev ~]$ python3 --version
Python 3.6.8
[zaki@manager-dev ~]$ cat /etc/centos-release
CentOS Linux release 7.9.2009 (Core)

インストール

OCI CLIのインストールは以下。

docs.oracle.com

インストールにはPython3が必要なため、Python2しか入っていないLinux OSだとアップグレードするか?と聞かれる。事前にPython3を入れておくと無難。

bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"

デフォルトだと$HOME/lib$HOME/binへインストールされる。
$HOME/localとか使いたい場合はインストール時に対話的に(3か所)聞かれるのでパスを入力する。

[zaki@manager-dev ~]$ bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 16053  100 16053    0     0  54223      0 --:--:-- --:--:-- --:--:-- 54416

    ******************************************************************************
    You have started the OCI CLI Installer in interactive mode. If you do not wish
    to run this in interactive mode, please include the --accept-all-defaults option.
    If you have the script locally and would like to know more about
    input options for this script, then you can run:
    ./install.sh -h
    If you would like to know more about input options for this script, refer to:
    https://github.com/oracle/oci-cli/blob/master/scripts/install/README.rst
    ******************************************************************************
Downloading Oracle Cloud Infrastructure CLI install script from https://raw.githubusercontent.com/oracle/oci-cli/v2.14.4/scripts/install/install.py to /tmp/oci_cli_install_tmp_zAYF.
######################################################################## 100.0%
Running install script.
python3 /tmp/oci_cli_install_tmp_zAYF 
-- Verifying Python version.
-- Python version 3.6.8 okay.

===> In what directory would you like to place the install? (leave blank to use '/home/zaki/lib/oracle-cli'): /home/zaki/local/oracle-cli/lib
-- Creating directory '/home/zaki/local/oracle-cli/lib'.
-- We will install at '/home/zaki/local/oracle-cli/lib'.

===> In what directory would you like to place the 'oci' executable? (leave blank to use '/home/zaki/bin'): /home/zaki/local/oracle-cli/bin
-- Creating directory '/home/zaki/local/oracle-cli/bin'.
-- The executable will be in '/home/zaki/local/oracle-cli/bin'.

===> In what directory would you like to place the OCI scripts? (leave blank to use '/home/zaki/bin/oci-cli-scripts'): /home/zaki/local/oracle-cli/bin/oci-cli-scripts
-- Creating directory '/home/zaki/local/oracle-cli/bin/oci-cli-scripts'.
-- The scripts will be in '/home/zaki/local/oracle-cli/bin/oci-cli-scripts'.

===> Currently supported optional packages are: ['db (will install cx_Oracle)']
What optional CLI packages would you like to be installed (comma separated names; press enter if you don't need any optional packages)?: 
-- The optional packages installed will be ''.
-- Trying to use python3 venv.
-- Executing: ['/usr/bin/python3', '-m', 'venv', '/home/zaki/local/oracle-cli/lib']
-- Executing: ['/home/zaki/local/oracle-cli/lib/bin/pip', 'install', '--upgrade', 'pip']
Cache entry deserialization failed, entry ignored
Collecting pip
  Downloading https://files.pythonhosted.org/packages/de/47/58b9f3e6f611dfd17fb8bd9ed3e6f93b7ee662fb85bdfee3565e8979ddf7/pip-21.0-py3-none-any.whl (1.5MB)
    100% |████████████████████████████████| 1.5MB 1.3MB/s 
Installing collected packages: pip
  Found existing installation: pip 9.0.3
    Uninstalling pip-9.0.3:
      Successfully uninstalled pip-9.0.3
Successfully installed pip-21.0

  [snip]

Successfully built PyYAML retrying terminaltables
Installing collected packages: pycparser, six, cffi, cryptography, pytz, python-dateutil, pyOpenSSL, configparser, certifi, terminaltables, retrying, PyYAML, oci, jmespath, click, arrow, oci-cli
Successfully installed PyYAML-5.1.2 arrow-0.17.0 certifi-2020.12.5 cffi-1.14.4 click-6.7 configparser-4.0.2 cryptography-3.2.1 jmespath-0.10.0 oci-2.29.0 oci-cli-2.20.0 pyOpenSSL-19.1.0 pycparser-2.20 python-dateutil-2.8.1 pytz-2020.5 retrying-1.3.3 six-1.14.0 terminaltables-3.1.0

===> Modify profile to update your $PATH and enable shell/tab completion now? (Y/n): y

===> Enter a path to an rc file to update (file will be created if it does not exist) (leave blank to use '/home/zaki/.bashrc'): 
-- Backed up '/home/zaki/.bashrc' to '/home/zaki/.bashrc.backup'
-- Tab completion set up complete.
-- If tab completion is not activated, verify that '/home/zaki/.bashrc' is sourced by your shell.
-- 
-- ** Run `exec -l $SHELL` to restart your shell. **
-- 
-- Installation successful.
-- Run the CLI with /home/zaki/local/oracle-cli/bin/oci --help
[zaki@manager-dev ~]$

.bashrcにPATH設定が追加されるのでsourceとかすればociが使えるようになる。
インストールできたら初期設定を行う。

初期設定

CLIをインストールしたら設定。
途中入力する各値は以下で確認できる。

ユーザーのOCIDを確認

f:id:zaki-hmkc:20210131141044p:plain

f:id:zaki-hmkc:20210131141606p:plain

テナンシのOCIDを確認

f:id:zaki-hmkc:20210131141843p:plain

f:id:zaki-hmkc:20210131141937p:plain

リージョンは

f:id:zaki-hmkc:20210131142120p:plain

f:id:zaki-hmkc:20210131142216p:plain

この辺で確認。

[zaki@manager-dev ~]$ oci setup config
    This command provides a walkthrough of creating a valid CLI config file.

    The following links explain where to find the information required by this
    script:

    User API Signing Key, OCID and Tenancy OCID:

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#Other

    Region:

        https://docs.cloud.oracle.com/Content/General/Concepts/regions.htm

    General config documentation:

        https://docs.cloud.oracle.com/Content/API/Concepts/sdkconfig.htm


Enter a location for your config [/home/zaki/.oci/config]: 
Enter a user OCID: ★ユーザーのOCIDを入力
Enter a tenancy OCID: ★テナンシのOCIDを入力
Enter a region by index or name(e.g.
1: ap-chiyoda-1, 2: ap-chuncheon-1, 3: ap-hyderabad-1, 4: ap-melbourne-1, 5: ap-mumbai-1,
6: ap-osaka-1, 7: ap-seoul-1, 8: ap-sydney-1, 9: ap-tokyo-1, 10: ca-montreal-1,
11: ca-toronto-1, 12: eu-amsterdam-1, 13: eu-frankfurt-1, 14: eu-zurich-1, 15: me-dubai-1,
16: me-jeddah-1, 17: sa-santiago-1, 18: sa-saopaulo-1, 19: uk-cardiff-1, 20: uk-gov-cardiff-1,
21: uk-gov-london-1, 22: uk-london-1, 23: us-ashburn-1, 24: us-gov-ashburn-1, 25: us-gov-chicago-1,
26: us-gov-phoenix-1, 27: us-langley-1, 28: us-luke-1, 29: us-phoenix-1, 30: us-sanjose-1): ★使用しているリージョンを入力
Do you want to generate a new API Signing RSA key pair? (If you decline you will be asked to supply the path to an existing key.) [Y/n]: y
Enter a directory for your keys to be created [/home/zaki/.oci]: 
Enter a name for your key [oci_api_key]: 
Public key written to: /home/zaki/.oci/oci_api_key_public.pem
Enter a passphrase for your private key (empty for no passphrase): 
Private key written to: /home/zaki/.oci/oci_api_key.pem
Fingerprint: ........
Config written to /home/zaki/.oci/config


    If you haven't already uploaded your API Signing public key through the
    console, follow the instructions on the page linked below in the section
    'How to upload the public key':

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#How2


[zaki@manager-dev ~]$ 

これでキーペアが作成される。

[zaki@manager-dev ~]$ ls -l ~/.oci/
合計 12
-rw-------. 1 zaki zaki  301  1月 31 11:02 config
-rw-------. 1 zaki zaki 1679  1月 31 11:02 oci_api_key.pem
-rw-------. 1 zaki zaki  451  1月 31 11:02 oci_api_key_public.pem

次は、作成された公開鍵(oci_api_key_public.pem)をクラウドに登録する。

公開鍵設定

docs.oracle.com

webの管理コンソールを開き、右上の「ユーザー設定」からユーザーのOCIDを確認したのと同じ画面下部にある「APIキー」の「APIキーの追加」で、作成した公開鍵(oci_api_key_public.pem)の中身(catなどで確認)を貼り付ける。

f:id:zaki-hmkc:20210131141128p:plain

f:id:zaki-hmkc:20210131141210p:plain

貼り付けるとプレビュー画面になるので、ダイアログを閉じれば公開鍵が登録される。
登録した公開鍵が有効になるまで若干のタイムラグがあるけど、認証できるようになれば、

[zaki@manager-dev ~]$ oci os ns get
{
  "data": "........"
}

という感じで、認証エラーにならず正常にレスポンスを得られる。

コマンド例

コンパートメントの一覧

$ oci iam compartment list

コンパートメント一覧から(1件目の)compartment-idをピックアップ

$ oci iam compartment list --query 'data[0]."compartment-id"' 

可用性ゾーン一覧

oci iam availability-domain list

使用可能イメージ一覧

$ oci compute image list -c $(oci iam compartment list --query 'data[0]."compartment-id"' | sed -e 's/"//g')

これは~/.oci/configに設定されてるコンパートメントID見てくれなくて(?)、-cオプションが必須みたい。

docs.oracle.com

docs.oracle.com