前回は、シェルとシェルスクリプトがどのようなものかということと、パスの取り扱いについて学びました。今回はその続きとして、実際にPythonをシェルスクリプトのように使い、その実例を紹介します。

シェルの呼び出し

シェルスクリプトは前回説明したように、「OSのコマンドの呼び出し」と「シェルスクリプトの文法」で構成されています。Pythonをシェルスクリプトのように使うには「PythonでOSのコマンドを呼び出し」、その結果を必要に応じて「Pythonの文法」で処理することで実現できます。つまり、PythonでどのようにOSのコマンドを呼べるかということさえ知っていれば、特に新しいことを学ばなくてもPythonをシェルスクリプトのように使うことが可能なわけです。

PythonがOSのコマンドを利用するには特定の関数を呼び出すだけでよいのですが、その内部には以下の流れがあります。

PythonがOSのコマンドを利用するときの流れ

Pythonとしては単にシェルに仕事を依頼し、その結果を習得しているだけですが、呼び出されたシェルはOSにアクセスを行い、コマンドに応じたアクションがkernelで実行されています。

シェルスクリプトとしてPythonを使う前に、OSのコマンドの呼び出し方を扱ってしまいます。これには、以下のような2通りの方法があります。

  • os.system('COMMAND')
  • commands.getoutput('COMMAND')

os.system関数は引数で受け取ったコマンドを実行しますが、その返り値は成功か失敗かを返すだけです。一方、commands.getoutput関数はコマンドを実行したときに、本来ターミナルに表示されるべきテキストを返り値として返します。そのほかに、subprocessモジュールを使う方法などもあり、こちらは機能的にも優れているのですが、複雑なので今回は割愛します。

コマンドの応答がいらない呼び出し

まず最初にos.systemです。具体的に試してみます。

import os

print(os.getcwd())
print(os.system('touch test_python.txt'))
print(os.system('ls -l'))

見ていただくとわかるように、まず最初に現在の作業ディレクトリの情報を表示し、次にtouchコマンドでtest_python.txtというファイルを作成しています。そして最後に'ls -l'として現在のディレクトリのファイル一覧を出力しています。

これを実行すると、たとえば以下のような出力が得られます。

/Users/yuichi/Desktop
0
0

作業ディレクトリは実行環境により異なりますが、注目して欲しいのはtouchとls -lの表示結果が0となっている点です。先にお伝えしたように、os.system関数は返り値が成功(0) or 失敗(0以外)を返すだけなので、今回は成功が返ってきています。確認してみると表示された作業ディレクトリ /Users/yuichi/Desktopでtest_python.txtというファイルが確認できるはずです。そのためコマンドが実行されていることに間違いはありません。ただ、コマンドが実行された際に表示される文字列を取得できていないので、'ls -l'に関してはまったく使い物にならないといえます。

表示結果を取得するコマンド呼び出し

次は、実際に画面に表示される文字列を取得してみます。特に難しいことはなく、先ほどのos.system関数の代わりにcommands.getoutput関数を使えばよいだけです。

import os, commands
print(os.getcwd())
print(commands.getoutput('touch test_python2.txt'))
print(commands.getoutput('ls -l'))

これを実行すると以下のような出力が返ってきます。

/Users/yuichi/Desktop

total 17648
drwxr-xr-x  10 yuichi  staff      340 Aug 28 15:10 CLV
-rw-r--r--@  1 yuichi  staff   283507 Aug 31 16:21 N7K_vs_Catalyst-an.png
drwxr-xr-x   3 yuichi  staff      102 Jun 15 17:09 SDN
…省略…
-rw-r--r--@  1 yuichi  staff  1176845 Aug 28 13:06 yuiito-slide.pdf
drwxr-xr-x  13 yuichi  staff      442 Dec  9  2014 yukumo

この出力は当然環境によって変わってきますが、2行目がtouchコマンドの出力(出力がないので空行)となり、3行目以降が'ls -l'の出力になっています。

コマンドが成功したか失敗したかは確認しづらくなってしまいましたが、出力を利用するプログラムではこちらのほうが使いやすいです。

たとえば、特定の宛先に対する「pingの到達率」の取得を行うプログラムを書いてみましょう。まずPythonが呼び出すことになるpingの出力がどのようになるか、コンソールで確認します。

[root@localhost ~]# ping 192.168.141.169 -i 0.1 -c 5
PING 192.168.141.169 (192.168.141.169) 56(84) bytes of data.
64 bytes from 192.168.141.169: icmp_seq=1 ttl=64 time=0.568 ms
64 bytes from 192.168.141.169: icmp_seq=2 ttl=64 time=0.899 ms
64 bytes from 192.168.141.169: icmp_seq=3 ttl=64 time=0.471 ms
64 bytes from 192.168.141.169: icmp_seq=4 ttl=64 time=0.445 ms
64 bytes from 192.168.141.169: icmp_seq=5 ttl=64 time=0.443 ms

--- 192.168.141.169 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 403ms
rtt min/avg/max/mdev = 0.443/0.565/0.899/0.173 ms

コマンドのオプション-iはpingのパケットを送る間隔(インターバル)で、-cは送る回数を指定するオプションです。出力をみるとpingの到達率を取得するためには“5 packets transmitted, 5 received, 0% packet loss, time 403ms”の行の“0%”に対応している箇所を取得できればよいことがわかりますね。これは、以前解説した正規表現を使えば実現できます。

実際に書いてみましょう。宛先はコマンドライン引数で指定するようにします。

import sys, commands, re

if(len(sys.argv) != 2):
    exit()
dest_ip = sys.argv[1]
ping_command = 'ping ' + dest_ip + ' -i 0.1 -c 5'
result = commands.getoutput(ping_command)

regex = re.compile('\d+ packets transmitted, \d+ received, (\d+)% packet loss, time \d+ms') 
for line in result.split('\n'):
    m = regex.match(line)
    if m:
        packet_loss = int(m.group(1))
        packet_receive = 100 - packet_loss
        print(str(packet_receive) + '% packets received')

少し復習も兼ねて解説します。sys.argvはコマンドライン引数へのアクセスです。ここから引数として与えられたpingの宛先IPを取得しています。そしてコマンド文字列を作り、commands.getoutputでコマンドの実行と実行結果の取得をしています。次にpingのpacket loss率を取得するための正規表現を作成しています。何度もマッチさせることになるので、高速化のためにコンパイルした正規表現を利用します。その次に、コマンドの返り値である文字列をsplit('\n')とすることで出力を「各行」ごとのリストにし、それをforループで回しています。forループの中では、先ほどの正規表現を使ってロス率を取得し、そこから到達率を算出して表示を行っています。

実行すると以下のようになります。

[root@localhost ~]# python ping.py 192.168.141.169
100% packets received

コマンドを作成したり正規表現を使ったりして面倒なのですが、もしこれをOSのpingコマンドを使わずに作るとしたら、もっと面倒なはずです。具体的にはPythonでICMP echo packetを作り、相手からのICMP echo replyを受け取るということを何度か繰り返し、最終的に到達率を算出するというプログラムになるでしょう。

PythonでOSのコマンドを利用するということは「OSのコマンドで実現できることを実装しなくてよい」ということです。イケているライブラリを使うとコード量を大幅に減らせるのと同じで、シェルの力を借りることでコード量を大幅に減らし、コードを単純化することができます。ぜひ積極的に活用してみてください。

PythonでOSのコマンドを利用するメリット

シェルスクリプトの実用例

さて、先ほどのpingの例は、実はこれから紹介する実用例への伏線でした。Pythonをシェルスクリプトとして利用する方法はさまざまでしょうが、今回は私の本職であるCiscoのTACエンジニアとして、機器の検証作業を行う際に利用した事例を紹介します。 具体的には、Ciscoのデータセンター向けSwitchである「Nexus」が「機器故障時のトラフィック断時間を最小化」するための特殊な技術VPCを使ってトラフィックをロードバランスしている状況でさまざまなフローを流し、すべてのフローが問題なく届くか確認するためのテストです(普段は「IXIA」などのトラフィック測定器などを使ってテストをしています)。

以下、テストの構成を記載します。

テストの構成図

図の下側は検証機器で、左側の「Cisco Nexus 2000 (FEX)」から入った通信が右側のFEXから出ているか、右から左への通信もきちんと通っているか、確認をとります。左右のFEXはそれぞれVMWareの「ESXi」と呼ばれるハイパーバイザーが載った「Cisco UCS Server」に接続されています。

ESXi上のLinuxのVirtual Machineはそれぞれ別のvSwitchに接続されるため、Linux間でお互いに通信するために一旦外に出なければいけないように設定されています。つまり、VM1がVM2にpingを打つと、一旦左のNexus 2000にパケットが届けられ、それが右側までネットワークの中を通り、最終的に右側のNexus 2000からVM2にパケットが届けられます。VM2からVM1への通信も同様です。

この状況でVM Linux1、2のインタフェースにそれぞれ50個のIPを与え、送信元50×宛先50の2500パターンの通信フローを作り、すべてが問題なくLinux1、2の間で到達するかというテストを実施します。

OSのレベルで見ると、以下のような通信テストだといえます。

通信テストのイメージ図

なお、このテストの設定は残っていないので、今回は単に同一vSwitch上に存在するVM間で10×10の100flowを順番に流していき、すべてのパケットが届くかどうかをテストする簡易版としています。検証時は、後で記載するマルチスレッドを使って複数フローを同時に流していました。

さて、実際にスクリプトの話に入りましょう。まず最初に、テストを行うにあたりインタフェースに複数のIPを与える必要があります。スクリプトは以下となります。

import os

INTF_PREFIX = 'eno16777736'
NET_IP = '192.168.141.'
HOST_IP_FROM = 10
HOST_IP_TO = 19

for index, hostip in enumerate(range(HOST_IP_FROM, HOST_IP_TO + 1)):
    intf = INTF_PREFIX + ':' + str(index + 1)
    ip = NET_IP + str(hostip)
    ifconfig_command = 'ifconfig ' + intf + ' ' + ip + ' netmask 255.255.255.0'
    print(str(os.system(ifconfig_command)) + ' ' + ifconfig_command)

同一インタフェースに複数のIPを与えるには“ifconfig インタフェース名:X IP_ADDRESS netmask NET_MASK”というコマンドを与えればよいです。そのため、このコマンドをfor文を使って連番で与えるためのプログラムとなっています。

なお、for文でenumerateという関数を使っていますが、これは以下のような動きをする関数です。

>>> for i,j in enumerate(['a', 'b', 'c', 'd']):
...   print(str(i) + ' ' + j)
... 
0 a
1 b
2 c
3 d

見てもらうとわかりますが、['a', 'b', 'c', 'd']というリストのループを回す際にそれが何週目であるかという情報を取得しています。“for i,j”のiに何週目か、jにオリジナルのリストの値が入っています。そして最後にos.systemでコマンドを実行し、その返り値(成功 or 失敗)と、コマンドをprint文で表示しています。

これを実行してみます。

[root@localhost ~]# python setip.py 
0 ifconfig eno16777736:1 192.168.141.10 netmask 255.255.255.0
0 ifconfig eno16777736:2 192.168.141.11 netmask 255.255.255.0
...
0 ifconfig eno16777736:9 192.168.141.18 netmask 255.255.255.0
0 ifconfig eno16777736:10 192.168.141.19 netmask 255.255.255.0

os.systemコマンドの返り値が0なので、コマンドの実行は成功しています。せっかくなので、きちんとipが設定されているかどうか確認してみます。

[root@localhost ~]# ifconfig | grep 'eno16777736\|192.168.141.'
eno16777736: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.141.161  netmask 255.255.255.0  broadcast 192.168.141.255
eno16777736:1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.141.10  netmask 255.255.255.0  broadcast 192.168.141.255
eno16777736:2: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.141.11  netmask 255.255.255.0  broadcast 192.168.141.255
...
eno16777736:9: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.141.18  netmask 255.255.255.0  broadcast 192.168.141.255
eno16777736:10: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.141.19  netmask 255.255.255.0  broadcast 192.168.141.255

問題ないですね。これで「eno16777736」というインタフェースに、オリジナルのIPと10個の今回設定したIPがふられていることがわかりました。同様のことをIPが重複しないように反対側のLinux 2でも実施します。

IPを設定できたので、次にLinux1からLinux2へpingするプログラムを書きます。Linux1のすべてのIPからLinux2のすべてのIPに対してpingします。

import re, commands

INTERVAL = '0.1'
COUNT = '10'

NET_IP = '192.168.141.'
SRC_HOST_IP_FROM = 10
SRC_HOST_IP_TO = 19
DEST_HOST_IP_FROM = 20
DEST_HOST_IP_TO = 29

def ping(dest_ip, src_ip):
    ping_command = 'ping ' + dest_ip + ' -I ' + src_ip + ' -i ' + INTERVAL + ' -c ' + COUNT
    result = commands.getoutput(ping_command)
    regex = re.compile('\d+ packets transmitted, \d+ received, (\d+)% packet loss, time \d+ms') 
    for line in result.split('\n'):
        m = regex.match(line)
        if m:
            packet_loss = int(m.group(1))
            packet_receive = 100 - packet_loss
            print(str(packet_receive) + '% ' + src_ip + ' -> ' + dest_ip)

for src_host_ip in range(SRC_HOST_IP_FROM, SRC_HOST_IP_TO + 1):
    src_ip = NET_IP + str(src_host_ip)
    for dest_host_ip in range(DEST_HOST_IP_FROM, DEST_HOST_IP_TO + 1):
        dest_ip = NET_IP + str(dest_host_ip)
        ping(dest_ip, src_ip)

途中にあるping関数は先ほどのpingの例とほとんど同じですが、送信元と送信先を示すようにprint文が若干変更されています。そのping関数を送信元IPと宛先IPの2重ループで呼び出すことにより、送信元10パターン、宛先10パターンの計100パターンのping を実施しています。

実行すると以下のようになります。

[root@localhost ~]# python ping.py 
100% 192.168.141.10 -> 192.168.141.20
100% 192.168.141.10 -> 192.168.141.21
100% 192.168.141.10 -> 192.168.141.22
100% 192.168.141.10 -> 192.168.141.23
100% 192.168.141.10 -> 192.168.141.24
100% 192.168.141.10 -> 192.168.141.25
100% 192.168.141.10 -> 192.168.141.26
100% 192.168.141.10 -> 192.168.141.27
100% 192.168.141.10 -> 192.168.141.28
100% 192.168.141.10 -> 192.168.141.29
100% 192.168.141.11 -> 192.168.141.20
100% 192.168.141.11 -> 192.168.141.21
…

実行してみるとわかりますが、1行の表示につき1秒以上かかっており、すべてが終わるまでになかなか時間がかかりそうです。

OSへのリクエストの高速化

最後にコマンド呼び出しをするプログラムの高速化についてお話します。シェルを使ったプログラムは、Pythonだけで実施するよりも比較的に遅いことが多いです。正確にはシェルというよりも、networkやdiskアクセスの遅さなどに起因するため、Pythonだけでも起こりえる問題ですが、実際にはシェルを使う場面で出会うことが多いです。

たとえば先ほどのpingのコマンドを実行する際、インターバル0.1秒で10発打つので最低1秒かかります。なかなかの遅さです。速度の遅さのボトルネックがCPUやメモリにないのであれば、OSのコマンドの呼び出しを多重化することでコマンドの合計実行時間を短くすることが可能かもしれません。先のpingの例は1コマンドにつき1秒かかるものの、5コマンドを並列に実行しても実行時間は1秒のままでしょうから、1コマンドあたりは0.2秒に短縮されていると考えることができます。

以下にこの並列実行による実行時間短縮の概念の図を記します。

並列実行による実行時間短縮のイメージ

上図の上側は今までの例のように、「PythonからShellの呼び出しをしたあと、結果を受け取るまで待つ」という際の動きです。今までのサンプルはすべてこのパターンです。

一方、下側は「複数の命令を同時に発行」しています。これは「並列実行」と呼ばれており、「マルチスレッド」といった技術を使うことで実現できます。マルチスレッドを現時点で学ぶのは早すぎるので、本連載の最後あたりで改めて取り扱います。


演習1

シェルの'ls -l'コマンドを使って「プログラムが実行されたディレクトリ」の配下にあるディレクトリ名のみ抽出してください。たとえば'ls -l'を実行すると以下のような出力になります。この各行の最初にある“drwxrwxrwx”はファイルのパーミッションなどの情報を示していますが、この最初の文字がdだとディレクトリです。

[root@localhost ~]# ls -l
total 3372
-rw-------.  1 root root    1475 Dec 22  2014 anaconda-ks.cfg
drwxrwxrwx. 18  500  500    4096 Dec 23  2014 click-2.0.1
-rw-r--r--.  1 root root 3423136 Sep 25  2011 click-2.0.1.tar.gz
-rw-r--r--.  1 root root     919 Sep  2 12:58 ping.py
-rw-r--r--.  1 root root     393 Sep  2 09:38 setip.py
-rw-r--r--.  1 root root     477 Sep  1 17:42 test.py
drwxr-xr-x.  2 root root    4096 Sep  2 13:12 testdir1
drwxr-xr-x.  2 root root    4096 Sep  2 13:12 testdir2
-rw-r--r--.  1 root root       0 Sep  2 13:12 testfile1
-rw-r--r--.  1 root root       0 Sep  2 13:12 testfile2

dがディレクトリなので、この階層でプログラムを実行した場合の出力は、

click-2.0.1
testdir1
testdir2

となります。

演習2

引数で与えられたドメインに対してpingを行い、RTT(Round Trip Time)の平均値をミリ秒で表示するプログラムを作成してください。たとえば以下のような出力となります。

test.py cisco.com
average RTT is 185.846 ms

※解答はこちらをご覧ください。


連載内容に一区切りついたので、次回は今までの演習の解説と解答を行います。今後の話題はオブジェクト指向が中心となります。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら