この記事は最終更新から1年以上経過しています。 気をつけてね。
実践Opscode Chefシリーズ、part.2の続きになります。
記事中のCookbookとChefSolo設定一式はこちらに公開しています。
https://github.com/OpsRockin/lwrp_http_userdb
:create
アクションに足りないものを書こうとしたところで切りました、今のResource/Providerはこんな感じです。
resources/auth_basic.rb
actions :create, :delete
default_action :create
attribute :user, :kind_of => String, :required => true
attribute :password, :kind_of => String
attribute :path, :kind_of => String, :required => true
attribute :filemode, :default => 0600
attr_accessor :crypted_passwd
providers/auth_basic.rb
require 'webrick'
def whyrun_supported?
true
end
action :create do
if @current_resource.crypted_passwd
Chef::Log.warn "====== Found http_auth user #{@new_resource.user}"
salt = @current_resource.crypted_passwd[0..1]
if @current_resource.crypted_passwd == @new_resource.password.crypt(salt)
Chef::Log.warn "====== http_auth user #{@new_resource.user} was not modified. (up to date)"
else
converge_by("====== Update http_auth user #{@new_resource.user}") do
Chef::Log.warn "====== Update http_auth user #{@new_resource.user}"
update_user!
end
end
else
converge_by("====== Create http_auth user #{@new_resource.user}") do
Chef::Log.warn "====== Create http_auth user #{@new_resource.user}"
update_user!
end
end
end
action :delete do
unless @current_resource.crypted_passwd
Chef::Log.warn "====== http_auth user #{@new_resource.user} was not found. Nothing to do. (up to date)"
else
converge_by("====== Delete http_auth user #{@new_resource.user}") do
Chef::Log.warn "====== Delete http_auth user #{@new_resource.user}"
delete_user!
end
end
end
def update_user!
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
htpasswd.set_passwd nil, @new_resource.user, @new_resource.password
htpasswd.flush
fix_permission!
end
def delete_user!
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
htpasswd.delete_passwd nil, @new_resource.user
htpasswd.flush
fix_permission!
end
def fix_permission!
FileUtils.chmod(@new_resource.filemode.to_i, @new_resource.path)
end
def load_current_resource
@current_resource = Chef::Resource::HttpsvAuthBasic.new(@new_resource.name)
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
@current_resource.crypted_passwd = htpasswd.get_passwd nil, @new_resource.user, true
end
バックアップをつくる?
今足りない要素、それはユーザDBのバックアップです。多分。
providers/auth_basic.rb
# --snip --
def update_user!
backup!
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
htpasswd.set_passwd nil, @new_resource.user, @new_resource.password
htpasswd.flush
fix_permission!
end
def delete_user!
backup!
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
htpasswd.delete_passwd nil, @new_resource.user
htpasswd.flush
fix_permission!
end
def fix_permission!
FileUtils.chmod(@new_resource.filemode.to_i, @new_resource.path)
end
def backup!
htpasswd_file = Chef::Resource::File.new(@new_resource.path)
backup = Chef::Util::Backup.new(htpasswd_file)
backup.backup!
end
# --snip --
Chef::Util::Backup
というのがありまして、引数にChef::Resource::File
のインスタンス(※正確には少し違う)を入れてあげるといつものリソース(file, cookbook_file, template, etc...)形式のバックアップを取ることができます。
これを使わない手はありませんね。
chef-solo run
ログレベルをINFOにしてChefを実行してみます。
$ chef-solo -c solo.rb -o 'httpsv::sample05' -l info
[2013-09-08T00:45:15+09:00] INFO: Forking chef instance to converge...
Starting Chef Client, version 11.6.0
[2013-09-08T00:45:15+09:00] INFO: *** Chef 11.6.0 ***
[2013-09-08T00:45:17+09:00] WARN: Run List override has been provided.
[2013-09-08T00:45:17+09:00] WARN: Original Run List: []
[2013-09-08T00:45:17+09:00] WARN: Overridden Run List: [recipe[httpsv::sample05]]
[2013-09-08T00:45:17+09:00] INFO: Run List is [recipe[httpsv::sample05]]
[2013-09-08T00:45:17+09:00] INFO: Run List expands to [httpsv::sample05]
[2013-09-08T00:45:17+09:00] INFO: Starting Chef Run for sawamba01.local
[2013-09-08T00:45:17+09:00] INFO: Running start handlers
[2013-09-08T00:45:17+09:00] INFO: Start handlers complete.
Compiling Cookbooks...
Converging 2 resources
Recipe: httpsv::sample05
* httpsv_auth_basic[var/www/site1:hoge1] action create[2013-09-08T00:45:17+09:00] INFO: Processing httpsv_auth_basic[var/www/site1:hoge1] action create (httpsv::sample05 line 3)
[2013-09-08T00:45:17+09:00] WARN: ====== Create http_auth user hoge1
[2013-09-08T00:45:17+09:00] INFO: file[/Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd] backed up to /Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/backup/Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd.chef-20130908004517
- ====== Create http_auth user hoge1
* httpsv_auth_basic[var/www/site1:hoge2] action create[2013-09-08T00:45:17+09:00] INFO: Processing httpsv_auth_basic[var/www/site1:hoge2] action create (httpsv::sample05 line 10)
[2013-09-08T00:45:17+09:00] WARN: ====== Create http_auth user hoge2
[2013-09-08T00:45:17+09:00] INFO: file[/Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd] backed up to /Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/backup/Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd.chef-20130908004517
- ====== Create http_auth user hoge2
[2013-09-08T00:45:17+09:00] INFO: Chef Run complete in 0.273085 seconds
[2013-09-08T00:45:17+09:00] INFO: Running report handlers
[2013-09-08T00:45:17+09:00] INFO: Report handlers complete
Chef Client finished, 2 resources updated
ファイルバックアップの様子がログに出ていますね。
backed up to /Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/backup/Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd.chef-20130908004517
バックアップの取得状況を調べてみましょう。
$ tree -a backup/`pwd`
backup//Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb
└── var
└── www
└── site1
└── .htpasswd.chef-20130908004517
3 directories, 1 file
いつものChefが作るバックアップです、やった!
世代数を増やしたい
さて、デフォルトのバックアップ世代数は5ですが、これは心持ち少ないかもしれません。
普通のChef::Resouce
で使う際はレシピで世代数を管理できますが、このように間借りする場合は場合どうしましょうか。
こうします、Chef::Resource::File
のインスタンス変数に強引に突っ込んでしまえば良いのです。
def backup!
htpasswd_file = Chef::Resource::File.new(@new_resource.path)
htpasswd_file.instance_variable_set(:@backup, 30)
backup = Chef::Util::Backup.new(htpasswd_file)
backup.backup!
end
これで世代数を30にすることができます。レシピ側で世代数を指定するやり方はもう分かりますね。
そんなタイムスタンプで大丈夫だろうか
先程のバックアップファイルに何か違和感を覚えませんでしたか?
そう、リソース更新は2回あったのにバックアップが一つしかありませんね。なぜならタイムスタンプが秒単位だからです。
通常のChef実行では同一のファイルをごそごそいじることは無いため、秒単位でも十分なのですがこのLWRPでは少々困ります。
折角なのでミリ秒も付けてもらいましょう。
#backup!
の内容はこのようになります。
def backup!
htpasswd_file = Chef::Resource::File.new(@new_resource.path)
htpasswd_file.instance_variable_set(:@backup, 30)
backup = Chef::Util::Backup.new(htpasswd_file)
backup.send(:backup_filename)
backup.instance_variable_set(:@backup_filename,backup.instance_variable_get(:@backup_filename).gsub(/[\d]+$/,Time.now.strftime("%Y%m%d%H%M%S.%6N")))
backup.backup!
end
ここを解説しようとすると長くなりそうなので割愛します、興味があったら各自でお調べ下さい。
新しい#backup!
ではファイル名がこうなります。
$ tree -a backup/`pwd`
backup//Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb
└── var
└── www
└── site1
├── .htpasswd.chef-20130908010118.944651
└── .htpasswd.chef-20130908010118.947960
3 directories, 2 files
リソース操作の間隔は3msしかなかったのですね。
しかしタイムスタンプの制度を変更することができたので、Chefの処理速度が10,000倍になるまでは問題ない。
これで:create
アクションは完成となります、:delete
時にも#backup!
が使いまわせていい感じですね。
最後のアクション :discard を実装する
.htpasswd
上のユーザ操作ができるようになりました、もう一つ必要な操作として考えられるのはユーザDB本体の破棄です。
認証自体を解除する場合には、.htpasswd
ファイル自体不要なものになりますが、既存ユーザを全てdeleteするレシピを書くのはイマイチ現実的ではありません。それに今のやり方ではファイル自体は中身が空っぽで残ってしまいます。
ということで、DB自体を削除するアクション:discard
を実装してみたResouce/Providerがこちらです。
resources/auth_basic.rb
actions :create, :delete, :discard
default_action :create
attribute :user, :kind_of => String, :required => true
attribute :password, :kind_of => String
attribute :path, :kind_of => String, :required => true
attribute :filemode, :default => 0600
attr_accessor :crypted_passwd
attr_accessor :db_exists
db_exists
属性が増えました。使っているWEBrick::HTTPAuth::Htpasswd
ライブラリの仕様のため必要になりました。
providers/auth_basic.rb
require 'webrick'
def whyrun_supported?
true
end
action :create do
if @current_resource.crypted_passwd
Chef::Log.warn "====== Found http_auth user #{@new_resource.user}"
salt = @current_resource.crypted_passwd[0..1]
if @current_resource.crypted_passwd == @new_resource.password.crypt(salt)
Chef::Log.warn "====== http_auth user #{@new_resource.user} was not modified. (up to date)"
else
converge_by("====== Update http_auth user #{@new_resource.user}") do
Chef::Log.warn "====== Update http_auth user #{@new_resource.user}"
update_user!
end
end
else
converge_by("====== Create http_auth user #{@new_resource.user}") do
Chef::Log.warn "====== Create http_auth user #{@new_resource.user}"
update_user!
end
end
end
action :delete do
unless @current_resource.crypted_passwd
Chef::Log.warn "====== http_auth user #{@new_resource.user} was not found. Nothing to do. (up to date)"
else
converge_by("====== Delete http_auth user #{@new_resource.user}") do
Chef::Log.warn "====== Delete http_auth user #{@new_resource.user}"
delete_user!
end
end
end
action :discard do
if @current_resource.db_exists
converge_by("====== Discard http_auth userdb #{@new_resource.path}") do
backup!
::File.unlink(@new_resource.path)
end
else
Chef::Log.warn "====== http_auth user database #{@new_resource.path} was not found. Nothing to do. (up to date)"
end
end
def update_user!
backup!
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
htpasswd.set_passwd nil, @new_resource.user, @new_resource.password
htpasswd.flush
fix_permission!
end
def delete_user!
backup!
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
htpasswd.delete_passwd nil, @new_resource.user
htpasswd.flush
fix_permission!
end
def fix_permission!
FileUtils.chmod(@new_resource.filemode.to_i, @new_resource.path)
end
def backup!
htpasswd_file = Chef::Resource::File.new(@new_resource.path)
htpasswd_file.instance_variable_set(:@backup, 30)
backup = Chef::Util::Backup.new(htpasswd_file)
backup.send(:backup_filename)
backup.instance_variable_set(:@backup_filename,backup.instance_variable_get(:@backup_filename).gsub(/[\d]+$/,Time.now.strftime("%Y%m%d%H%M%S.%6N")))
backup.backup!
end
def load_current_resource
@current_resource = Chef::Resource::HttpsvAuthBasic.new(@new_resource.name)
unless ::File.exists?(@new_resource.path)
@current_resource.db_exists = false
@current_resource.crypted_passwd = nil
return
end
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@new_resource.path)
@current_resource.crypted_passwd = htpasswd.get_passwd nil, @new_resource.user, true
@current_resource.db_exists = true
end
:discard
についてはunlinkしているだけです、@current_resource
の収集処理に手を加えています。
WEBrick::HTTPAuth::Htpasswd
がnewする度にファイルを実際に作るので、load_current_resource
が無条件にnewするとファイルが毎度作成されていました。
それでは:discard
が永遠に収束しない(毎回空ファイル作成&unlinkのマッチポンプ)ので、ユーザDBの存在確認を入れてあげました。
chef-solo run
Why-Runから行ってみます。
$ chef-solo -c solo.rb -o 'httpsv::sample08' -W
Starting Chef Client, version 11.6.0
[2013-09-08T12:08:41+09:00] WARN: Run List override has been provided.
[2013-09-08T12:08:41+09:00] WARN: Original Run List: []
[2013-09-08T12:08:41+09:00] WARN: Overridden Run List: [recipe[httpsv::sample08]]
Compiling Cookbooks...
Converging 1 resources
Recipe: httpsv::sample08
* httpsv_auth_basic[var/www/site1:delete_dummy] action discard
- Would ====== Discard http_auth userdb /Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd
Chef Client finished, 1 resources would have been updated
レシピはこうなります。
recipes/sample08.rb
base_dir = ENV['PWD']
httpsv_auth_basic 'var/www/site1' do
action :discard
user 'delete_dummy'
path File.join(base_dir, self.name, '.htpasswd')
name [self.name, self.user].join(':')
end
chef-solo run
続いて実行。
$ chef-solo -c solo.rb -o 'httpsv::sample08'
Starting Chef Client, version 11.6.0
[2013-09-08T12:09:12+09:00] WARN: Run List override has been provided.
[2013-09-08T12:09:12+09:00] WARN: Original Run List: []
[2013-09-08T12:09:12+09:00] WARN: Overridden Run List: [recipe[httpsv::sample08]]
Compiling Cookbooks...
Converging 1 resources
Recipe: httpsv::sample08
* httpsv_auth_basic[var/www/site1:delete_dummy] action discard
- ====== Discard http_auth userdb /Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd
Chef Client finished, 1 resources updated
chef-solo run
もう一度実行。
$ chef-solo -c solo.rb -o 'httpsv::sample08'
Starting Chef Client, version 11.6.0
[2013-09-08T12:09:27+09:00] WARN: Run List override has been provided.
[2013-09-08T12:09:27+09:00] WARN: Original Run List: []
[2013-09-08T12:09:27+09:00] WARN: Overridden Run List: [recipe[httpsv::sample08]]
Compiling Cookbooks...
Converging 1 resources
Recipe: httpsv::sample08
* httpsv_auth_basic[var/www/site1:delete_dummy] action discard[2013-09-08T12:09:28+09:00] WARN: ====== http_auth user database /Users/sawanoboriyu/github/opsrockin/lwrp_http_userdb/var/www/site1/.htpasswd was not found. Nothing to do. (up to date)
(up to date)
Chef Client finished, 0 resources updated
これで今回のLWRP作成はひと通り終了しました。
おわりに
パートが3つにもなってしまいましたが、LWRPを理解しましたね。
何故LWRP?
いくら内部DSLといっても、レシピ内で様々な処理を行ってしまうとレシピとしての可読性は下がる一方になります。
レシピの保守性を下げないようには、結局のところChefのフレームワークらしさを活かすためにもLWRPとしてリソースを実装することが最善でしょう。
作ってみよう
ここまで読むことが出来たのなら、次は自分のLWRPを作ってみましょう。実際大した手間ではありません。
しかし事前に言っておくと、そこそこハマる事うけあいです。今回も@new_resource.action
の型が省略時Symbol, 指定時はArray(中身Symbol)などという妙な仕様に気づいたりもしました。
作らなくてもいい
さんざん解説しておいてなんですが、LWRPにまでしなくてもexecute & not_if
程度で十分なケース、そもそもChefでやらないほうが楽なケースもあります。知っておくのは良いことですがこだわらないこともまた大事ですよね。
ただ、危なっかしいexecuteやRuby処理が多すぎて読みにくいレシピがあったら、LWRPの実装を検討してみてください。
実践LWRP、HTTP認証用ファイル(htpasswd,htdigest)をChefのリソースとして管理する part.1 of 3
実践LWRP、HTTP認証用ファイル(htpasswd,htdigest)をChefのリソースとして管理する part.2 of 3
実践LWRP、HTTP認証用ファイル(htpasswd,htdigest)をChefのリソースとして管理する part.3 of 3