仮想マシン環境と言えば色々ありますが、個人的にはVirtualBoxがPC環境でも使いやすいと思っています。安定性や品質、機能面では他の仮想化ソフトに道を譲りますが、開発用途では十分な印象があります。
何台か運用しているうち、CLIやスクリプトからも扱いたいと思い、PythonでのAPI連携を試してみました。
VirtualBoxが標準で提供しているAPI、vboxapiはあまり積極的にはエンハンスされていないようで、Pythonから使うのはひと工夫必要なようです。
歴史上の経緯で若干制限もあるため、最初からサードパーティ製のものを使うという選択肢もありますが、せっかくなので標準のものもちょっと調べてみました。
今回のサンプルは最後のほうで掲載します。環境はWindows10 Pro, Python2.7, VirtualBox 5.2 (一応6.0でも確認済)です。
もくじ:
1. vbox APIクライアントのセットアップ
基盤となるVirtualBox APIはCOM (XPCOM)で実装されており、様々な言語から制御できるようになっているようです。
SDKとしてPython向けのバインディングが用意されているので、マニュアルでCOMインタフェースの使い方を調べればPythonでもおおよそ同じ操作を実装することができます。
また、VirtualBoxではSOAP経由で同じインタフェースを公開しているRPCが提供されているので、VirtualBox(ハイパバイザ)がインストールされていないリモートのマシンからもAPIの呼び出しが可能です。
1.1 Python用クライアント環境のインストール
Python2.x系のみサポートなので、システムを汚したくなければvirtualenv,pyenvなどの仮想環境を使ったほうが良いと思います。
私が試した環境ではAnaconda (Miniconda)が導入されていましたので、condaコマンドで仮想環境を作ることにしました。
仮想環境の名前{env name}
は何か適当に読み替えて下さい。
1 2 | > conda create -n {env name} python=2.7 -y > activate {env name} |
VirtualBoxがローカルにインストールされている環境ではSDKのインストーラもデフォルトで同梱されています。バージョンを気にしたくない場合は素直に同梱されているものを使うと良いと思います。
Windowsの場合、デフォルトでは下記のような感じです。
1 2 | > cd "C:\Program Files\Oracle\VirtualBox\sdk\install" > python vboxapisetup.py |
あるいは、VirtualBoxがインストールされていない環境なら(もちろん、されている環境でも)pip経由で入手できます。もちろんPython2(pip2)系でないとエラーになります。
1 | > pip install vbox-sdk |
Windows環境の場合はその他(COM操作関連)に依存パッケージpywin32(またはpypiwin32)が必要です。
1 | > pip install pywin32 |
ローカルで利用するのみ(つまりVirtualBoxホストで使う場合)であればここでインストール作業は終了です。
リモートで利用する場合はさらに必要な依存関係があります。
1.2 リモート管理用クライアント環境のインストール
リモートでAPIを扱うには、SOAPクライアントが必要になります。SDK(のサンプル)ではちょっと古いモジュールを使っているので、そのままでは使えません。
SOAPを扱うためのZSIモジュールがそもそも古いですが、ZSIが依存しているPyXMLも既にサポートが停止されているようです。
そのまま頑張るなら、どこぞから拾ってきたPyXMLをオフラインインストールし、さらにZSIをpip経由で導入します。
そもそも標準のSDKに頼る理由がなければ、SOAPクライアントをzeepなどモダンなモジュールで置き換えたRemote VirtualBox (https://github.com/ilyaglow/remote-virtualbox) などサードパーティ製のものを使うのも手です。インタフェース定義(WSDL)もSDKに同梱されているので自前でSOAPの手続きを実装することも一応可能なようです。
さらにSOAPを使う必要性も感じなければ、リモート呼び出し部分のインタフェースは自作する、というやり方でもいいかもしれません。
ここではとりあえずSDKのサンプルに準拠してみます。
PyXMLをWindowsにインストールするには、VC++コンパイラ(vcpython27
)が必要です。Microsoftのサイトから入手してインストールします。
https://www.microsoft.com/en-us/download/details.aspx?id=44266
PyXMLは既にPyPIから削除されているので、Tarアーカイブをネットから入手します。現時点ではSourceForgeで配布していました。
https://sourceforge.net/projects/pyxml/files/
仮想環境でインストールします。
1 2 3 4 5 6 | > activate {env name} > cd {location where tarball downloaded} > pip install PyXML-0.8.4.tar.gz > pip install ZSI # 未インストールの場合 > pip install vbox-sdk |
一部のリモート呼び出し用のバインディングは上記のvbox-sdkパッケージに含まれていないようなので別途設置が必要です。
公式サイトからSDK(ZIPアーカイブ)を入手し、個別にパスを通します。condaのような仮想環境を使っている場合はvboxapiが入っている場所にコピーしてしまうのが簡単です。
より具体的には、vboxapi.__file__
が指しているパスと同じ階層ですね。
1 | > python -c "import vboxapi,os;print(os.path.dirname(vboxapi.__file__))" |
公式サイトから入手したzipアーカイブを解凍し、sdk/bindings/webservice/python/lib
のパスにあるスクリプト(VirtualBox_client.py
、VirtualBox_types.py
、VirtualBox_wrappers.py
)を上記のvboxapiと同じ場所にコピーします。
1.3 リモートアクセス用Webサーバのセットアップ
ローカルで実行する際には不要ですが、リモートから操作する場合にはVirtualBoxに同梱のWebサーバVBoxWebSrv
を使います。
1). Windows編
Windowsの場合は、VirtualBoxがインストールされたディレクトリにあるVBoxWebSrv.exe
で、リモートからの通信を受け付けるために起動しておきます。
実行には管理者権限が必要ですので、管理者として起動したコマンドプロンプトまたはPowershellから実行するなどします。リモートからのアクセスを受け付けるには、明示的にホスト名(またはIPアドレス){host name}
を指定します。
1 2 | > cd "C:\Program Files\Oracle\VirtualBox" > VBoxWebSrv.exe -p 18083 -H {host name} |
使用している場合はファイヤウォールを設定します。Listenポートは変えられますがデフォルトでは上記の起動オプションのように18083番(http, TCP-in)です。
終了する際にはタスクキルではなくCtrl+Cを使ったほうが良いです。リモートから設定変更中にキルすると、VMがabort状態になったりします。
2). Linux編
Linuxの場合、例えばUbuntuでは普通にaptを使って(例によって古いバージョンですが)VirtualBoxをインストールすることができ、Webサーバもサービスとしてセットアップされます。
ちなみにVirtualBoxをsudo apt install
でインストールしようとすると失敗することがあるので、rootにスイッチして(sudo su -
)aptインストールしたほうが無難です。
せっかくなのでサービスとしてセットアップされたものをちょっと修正して使うことにしました。起動オプションの指定は上記のWindowsの場合と同じで、ExecStart
のところに追加します。
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 | # VirtualBoxのインストール $ sudo su - $ apt install virtualbox $ exit # GUI起動 $ sudo VirtualBox # サービスの設定 $ sudo vi /lib/systemd/system/vboxweb.service # execstartで起動オプションを指定 [Unit] Description=VirtualBox Web Service After=network.target [Service] Type=forking ExecStart=/usr/bin/vboxwebsrv -p 18083 -H {host name} --pidfile /run/vboxweb.pid --background PIDFile=/run/vboxweb.pid [Install] WantedBy=multi-user.target # サービスを起動 $ sudo systemctl start vboxweb.service # 自動起動を設定 $ sudo systemctl enable vboxweb.service |
ファイヤウォールを使っている場合は設定しておきます。
上記のようにWebサーバは管理者アカウントで起動されるので、GUIを使いたい際にもsudoする必要があります。
1 | $ sudo VirtualBox |
1.4 VirtualBoxManager
実際のクライアント用スクリプトでは、ローカルでもリモートでも、基本的にはVirtualBoxManager
オブジェクトを使って操作していきます。
このVirtualBoxManager
によって隠蔽されるので、後で呼び出すメソッドはローカル、リモートに関わらず同じです。
1 2 3 4 5 6 7 8 9 10 | from getpass import getpass from vboxapi import VirtualBoxManager # ローカルの場合 manager = VirtualBoxManager(None, None) # リモートの場合 manager = VirtualBoxManager("WEBSERVICE", {"url": "http://vboxserver:port/", "user": "myuser", "password": getpass()}) |
リモートで操作する場合には第一引数に"WEBSERVICE"
をセットします。URLは普通にhttp/httpsの、VBoxWebSrv
の起動オプションに使用したホスト名とポートの組合せになります。
認証はデフォルトではVBoxWebSrv
を起動しているのと同じものです。Windowsならログイン中のアカウント、Linuxならプロセスを起動している(PAM)アカウントです。
https/SSL通信や外部認証の利用など、標準で他のカスタマイズもサポートされています。
ローカル/リモートともに、VirtualBoxへの操作は個別にセッション的なものを張る仕様になっていますが、複数生成されても暗黙的には同期されません。つまり、Pythonから呼び出したセッションと、例えば普通のGUIとはそのままでは見かけのVMの状態が一致しないこともあります。設定系の操作のためにロック機構が用意されているので実質的に不整合になることが無いように設計されていますが、明示的に同期させるか、単一のセッションのみ使用するようにしたほうが良いです。
また、VMを対象とする操作に関してはVirtualBox
インスタンスを取得する必要があります。
1 | vbox = manager.getVirtualBox() |
2. VMの操作
ここでは基本的と思われる操作について記載していきます。
2.1 VM一覧の取得
VirtualBoxManager.getArray()
メソッドに'machines'
を指定すると、VMのリストに相当するものを取得することができます。
リストの各要素はIMachine
インタフェースになっており、VMの属性や設定値を表すプロパティを参照できます。
1 2 3 | for vm_ in self.manager.getArray(self.vbox, 'machines'): print("{}".format(vm_.name)) ... |
サンプルでは特定のプロパティを文字列にキャストして表示させる処理を実装しており、実行するとこんな感じの結果になると思います。
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 | >>> from vbox_client import vboxClient >>> _client = vboxClient() >>> _client.list_machines() {'CPUCount': '4', 'autostartEnabled': 'false', 'chipsetType': 'PIIX3', 'id': 'da803...33', 'logFolder': 'C:\\...VirtualBox VMs\\vm1\\Logs', 'memorySize': '4096', 'name': 'vm1', 'sessionName': 'headless', 'sessionPID': '5952', 'sessionState': 'Locked', 'settingsFilePath': 'C:\\...VirtualBox VMs\\vm1\\vm1.vbox', 'state': 'FirstOnline'} {'CPUCount': '2', 'autostartEnabled': 'false', 'chipsetType': 'PIIX3', 'id': '6e1dbf...87', 'logFolder': 'D:\\...\\vm2\\Logs', 'memorySize': '4096', 'name': 'vm2', 'sessionName': '', 'sessionPID': '4294967295', 'sessionState': 'Unlocked', 'settingsFilePath': 'D:\\...\\vm2\\vm2.vbox', 'state': 'PoweredOff'} |
2.2 VM(ゲスト)の起動
うまく起動に成功した場合、状態(state
)が停止(PoweredOff)から実行中(FirstOnline)になります。
今回はホストにログインしてGUI操作するのが面倒という動機なので、ヘッドレスで起動させることにします。
サンプルの使用例はこんな感じ。VMの名前を指定して起動します。
1 2 3 | >>> from vbox_client import vboxClient >>> _client = vboxClient() >>> _client.start_vm("vm1") |
まず、所望のVMへの参照を取得し、セッションを作成します。
1 2 3 4 | # this may raise exception if specified named vm does not exist vm_ = self.vbox.findMachine(vm_name) # invoke lauching VM process session_ = self.manager.getSessionObject(self.vbox) |
ゲストの起動はIMachine.launchVMProcess()
で実行でき、下記のようにヘッドレス(コンソールを表示しない)を指定することができます。起動状態への遷移にはある程度時間がかかるので、当該タスクの完了を待つ処理を入れます。
1 2 3 4 | prog_ = vm_.launchVMProcess(session_, "headless", "") # wait and terminate current session prog_.waitForCompletion(self.__timeout_msec) self.manager.closeMachineSession(session_) |
完了後にはセッション終了(closeMachineSession()
)を呼んだほうが無難です。
2.3 VMのシャットダウン
VMを終了する際には他のハイパバイザと同様に、いわゆる強制停止と、ゲストOSのシャットダウンとの2種類があります。
大抵のディストリビューションで問題なく動くので、もちろんACPIシャットダウンを使ったほうが良いでしょう。
サンプルの使用例はこんな感じ。VMの名前を指定してシャットダウンします。
1 2 3 | >>> from vbox_client import vboxClient >>> _client = vboxClient() >>> _client.stop_vm("vm2", force=False) |
内部的には、明示的にロックを取得した上で、停止IConsole.powerDown()
、またはACPIシャットダウンIConsole.powerButton()
を実行します。
1 2 3 4 5 6 7 8 9 10 11 12 | vm_ = self.vbox.findMachine(vm_name) session_ = self.manager.getSessionObject(self.vbox) # expecting this lock will be freed after power down vm_.lockMachine(session_, self.cst.LockType_Shared) if force: # explicitly stop VM prog_ = session_.console.powerDown() prog_.waitForCompletion(self.__timeout_msec) else: # try ACPI shutdown session_.console.powerButton() |
3. サンプル
上記の操作の実装はこんな感じです。
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 53 54 55 56 57 58 59 | from pprint import pprint from vboxapi import VirtualBoxManager class vboxClient: def __init__(self, config=None): if config is None: # work on local host self.manager = VirtualBoxManager(None, None) else: # work with remote host (requires SOAP client) url_ = "http://{}:{}".format(config["host"], config["port"]) self.manager = VirtualBoxManager("WEBSERVICE", {"url": url_, "user": config["user"], "password": config["password"]}) # instanciate virtualbox interface self.vbox = self.manager.getVirtualBox() self.vm = None self.cst = self.manager.constants self.__timeout_msec = 10000 def list_machines(self): # here enumerates interested properties attrs = ["name", "id", "state", "autostartEnabled", "CPUCount", "memorySize", "settingsFilePath", "logFolder", "chipsetType", "sessionName", "sessionState", "sessionPID"] for vm_ in self.manager.getArray(self.vbox, 'machines'): buf_ = {} for k_ in attrs: buf_[k_] = str(vm_.__getattr__(k_)) # just print on console pprint(buf_) def start_vm(self, vm_name): # this may raise exception if specified vm name does not exist vm_ = self.vbox.findMachine(vm_name) # invoke lauching VM process session_ = self.manager.getSessionObject(self.vbox) prog_ = vm_.launchVMProcess(session_, "headless", "") # wait and terminate current session prog_.waitForCompletion(self.__timeout_msec) self.manager.closeMachineSession(session_) def stop_vm(self, vm_name, force=False): vm_ = self.vbox.findMachine(vm_name) session_ = self.manager.getSessionObject(self.vbox) # expecting this lock will be freed after power down vm_.lockMachine(session_, self.cst.LockType_Shared) if force: # explicitly stop VM process prog_ = session_.console.powerDown() prog_.waitForCompletion(self.__timeout_msec) else: # try ACPI shutdown session_.console.powerButton() |
別記事に続く。