こんにちは!デバイスソフトウェア開発部の本間です。
弊社ではデータロガーや通信型ドライブレコーダーなど、様々な IoT デバイスを開発・販売しておりますが、これまでに多くのデバイスの FW 開発・保守に携わらせて頂きました。
FW を開発・保守する際には、プログラムパッチやログ解析の為に、デバイス毎に異なる手順でデバイス内部へアクセス(シェルログイン)する必要があります。また、アクセスする経路もイーサネット、 LTE 回線、RS-232C、UART など多岐にわたります。その為、デバイス毎に接続手順やログイン情報(鍵)などを適切に管理することが重要です。また、作業効率化のために、接続・ログインの一連の流れを自動化することは欠かせません。
私は、デバイスへの接続・ログインを自動化するツールとして、Tera Term というターミナルエミュレータを使用しています。日本のシステム開発現場ではよく使われるツールではないでしょうか。しかし、Tera Term は Windows 専用のソフトウェアで Windows 以外の環境では使用できないという課題があります。
そこで今回は、Tera Term の代替として、かつクロスプラットフォームで動作するツールである Tcl/Expect を紹介させて頂きます。Tcl/Expect というツールを使用することで、Tera Termのようにターゲットへの接続・ログインを自動化できます。
Tera Term について
Tera Term は Windows 向けのターミナルエミュレータです。ターミナルエミュレータとしての機能以外に、SSH クライアント、Telnet クライアント、シリアル通信クライアントを内蔵しており、ネットワークやシリアル通信を介してターゲットへ端末接続&ログインすることが可能です。SCP や XMODEM によるファイル転送を GUI で簡単に実行することもできます。
中でも強力な機能が Tera Term マクロです。マクロ言語 “Tera Term Language (TTL)” を記述することで、ターミナルの起動・制御、SSH やシリアルによる接続・ログインなどの一連の作業を自動化することができます。Tera Term マクロのマニュアルはこちらにあります。
Tcl/Expect とは?
Tcl/Expect とは、対話型 CUI プログラムとのやり取りを自動化するためのツールです。Tcl というスクリプト言語を拡張したものであり、Tcl の文法で対話スクリプトを記述します。マイナーな言語と思うかもしれませんが、他に Tcl/Tk という歴史のある有名な GUI ツールキットがあり、短いコードで簡単に GUI を作成できます。Tcl は Lisp を彷彿とさせる特殊な言語ですが、Expect や Tk という強力なライブラリの存在から、今でも学ぶ価値はあるのではないでしょうか。
Tcl/Expect を含むこれらのツールは歴史があり枯れたソフトウェアである為、様々な環境に移植されておりクロスプラットフォームで使用できます。
なお、Tcl 拡張であることを強調するために “Tcl/Expect” と表記していましたが、Expect が正式名称であるため、以降は “Expect” と表記します。
Expect で自動化してみる
Expect で接続・ログインを自動化する環境を準備します。Expect 以外にターミナルエミュレータや各種通信クライアントのインストールが必要です。具体的なソフトウェアの例を以下に挙げました。これらのソフトウェアは、cygwin (msys) や macOS, 各種 Linux ディストロなど、様々な環境でパッケージとして利用できると思います。
種別 | ソフトウェア名 |
---|---|
ターミナルエミュレータ | mintty, iTerm, Xterm, URxvt |
SSH クライアント | OpenSSH |
Telnet クライアント | netkit-telnet, inetutils-telnet |
シリアル通信クライアント | cu, picocom ※ |
※ minicom や screen でもシリアル通信できますが、これらは curses による TUI を出力する為、行指向の Expect と相性が悪いです。
SSH 接続を自動化する
それでは Expect で SSH 接続を自動化してみます。
以下は localhost で起動している WSL2 (Ubuntu 24) へ SSH 接続する Tera Term マクロです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
; SSH 接続先 HOSTNAME = 'localhost:22' ; 接続先ホスト・ポート ; ログイン情報 USERNAME = 'testuser' ; ログインユーザ USERPASSWD = '12345' ; ログインパスワード ROOTPASSWD = 'root' ; root パスワード ; コマンド構築 COMMAND = HOSTNAME strconcat COMMAND ' /ssh /2 /auth=passwd /user=' strconcat COMMAND USERNAME strconcat COMMAND ' /passwd=' strconcat COMMAND USERPASSWD ; SSH 接続 connect COMMAND ; root に昇格 wait '$' sendln 'su' wait 'Password:' sendln ROOTPASSWD wait '#' sendln 'whoami' |
テストユーザでシェルへログインした後、root へ昇格しています。まず、connect コマンドでターミナルエミュレータ起動し、SSH 接続・ログインを実行します。続いて、wait コマンドでシェルプロンプトやパスワードプロンプトの文字列表示を待機し、sendln コマンドでコマンド実行やパスワード入力のための文字列を送信します。
この Tera Term マクロを Expect スクリプトで書き直してみましょう。以下が書き直した後のスクリプトです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#!/usr/bin/expect # SSH 接続先 set HOSTNAME "localhost" ; # 接続先ホスト set SSHPORT "22" ; # 接続先ポート # ログイン情報 set USERNAME "testuser" ; # ログインユーザ set USERPASSWD "12345" ; # ログインパスワード set ROOTPASSWD "root" ; # root パスワード # SSH 接続 spawn ssh -p$SSHPORT $USERNAME@$HOSTNAME expect { -re "yes/no" { send "yes\r" exp_continue } -re "\[Pp\]assword:" { send "$USERPASSWD\r" } } # root に昇格 expect "$ " send "su\r" expect "Password:" send "$ROOTPASSWD\r" expect "# " send "whoami\r" # ssh コマンドの入出力を疑似端末(ユーザ)へ接続 interact |
Expect と対話させたいコマンドは spawn コマンドで起動します。ここでは ssh 接続を自動化するため、ssh コマンドを spawn で起動します。
パスワード入力の為、ssh コマンドが出力する特定の文字列を待機する必要がありますが、これは expect コマンドで処理します。-re オプションを付けることで正規表現によるパターンマッチも可能です。接続先が初めて接続するホストの場合は、公開鍵確認のプロンプト(yes/no)が表示されるため、この場合は send コマンドで yes を入力し、exp_continue コマンドで待機を継続します。
Expect による対話が完了しログインできた後は、interact コマンドで ssh コマンドの入出力制御を Expect からユーザへ切り替えます。
それでは実演してみましょう。上記スクリプトを ssh-login.tcl として保存して実行権限を付け、Xterm からそのスクリプトを実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
kiyoshi@ecmt-khomma2: ~/Work [---] $ ssh-login.tcl spawn ssh -p22 testuser@localhost The authenticity of host 'localhost (127.0.0.1)' can't be established. ED25519 key fingerprint is SHA256:QtRNXJo5btAykyJJnF/JWflzFREYs/2FFDY0V0V8Htk. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added 'localhost' (ED25519) to the list of known hosts. testuser@localhost's password: Welcome to Ubuntu 24.04.1 LTS (GNU/Linux 5.15.167.4-microsoft-standard-WSL2 x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/pro Last login: Wed Feb 26 15:14:57 2025 from 127.0.0.1 testuser@ecmt-khomma2:~$ su Password: root@ecmt-khomma2:/home/testuser# whoami root root@ecmt-khomma2:/home/testuser# |
無事 SSH ログインし root ユーザに切り替えることができました。スクリプト実行中は一切キー入力していません。ssh コマンド実行から公開鍵確認とパスワード入力、root 昇格までの一連の流れを自動化することができました。
シリアル接続を自動化する
次にシリアル接続を自動化してみます。
下記は COM3 ポートへシリアル接続されたターゲットへログインする Tera Term マクロです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
; シリアル通信設定 COM_PORT = '3' ; ターゲットが接続されている COM ポート番号 BAUDRATE = '38400' ; ボーレート DATABIT = '8' ; データ PARITY = 'none' ; パリティ STOPBIT = '1' ; ストップビット FLOWCTRL = 'none' ; フロー制御 DELAYPERCHAR = '0' ; 送信遅延時間(ミリ秒) DELAYPERLINE = '0' ; 送信遅延時間(ミリ秒) ; ログイン情報 USERNAME = 'testuser' ; ログインユーザ USERPASSWD = '12345' ; ログインパスワード ROOTPASSWD = 'root' ; root パスワード ; コマンド構築 COMMAND = '/C=' strconcat COMMAND COM_PORT strconcat COMMAND ' /BAUD=' strconcat COMMAND BAUDRATE strconcat COMMAND ' /CDATABIT=' strconcat COMMAND DATABIT strconcat COMMAND ' /CPARITY=' strconcat COMMAND PARITY strconcat COMMAND ' /CSTOPBIT=' strconcat COMMAND STOPBIT strconcat COMMAND ' /CFLOWCTRL=' strconcat COMMAND FLOWCTRL strconcat COMMAND ' /CDELAYPERCHAR=' strconcat COMMAND DELAYPERCHAR strconcat COMMAND ' /CDELAYPERLINE=' strconcat COMMAND DELAYPERLINE ; シリアル接続 connect COMMAND ; getty プロンプトを表示させるために改行送信 sendln '' ; シェルへログイン wait 'login:' sendln USERNAME wait 'Password:' sendln USERPASSWD ; root に昇格 wait '$ ' sendln 'su' wait 'Password:' sendln ROOTPASSWD wait '# ' sendln 'whoami' |
テストユーザでシェルへログインした後、root へ昇格しています。connect コマンドでは、適切なシリアル通信パラメータを設定してポートへ接続します。また SSH 接続とは違い、場合によっては接続後にキー入力によるログインが必要になります(下記では getty に対してログイン処理している)
この Tera Term マクロを Expect スクリプトで書き直してみましょう。下記が書き直した後のスクリプトです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#!/usr/bin/expect # シリアル通信設定 set DEVICE "/dev/ttyS0" ; # ターゲットが接続されている RS-232C I/F set BAUDRATE "38400" ; # ボーレート set DATABIT "cs8" ; # データビット set PARITY "-parenb" ; # パリティなし set STOPBIT "-cstopb" ; # ストップビット1 set SWFLOWCTRL "-ixon" ; # ソフトウェアフロー制御なし set HWFLOWCTRL "-crtscts" ; # ハードウェアフロー制御なし # ログイン情報 set USERNAME "testuser" ; # ログインユーザ set USERPASSWD "12345" ; # ログインパスワード set ROOTPASSWD "root" ; # root パスワード # シリアル通信設定を適用 exec stty -F $DEVICE $BAUDRATE $DATABIT $PARITY $STOPBIT $SWFLOWCTRL $HWFLOWCTRL # シリアル接続 spawn cu -l $DEVICE -s $BAUDRATE # getty プロンプトを表示させるために改行送信 send "\r" # シェルへログイン expect { "login:" { send "$USERNAME\r" exp_continue } "Password:" { send "$USERPASSWD\r" } } # root に昇格 expect "$ " send "su\r" expect "Password:" send "$ROOTPASSWD\r" expect "# " send "whoami\r" # cu コマンドの入出力を疑似端末(ユーザ)へ接続 interact |
ssh 接続時と違う点としては、spawn コマンドで起動するプログラムです。ここではシリアル接続を自動化するため、cu コマンドを spawn で起動します。また、シリアル通信の動作は事前に stty コマンドで設定しています。
それでは実演してみましょう。上記スクリプトを serial-login.tcl として保存して実行権限を付け、Xterm からそのスクリプトを実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
kiyoshi@ecmt-khomma2: ~/Work [---] $ serial-login.tcl spawn cu -l /dev/ttyS0 -s 38400 Connected. test-machine login: testuser Password: Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-67-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/pro testuser@test-machine:~$ su Password: root@test-machine:/home/testuser# whoami root root@test-machine:/home/testuser# |
無事にシリアル接続して root ユーザに切り替えることができました。スクリプト実行中は一切キー入力していません。シリアル接続時においても、一連の流れを自動化することができました。
応答待ちタイムアウトを設定する
何かしらの理由でターゲットが応答せず、接続・ログインできない状況はよくあります。そのため、ターゲットからの応答を待つタイマーを設定し、タイムアウトした場合は接続失敗として適切な措置(スクリプトの終了など)を実行する必要があります。
下記は Tera Term マクロにおける応答待ちタイムアウトの設定例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
; ターゲットへ SSH 接続 connect 'localhost:22 /ssh /2 /auth=passwd /user=testuser /passwd=12345' ; タイムアウトを5秒に設定 timeout = 5 ; シェルプロンプトが表示されるまで待機 wait '$' ; タイムアウトした場合(result が 0)、マクロを終了 if result=0 then exit endif |
Expect でも、下記のように対話コマンドの応答待ちにタイムアウトを設定することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#!/usr/bin/expect # ターゲットへ SSH 接続 spawn ssh testuser@localhost # タイムアウトを5秒に設定 set timeout 5 # パスワードプロンプトが表示されるまで待機 # (タイムアウトした場合、スクリプトを終了) expect { -re "yes/no" { send "yes\r" exp_continue } -re "\[Pp\]assword:" { send "12345\r" } timeout { puts "Connection timed out." exit 1 } } |
作業ログを記録する
試験や本番環境のメンテナンス時に、エビデンスとしてターゲットやリモートの作業履歴を記録する必要があったことはないでしょうか?
もしそんなケースに遭遇した場合は、Tera Term のログ記録機能が役に立つかもしれません。Tera Term では端末接続後の出力(リモートから受信した文字列)をログ保存でき、作業履歴として記録できます。以下はそれをマクロで実行する例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
; ターゲットへ SSH 接続 connect 'localhost:22 /ssh /2 /auth=passwd /user=testuser /passwd=12345' ; ログ記録先のファイルパスを生成 gettime LOGFILE 'C:\Temp\%Y%m%d-%H%M%S.log' ; ログ記録開始 logopen LOGFILE 0 0 ; 作業終了まで待機 timeout = 0 waitevent 4 ; ログ記録終了 logclose exit |
Expect でも、スクリプト実行中の出力をログ保存することができ、端末接続後の操作を記録することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#!/usr/bin/expect # ログ記録先のファイルパスを生成 set logfile "/tmp/[clock format [clock seconds] -format {%Y%m%d-%H%M%S}].log" # ログ記録開始 log_file -noappend $logfile # ターゲットへ SSH 接続 spawn ssh testuser@localhost expect { -re "yes/no" { send "yes\r" exp_continue } -re "\[Pp\]assword:" { send "12345\r" } } # シェル操作(作業終了まで待機) interact # ログ記録終了 log_file |
おわりに
今回は Expect を用いたシェルログインの自動化について紹介しました。紹介した機能や事例はほんの一部にすぎません。コマンド引数やユーザー入力を受け付けられるようにすれば、より汎用的な自動ログインスクリプトを作成できます。また、Tk を組み込むことでスクリプトの入出力を GUI 化することも可能です。ログイン処理に限らず、テストや運用管理などの作業も自動化して、日々の業務の効率化を進めていきましょう!
エコモットでは一緒にモノづくりをしていく仲間を随時募集しています。弊社に少しでも興味がある方はぜひ下記の採用ページをご覧ください!