読者です 読者をやめる 読者になる 読者になる

CentOS を Kickstart の liveimg で rootfs の tgz からインストールする

CentOS

最近は CentOS のインストールは Kickstart でなるべく自動化して GUI でぽちぽちしなくて良いようにしていますが、Kickstart で liveimg というものを使えばインストール自動化だけでなく、あらかじめ作成しておいた rootfs のディレクトリツリーをそのまま展開してインストールすることができることを知ったので、やってみました。

ざっくり説明すると下記のような流れでインストールします。

  • あらかじめ rootfs のディレクトリを作成して tar.gz(tgz) にまとめる
  • Kickstart の ks.cfg で liveimg で http 経由の tgz を指定する
  • 通常ならパッケージからインストールされるところが tgz をダンロードして展開するだけになる
  • ディスクの構成とかネットワークの構成とかは通常のインストールと同じように Kickstart で構成される

rootfs の tgz を作成

適当な CentOS 7 のサーバで tgz を下記のように作成します。

# chroot するディレクトリを作成
mkdir -p rootfs/

# yum リポジトリの設定ファイルをコピー
rsync /etc/yum.repos.d/CentOS* rootfs/ -Rav

# resolv.conf をコピー
cp /etc/resolv.conf rootfs/etc/resolv.conf

# 主要なパッケージをインストール
yum -y --installroot="$PWD/rootfs/" --releasever=7 install @core kernel grub2 authconfig mdadm lvm2

# chroot で中に入っていろいろ弄る
chroot rootfs/

# sudoers wheel
tee /etc/sudoers.d/wheel <<'EOS'
%wheel ALL=(ALL) NOPASSWD: ALL
Defaults:%wheel env_keep += SSH_AUTH_SOCK
Defaults:%wheel !requiretty
Defaults:%root  !requiretty
EOS
chmod 0440 /etc/sudoers.d/wheel

# ipv6 disable
tee /etc/sysctl.d/ipv6-disable.conf <<'EOS'
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOS

# sshd_config
sed -i '/UseDNS /c UseDNS no' /etc/ssh/sshd_config
sed -i '/PermitRootLogin /c PermitRootLogin without-password' /etc/ssh/sshd_config
sed -i '/AddressFamily /c AddressFamily inet' /etc/ssh/sshd_config

# postfix
postconf -e inet_protocols=ipv4

# authorized_keys
mkdir -p /root/.ssh/pub
curl -fsSL https://github.com/ngyuki.keys | awk 1 > /root/.ssh/pub/ngyuki
cat /root/.ssh/pub/* > /root/.ssh/authorized_keys
chown -R root: /root/.ssh
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys

# chroot から抜ける
exit

# tgz にまとめて HTTP で見れる場所に置く
tar czvf /var/www/html/rootfs.tgz --directory rootfs/ .

Kickstart

次のように Kickstart ファイルを作ります。

ks.cfg

#version=RHEL7

install
liveimg --url=http://example.com/rootfs.tgz
text
cmdline
skipx

lang en_US.UTF-8
keyboard --vckeymap=jp106 --xlayouts=jp
timezone Asia/Tokyo --isUtc --nontp

network --activate --device=link --onboot=yes --bootproto=dhcp --noipv6

zerombr
clearpart --all
bootloader --location=mbr

part raid.11 --ondrive=sda --asprimary --size=500
part raid.12 --ondrive=sdb --asprimary --size=500

part raid.21 --ondrive=sda --asprimary --grow
part raid.22 --ondrive=sdb --asprimary --grow

part swap --ondrive=sda --asprimary --size=1024
part swap --ondrive=sdb --asprimary --size=1024

raid /boot --fstype=xfs --device=md0 --level=RAID1 raid.11 raid.12
raid pv.01 --fstype=xfs --device=md1 --level=RAID1 raid.21 raid.22

volgroup vg0 pv.01 --pesize=32768

logvol / --vgname=vg0 --size=8192 --name=lv_root

rootpw --plaintext password
auth --enableshadow --passalgo=sha512
selinux --disabled
firewall --disabled
firstboot --disabled

reboot

%post --log=/root/ks-post.log
set -eux
sed -i -r '/^GRUB_CMDLINE_LINUX=/s/\s+rhgb//' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg
%end

Install

CentOS 7 の minimal の CD-ROM からブートして、インストールのカーネルパラメータで下記のように Kickstart ファイルを指定します。

inst.ks=http://example.com/ks.cfg

さいごに

あらかじめいろいろ構成した一式でインストールできるため、普通に「CentOS をインストール → 追加のパッケージをインストールしたり設定したり」と比べるとかなり早いです。

物理サーバをたくさんセットアップしなければならないときの時間短縮に使えそうです。

また、いざというときにインターネットに接続できない環境で再セットアップする必要に迫られたときも、この方法ならどこか適当な Web サーバにアーカイブを配置するだけなので、普通にやるのと比べれば楽です(yum リポジトリのミラーとか作らなくても良い)。

補足

パッケージのインストールで @core kernel grub2 authconfig mdadm lvm2 とかを指定していますが、@core kernel grub2 authconfig あたりがないと Kickstart でコケます。

mdadmlvm2 はルートパーティションや /boot が MD とか LVM とかの場合に必要です。

参考

CentOS 7 で Pacemaker/Corosync 使うなら LinuxHA Japan と CentOS のどっちが良い?

CentOS Pacemaker
  • 元は 1 年くらい前に書いたものなので古いです
  • 今は CentOS の公式レポから入れた Pacemaker/Corosync が元気に動いています

Pacemaker/Corosync を使うにあたり、LinuxHA Japan のパッケージと CentOS のパッケージのどちらが良いかのメモ。

LinuxHA Japan のパッケージは下記から入手できる。

ざっくりバージョンの比較

  • LinuxHA Japan
    • corosync-2.3.4-1
    • pacemaker-1.1.13-1
    • resource-agents-3.9.6-1
  • CentOS
    • corosync-2.3.4-7
    • pacemaker-1.1.13-10
    • resource-agents-3.9.5-54

メンテナンスのポリシー

CentOS は updates にパッケージがあったので、バージョンを維持したまま CentOS 7 のライフサイクルの終了まで保守される。

LinuxHA Japan の方は新しいバージョンに更新されている。ポリシーも良くわからない(たぶん無い?)

LinuxHA Japan 特有のパッケージ

下記は LinuxHA Japan がメンテしているものなので LinuxHA Japan のパッケージにしか含まれていない。

  • pm_crmgen
    • Excel から crm.xml を作成するツール
  • pm_diskd
    • ディスクの正常性の監視(pingd の ディスク版)
  • pm_extras
    • いくつかの追加のリソースエージェントとか
    • インタフェースの状態をアトリビュートに記録するデーモン(crm_mon で表示できる)
  • pm_ctl
    • SSH 経由でクラスタのすべての Pacemaker を開始・停止などの制御を行なうツール
    • リソースのマイグレーションもコマンド一発で出来たりするっぽい

下記は他所でメンテされているものだが LinuxHA Japan のパッケージに含まれている。

pssh は次のように使えるものでこれ単体で非常に便利だけど Pacemaker と直接の関係はない。

$ pssh -H 192.168.33.10 -H 192.168.33.11 -H 192.168.33.12 mkdir -p /tmp/hoge
[1] 08:07:14 [SUCCESS] 192.168.33.12
[2] 08:07:14 [SUCCESS] 192.168.33.10
[3] 08:07:14 [SUCCESS] 192.168.33.11

crm と pcs

crm (crmsh パッケージのコマンド) での下記は・・・

crm configure

property stonith-enabled="false"
property no-quorum-policy="ignore"

rsc_defaults migration-threshold="5"
rsc_defaults resource-stickiness="INFINITY"
rsc_defaults failure-timeout="3600s"

primitive vip1 ocf:heartbeat:IPaddr2 \
  params ip="192.168.33.21" cidr_netmask="24" nic="enp0s8" \
  op monitor interval="10" timeout="20" on-fail="restart" \
  op start interval="0" timeout="20" \
  op stop interval="0" timeout="20"

primitive vip2 ocf:heartbeat:IPaddr2 \
  params ip="192.168.33.22" cidr_netmask="24" nic="enp0s8" \
  op monitor interval="10" timeout="20" on-fail="restart" \
  op start interval="0" timeout="20" \
  op stop interval="0" timeout="20"

primitive vip3 ocf:heartbeat:IPaddr2 \
  params ip="192.168.33.23" cidr_netmask="24" nic="enp0s8" \
  op monitor interval="10" timeout="20" on-fail="restart" \
  op start interval="0" timeout="20" \
  op stop interval="0" timeout="20"

group vips vip1 vip2 vip3

verify
commit
show

quit

pcs だと下記の通り。

pcs property set stonith-enabled="false"
pcs property set no-quorum-policy="ignore"

pcs resource defaults migration-threshold="5"
pcs resource defaults resource-stickiness="INFINITY"
pcs resource defaults failure-timeout="3600s"

pcs resource create vip1 ocf:heartbeat:IPaddr2 \
  ip="192.168.33.21" cidr_netmask="24" nic="enp0s8" \
    op monitor interval="10" timeout="20" on-fail="restart" \
    op start interval="0" timeout="20" \
    op stop interval="0" timeout="20"

pcs resource create vip2 ocf:heartbeat:IPaddr2 \
  ip="192.168.33.22" cidr_netmask="24" nic="enp0s8" \
    op monitor interval="10" timeout="20" on-fail="restart" \
    op start interval="0" timeout="20" \
    op stop interval="0" timeout="20"

pcs resource create vip3 ocf:heartbeat:IPaddr2 \
  ip="192.168.33.23" cidr_netmask="24" nic="enp0s8" \
    op monitor interval="10" timeout="20" on-fail="restart" \
    op start interval="0" timeout="20" \
    op stop interval="0" timeout="20"

pcs resource group add vips vip1 vip2 vip3

pcs config
pcs status

まとめ

LinuxHA Japan の特有のパッケージに特別必要なものがないなら CentOS で良いように思う。

Pacemaker 1.0 時代に慣れていると crmsh が無いのが辛いかと思ってたけど、代替の pcs が対話型シェル風に使えないだけでほとんど同じように使えてとくに困らなさそう。

最近の snmptrapd は DISPLAY-HINT 255t でもマルチバイト文字が置換されてしまう件

NetSNMP

諸事情でアプリやサーバからいろいろな通知を SNMP トラップで送ることがよくあるのですが、データバインディングの中身に日本語(UTF-8)を含めなければならないことがあります。

送るのは snmptrap コマンドで普通に送れますが・・snmptrapd でそれを受信するためには少し工夫が必要でした。

snmptrapd で日本語を受ける

普通に受けるとマルチバイト部分が "E3 81 82 E3 81 84 E3 81 86 E3 81 88 E3 81 8A" のように16進の文字列に置き換えられて traphandle には渡されます。

これをどうにかするために、まずトラップの MIB で次のように型を定義します。

HogeString ::= TEXTUAL-CONVENTION
    DISPLAY-HINT    "255t"
    STATUS          current
    SYNTAX          OCTET STRING

トラップのデータバインディングのオブジェクトの定義で↑の型を使います。

hogeMessage OBJECT-TYPE
    SYNTAX      HogeString
    MAX-ACCESS  read-only
    STATUS      current
    ::= { hogeAlarmObjs 1 }

この MIB を snmptrapd に食わせて snmptrap で日本語(UTF-8)を含むトラップを送ると UTF-8 のバイナリそのままで traphandle に渡されます。

CentOS 7 で問題発生

CentOS 5 の snmptrapd なら↑の方法で大丈夫でした。

がしかし、CentOS 7 の snmptrapd だとマルチバイト部分が . で置換されてしまいました。

原因

これ↓です。

そのままバッファにコピーされていたものが sprint_realloc_asciistring を通るようになっています。

sprint_realloc_asciistring では isprint でも isspace でもない文字はすべて . に置換されます。

簡単な解決方法

たぶんない。

強いて言えばトラップで日本語などのマルチバイト文字を使わないようにすることですけど。

面倒な解決方法

16進の文字列を traphandle に指定するスクリプトでデコードします。

実はもっと昔は、日本語(UTF-8)ではなく日本語(Shift_JIS)を使っていました。その頃は16進になった文字列を自前でデコードしていました。

ので、そんなに面倒なことでは無いはずですが・・・

MIB を食わさずに -Oa を付けて snmptrapd を起動すると同じ動き(. に置換)になり、-Ox なら16進表記になります。

-Oa は文字列をそのまま表示するオプションで、-Ox は文字列を16進で表示するオプションです。どちらのオプションも指定しないときは文字列がそのままで表示可能かを自動で判断されます。

がしかし、MIB で DISPLAY-HINT が指定されているときは -Oa-Ox は無視して DISPLAY-HINT に従います。

なので、↑の MIB を食わすと -Ox を指定したとしてもマルチバイト文字は . に置換されてしまい、16進文字列にはなりません。

つまり、↑の MIB を食わせる限りマルチバイト文字が . に置換されてしまい、デコードも不可能です。

MIB を食わせなければ良いのですが・・・下記のような列挙型のオブジェクトは値そのものではなく名前に置き換わって欲しいので、MIB は食わせたいです。

HogePriority ::= INTEGER {
    EMERG   (0),
    ALERT   (1),
    CRIT    (2),
    ERR     (3),
    WARN    (4),
    NOTICE  (5),
    INFO    (6),
    DEBUG   (7)
}

もちろん MIB を書き換えて DISPLAY-HINT を削除してしまえば良いんですけど・・・

MIB をそのままで解決する

/etc/snmp/snmp.conf に下記を追記します。

noDisplayHint yes

すると DISPLAY-HINT が無視されて16進文字列になるので、自前でデコードできます。

ただ、この方法だと↑の列挙型のオブジェクトの名前への解決が行われないっぽい気が・・・やっぱり MIB 書き換えるしか無いか。

なお、この設定(noDisplayHint)はオプションの -Ih と同じ意味ですが、snmptrapd は -I が別の意味に割り当てられているので設定ファイルで指定する必要があります。

Jenkins Pipeline を使ってみたメモ その2

Jenkins

notifyCommit を呼び出したときに実行されるビルドについて、フリースタイルと Pipeline で下記の違いが合った。

  • フリースタイル
    • 1回の notifyCommit で複数のブランチがビルドされる(こともある)
    • Git ポーリングで見つかった、更新されたすべてのブランチのビルドがキューに入る
  • Pipeline
    • 1回の notifyCommit で1つのブランチしかビルドされない
    • Git ポーリングで見つかった、更新されたブランチのうち1つだけがキューに入る
    • さらに、Pending のビルドがあると notifyCommit を呼んでもキューに入らない

なので、下記のようにブランチ [A] が「Pending 〜 開始」の間に別のブランチ [B] がプッシュされるとビルドされない。

  1. [A] ブランチをプッシュ
  2. [A] notifyCommit 呼び出し
  3. [A] ビルドが Pending
  4. [B] ブランチをプッシュ
  5. [B] notifyCommit 呼び出し
  6. [A] ビルドが開始

もっとも、[A] のビルドが開始された後に何らかの方法で notifyCommit を呼べば [B] のブランチはビルドされるし、3. から 6. までの時間はそんなに掛からないはずなので(待機時間を 0 すれば Git のポーリングの時間だけのはず)、あまり困らないような気もしたけど・・

下記のように3つのブランチがプッシュされた状況だと普通に起こりえる。

  1. [A] ブランチをプッシュ
  2. [A] notifyCommit 呼び出し
  3. [A] ビルドが開始
  4. [B] ブランチをプッシュ
  5. [B] notifyCommit 呼び出し
  6. [B] ビルドが Pending (並列実行は無効にしている)
  7. [C] ブランチをプッシュ
  8. [C] notifyCommit 呼び出し
    • [B] が Pending なのでビルドのキューに入らない
  9. [A] ビルドがが終了
  10. [B] ビルドが開始

このケース、[C][A] だった場合も同じだと思う。

うーん、Pipeline は Git の notifyCommit との連携が微妙な感じ・・・

解決?

と思ってたんだけど、Pipeline の前段にフリースタイルジョブを置いて Parameterized Trigger の Pass-through Git Commit that was built でコミット ID を渡せばすべて解決した気がする。

↑の問題はそもそもフリースタイルなら問題ないので、フリースタイルのジョブから Pipeline をトリガすれば良い。

また、下記の記事に書いた checkout scm でコミットが指定されない問題も解決できているように思う。

checkout scm でコミットが指定されない問題は、要するに次のような Jenkinsfile だったときに

node ('ore') {
    stage 'first'
    checkout scm
    sleep 60
}

node ('are') {
    stage 'second'
    checkout scm
}

sleep 60 している間にブランチがプッシュされると、次のステージの checkout scm で異なるコミットがチェックアウトされてしまう、ということ。

がしかし、Parameterized Trigger でコミット ID を渡してやれば大丈夫。

また、BRANCH_NAME が設定されない問題も Parameterized Trigger で $GIT_BRANCH を元に設定してやれば良い。

ただ、Pipeline のジョブを手動で実行した場合はダメなので注意が必要。手動でビルドしたければ前段のフリースタイルジョブをビルドしなければならない・・・

Jenkins Pipeline を使ってみたメモ

Jenkins

次のようなことがやりたかった。

  • テストを実行するジョブと、テストが通った特定(master)のブランチをデプロイするジョブを作る
    • さらにデプロイが完了したブランチを Redmine に同期するジョブも作るけどそれは省略
    • 機能ブランチをテスト → master にマージしてテスト → デプロイ → Redmine に同期
  • デプロイ対象のサーバ自体を Jenkins スレーブにする
    • デプロイは Jenkins がソースをチェックアウトしてローカルコピーするだけ
    • テストとデプロイは別のスレーブで実行されることがある

Pipeline

  • 環境変数 BRANCH_NAME が設定されない
    • シェルで同じ情報を得られなくもないけど
  • properties ビルド世代を指定するとジョブの実行時に一部の設定が消える?
    • 後述の Multibranch Pipeline のように設定してると SCMをポーリング の設定が消える
    • 上書きされてしまうため?
  • checkout scm にコミットのIDが渡されないっぽい
    • 連続でばばばっとプッシュされると同じコミットが2回ビルドされることがある
    • ジョブが実行されたときの最新コミットでビルドされるため
    • 普通は Git のポーリングで検出されたコミットになるはずなのに
    • ので Pipeline の後段で checkout scm していると前段とは異なるコミットになることがある
    • ので前段と後段で同じコミットをチェックアウトするためにはIDを指定する必要がある
    • ので checkout scm は使えない(使いにくい)

次のように test のステージでチェックアウトされたコミットを取得して deploy のステージで使う。

def git_url = 'http://gitlab.example.com/goto/testing.git'
def git_branch
def git_commit

node('test'){
   stage 'test'
   git url: git_url, branch: "**"
   echo "this is test"

   git_branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
   git_commit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()

   if (git_branch == 'master') {
      node('dev'){
         stage 'deploy'
         checkout([$class: 'GitSCM', branches: [[name: git_commit]], userRemoteConfigs: [[url: git_url]]])
         echo "this is deploy"
      }
   }
}

あるいは test のステージのツリーを stash して deploy のステージで unstash でも良いかもしれないけどツリーが巨大だと辛そう。

node('test'){
   stage 'test'
   checkout scm
   echo "this is test"
   def git_branch = sh(script: 'git describe --all', returnStdout: true).trim().replaceFirst(/^\w+\/\w+\//, '')
   if (git_branch == 'master') {
      stash "ok"
      node('dev'){
        stage 'deploy'
        unstash "ok"
        echo "this is deploy"
      }
   }
}

Multibranch Pipeline

  • ブランチごとにジョブが自動的に作成される
    • Jenkinsfile が含まれるブランチだけ対象
  • Branch SourcesGitSingle repository ってのがあるけど何が違うの?
    • Single repository だとジョブは指定した名前で1つだけできる?
    • なぜか設定を保存してもう一度開くと設定がクリアされている?
    • よくわからない
  • Discard old items で保持数に 1 以上を指定するとブランチが消えてもジョブが消えないっぽい?
    • 0 を指定すれば大丈夫だけどそういうものなの?
  • ブランチが消えてジョブが削除されてもワークスペースが削除されない
    • master には下記のディレクトリができる
      • workspace/${multi}/${branch}@script/
    • node で指定した slave には下記のディレクトリができる
      • workspace/${multi}/${branch}/
      • workspace/${multi}/${branch}@tmp/
    • ブランチ作成→削除を繰り返すとものすごいゴミがたまる
  • ブランチジョブのビルド履歴は properties で指定しないとダメ
    • 親の Discard old items が継承されるわけではない

下記のような Jenkinsfile でそこそこ良い感じにできるけど、ブランチ作成→削除でゴミがたまる。

properties properties: [
  [$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', numToKeepStr: '10']]
]

node('test'){
   stage name: 'test', concurrency: 1
   checkout scm
   echo "this is test"
}

if (env.BRANCH_NAME == 'master') {
   node('dev'){
      stage name: 'deploy', concurrency: 1
      checkout scm
      echo "this is deploy"
   }
}

例えばジョブの実行時にカレントディレクトリの兄弟のディレクトリが古かったら削除するとか。

properties properties: [
  [$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', numToKeepStr: '10']]
]

node('test'){
   stage name: 'test', concurrency: 1
   checkout scm
   echo "this is test"
}

if (env.BRANCH_NAME == 'master') {
   node('dev'){
      stage name: 'deploy', concurrency: 1
      checkout scm
      echo "this is deploy"
   }
}

stage 'purge'
node('test'){
  sh "find ../ -mindepth 1 -maxdepth 1 -mmin +60 -print0 | xargs -0 rm -vfr"
}
node('master'){
  sh "find ../ -mindepth 1 -maxdepth 1 -mmin +60 -print0 | xargs -0 rm -vfr"
}

ブランチの有無、あるいはジョブの有無を確認して削除するかどうかを判断できると理想。

まとめ

そんなに多段にしているわけでもないし、Jenkins DSL でいまのところ満足しているので、当面はそのままでいいや。

Windows と Linux (*nix) のコマンドライン引数の違い

Windows Linux PHP Node.js

元は下記で回答したものですけど。。。


Linux でプログラムを実行するとき、最終的に次の関数が実行されます。

int execve(const char *filename, char *const argv[], char *const envp[]);

つまり、次のものが必要とされます。

  • 実行ファイル名
  • コマンド引数の配列
  • 環境変数の配列

一方、PHP のような言語から外部コマンドを実行したい場合、大抵は次のようなものを指定すると思います。

  • コマンドライン文字列(実行ファイル名+コマンド引数をスペース区切りで結合したもの)

とりあえず環境変数は無視しまして・・・前述の通り最終的に必要なのは「実行ファイル名」と「コマンド引数の配列」なので「コマンドライン文字列」をバラバラに分割する必要がありますが、それを行っているのがいわゆる「シェル」です。

なので「コマンドライン文字列」を指定してコマンドを実行する場合は、大抵の場合シェルを経由します。

例えば、Node.js の child_process モジュールを見てみると、シェルを経由する child_process.exec は「コマンドライン文字列」を指定するのにたいして、シェルを経由しない child_process.spawn では「実行ファイル名」と「コマンド引数の配列」を指定するようになっています。

PHP でもシェルを経由しない pcntl_exec は「実行ファイル名」と「コマンド引数の配列」を指定します。

つまり「コマンドライン文字列」を指定して外部プログラムを実行できるようにするためにはシェルを経由する必要があります。

もちろん「コマンドライン文字列」をプログラミング言語の方でバラバラに分割するようにすればシェルを経由せずに実行することも可能です。例えば Ruby では、一部のメタ文字を含んでいない場合は Ruby の側で「コマンドライン文字列」を「実行ファイル名とコマンド引数の配列」に分割することでシェルを経由せずに実行するようです。

メタ文字を含まない、ということはスペースで分割するだけで簡単にコマンドライン文字列をバラすことができるので、そのような実装になっているのだと思います(メタ文字を含む場合はシェルと同等のパーサが必要になりますしリダイレクトやパイプなどのシェル特有の事情も出てきてしまいます)。

なお、これらの事情は Linux などの *nix 系 OS の事情であって Windows では異なります。

Windows だと外部プログラムを実行するときの API で「実行ファイル名」と「コマンドライン引数(コマンド引数をスペースで繋げた文字列)」を指定します。

BOOL CreateProcess(
  LPCTSTR lpApplicationName,                 // 実行可能モジュールの名前
  LPTSTR lpCommandLine,                      // コマンドラインの文字列
  LPSECURITY_ATTRIBUTES lpProcessAttributes, // セキュリティ記述子
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // セキュリティ記述子
  BOOL bInheritHandles,                      // ハンドルの継承オプション
  DWORD dwCreationFlags,                     // 作成のフラグ
  LPVOID lpEnvironment,                      // 新しい環境ブロック
  LPCTSTR lpCurrentDirectory,                // カレントディレクトリの名前
  LPSTARTUPINFO lpStartupInfo,               // スタートアップ情報
  LPPROCESS_INFORMATION lpProcessInformation // プロセス情報
);

それどころか lpApplicationName は省略可能なので、「コマンドライン文字列(実行ファイル名+コマンド引数をスペース区切りで結合したもの」だけでも可能です。

つまり Windows の場合はシェルを経由させなくても「コマンドライン文字列」を指定して外部プログラムを実行することができます。

そのためなのかどうかわかりませんが PHP の proc_open には Windows にだけ bypass_shell というシェルを経由しないオプションがあります。


そういえば @do_aki さんに教えてもらったんですけど、過去に *nix でも bypass_shell をサポートするコミットがあったらしいです、Revert されていますけど。

bypass_shell を指定したときの第1引数が Windows だと文字列(コマンドライン)なのに対して *nix だと配列(コマンドとコマンド引数の配列)となってしまうためなのかなーと想像しています。


ちなみに libuv を見た感じ、配列で渡されたコマンドライン引数を CreateProcess に文字列で渡すために自前で結合とエスケープ処理しているみたいですね。

もちろん *nix なら配列をそのまま渡すだけなのでそういう面倒なことはしていません。


ちなみに *nix だと execveargv がそのまま mainargv に渡ってくる感じだと思うんですけど、Windows だとそもそもプロセス作成時には引数を配列ではなく単一の文字列で指定するので、それをバラして argv に入れるのは CRT のスタートアップコードの仕事です。

CRT なんてコンパイラによって違うため(VC とか gcc とか)、コマンドライン文字列がどのようにバラされて argv になるかは微妙にブレがあります。

また、噂によると Ruby の Windows 版は CRT には頼らずに GetCommandLine かなにかで得たコマンドライン文字列を自前でバラしているとか。

そういう事情を鑑みると Windows で汎用的な escapeshellarg を作ることなんて不可能で とてもじゃないけど使い物になるようなシロモノではない のもしかたがないことだと思いますね。

redmine_dmsf を試してみたメモ

Redmine

Redmine のファイル管理プラグイン redmine_dmsf をちょっと試してみたメモ。


インストールします。

sudo su - redmine
cd /var/lib/redmine/plugins
git clone https://github.com/seventhsense/redmine_dmsf
cd /var/lib/redmine/
bundle install

なにやらエラーになりました。

:
checking for uuid/uuid.h... no
checking for uuid.h... no
configure: error: Neither uuid/uuid.h nor uuid.h found - required for brass, chert and flint (you may need to install the uuid-dev, libuuid-devel or e2fsprogs-devel package)
rake aborted!
:

libuuid-devel が必要なようです。

sudo yum install libuuid-devel

改めて bundle install します。

cd /var/lib/redmine/
bundle install

マイグレーションやアセットのコンパイルをします。

bundle exec rake redmine:plugins:assets RAILS_ENV=production
bundle exec rake redmine:plugins:migrate RAILS_ENV=production

アプリケーションサーバを起動します。

RAILS_ENV=production bundle exec rails server -b 0.0.0.0 -p 9876

使ってみた感想です。

  • Redmine をファイルサーバっぽく使える
  • Redmine の画面から D&D で複数アップロードできる
  • 同じファイルをアップロードするとバージョン管理される
  • WebDAV でもアクセスできる
  • ファイルに承認フローを設定できる
    • 具体的に何が起こるのかは試していない
    • なぜか承認者に一部のユーザーしか選択できない? 謎い?
  • ファイルの編集をロックできる
    • 自分以外は編集できないようにできる
  • Redmine の画面からメールの添付ファイルで送信することができる
    • From は redmine のグローバル設定の From のまま

メールで送信するのがためらわれるようなサイズのファイルを取引先に送りたいとき、今は Apache で作った WebDAV とかにアップロードして、IP アドレスとか Basic 認証とかで制限していたのですが、Redmine を WebDAV として使えるなら Redmine のアカウントの管理だけで良くなるので便利そうです。

と、思ったんだけど、チケットやフォーラムでファイルを添付するなり、「ファイル」の機能を使えばいいんじゃないか? って気がした。

そもそも、このプラグインは Redmine 上で文章管理するためのものであって、ファイルの受け渡しのために一時的なファイルの置き場所にするような用途には向かないと思う。

ので、自社で使用している Redmine への導入は見送り。