Ansibleのtelnetモジュールでコマンド出力に#が含まれる場合のtips

(諸事情あり)Ansibleのnetworkモジュールではなく、telnetモジュールを使いネットワーク機器の情報を収集しようとした時に、ある行以降の情報が取得できなくて困った。調べてみると、実行したコマンド出力中の # が、プロンプトを識別するための文字指定 [>#] にマッチしてしまっていた。そこで、プロンプトの指定を \n[^\n ]*[>#] に変更したら解決した。

以下、少し詳細。

問題

具体的には、以下のようなタスクを実行した際に、インターフェースのDescriptionに # が使われていると、それをプロンプトとして認識してしまい、出力の読み取りがそこで中断されてしまう。最初は、この後のタスクのTextFSMのパースを疑ったけど、前段の問題だった。今回の話とは関係ないけど、TextFSMすごい。まるで魔法(

- name: show interfaces
  telnet:
    user: user
    password: hogehoge
    login_prompt: "Username: "
    prompts:
      - "[>#]"
    command:
      - terminal length 0
      - show interfaces
    register: interfaces

問題の起きる出力の例:

FastEthernet0/1 is up, line protocol is up (connected)
  Hardware is Lance, address is 0000.0000.0000 (bia 0000.0000.0000)
  Description: hoge-str C#1 P#0
  MTU: 1500bytes, BW 100000 Kbit, DLY 1000 usec, reliability 255/255, txload 1/255, rxload 1/255
  Encapsulation ARPA, loopback not set
(略)

対応

promptsの正規表現を以下のように修正したら、うまくいった。

    prompts:
      - "\n[^\n ]*[>#]"

# が出てくるのはDescription行ぐらいだろうという前提で、# の前にはスペースと改行以外の文字が0個以上、という事を表した。なお、行頭を表す ^ はshow interfacesの一番最初の行の行頭にマッチしてしまうようで、改行記号を使った。

余談

Descriptionに使われている # を無視したいのだから、最初は「# より前かつ同じ行に Description:.* がない #」を表す正規表現を考えた。後読み否定を使って (?<!Description:).*[>#] もしくは (?<!Description:.*)[>#]と表せるかと思ったが、使えなかった。

前者は Description: の直後を示しているだけなので、その次の文字からは否定の効果がなく、意図した動作にはならない。後者は、正規表現としてはよさそうな気がするけど、Pythonの仕様では、後読みでは文字数が可変な指定はできないとドキュメントにあり、ダメだった。

re — Regular expression operations — Python 3.7.6rc1 documentation

The contained pattern must only match strings of some fixed length, meaning that abc or a|b are allowed, but a* and a{3,4} are not.

少し調べると、後読みで可変文字数を使える言語は限られている模様。へぇー。

Regex Tutorial - Lookahead and Lookbehind Zero-Length Assertions

Many regex flavors, including those used by Perl, Python, and Boost only allow fixed-length strings.

関係ないけど、先読み、後読みと聞くと、それぞれ逆の意味を想像してしまう。先読みの場合、先「を」読む、ではなく先「に」読むだと捉えて、左にあるパターンだと考えてしまう。英語のlookaheadとlookbackだと分かりやすいのに(ネイティブの人だと同様にまぎらわしかったりするのだろうか..?)。