michihide's blog

技術メモおよび雑感

Laravelでサービスプロバイダを追加した場合の、引っ越しにおける注意事項

laravel 5.2 で開発中のプロジェクトの別環境を作ろうとして、しばらく悩んだのでメモです。作業手順は概ね以下の通りです:

  1. ソースツリーを git で持ってくる
  2. vendor 配下と composer.lock を消して composer install
  3. DB ユーザーと DB インスタンスを作成(中身はまだカラの状態)
  4. php artisan migrate でテーブルを作る

(4) のところで以下のようなエラーが発生:

$ php artisan

  [Illuminate\Database\QueryException]
  SQLSTATE[42P01]: Undefined table: 7 ERROR:  relation "manuals" does not exist
  (以下略)

エラーログを遡ると、app/Providers/Ec2ServiceProvider.php という自作のソースが走っており、その中で Manual モデルの初期化に行って、SELECT しようとして引っかかってました。

artisan コンソールアプリをいくつか作って、当初はそれらを app/Console/Kernel.php に登録して使っていたんですが、本格的に使う場合(*1)はサービスプロバイダとして登録するべきらしいということで作ったのがこのファイルでした。

で、サービスプロバイダとして登録すると、php artisan レベルですでに使える状態になっていないといけないということらしいです。

しょうがないので、app/Providers/Ec2ServiceProvider.php の拡張子を .php_ にリネームして見えなくしてみると、今度は以下のエラー:

$ php artisan

  [Symfony\Component\Debug\Exception\FatalThrowableError]
  Class 'App\Providers\Ec2ServiceProvider' not found

Ec2ServiceProvider のエントリが config/app.config にいたので、その行をコメントアウト。これでやっと php artisan でコマンドの一覧表が出るようになりました。

その後、php artisan migrate でテーブルを作成し、コメントアウトと拡張子の変更を戻して php artisan すると、自分で作ったコマンドも表示されるようになりました。めでたしめでたし。

$ php artisan | grep ec2
 ec2
  ec2:autostop        インスタンスの自動停止制御
  ec2:list            EC2 インスタンスの一覧を表示します
  ec2:reboot          インスタンスを再起動します
  ec2:start           インスタンスを起動します
  ec2:stop            インスタンスを停止します

(*1)… Laravel パッケージにコマンドライン機能を含める場合がこれにあたるようです。

Apacheが起動できなくなった

CentOS7 でいろいろやってたら、ふと Apache が起動できなくなって10分ほど焦ったのでメモ。

[Wed Aug 19 16:07:31.191392 2015] [suexec:notice] [pid 14555] AH01232: suEXEC mechanism enabled (wrapper: /usr/sbin/suexec)
[Wed Aug 19 16:07:31.238273 2015] [auth_digest:notice] [pid 14555] AH01757: generating secret for digest authentication ...
[Wed Aug 19 16:07:31.238584 2015] [auth_digest:error] [pid 14555] (28)No space left on device: AH01762: Failed to create shared memory segment on file /run/httpd/authdigest_shm.14555
[Wed Aug 19 16:07:31.238608 2015] [auth_digest:error] [pid 14555] (28)No space left on device: AH01760: failed to initialize shm - all nonce-count checking, one-time nonces, and MD5-sess algorithm disabled

ログによると、共有メモリが容量オーバーで作れなくなっているらしい。

 m-hotta@estonia:~/as-user-dev$ ipcs
 ------ メッセージキュー --------
 キー     msqid      所有者  権限     使用バイト数 メッセージ
 ------ 共有メモリセグメント --------
 キー     shmid      所有者  権限     バイト  nattch     状態
0x00000000 xxxxx1     m-hotta    600        4194304    2          対象
0x00000000 xxxxx2     m-hotta    600        4194304    2          対象
0x00000000 xxxxx3     m-hotta    600        4194304    2          対象
0x00000000 xxxxx4     m-hotta    600        4194304    2          対象
0x00000000 xxxxx5     m-hotta    600        4194304    2          対象
0x00000000 xxxxx6     m-hotta    600        4194304    2          対象
 (以下大量:xxxxxx は、実際にはランダムな6桁の10進数)
 ------ セマフォ配列 --------
 キー     semid      所有者  権限     nsems

自分が所有者の共有メモリで埋もれている。なんだこれは?

どうも、vncserver 利用終了時に(ウィンドウを「×」で終わらせたりして)
ちゃんと終わらせないと、リソースが解放されないみたいです。

ipcrm -a

なぜかこれでも全部は消えてくれなかった。面倒なのでリブート。
これでリソースが開放されて、httpd自動起動しました。
めでたしめでたし。

httpd(+php) で SEGV

CentOS7 上で動いている httpd が、ログローテーションに伴う再起動時に
落ちていました。"[abrt] full crash report" というタイトルのメールが
管理者宛に来ていたので見てみると、以下のようになっていました(抜粋)。

core_backtrace:
:{   "signal": 11
:,   "executable": "/usr/sbin/httpd"
:,   "stacktrace":
:      [ {   "crash_thread": true
:        ,   "frames":
:              [ {   "address": 139643714280712
:                ,   "build_id_offset": 25144584
:                ,   "function_name": "profile_free_file_data"
:                ,   "file_name": "/usr/lib/oracle/12.1/client64/lib/libclntsh.so.12.1"
:                }

心当たりがあったので、/etc/php.d/{oci8, pdo_oci}.ini を呼ばれないように
リネームしてから httpd を再起動しました。現在は順調に動いています。
なお、当該ホストでは Oracle へのアクセス機能は使っていません。

問題のあった環境では、(他ホストとの環境統一の兼ね合いで)oci8 と
pdo_oci を入れていました。oci8 は pecl 経由で導入し、pdo_oci は
オレオレです。

http://php.net/manual/ja/oci8.requirements.php
https://github.com/hotta/pdo_oci

また、PHP 本体は以下の yum リポジトリを使わせてもらっていて、常に
本家リリースからあまり間をおかずに php-5.6.x の最新が入るように
なっています。
https://webtatic.com/packages/php56/
(すでに PHP7 系のパッケージもあるみたいです)

Apache がコケた原因は、以下の通りだと推測されます。

  1. yum update により PHP のバージョンが上がった
  2. oci8 と pdo_oci は yum の管理外なので対応が漏れて、バイナリが不整合となった。

普段は ansible 経由で環境を作っているのでその時点の最新が入りますが、
今回は個別に yum update した後の障害でした。

なお、pdo_oci を php でシミュレートする
https://github.com/taq/pdooci
という実装もあるみたいです。どっちがいいのかな。

みなさん、PHP+Oracle の環境って、どうやってメンテしているんでしょうか?
Oracle のライセンスがこんな状態だと、いつまでたっても yum のサポート
対象にはなり得ないんでしょうか。Oracle さんが Instant Client を OSS
してくれれば解決?

TLS Support for openldap-2.4.x / php on CentOS7

やっと動いたので簡単にメモしておく。

前提

  • LDAP Consumer として動作中のホストに対して TLS Support を追加する。
  • 証明書は例によってオレオレ。
    • Webサーバ用のワイルドカード証明書も取得済みだが、通信路の暗号化だけなら証明書の有効期限到達時の切り替えなどを考慮しなくていいのでオレオレが楽。

オレオレ証明書の作成

  • openssl パッケージに含まれる make-dummy-cert コマンドを使うのが簡単
/etc/pki/tls/certs/make-dummy-cert /tmp/self-cert
sed -n '/BEGIN PRIVATE KEY/,/END PRIVATE KEY/'p /tmp/self-cert > /etc/openldap/certs/localhost.key
sed -n '/BEGIN CERTIFICATE/,/END ERTIFICATE/'p /tmp/self-cert  > /etc/openldap/certs/localhost.crt

LDAP サーバへの設定追加

$ cat tls.ldif
dn: cn=config
changetype: modify
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/openldap/certs/localhost.crt
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/openldap/certs/localhost.key
$ /usr/bin/ldapmodify -x -D cn=config -w (内緒) -H ldap://localhost:389 -f tls.ldif

動作確認(別ホストからでも可)

$ cat /etc/openldap/ldap.conf
....
TLS_REQCERT never ←この行がなければ追加
  • 従来の非暗号化でアクセスできることを確認
$ ldapsearch -x -LLL -h example.com -b ou=Users,dc=example,dc=com '(uid=test1)' dn
dn: uid=test1,ou=Users,dc=example,dc=com
  • TLS 接続 ( -Z オプション)でアクセスできることを確認
$ ldapsearch -Z -x -LLL -h example.com -b ou=Users,dc=example.com '(uid=test1)' dn
dn: uid=test1,ou=Users,dc=example,dc=com
  • 出力は全く同じだが、これで TLS 通信できている。
  • 利用する通信ポートは、デフォルトでは平文の場合と同じ(389)。
    • なので、ファイアーウォールの設定等はそのままでよい。
  • -d 1 (デバッグオプション)を付けると、TLS で通信できていることが確認できる。
  • パケットを拾ってみると、クエリー自体が暗号化されていることが確認できる。

20150615171852

PHPからの認証動作確認

  • 実際にはPHPで書かれたアプリからの認証で使いたいので、以下のようなサンプルプログラムを書いて検証
$ cat ldap_tls.php
#!/usr/bin/php
<?php
define('LDAP_SERVER', 'ldap://example.com:389');
define('ACCOUNT', 'test1');
define('PASSWD', 'ok-pass');
$ds = ldap_connect(LDAP_SERVER)                    or die("ldap_connect()\n");
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3) or die("LDAP_OPT_PROTOCOL_VERSION\n");
ldap_set_option($ds, LDAP_OPT_REFERRALS, 0)        or die("LDAP_OPT_REFERRALS\n");
ldap_start_tls($ds)                                or die("ldap_start_tls()\n");
$dn = sprintf("uid=%s,ou=Users,dc=example.com", ACCOUNT);
if (ldap_bind($ds, $dn, PASSWD))    {
    echo "Authentication succeeded.\n";
} else  {
    echo "Authentication failure.\n";
}
  • パスワードが正しければ Authentication succeeded が表示され、
  • わざと間違えたら Authentication failure が表示されることを確認。

ということで、めでたしめでたし。

自動再表示の際の「フォームを再送信」ダイアログを抑制したい

リアルタイム監視のような用途の Web ページを作成する場合、
定周期(1分おきなど)で自動リロードするようにしておくと、
常に最新の情報が表示されて便利です。

HTMLでやる場合は

<meta http-equiv='refresh' content='60'>

JavaScriptでやる場合は

windows.location.reload();

といった感じで実現できます。

ところが HTML の中にフォームを含む場合、ブラウザによっては再表示の際、以下のようなダイアログが表示される場合があります。

20150605194354

いろいろ調べた結果、一番簡単な対処方法は URL を変えることのようです。なので、以下のように実装しました。

JS側で、POST先のURLをたとえば以下のように変更します。

windows.location = '/_reloaded';

次に、CakePHPのルーティング機能で本来のURLに戻してやります。

app/Config/route.php:
Router::connect('/_reloaded', [
   'controller' => 'OriginalClassName', 'action' => 'index'
]);

これでダイアログが出なくなりました。めでたしめでたし。

Apache起動とCLI起動における、PHP の実行環境の違いを調べてみた

概要

項目 Apache(libphp5.so) CLI(/usr/bin/php) 備考
Server API Apache 2.0 Handler Command Line Interface php_sapi_name()で判定可能
Configuration File (php.ini) Path /etc /etc -c で制御可能
Loaded Configuration File /etc/php.ini /etc/php.ini -n で制御可能

$GLOBALS

<?php echo (nl2br(var_dump($GLOBALS)));

<?php var_dump($GLOBALS);

メンバー名 Web(apache2handler) CLI(cli)
_GET [ ] [ ]
_POST [ ] [ ]
_COOKIE [ ] [ ]
_FILES [ ] [ ]
argv (なし) [ ]
argc (なし) 0
_ENV [ ] [ ]
_REQUEST [ ] [ ]
_SERVER (後述) (後述)

$_SERVER:WebとCLIいずれにも含まれるもの

  • $GLOBALS に含まれるものは除いています。

メンバー名 Web CLI
DOCUMENT_ROOT 絶対パス ""
PATH "/sbin:/usr/sbin:/bin:/usr/bin" "/usr/lib64/qt-3.3/bin..."
PHP_SELF DOCUMENT_ROOT からの相対パス ファイル名
REQUEST_TIME int(1433378049) int(1433390080)
REQUEST_TIME_FLOAT float(1433390080.484) double(1433378049.3198)
SCRIPT_FILENAME スクリプトの物理パス名 ファイル名
SCRIPT_NAME DOCUMENT_ROOT からの相対パス ファイル名

$_SERVER:Webのみに含まれるもの

  • REDIRECT_STATUS
  • REMOTE_{ADDR,PORT}
  • SERVER_{ADDR,ADMIN,NAME,PORT,PROTOCOL,SIGNATURE,SOFTWARE}

$_SERVER:CLIのみに含まれるもの(大半は /bin/env 由来)

'_' , CVS_RSH , G_BROKEN_FILENAMES , HISTCONTROL , HISTSIZE
HOME, HOSTNAME , LANG, LESSOPEN, LOGNAME, LS_COLORS, MAIL,
OLDPWD, PATH_TRANSLATED, PWD, QTDIR, QTINC, QTLIB
SHELL, SHLVL, SSH_{ASKPASS, CLIENT, CONNECTION, TTY},
TERM, USER, XDEBUG_CONFIG

NetBeansでリモートデバッグできなくなった

最近はCakePHPで開発をしていますが、フレームワークIDEなしで開発するのは(少なくとも自分には)非現実的だと悟ったので、デバッグはもっぱら Xdebug+NetBeansによるCakePHPのデバッグ の手順に従ってNetBeansを使っています。

今日 NetBeans を使ったら、ふとしたことからリモートデバッグが動かなくなったので、原因を調査した時のメモです。

最初の症状としては、デバッグ開始後サーバからの応答待ちのまま先に進まない(ソースの開始行まで到達しない)が、IDE単体としては問題なく動いている、という感じです。

サーバ側で Xdebug の設定は以下のようにしています。Xdebug はポート 9000 を使っています。

$ grep ^xdebug /etc/php.ini
xdebug.remote_connect_back = on
xdebug.remote_port    = 9000
xdebug.remote_handler = dbgp
xdebug.remote_enable = On
xdebug.remote_log = /tmp/xdebug_remote.log
xdebug.idekey = netbeans-xdebug

端末(Windows)側でネットワークの状態を確認します。ちなみにすっかり忘れてましたが、以前 Windowsgow を入れていたようで grep 等が使えるようになっていました。

gow - Unix command line utilities installer for Windows.

W:\>which grep
C:\Program Files (x86)\Gow\bin\grep.EXE
W:\>netstat -an | grep -w 9000
  TCP    10.28.1.53:9000           10.30.8.31:54321           CLOSE_WAIT

10.28.1.53 が開発機(Windows7 Pro)、10.30.8.31 がサーバ(CentOS7)です。

デバッグを中止してからしばらく待っても CLOSE-WAIT のまま。これは端末側のネットワークの内部状態がおかしくなっていると判断し、パソコンをリブートしたらこの状態は解消されました。

ただ、ブレークポイントで止まらないのは相変わらず。

今度はサーバ側で tcpdump を仕掛けてからデバッグを開始し、パケットを監視してみました。

root@germany:~# tcpdump -n -nn port 9000
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
11:07:54.998420 IP 10.30.8.11.36562 > 10.28.1.53.9000: Flags [S], seq 2531464156, win 14600, options [mss 1460,sackOK,TS val 1533355514 ecr 0,nop,wscale 7], length 0
11:07:55.238767 IP 10.30.8.11.36563 > 10.28.1.53.9000: Flags [S], seq 4273096071, win 14600, options [mss 1460,sackOK,TS val 1533355754 ecr 0,nop,wscale 7], length 0

サーバから端末に対して ポート 9000 への接続を張りに来ていますが、相変わらず端末がそれを無視しています。

W:\>netstat -an | grep -w 9000
  TCP    0.0.0.0:9000           0.0.0.0:0              LISTENING
  TCP    [::]:9000              [::]:0                 LISTENING

端末側ではポートの待ち受けはしているようですが、まったくパケットを受けていない状態。 これは Windows 側のファイアーウォールが疑われるので、設定を見てみます。 まず、動いているプロセスのパスを調べます。

20150528105011

netbeans64.exe で右クリックしてプロパティを開きます。

20150528105012

フルパスは C:\Program Files\NetBeans 8.0\bin\netbeans64.exe ということがわかります。次は コントロール パネル>システムとセキュリティ>Windows ファイアウォール>詳細設定>受信の規則 を開きます。

20150528130922

実はここを開いた直後は、NetBeans の過去のバージョンに対するルールがたくさんゴミとして残っていました。気持ちが悪いので、旧バージョンの分はすべて削除しました。プログラムのアンインストールをしても、ルールは自動では消えてくれないんでしょうかね?

これによると、現在の状態は「ドメイン」は許可、「パブリック」はブロックになっています。 これは Windows(NT/AD) ドメインではなくネットワークプロファイルの用語のようです。 画面右下のアイコンにマウスを乗せると、現在の状態が表示されます。

20150528130448

この表示は「識別されていないネットワーク」につながってると言いたいのか、 それとも「アクセスできるネットワークがない」と言いたいのか…。

というか、そもそもルールが二重に登録されている時点でおかしい。

ということで、二重になっている部分を「ドメイン」と「パブリック」がそれぞれ1行だけになるようにしました。

20150528131732

ここまでシンプルにして、再度挑戦します。

どう見ても「パブリック」がブロックになっていることが原因のように思えます。 と思いきや、「パブリック」を許可しても、状況は変わりませんでした。

ファイアーウォールルールのエントリに不整合が出ているようなので、 いったん「NetBeans」のルールをすべて削除した後、 再度デバッグを開始したら、以下のダイアログが表示されました。

20150529113752

これで再度「アクセスを許可」してやったらうまくいくようになりました。めでたしめでたし。