あなたの知らない(かもしれない)パッケージ管理。 | サイバーエージェント 公式エンジニアブログ

二回目を書いた理由。


こんにちは、二回目の登場の前田です。ブログの運営さんから後述の宣伝をしてもよいからブログ書かないか?というバーターで引き受けました。二つ返事で引き受けたものの、特にネタを考えていなかったので締め切り迫って途方にくれました。


もっとも、よくよく考えたら私が仕事で普段やっている内容も、他の人があまり人がやらないものが多い上、一般受けしそうなネタも持ってないのでした。ですので、以前使うことのあったパッケージ関連のネタについて書きたいと思います。


パッケージを扱う


パッケージといってもいろんな種類がありますが、今回は Debian で RPM を扱う方法についてです…。








ウソです。




Debian には、 rpm, rpm2cpio, yum, createrepo などの Debian パッケージが存在します。これらのパッケージをインストールすれば、 Debian 上で RPM パッケージを作ったり、 YUM のリポジトリを作成したり、といったことも普通にできます(*1)。ですが別にこの話ではありません。


ネタとして扱うのはやはり Debian パッケージと RPM を扱う方法についてです。普通は Debian パッケージなら APT や dpkg コマンドを、 RPM なら yum コマンドや rpm コマンドを使いますが、今回はそれらを python で扱う方法についてお話します。前者は libapt-pkg ライブラリのバインディングである python-apt を、後者は librpm ライブラリへのバインディングである python-rpm を使います(*2)。それぞれ開発元によってドキュメントが公開されています。(参考文献参照)


事前準備


環境としては、Debian GNU/Linux Sid(Jessie/Sid)を、 Python のバージョンは 2.7.3 を使います。 Python3 理由は単に Python3 対応の python-rpm パッケージがまだ無いためです。


さて、 python-apt を使う場合には python-apt パッケージをインストールします。 Python3 版を使いたい人は python3-apt パッケージをインストールして下さい。


$ sudo apt-get install python-apt

python-rpm を使う場合には、同様に python-rpm パッケージをインストールして下さい。


$ sudo apt-get install python-rpm

普段、職場でサーバとして使っている Ubuntu 12.04 LTS の環境や、先日リリースされた、Debian GNU/Linux Wheezy でもほぼ同じです。RHEL系の環境は知りませんので、もし使いたければご自分で調べてみて下さい。


python-apt を使う


python-apt は、次のようのことができます。


  • APT Lineの情報を変更
  • パッケージアーカイブの情報を取得
  • パッケージをインストール、アンインストール
  • ローカルのパッケージデータベースの情報を扱う
  • ローカルの .deb ファイルを扱う


今回はそのうち、ローカルのパッケージデータベース最後の二点について説明します。これらを選んだのは、前述の通り、諸事情で使ったことがあるから、というただそれだけの理由です。


APT経由でのパッケージ管理


まず、ローカルのパッケージデータベースの情報を扱う方法についてからですが、 apt モジュールの Cache クラスを使います。あるパッケージのチェックを行うには、下記のようにします。


>>> import apt
>>> cache = apt.Cache()
>>> cache['bash']
<Package: name:'bash' architecure='amd64' id:742L>

apt-cache search コマンドで見つけられるパッケージの場合には上記のような表示になります。一方、実際には存在しないパッケージ名(例えば"hoge"など)を指定した場合には KeyError exception が発生します。パッケージがインストール状態を調べるには is_install プロパティを使います。


>>> cache['bash'].is_installed
True
>>> cache['libpython2.6'].is_installed
False

root 権限があれば、パッケージのインストール、削除などもできます。


$ sudo python
>>> import apt
>>> cache = apt.Cache()
>>> cache['zsh'].is_installed
False
>>> cache['zsh'].mark_install()
>>> cache['zsh'].marked_install
True
>>> cache.commit()
Selecting previously unselected package zsh-common.
(Reading database ... 269347 files and directories currently installed.)
Unpacking zsh-common (from .../zsh-common_5.0.2-3_all.deb) ...
Selecting previously unselected package zsh.
Unpacking zsh (from .../archives/zsh_5.0.2-3_amd64.deb) ...
Processing triggers for man-db ...
Processing triggers for menu ...
Setting up zsh-common (5.0.2-3) ...
Installing new version of config file /etc/zsh/zshrc ...
Setting up zsh (5.0.2-3) ...
update-alternatives: using /bin/zsh5 to provide /bin/zsh (zsh) in auto mode
update-alternatives: using /bin/zsh5 to provide /bin/rzsh (rzsh) in auto mode
Processing triggers for menu ...
[master 3de9c60] committing changes in /etc after apt run
 Author: mkouhei >mkouhei@testnode<
 7 files changed, 111 insertions(+), 43 deletions(-)
 create mode 120000 alternatives/rzsh
 create mode 120000 alternatives/rzsh.1.gz
 create mode 120000 alternatives/zsh
 create mode 120000 alternatives/zsh-usrbin
 rewrite zsh/zshrc (88%)
True

この直後に is_installed プロパティでインストールの状態を見ると、cache オブジェクト自体は更新されていないため False が返ってきます。


>>> cache['zsh'].is_installed
False

ですので、最新状態に反映するには、 open() メソッドを使います。


>>> cache.open()
>>> cache['zsh'].is_installed
True

パッケージを削除する場合には、 mark_delete() メソッドを使います。これは apt-get remove に相当します。


>>> cache['zsh'].marked_delete
False
>>> cache['zsh'].mark_delete()
>>> cache['zsh'].marked_delete
True
>>> cache.commit()            
(Reading database ... 270508 files and directories currently installed.)
Removing zsh ...
Processing triggers for menu ...
Processing triggers for man-db ...
[master 5e8a38d] committing changes in /etc after apt run
 Author: mkouhei <mkouhei@testnode>
 9 files changed, 115 insertions(+), 125 deletions(-)
 delete mode 120000 alternatives/ksh
 delete mode 120000 alternatives/ksh.1.gz
 delete mode 120000 alternatives/rzsh
 delete mode 120000 alternatives/rzsh.1.gz
 delete mode 120000 alternatives/usr.bin.ksh
 delete mode 120000 alternatives/zsh
 delete mode 120000 alternatives/zsh-usrbin
True
>>> cache.open()
>>> cache['zsh'].is_installed
False


ローカルのパッケージファイルの操作


次にローカルのパッケージファイルからメタ情報を取得する方法について説明します。まず、 apt_inst モジュールの DebFile クラスでローカルの Debian バイナリパッケージを読み込みます。


>>> import apt_inst
zsh_deb = '/var/cache/apt/archives/zsh_5.0.2-3_amd64.deb'
>>> deb_obj = apt_inst.DebFile(zsh_deb)

メタ情報を取得するにはこの DebFile オブジェクトの control プロパティで取得したオブジェクトを extractdata() メソッドを使って文字列に変換します。


>>> control_s = deb_obj.control.extractdata('control')
>>> 'Package: zsh\nVersion: 5.0.2-3\nArchitecture: amd64\nMaintainer: Debian Zsh Maintainers <pkg-zsh-devel@lists.alioth.debian.org>\nInstalled-Size: 1836\nPre-Depends: dpkg (>= 1.15.6~)\nDepends: libc6 (>= 2.15), libcap2 (>= 2.10), libtinfo5, zsh-common (= 5.0.2-3)\nRecommends: libncursesw5 (>= 5.6+20070908), libpcre3 (>= 8.10)\nSuggests: zsh-doc\nSection: shells\nPriority: optional\nHomepage: http://www.zsh.org/\nDescription: shell with lots of features\n Zsh is a UNIX command interpreter (shell) usable as an\n interactive login shell and as a shell script command\n processor. Of the standard shells, zsh most closely resembles\n ksh but includes many enhancements. Zsh has command-line editing,\n built-in spelling correction, programmable command completion,\n shell functions (with autoloading), a history mechanism, and a\n host of other features.\n'

control ファイルの文字列を取得したら、これを apt_pkg モジュールの TagSection クラスを使ってパースします。


>>> import apt_pkg
>>> tag_section = apt_pkg.TagSection(control_s)

apt_pkg.TagSection オブジェクトは、次のメタ情報を持っています。


>>> tag_section.keys()
['Package',
 'Version',
 'Architecture',
 'Maintainer',
 'Installed-Size',
 'Pre-Depends',
 'Depends',
 'Recommends',
 'Suggests',
 'Section',
 'Priority',
 'Homepage',
 'Description']

get() メソッドで文字列としてメタ情報を取得できます。


>>> tag_section.get('Package')
'zsh'
>>> tag_section.get('Version')
'5.0.2-3'
>>> tag_section.get('Architecture')
'amd64'

python-rpm を使う


さて今度は python-rpm を見てみます。こちらについては、ローカルのパッケージファイルからメタ情報を取得するだけの紹介とします(*3)。
python-rpm では rpm.TransactionSet のインスタンスの生成が必須です。


>>> import rpm
>>> import os
>>> ts = rpm.TransactionSet()

ローカルの rpm ファイルを扱う際、GPG での署名が無いパッケージは必ずエラーになります(*4)。もし、署名無しのパッケージを扱う場合は、rpm._RPMVSF_NOSIGNATURES を引数にして setVSFlags() メソッドを実行する必要があります(*5)。


>>> ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
983040

次に、RPM パッケージファイルを読み込みます。これはファイルディスクリプタを ts.hdrFromFDno() メソッドに渡します。


>>> fd = os.open('git-all-1.7.1-3.el6_4.1.noarch.rpm', os.O_RDONLY)
>>> header = ts.hdrFromFDno(fd)

このときに読み込んだファイルが RPM でなければ、 TypeError exception が発生します。ヘッダ情報を読み込めたら、ファイルディスクリプタは必ず close() しておきましょう。


>>> os.close(fd)

あとは header から欲しいメタ情報を取得するだけです。が! header.keys() を実行しても、悲しいことに数値しか返ってきません。これでは欲しい情報がどれか分かりません。


>>> header.keys()
[100,
 257,
 259,
 261,
 268,
 269,
 1000,
 1001,
 1002,
 1004,
 1005,
 1006,
 1007,
 1009,
 1011,
 1014,
 1015,
 1016,
 1020,
 1021,
 1022,
(snip)
 1126,
 1132,
 5011]

例えばパッケージ名は header['name'] で取得できるのですが、どの数値が name に相当するのかは分かりません。無論、これらの数値をキーにすれば値は取得できます。しかし、返り値から欲しい情報を類推するのはナンセンスですね。しかも、一部はバイナリデータを返すので python のインタラクティブモードや ipython などでうっかり

>>> for i in header: print(header[i])

などと実行しようものなら、コンソールが文字化けしてしまいます。


実は、この数値とタグ名のマッピングは librpm ライブラリの rpmtag.h に記述されています。 Debian では librpm-dev パッケージの、 /usr/include/rpm/rpmtag.h になります。このパッケージをインストールし、中を見ると下記のようになっています。


(snip)
    RPMTAG_NAME                 = 1000, /* s */
#define RPMTAG_N        RPMTAG_NAME     /* s */
    RPMTAG_VERSION              = 1001, /* s */
#define RPMTAG_V        RPMTAG_VERSION  /* s */
    RPMTAG_RELEASE              = 1002, /* s */
#define RPMTAG_R        RPMTAG_RELEASE  /* s */
    RPMTAG_EPOCH                = 1003, /* i */
#define RPMTAG_E        RPMTAG_EPOCH    /* i */
    RPMTAG_SUMMARY              = 1004, /* s{} */
    RPMTAG_DESCRIPTION          = 1005, /* s{} */
    RPMTAG_BUILDTIME            = 1006, /* i */
    RPMTAG_BUILDHOST            = 1007, /* s */
    RPMTAG_INSTALLTIME          = 1008, /* i */
    RPMTAG_SIZE                 = 1009, /* i */
    RPMTAG_DISTRIBUTION         = 1010, /* s */
    RPMTAG_VENDOR               = 1011, /* s */
    RPMTAG_GIF                  = 1012, /* x */
    RPMTAG_XPM                  = 1013, /* x */
    RPMTAG_LICENSE              = 1014, /* s */
    RPMTAG_PACKAGER             = 1015, /* s */
    RPMTAG_GROUP                = 1016, /* s{} */
    RPMTAG_CHANGELOG            = 1017, /* s[] internal */
    RPMTAG_SOURCE               = 1018, /* s[] */
    RPMTAG_PATCH                = 1019, /* s[] */
    RPMTAG_URL                  = 1020, /* s */
    RPMTAG_OS                   = 1021, /* s legacy used int */
    RPMTAG_ARCH                 = 1022, /* s legacy used int */
(snip)

prefix である "RPMTAG_" を取り除いて小文字にしたものが rpm.hdr オブジェクトに渡すキーになります。なので、 "rpm -qip some.rpm" を実行して表示されるメタ情報を取得するには下記のようになります。


>>> header['name'], header['summary'], header['packager'], header['buildtime'], header['version'], header['release'], header['arch'], header['sourcerpm']
('git-all',
 'Meta-package to pull in all git tools',
 'CentOS BuildSystem ',
 1362435563L,
 '1.7.1',
 '3.el6_4.1',
 'noarch',
 'git-1.7.1-3.el6_4.1.src.rpm')

まとめ


以上、 python-apt と python-rpm の機能の一部を紹介しました。これらはどちらも前述の通り、公式ドキュメントが充実しているのですが、前述の rpmtag.h での rpm.hdr オブジェクトで使う key のマッピングについては載っていなかったりします。今回紹介した内容が読者の皆さんにお役に立てば幸いです。


といっても、今回の内容はそもそも使う人が限定されるので、かなり誰得な感じですね。


参考文献


python-apt
python-apt v0.8.0 documentation
python-rpm
Chapter 16. Programming RPM with Python



(バーターでもらった)宣伝コーナー



さて、(私にとっての)本題です。


既にご存知の方もいらっしゃるかと思いますが、来る6月29日(土)に大統一Debian勉強会 2013が開催されます。


2回目の今年は東京での開催です。CFP(Call for presentation)の募集は既に終わり、セッションテーブルも公開されていますが、LTは当日まで募集しています。また、アンカンファレンスも開催予定です。Debian関連ネタをお持ちの方はぜひ応募してみてください。一般参加の申し込みも5/29(水)から始まっています。 Debian 勉強会ではおなじみの事前配布資料は今回はスポンサー様からの支援もあって(*6)、年二回の"あんどきゅめんてっどでびあん"と同様の装丁の冊子として、先着で配布できることになりました。


なお、弊社からは私と、私と同じチームの某さんの二人が発表できることになりました(*7)。某さんが誰なのか&発表内容が気になる人は当日ぜひ会場へ!


ぜひぜひ、たくさんの方のご参加をお待ちしております。(以上宣伝終わり。)




脚注

  1. RPM を作ることができてもそれをそのまま使うか、と言えばそれは別ですね。
  2. これ以外にも、 Smart Package Manager というRPM, Debian パッケージ, Slackware などのリポジトリを扱うことのできるツールがあります。これにも python バインディングもあるようです。)それぞれ開発元によってドキュメントが公開されています。
  3. 単に以前使ったのがこの機能だけなためです。 rpm で Debian にパッケージをインストールしたくないですしね。
  4. 私は Debian パッケージを作成する場合、サインするのが当たり前だったのですが、以前 python-rpm を使ってツールを作った時の都合上、サイン無しの RPM パッケージを扱う必要がありました。
  5. fedora のドキュメント "Setting the verification flags" を参照

  6. 弊社もシルバースポンサーおよび、ペットボトル(ミネラルウォーター)の配布での協賛となっています。
  7. 念の為補足しておくと、スポンサーだからというわけではなく、二人ともCFPが選考されたからです。ちなみに他の実行委員も、もう一人が誰なのかは多分知らないと思います。