著者:しょっさん
前回は、インフラ管理アプリ「Reins」の今後の方針について、「プログラムを俯瞰してリファクタリングしよう」「Reins Agentを監視できるようにしよう」「データフォーマットを標準化しよう」という三つのポイントに着目して定めました。
今回は、前回定めた方針に従って、プログラムを更新します。ただし、すべての変更点を載せるとプログラムだけでも相当な分量になりそうだったので、次の二つについてのみ、プログラムと変更点を説明します。
記事本文掲載のシェルスクリプトマガジンvol.52は以下のリンク先でご購入できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# coding: utf-8 # filename: reins_spec.rb require "spec_helper" require "socket" RSpec.describe Reins do describe "定数/変数の設定" do it { expect(Reins::VERSION).not_to be nil } it { expect(Reins.port).not_to be nil } it { expect(Reins.logger).not_to be nil } it { expect(Reins.auth_service).not_to be nil } it { expect(Reins.regist_host).not_to be nil } end # AuthService class のテスト describe "AuthService" do describe "#authenticate_key" do let(:sha512) { "106a6484291b9778c224731501d2deeb71f2b83558a0e9784fe33646f56182f69de448e92fe83fd4e57d629987f9d0dd79bf1cbca4e83b996e272ba44faa6adb" } let(:normal) { Reins::AuthService.new } let(:other) { Reins::AuthService.new(sha512) } before do allow(Reins.regist_host).to receive(:store).and_return(true) end context "ハッシュキーを作成する場合" do it { expect(normal.create_key('192.168.0.10')).to match(/[0-9a-f]{128}/) } end context "正常に認証された場合" do it { expect(normal.authenticate_key('DEMO', '192.168.0.10')).not_to eq(false) } it { expect(other.authenticate_key('40ruby', '192.168.0.10')).not_to eq(false) } end context "異なる認証キーで呼び出された場合" do it { expect(normal.authenticate_key('TEST', '192.168.0.10')).to eq(false) } it { expect(other.authenticate_key('DEMO', '192.168.0.10')).to eq(false) } end end describe "#varid?" do let(:auth) { Reins::AuthService.new } let(:key) { auth.authenticate_key('DEMO', '192.168.0.10') } it { expect(auth.varid?(key)).to eq('192.168.0.10') } end end # HostRegistry Class のテスト describe 'HostRegistry' do let(:regist_test) { Reins::HostRegistry.new("test_db.json") } let(:test_key) { "TestKey" } let(:localhost) { '127.0.0.1' } let(:correct_hosts) { ['192.168.0.10', '1.0.0.1', '239.255.255.254'] } before do allow(regist_test).to receive(:store).and_return(true) end describe '#create' do subject { regist_test.create(localhost, test_key) } before do regist_test.create(localhost, test_key) end it { is_expected.to eq(false) } end describe '#get_status' do subject { regist_test.get_status(localhost, test_key) } before do regist_test.create(localhost, test_key) end context '登録直後の場合' do it { is_expected.to eq("alive") } end context 'ステータスを"alive"へ変更した場合' do it '#set_status で "alive" をセット' do regist_test.set_status(localhost, test_key, "alive") is_expected.to eq("alive") end end context 'ステータスが"dead"の場合' do it '#set_status で "dead" をセット' do regist_test.set_status(localhost, test_key, "dead") is_expected.to eq("dead") end end context '未登録のホストの場合' do it '登録していないホストのステータスを確認するとfalse' do expect(regist_test.get_status("192.168.0.10", test_key)).to eq(false) end it '未登録のホストステータスを変更するとfalse' do expect(regist_test.set_status("192.168.0.10", test_key, "alive")).to eq(false) expect(regist_test.get_status("192.168.0.10", test_key)).to eq(false) end end end describe '#read_hosts' do subject { regist_test.read_hosts } context '正常に登録されている場合' do it { is_expected.to eq([]) } it 'localhost を1つ登録すると、[127.0.0.1]' do regist_test.create(localhost, test_key) is_expected.to eq([localhost]) end it '複数のアドレスを登録した場合は、複数のアドレス' do correct_hosts.each { |host| regist_test.create(host, test_key) } is_expected.to match_array(correct_hosts) end end end describe '#read_hostkeys' do it '登録しなければ、空' do expect(regist_test.hosts).to eq({}) expect(regist_test.read_hostkeys).to eq({}) expect(regist_test.read_hostkeys.size).to eq(0) end it 'ハッシュ化された接続キーの一覧' do correct_hosts.each { |host| regist_test.create(host, test_key) } expect(regist_test.read_hostkeys.size).to eq(3) end end describe '#delete' do before { regist_test.create("192.168.0.1", test_key) } subject { regist_test.delete(localhost) } context '正常に削除できる場合' do it '登録済みのアドレスを削除すると、そのアドレス' do regist_test.create(localhost, test_key) is_expected.to eq(localhost) end end context '削除できない場合' do it { is_expected.to eq(nil) } end end end # TaskControl class のテスト describe 'TaskControl' do before { @server = TCPServer.new(24_368) } after { @server.close } let(:tasks) { Reins::TaskControl.new } let(:no_task) { Reins::TaskControl.new('localhost', 65_000) } describe '#connect' do context 'クライアントへ接続できる場合' do subject { tasks.connect } it 'モックを使って正常接続のコール' do allow(tasks).to receive(:connect).and_return(true) is_expected.to eq(true) end end context 'クライアントへ接続できない場合' do it { expect(no_task.connect).to eq(false) } it 'クライアントが停止していたら false' do allow(tasks).to receive(:connect).and_return(false) expect(tasks.connect).to eq(false) end end end describe '#disconnect' do context '正常に切断できた場合' do subject { tasks.disconnect } it 'こちらから、クライアントとの接続を切断して、成功すると nil' do is_expected.to eq(nil) end end end end # Dispatcher class のテスト describe 'Dispatch' do let(:test_key) { "TestKey" } let(:correct_host) { Reins::Dispatch.new("192.168.0.10", test_key) } before { allow(correct_host).to receive(:varidate).and_return(true) } describe '#command' do context 'ホスト一覧を出力する場合' do it 'ホスト一覧が出力された' do allow(Reins.regist_host).to receive(:read_hosts).and_return([]) expect(correct_host.command("list", "")).to eq([]) end end context 'アドレスを削除する場合' do it 'アドレスを正常に削除' do allow(Reins.regist_host).to receive(:delete).and_return("192.168.0.10") expect(correct_host.command("delete", "")).to eq("192.168.0.10") end end end end end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# coding: utf-8 require 'digest/sha2' module Reins class AuthService # サーバの認証キーを定義する # == パラメータ # secret_key:: SHA512 でハッシュ化されたキーを指定する # == 返り値 # nil def initialize(secret_key = 'ac58c2bedf7c1d4e35136f2ca4f81acdece03fa9e90aeefa0d363488649c7f52d7064285923f814592d53c419f5c4db59ee1a867b4852d18e0fac6efd5874072') @secret_key = secret_key nil end # IPアドレスをキーにした専用のキーを発行し、新しいキーを登録する # == パラメータ # ipaddr:: IPアドレス # == 返り値 # キー:: ハッシュ化された識別キー def create_key(ipaddr) new_key = Digest::SHA512.hexdigest("#{ipaddr}:#{Random.new_seed}") Reins.regist_host.create(ipaddr, new_key) end # クライアント認証を行う # 接続元が要求してきたキーが、サーバ側で設定されているハッシュ値と比較する # == パラメータ # key:: ハッシュ化される前のキー # ipaddr:: 接続元のIPアドレス # == 返り値 # キー:: 新規にホスト登録が必要なクライアントキーを発行 # true:: 認証成功 # false:: 認証不可 または、新規登録不可 def authenticate_key(key, ipaddr) if @secret_key == Digest::SHA512.hexdigest(key) Reins.logger.info("#{ipaddr} : 認証が成功しました") Reins.regist_host.read_hosts.include?(ipaddr) ? Reins.regist_host.read_hostkeys[ipaddr] : create_key(ipaddr) else Reins.logger.fatal("#{ipaddr} : 認証が失敗しました") false end end # クライアントの識別を行う # 要求されたクライアント固有の識別キーが登録されているものか判断する # == パラメータ # key:: クライアント固有の識別キー # == 返り値 # 識別された場合:: 登録されているIPアドレス # 否認された場合:: nil def varid?(keycode) Reins.regist_host.read_hostkeys.key(keycode) end end end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# coding: utf-8 # filename: dispatcher.rb module Reins class Dispatch # 対象となるクライアントのIPアドレスを保持 # == パラメータ # ip:: 対象となるクライアントのIPアドレス # == 返り値 # 特になし def initialize(addr, keycode) @addr = addr @keycode = keycode end def varidate return true if Reins.regist_host.read_hostkeys[@addr] == @keycode Reins.logger.error("認可されていないコマンドです") false end # コマンドを受け取って、対象の機能へ振り分ける # == パラメータ # comm:: クライアントからの要求コマンド # value:: コマンドの実行に必要な引数 # == 返り値 # exception:: 失敗した場合 # exception以外:: コマンドの実行結果 def command(comm, value) Reins.logger.debug("#{comm}(#{value}) : 指定のコマンドへディスパッチします") return "false" unless varidate case comm when /^list/ Reins.regist_host.read_hosts when /^delete/ Reins.regist_host.delete(@addr) end end end end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# coding: utf-8 # filename: host_registry.rb require 'json' require 'ipaddr' module Reins class HostRegistry attr_reader :hosts # データベースの読込・初期化 # ファイルが存在していれば内容を読込、なければファイルを新規に作成 # == パラメータ # filename:: 読込・または保管先のファイル名 # == 返り値 # 特になし。但し、@hosts インスタンス変数へ、データベースの内容を保持 def initialize(filename) @filename = filename File.open(@filename) do |file| @hosts = JSON.parse(file) end rescue @hosts = {} end # メモリ上のアドレスリストを、ファイルへ保管する # == パラメータ # filename:: 保管先ファイル名。指定がない場合は、初期化時に採用したファイル名 def store(filename = @filename) Reins.logger.debug("#{filename} : ホスト一覧を保存します") File.open(filename, "w") do |file| file.puts(JSON.pretty_generate(@hosts)) end true rescue Reins.logger.fatal("#{e}: データの保管に失敗しました") false end # 登録可能かどうかを検査する # == パラメータ # ipaddr:: 検査するIPアドレス # == 返り値 # IPアドレス:: 登録可能な場合 IPアドレスを返す # false:: 登録不可 def varid_ip?(ipaddr) addr = IPAddr.new(ipaddr).native.to_s @hosts.key?(addr) ? false : addr rescue => e Reins.logger.error("#{e}: #{ipaddr} は登録可能なIPアドレスではありません") false end # アドレスを新規に登録する。既に登録済みのものであれば登録しない。 # == パラメータ # ipaddr:: 登録するIPアドレス # key:: 登録する IPアドレスをキーにした、接続用Keyをもつハッシュデータ # == 返り値 # true:: 登録できた # false:: 既に同じアドレスまたはIPアドレスではないため、登録せず def create(ipaddr, key) if (addr = varid_ip?(ipaddr)) @hosts[addr] = {} @hosts[addr]["keycode"] = key @hosts[addr]["created_date"] = @hosts[addr]["updated_date"] = Time.now.getlocal @hosts[addr]["status"] = "alive" Reins.logger.info("#{addr} を追加しました") key if store else false end end # 登録されたステータスを変更する # == パラメータ # ipaddr:: 登録済みIPアドレス # key:: 登録する IPアドレスをキーにした、接続用Keyをもつハッシュデータ # status:: "alive" 稼働中, "dead" 異常発生し停止 # == 返り値 # "alive" or "dead":: 変更したホストのステータス # false:: 未登録ホストの場合、または Keyがまちがっている def get_status(ipaddr, key) @hosts[ipaddr]["keycode"] == key ? @hosts[ipaddr]["status"] : false rescue false end # 登録されたステータス情報を得る # == パラメータ # ipaddr:: 登録済みIPアドレス # key:: 登録する IPアドレスをキーにした、接続用Keyをもつハッシュデータ # == 返り値 # "alive" or "dead":: 登録されているホストのステータス # false:: 未登録、または停止中、または Keyがまちがっている def set_status(ipaddr, key, status) if @hosts[ipaddr]["keycode"] == key @hosts[ipaddr]["status"] = status @hosts[addr]["updated_date"] = Time.now.getlocal store status else false end rescue false end # 登録済みホスト一覧を、IPアドレスをKey、接続KeyをValueとするハッシュで返す # == 返り値 # hash: hash[ipアドレス] = key を一つの要素とする def read_hostkeys @hosts.each_key.map do |host| [host, @hosts[host]["keycode"]] end.to_h end # 登録済みホスト一覧を配列で返す # == 返り値 # array: 登録済みのIPアドレス一覧 def read_hosts @hosts.each_key.map { |host| host } end # 登録済みのアドレスを削除する # == パラメータ # addr: 削除対象のアドレス # == 返り値 # string:: 削除された要素 # nil:: 削除すべき要素が見つからなかったとき def delete(addr) if @hosts.delete(addr) Reins.logger.info("#{addr} を削除しました.") store addr else Reins.logger.warn("#{addr} を削除できません.") nil end end # まだアドレスが登録されていないかどうか # == 返り値 # true:: アドレス未登録 # false:: 1つ以上のアドレスが登録済み def empty? @hosts.empty? end end end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# coding: utf-8 # filename: lib/reins.rb require "reins/version" require "reins/dispatcher" require "reins/auth_service" require "reins/host_registry" require "reins/task_control" require "reins/config" require "json" require "socket" require "ipaddr" module Reins class << self # 指定されたポートでサーバを起動する # == パラメータ # port:: 割り当てるポート番号 # == 返り値 # Exception:: サーバが起動できなかった場合、例外を発生させ起動させない # TCPSocket:: 正常に起動した場合、Socket オブジェクトを返す def run_server(port) Reins.logger.info("Reins #{VERSION} を #{port} で起動します") TCPServer.new(port) rescue => e Reins.logger.error("Reins が起動できませんでした: #{e}") puts(e.to_s) exit end # 起動されているサーバを停止する # == パラメータ # server:: 起動中のサーバの Socket # == 返り値 # 特になし def exit_server(server) Reins.logger.info("Reins #{VERSION} を終了します") Reins.regist_host.store server.close exit end # def connect_client(server) Thread.start(server.accept) do |c| client = Reins::Clients.new(c) status = {} status["keycode"] = client.keycode status["result"] = client.command == 'auth' ? client.run_auth : client.run_command c.puts JSON.pretty_generate(status) c.close end rescue Interrupt exit_server(server) end end class Clients attr_accessor :addr, :keycode, :command, :options def initialize(client) @message = JSON.parse(client.gets) Reins.logger.debug(@message) @addr = IPAddr.new(client.peeraddr[3]).native.to_s @keycode = @message["keycode"] @command = @message["command"] @options = @message["options"] end # 認証処理を行う # == パラメータ # 特になし # == 返り値 # key:: 認証が成功した場合は接続用の認証キーを返す # false:: 認証が失敗した時は "false" 文字列を返す def run_auth Reins.logger.debug("#{addr} : 認証を行います") if (key = Reins.auth_service.authenticate_key(keycode, addr)) key else "false" end end # サーバで実行されるコマンドを受け渡す # == パラメータ # 特になし # == 返り値 # false:: 実行できなければ "false" 文字列を返す # false以外:: 実行された結果を、改行を含む文字列で返す def run_command Reins::Dispatch.new(addr, keycode).command(command, options) end end def start server = run_server(Reins.port) loop do connect_client(server) end end module_function :start end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# coding: utf-8 # filename: reins_agent.rb require "reins_agent/config" require "reins_agent/version" require "json" require "ipaddr" require "socket" module ReinsAgent class << self # サーバへ接続した後、指定したポート番号でサービスを開放する # == パラメータ # port:: エージェントが起動するためのポート番号 # == 返り値 # agent:: 起動されたサービスのソケット情報(@agent でも参照可) def run_agent(port) ReinsAgent.logger.info("Reins Agent #{VERSION} を #{port} で起動します") auth_command = JSON.generate("command" => "auth", "keycode" => ReinsAgent.client_key.to_s) throw unless (status = JSON.parse(ReinsAgent.exec(auth_command))) @cert_key = status["result"] ReinsAgent.logger.debug("認証キー : #{@cert_key} で認証が完了しました") @agent = TCPServer.new(ReinsAgent.client_port) rescue => e ReinsAgent.logger.fatal("Reins Agent が起動できませんでした: #{e}") exit end # サーバからのデータをパースする # == パラメータ # r:: リモートサーバのソケット情報 # == 返り値 # 特になし、ただし @message 連想配列へ、取得した情報を取得 def define_value(r) @message = JSON.parse(r.gets) @message["IP address"] = IPAddr.new(r.peeraddr[3]).native.to_s ReinsAgent.logger.debug("addr = #{@message['IP address']}, keycode = #{@message['keycode']}, command = #{@message['command']}, options = #{@message['options']}") end # エージェントの終了処理 # == パラメータ # 特になし # == 返り値 # 常に終了 def exit_agent ReinsAgent.logger.info("Reins Agent #{VERSION} を終了します") delete_command = JSON.generate("command" => "delete", "keycode" => @cert_key.to_s) ReinsAgent.exec(delete_command) @agent.close exit end # サーバへコマンドを送信し、その返り値を取得する # == パラメータ # command:: サーバへ送信するコマンド+オプションを指定 # == 返り値 # response:: サーバからの返り値(改行付き/複数行) def exec(command) ReinsAgent.logger.debug("コマンドを実行します : #{command}") s = TCPSocket.open(ReinsAgent.server_host, ReinsAgent.server_port) s.puts command response = s.read s.close if s response end # サーバからの接続用常駐エージェントを起動 # == パラメータ # 特になし # == 返り値 # 特になし def connect_agent Thread.start(@agent.accept) do |r| define_value(r) r.puts("OK") r.close end rescue Interrupt exit_agent end end # def start run_agent(ReinsAgent.client_port) list_command = JSON.generate("command" => "list", "keycode" => @cert_key.to_s) puts ReinsAgent.exec(list_command) loop do connect_agent end end module_function :start end |