RHELのリリース番号を調べる

RedHatディストリビューション(RHEL4, CentOSなど)では,リリースパッケージの識別用にRPMパッケージが導入されている.このリリースパッケージは,"redhat-release" をProvideしている.RHELであればこの識別子を調べることで,そのシステムのリリース番号を調べる事ができる.

% rpm -q --provides `rpm -q --whatprovides redhat-release`

または,もっと単純に以下のようにしても良い.

% rpm -qi redhat-release

この識別子は,例えば,自分でRPMパッケージを作る際,「このRPMは,RHELの特定のリリースにのみインストールを許可したい」という制限を課すために使うことができる.例えば,specファイル中で "Required: redhat-release >= 5.0" のように表記することでこのような動作が実現できる.

RPMマクロ

便利に使えるRPMのマクロたち

%define foo bar

%fooをbarに定義するマクロ.cpp(1)の#defineみたいなもの.
後から上書きできる.

%define foo 1
%define foo 2
%foo  # => 2
%{?name:expr}

nameが定義されていたらexprを評価.cpp(1)の#ifdefみたいなもの.

%{!?name:expr}

nameが定義されていなかったらexprを評価.cpp(1)の#ifndefみたいなもの.

%(expr)

exprの中身を/bin/shで実行する.評価結果は標準出力に出力された文字列となる.cpp(1)にはこんな変態マクロは無い.

組み合わせても使える
%{!?uname:%define uname %(uname)}
%ifもあるよ

condは,少なくとも 0 or 1 の場合に,期待通りに動くようだ.

%if cond
commands
%endif

RPMマクロの働きの調べ方

RPMのspecファイル中に現れる%{...}という形の式は,rpm(8)がマクロと判断して評価する.RedHatベースのシステムには,いくつか定義済みのマクロがあり,これらは/usr/lib/rpm/macrosファイルを調べる事で確認することができる.

しかし,わからないマクロが出てくるたびにいちいちこのファイルを読むのはとても面倒である.そんなときのために,rpm(8)には--evalオプションがある.これを使うと,マクロがどのように展開されるのか確認することができる.

実行例
% rpm --eval '%configure'

evalオプションを複数書くこともできる.この場合,オプションの順番通りに評価が行われる.

% rpm --eval '%{!?python_sitelib: %define python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}' \
      --eval '%{python_sitelib}'
/usr/lib/python2.3/site-packages

PAM設定ファイルは,どれが読み込まれるのか?

Linux-PAM(pam(8))が入っているような,いまどきの普通のシステムでは,/etc/pam.dディレクトリ(または/etc/pam.confファイル)が存在する.各プログラム(デーモンなど)が認証処理を行う必要があるときに,ここから設定ファイルを読み込んで,PAMを通じて必要な処理を行う.例えば,sshdの場合,/etc/pam.d/sshdを読み込んで認証処理を行う.

ところで,各プログラムはどのようにして自身が読み込むファイル名(サービス名)を決めているのだろう? 結論を先に書くと,各プログラムがpam_start(3)を呼ぶ時の第一引数(サービス名)によって決まる.

例えば,OpenSSH(sshd)のコードを追ってみると,次のようなコードが見つかる.

auth-pam.c
pam_start(SSHD_PAM_SERVICE, user, &store_conv, &sshpam_handle);

SSHD_PAM_SERVICEは #define SSHD_PAM_SERVICE __progname と定義されていて,結局のところこれはsshd.cではargv[0]のbasename(ファイル名の部分)である.つまり,自身のファイル名となっていることがわかる.

まとめ

  • PAMを用いるプログラムが読み込むPAM設定ファイルは,pam_start(3)の第一引数(サービス名)によって決まる
  • サービス名は,OpenSSHのsshdではプログラム自身のファイル名となっている
  • そういう事情なので,sshdのファイル名を変更した場合は,/etc/pam.d/sshdも名前を変更しよう
  • 他のプログラムでは当然事情が異なる.不明な場合はpam_start(3)を呼んでいる所を探して調べよう

freebsd-updateを使いたければRELEASEにしとかないとだめ

freebsd-update(8)を使ってシステムをアップデートしたい人は,セキュリティブランチ(RELENG_7_0等)でcsupしてビルドしましょう.freebsd-update(8)はRELEASEのみをサポートしているので,うっかりSTABLEをビルドするとはまります.

いきさつ

RELEASE-6.2なシステムを7.0-STABLE(RELENG_7)に上げた.

% uname -a
FreeBSD yourhost.yourdomain 7.0-STABLE FreeBSD 7.0-STABLE #1: Sun Jun 15 02:29:39 JST 2008
root@yourhost.yourdomain:/usr/obj/usr/src/sys/GENERIC  i386

次回からは,自分でちまちまとビルドしたりといった作業は避けたかったので,freebsd-update(8)を試す.ところが,下記のようなよくわからないエラーとなりだめ.

% sudo freebsd-update fetch
Looking up update.FreeBSD.org mirrors... 1 mirrors found.
Fetching public key from update1.FreeBSD.org... failed.
No mirrors remaining, giving up.

素直にエラーメッセージだけを読むと,update1.freebsd.orgが落ちているように読めるが,pingは通るし当該サーバが落ちたという情報も特に見当たらない.
そこで,エラーメッセージでググるFreeBSD PR: misc/12137に行き着いた.がーん,これだ.ショック.
あと,よく見るとfreebsd-update(8)のmanにも「RELEASEしかサポートしてないもんね」と書いてあった.がーん.

回避

(1) RELENG_7_0でビルドし直す.RELENG_7_0を指定してcsupし,通常の手順でビルド (/usr/src/UPDATINGを参照)
(2) 再度freebsd-update
これでうまくアップデートできるようになった.しかし,あまりSTABLEとの差が大きかったらはまったかもしれないので,お勧めはできない.

portsnapを使ってportsツリーを更新

csupを使うかわりに,portsnapを使ってportsツリーを更新することにしました.
手順は以下の通りです.「BSD にくびったけ - portsnap」にすばらしい解説記事があるので,ここを参考に作業しました.ほとんどこの記事の通りです.

手順

使い始める時にやること

(1) とりあえずportsnap fetch

% sudo portsnap fetch

(2) 次にportsnap extract

% sudo portsnap extract

(3) 最後にportsnap update

% sudo portsnap update

(4) 更新されたportsパッケージを確認

% sudo portversion -vL=

(5) 必要に応じてアップデート
とりあえず全部更新な場合は次のような感じで.

% sudo portupgrade -aRP
2回目以降にやる事

(1) portsnap fetch && portsnap update

% sudo portsnap fetch && sudo portsnap update

portsパッケージの確認や更新は同じ.

/etc/crontabに自動更新を仕込む

いちいち自分でportupgradeするのもだるい.というか,このくらい自動でアップデートして頂きたい.こんな事を毎回手動でやっていたらIT土方そのものである.そんなわけで.スクリプトを書いてcrontabに仕込むことにした.

まず,次のようなスクリプトを書いて/usr/local/sbinあたりに放り込んでchmod 755する.

  • /usr/local/sbin/update-ports-cron.sh
#!/bin/sh
# $Id: update-ports-cron.sh 204 2008-06-15 11:30:39Z genta $
( /usr/sbin/portsnap fetch && /usr/sbin/portsnap update ) 2>&1 >/dev/null
/usr/local/sbin/pkgdb -F

echo "---- portversion:"
/usr/local/sbin/portversion -vL=
echo

echo "---- portupgrade:"
/usr/local/sbin/portupgrade -aRP --batch
echo

echo -n "Finished at: "
LANG=C date

次に,/etc/crontabに以下の行を追加.

0 5 * * * root /usr/local/sbin/update-ports-cron.sh

これでうまく行くんじゃないだろうか.いくといいな.

WWW::Mechanize(Rubyの)で一部のフォームが取れない問題

どうも,well-formedじゃないHTMLの場合に,フォームの一部を取れないみたい.例えば,以下のようなフォームがあるときにbarが取れない(WWW::Mechanize#page.forms.first.field('bar').nil? == trueになる).

<p>
<form>
<input name="foo">
</p>
<input name="bar">

検索してみたけど情報が無い.みんな困っていないのだろうか?

まだ対策できていないけど,テストだけ張っておく.これがpassすれば問題解決.

#!/usr/local/bin/ruby
# $Id$
require 'rubygems'
require 'mechanize'
require 'logger'
require 'webrick'
require 'test/unit'
require 'ruby-debug'

class DumbHTTPD
  class Servlet < WEBrick::HTTPServlet::AbstractServlet
    @@tmpl = lambda do |val|
      <<_EOT_
<html>
<body>
<h1>Fill out below form:</h1>
<p>
  <form action="/" method="POST">
  <input type="text" name="foo" value="#{val['foo']}">
</p>
  <input type="text" name="bar" value="#{val['bar']}">
  <input type="submit" value="save">
</form>
</body>
</html>
_EOT_
    end
    def do_GET(req, res)
      res['Content-Type'] = 'text/html; charset=utf-8'
      res.body = @@tmpl.call({
        'foo' => 'Hello world',
        'bar' => 'hoge',
      })
      return res
    end
    def do_POST(req, res)
      res['Content-Type'] = 'text/html; charset=utf-8'
      res.body = @@tmpl.call(req.query)
      return res
    end
  end

  attr_accessor :webrick, :runtime, :bindaddr, :port
  def initialize(bindaddr, port)
    self.bindaddr, self.port = bindaddr, port
  end

  def init_webrick
    self.webrick = WEBrick::HTTPServer.new(
      :BindAddress => self.bindaddr,
      :Port => self.port)
    self.webrick.mount('/', Servlet)
  end

  def start
    self.init_webrick
    self.runtime = Thread.new(self.webrick) {|w| w.start}
    return self
  end

  def stop
    self.runtime.kill.join
    self.webrick.shutdown
    return self
  end
end


class TC_Mech < Test::Unit::TestCase
  attr_accessor :agent, :servlet
  def setup
    self.agent = WWW::Mechanize.new {|a| a.log = Logger.new($STDERR) }
    self.agent.max_history = 1
    self.agent.user_agent_alias = 'Windows IE 6'

    self.servlet = DumbHTTPD.new('localhost', 10182).start
  end

  def teardown
    self.servlet.stop
  end

  def test_scrape_not_wellformed_html
    page = self.agent.get('http://localhost:10182')
    form = page.forms.first
    {'foo' => 'Hello world',
     'bar' => 'hoge',
    }.each do |k, v|
      field = form.field(k)
      assert(!field.nil?, "form field '#{k}' is not exists")
      assert_equal(v, field.value, "form field '#{k}'.value != '#{v}'")
    end
  end
end

対処法の考察(メモ)

WWW::Mechanize::Page#formsは,最初の一回呼び出された時にHpricot.parseしてsearch('form')して,出てきたHpricot::Elements分だけformを作って(WWW::Mechanize::Pageのインスタンス変数に)キャッシュする.んだけども,Hpricotは上記のような壊れたHTMLを読む時,後続のinputを無視してしまう.

だけど,Hpricot的には多分悪くない動作.多分,WWW::Mechanizeで対策するべきで,Mechはplaggable_parserという機構がありHTMLパーサを動的に差し替える事が可能.これを使って「ゆるくHTMLフォームを解釈するWWW::Mechanize::Pageの子孫クラス」を適当に作って以下のようにすればよい.

class WWW::Mechanize::LamePage < WWW::Mechanize::Page
  def initialize(uri=nil, response=nil, body=nil, code=nil)
    super(uri, response, body, code)
  end

  def forms
    # ここでformsを再定義
  end
end

agent = WWW::Mechanize.new
agent.pluggable_parser.html = WWW::Mechanize::LamePage  # 標準のパーサを差し替え
# 後は普通に使う

と,対策手法はわかっているんだけど,Hpricotの使い方が難しくてなかなか進みません…….

追記 (2009/2/24)

WWW::Mechanize 0.9.0で追試したところ,本エントリで触れた問題は解決していた.よかった.
どうやら,HTMLパーサがHpricotからNokogiriに変わったことで,このような問題がなくなった模様.

Twitterの自動follow返しをRubyで

Twitter でイチイチ follow するのが面倒くさい んだけど自前でメールサーバも立ててない - Djangoへの片思い日記 関連.

Rubyだとこんな感じかな? 以下,付属ライブラリのみ使用.
自前のIMAP4サーバに対してしかテストしてないけど,たぶんGmailでも動くんじゃないかと思う.

#!/usr/local/bin/ruby
require 'net/imap'
require 'open-uri'

# IMAP4 configuration:
SERVER = 'imap.gmail.com'
USER = 'exampleuser@gmail.com'
PASSWD = 'gmail_password_here'

class Twitter
  USER = 'twitter_username_here'
  PASSWD = 'twitter_password_here'
  TwitterURI = 'http://twitter.com/friendships/create/%s.json'
  def self.add(id)
    open(TwitterURI % [id], :http_basic_authentication => [USER, PASSWD]).read
  end
end

imap = Net::IMAP.new(SERVER, 993, true)
imap.login(USER, PASSWD)
imap.select('INBOX')

imap.search(['FROM', 'noreply@twitter.com',
             'SUBJECT', 'is now following you on Twitter!', 'UNSEEN']).
each do |num|
  data = imap.fetch(num, 'BODY[TEXT]')
  body = data[0].attr['BODY[TEXT]']
  Twitter.add(id = body.match(%r|http://twitter\.com/(\w+)|).to_a[1])
  imap.store(num, '+FLAGS.SILENT', [:Seen])
  puts "add #{id}"
end
imap.close