2019年3月31日日曜日

NativeExteions で別のクラスの Struct を return する関数を考える

概要

NativeExtensions でインスタンスメソッドを定義する場合大抵は FIX2INTrb_str_new2 を使って値を返します
ただケースとしてインスタンスメソッドが Struct (VALUE) を返したい場合があると思います
かつ self であれば簡単に返せますが self ではない Struct を返したいケースもあると思います
今回はその方法を紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプルコード

FirstExt というクラスと FirstExt という Struct 、SecondExt と SecondExt の Struct が紐付いています
FirstExt のインスタンスメソッドに second というメソッドがありこれが SecondExt のインスタンスを返却しています
詳細は後述しています

#include <ruby.h>

VALUE se;

typedef struct {
  char *name;
  int age;
} FirstExt;

typedef struct {
  char *address;
  char *phone_number;
} SecondExt;

static void first_ext_free(FirstExt *s) {
  free(s);
}

static VALUE first_ext_alloc(VALUE self) {
  FirstExt *s = ALLOC(FirstExt);
  return Data_Wrap_Struct(self, 0, first_ext_free, s);
}

static void second_ext_free(SecondExt *s) {
  free(s);
}

static VALUE second_ext_alloc(VALUE self) {
  SecondExt *s = ALLOC(SecondExt);
  return Data_Wrap_Struct(self, 0, second_ext_free, s);
}

static VALUE wrap_second(VALUE self, VALUE _address, VALUE _phone_number) {
  char *address = StringValuePtr(_address);
  char *phone_number = StringValuePtr(_phone_number);
  SecondExt *s;
  Data_Get_Struct(self, SecondExt, s);
  s->address = address;
  s->phone_number = phone_number;
  return Data_Wrap_Struct(se, 0, 0, s);
}

static VALUE wrap_show(VALUE self, VALUE _address, VALUE _phone_number) {
  SecondExt *s;
  Data_Get_Struct(self, SecondExt, s);
  printf("%s\n", s->address);
  printf("%s\n", s->phone_number);
  return Qnil;
}

void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  se = rb_define_class("SecondExt", rb_cObject);
  rb_define_alloc_func(fe, first_ext_alloc);
  rb_define_alloc_func(se, second_ext_alloc);
  rb_define_method(fe, "second", wrap_second, 2);
  rb_define_method(se, "show", wrap_show, 0);
}

ポイント

基本的な allocfreerb_define_classrb_define_method の説明は省略します
最大のポイントは wrap_second メソッドです

static VALUE wrap_second(VALUE self, VALUE _address, VALUE _phone_number) {
  char *address = StringValuePtr(_address);
  char *phone_number = StringValuePtr(_phone_number);
  SecondExt *s;
  Data_Get_Struct(self, SecondExt, s);
  s->address = address;
  s->phone_number = phone_number;
  return Data_Wrap_Struct(se, 0, 0, s);
}

一度 Data_Get_Struct で構造体情報を取得し各フィールドに値をセットします
そして事前に rb_define_class で定義した VALUE se クラスを元に Data_Wrap_Struct に構造体のポインタ (s) と返り値として返したいクラス (se) の情報を渡すことで Ruby で扱えるクラスのオブジェクトに変換し return しています
この際に second_ext_free の関数は指定しません
なぜならばここで作成したオブジェクトが参照されなくなった際には second_ext_free が自動でコールされます
そこに更に second_ext_free をコールするように指定してしまうと double free というメモリ参照エラーを起こしてしまうからです

テスト

テストしてみます

RSpec.describe FirstExt do
  it "create a instance" do
    cli = FirstExt.new
    cli.second('123-4567', '080-1234-5678').show
  end
end

こんな感じでメソッドチェイン的にコールできます
second メソッドの返り値が SecondExt クラスのインスタンスなのでそのまま show が呼べるようになっています

最後に

NativeExtensions の関数で self ではない別のクラスのオブジェクトを返却する方法を紹介しました
Ruby だとあるメソッドの返り値がクラスになっていて、そのフィールドを参照するという使い方はよくあるので、NativeExtensions でそれを実現するためには必須の方法かなと思います
もしかするとこれがベストプラクティスではなく、これ以外にも方法があるかもしれません

2019年3月30日土曜日

NativeExtensions で method chain 的な使い方をする方法

概要

NativeExtensions で作ったライブラリでも Ruby 側で使うときは method chain 的にドットを使ってメンバなどを参照したいです
そんな場合に使えるサンプルを紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプルコード

ポイントは wrap_chain 関数が self を return している点です
こうすることで別のメソッドをドットで繋げてコールすることができます

static VALUE wrap_chain(VALUE self, VALUE _name) {
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  char *name = StringValuePtr(_name);
  s->name1 = name;
  return self;
}

static VALUE wrap_name(VALUE self) {
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  return rb_str_new2(s->name1);
}

ちなみに MyStruct は以下のような感じです

typedef struct {
  char *name1;
} MyStruct;

使う

こんな感じで chanin.name という感じで参照できます
chain が Qnil を返すような関数にすると再度 cli から参照する必要があります

cli = FirstExt.new
puts cli.chain("hawksnowlog").name

2019年3月29日金曜日

Ruby の NativeExtensions で boolean を引数に取る方法

概要

C 側で Ruby の Boolean を扱う方法です

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

サンプル

C 側のコードは以下の通りです

static VALUE wrap_bool_test(VALUE self, VALUE _bool) {
  int bool = RTEST(_bool);
  if (bool == 1) {
    return Qtrue;
  } else {
    return Qfalse;
  }
}

ポイントは RTEST で C 側で扱える int に変換している点です
RTESTQfalse or Qnil が Ruby から渡ってきた場合に真 (1) を返すマクロです

テストコード

Ruby 側でコールする方法です

RSpec.describe FirstExt do
  it "create a instance" do
    cli = FirstExt.new
    puts cli.bool_test(true)
    puts cli.bool_test(false)
    puts cli.bool_test(1)
    puts cli.bool_test('a')
  end
end

結果は以下の通りです

true
false
true
true

結果を見るとわかりますが完全に Ruby の Bool と C 側の変換が完全互換でできているわけでありません
false を渡しているときだけ false が返ってきていますが true 以外を渡しても true が返ってきてしまっています

最後に

NativeExtensions で Boolean を扱う方法を紹介しました
おそらくこれが一番簡単な方法だと思います

参考サイト

2019年3月28日木曜日

rspec-test でブロックが渡される関数をスタブする方法

概要

例えば Sidekiq.redis { |c| c.get("hoge") } みたいな使い方をしているときに redis にはアクセスしないでテストしたい場合があると思います
そんな場合に rspec-mocks を使ってブロックをスタブする方法を紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

実行コード

例えば以下のような感じで Sidekiq.redis をコールしているとします

require 'sinatra/base'
require './worker.rb'

class Web < Sinatra::Base
  get '/:key' do
    Sidekiq.redis { |c|
      return c.get(params[:key])
    }
  end
end

これを rspec-mocks を使ってスタブするテストを作成します

スタブテストコード

and_yield を使うとブロック内の処理をスタブすることができます

ENV['RACK_ENV'] = 'test'

require './app.rb'
require 'rspec'
require 'rack/test'

describe "Web Tests" do
  include Rack::Test::Methods

  def app
    Web
  end

  it "could be fetch a value from redis" do
    redis = instance_double(Redis, :get => "fuga_test")
    allow(Sidekiq).to receive(:redis).and_yield(redis)
    get '/hoge'
    expect(last_response.body).to eq "fuga_test"
  end
end

これでテストすると Redis にはアクセスすることなくテストできます

応用

同じメソッド名で引数の key が異なる呼び出しをブロック内で複数しているとします
その場合は引数の key を正規表現を使って判断し引数に応じて別々のレスポンスを返却するように stub を定義します

require 'sinatra/base'
require './worker.rb'

class Web < Sinatra::Base
  get '/:key' do
    Sidekiq.redis { |c|
      if c.get("fuga") == "fuga_test"
        return c.get(params[:key])
      end
    }
  end
end
ENV['RACK_ENV'] = 'test'

require './app.rb'
require 'rspec'
require 'rack/test'

describe "Web Tests" do
  include Rack::Test::Methods

  def app
    Web
  end

  it "could be fetch a value from redis" do
    redis = instance_double(Redis)
    allow(redis).to receive(:get).with(/hoge/).and_return("hoge_test")
    allow(redis).to receive(:get).with(/fuga/).and_return("fuga_test")
    allow(Sidekiq).to receive(:redis).and_yield(redis)
    get '/hoge'
    expect(last_response.body).to eq "hoge_test"
  end
end

allow(redis).to receive(:get).with(/hoge/).and_return("hoge_test") を引数の key が異なる呼び出しごとに定義しています
これで c.get("fuga") の場合は fuga_test が返り c.get("hoge") の場合は hoge_test が stub から返るようになります

参考サイト

2019年3月27日水曜日

Ruby から Gmail を送信する方法

概要

Ruby から Gmail を使ってメール送信する方法を紹介します
認証時にポイントがあるのでそこ注意が必要です

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

アプリケーションパスワードを使って送信する

  • vim Gemfile
gem "gmail"
  • bundle install --path vendor
  • vim app.rb
require 'gmail'

gmail = Gmail.new('your-account@gmail.com', 'application_password')
ret = gmail.deliver do
  to "your-account@gmail.com"
  from "Your Name <your-account@gmail.com>"
  subject "subject"
  text_part do
    body "Hello"
  end
end
gmail.logout
  • bundle exec ruby app.rb

これだけで送信できます
ポイントは認証にアプリケーションパスワードを使う点です
アプリケーションパスワードは普通にログインする際のパスワードとは違い Ruby や自作のスクリプトなどから認証する際に発行する専用のパスワードです
このパスワードを使えば Ruby から Gmail に認証することができます

またアプリケーションパスワードは Google の二段階認証を有効にしていないと使えません
なのでまずは二段階認証を有効にするところから始めなければなりません

ちなみに普通に Google アカウントの ID/PW でログインしようとすると「重大なセキュリティ通知」というメール通知が届きます
「ログインをブロックしました」という旨のメールで結局ログインすることができません
が、Google アカウントの設定で「安全性の低いアプリからのアカウントへのアクセスを許可する」という設定がありこれを有効にすることで普通の ID/PW でログインすることができるようになります
が、これはアンセキュアな方法なのでおすすめできません

IFTTT 経由で送信する

他に IFTTT を使って Ruby から Gmail を送信する方法を紹介します
事前に IFTTT で webhook 経由で Gmail を送信するレシピを作成しておきます

また Gmail 側の設定で Webhook で送信される VALUE を以下のように設定しておきましょう

この VLAUE1VALUE2 を Ruby から設定して IFTTT に送ることでメール本文とタイトルを設定することができます

  • vim ifttt.rb
require 'net/http'
require 'json'

class IFTTT

  def initialize
    @url = "https://maker.ifttt.com/trigger/event_name/with/key/xxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx"
  end

  def call_maker(value1, value2)
    uri = URI.parse(@url)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true
    req = Net::HTTP::Post.new(uri.request_uri)
    req["Content-Type"] = "application/json"
    maker_body = {
      "value1" => value1,
      "value2" => value2
    }
    payload = maker_body.to_json
    req.body = payload
    res = https.request(req)
    return res.code
  end

end
  • vim app.rb
value1 = "title"
value2 = "subject<br>"

cli = IFTTT.new
cli.call_maker(value1, value2)
  • bundle exec ruby app.rb

これでメール送信できるはずです
IFTTT の Webhook 経由の Gmail 送信は Content-Typemultipart/alternative になるっぽいです
添付ファイルや HTML の埋め込みができるので本文となる文字列に HTML を記載することができます

IFTTT の Gmail サービスは OAuth で認証/許可しているので、この方法のほうがセキュアです

最後に

Ruby から Gmail を送信する方法を 2 通り紹介しました
他にもやり方はいろいろあると思いますがてっとり早そうな方法かなと思います
IFTTT で Gmail サービスをすでに追加している人は IFTTT で Applet を作るのが簡単だと思います
もしそっちのほうが面倒だという場合はアプリケーションパスワードを使う方式にし、それも嫌な場合はセキュリティリスクを覚悟で安全性の低いアプリを信頼する設定をする感じかなと思います

参考サイト

2019年3月26日火曜日

Ruby で mock なテストをする

概要

RSpec で mock を使うには rspec-mocks を使います
基本的な rspec-mocks の使い方を紹介します
また Sinatra アプリと Sidekiq のワーカーのテストを書く際に rspec-mocks を使ってテストする方法も紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57
    • sinatra 2.0.5
    • sidekiq 5.2.5
    • rspec, rspec-mocks 3.8.0

環境準備

今回は以下のような環境で行います

  • Sinatra が動いている
  • Sinatra が受けたリクエストは Sidekiq に流す
  • Sidekiq のワーカーが外部のサイトにリクエストしてレスポンスを得る

テストを作成するケースとして今回は

  • Sinatra アプリのテストを mock を使って実装する
  • ワーカーのテストを mock を使って実装する

をやってみたいと思います
各種テスト用のコードは以下の通り

  • tree -I "vendor*|tmp*"
.
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── lib
│   ├── my_http.rb
│   └── my_http.rbu
├── spec
│   ├── app_spec.rb
│   ├── spec_helper.rb
│   └── worker_spec.rb
└── worker.rb
  • vim Gemfile
gem "sinatra"
gem "rspec"
gem "rspec-mocks"
gem "rack-test"
gem "sidekiq"
  • cat app.rb
require 'sinatra/base'
require './worker.rb'

class Web < Sinatra::Base
  get '/' do
    Worker.perform_async
  end
end
  • cat config.ru
require './app.rb'
run Web
  • cat worker.rb
require 'sidekiq'
require './lib/my_http.rb'

class Worker
  include Sidekiq::Worker

  def perform
    res = MyHTTP.new.call
  end
end
  • cat lib/my_http.rb
require 'net/https'

class MyHTTP
  def call
    uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    req = Net::HTTP::Get.new uri.request_uri
    res = http.request req
    res.code
  end
end

アプリは

  • bundle exec rackup config.ru

ワーカーは

  • redis-server
  • bundle exec sidekiq -r ./worker.rb -P ./tmp/sidekiq.pid

で起動できます
これらのテストを mock を使って実装してみます

rspec テストの初期化

まずは spec ファイルを作成しましょう

  • bundle exec rspec --init

そしてそれぞれのテストを作成します

  • touch spec/worker_spec.rb
  • touch spec/app_spec.rb

Sinatra アプリのテスト

まずは Web アプリのテストを作成してみましょう
mock するのは Sidekiq のワーカーになります

  • vim spec/app_spec.rbv
ENV['RACK_ENV'] = 'test'

require './app.rb'
require 'rspec'
require 'rack/test'

describe "Web Tests" do
  include Rack::Test::Methods

  def app
    Web
  end

  it "could be fetch the job id" do
    allow(Worker).to receive(:perform_async).and_return("test_jid")
    get '/'
    expect(last_response.body).to eq "test_jid"
  end
end
  • bundle exec rspec -f d spec/

でテストが実行されます
Worker.perform_async を mock (どちらかと言えば stub) しているため実際に redis へはデータの登録は行われません
なのでワーカーは起動してない状態でもテストが通ります

ポイント

allow(Worker).to receive(:perform_async).and_return("test_jid") これかなと思います
allow は指定のクラスを mock するためのメソッドです
receive に mock するメソッドを指定します
返り値も mock したい場合は続いて .and_return で返り値を指定すれば OK です

これを get '/' の前に設定するのもポイントです
get '/' は実際に Siantra アプリにリクエストするので mock を作成スル前にコールしてしまうと redis にデータが登録されてしまいます

Sidekiq ワーカーのテスト

今度はワーカーのテストを書いてみます
先程とは違い mock するのは my_http.rb になります

require './worker.rb'

describe "Worker Tests" do
  it "could be fetch the response code" do
    my_http = instance_double(MyHTTP, :call => 200)
    allow(MyHTTP).to receive(:new).and_return(my_http)
    w = Worker.new
    expect(w.perform).to eq 200
  end
end

Worker.perform はクラスメソッドですが MyHTTP.call はインスタンスメソッドになります
なので mock の作り方も少し異なります
まず instance_doubleMyHTTP.call の mock を作ります
レスポンスは整数の 200 を返却するようにします
そして allowMyHTTP インスタンスメソッドとして call が呼ばれることを定義します
これで Sidekiq の Worker.perform 内で呼んでいる MyHTTP を mock することができます
実際テストを実行してみるとわかりますが HTTP の通信をしていないことが確認できると思います

最後に

rspec-mocks の簡単な使い方と Sinatra, sidekiq でのテストの書き方を紹介しました
まだまだいろいろな機能があるので興味があれば公式のページを見ると良いと思います

参考サイト

2019年3月25日月曜日

GCE にノンパス SSH でログインしよう

概要

デフォルトだと自動で生成されるパスワードありの公開鍵を使って SSH します
パスなしの鍵を使って GCE に登録すれば gcloud コマンドでもノンパスログインできるようになります

環境

  • macOS 10.14.3

パスなし鍵生成

SSH するクライアントで行います
今回は Mac で行いました
すでに鍵がある場合はそれを使っても OK です

  • ssh-keygen -t rsa
  • cat id_rsa.pub

この情報を GCE の .ssh/authorized_keys に記載します

SSH する

ssh コマンドを使ってもいいですが gcloud コマンドを使ってみます

  • gcloud compute --project "project-123456" ssh --zone "us-central1-c" --ssh-key-file="~/.ssh/id_rsa" "server01"

ポイントは ssh サブコマンドで --ssh-key-file を指定するところです
先程作成した秘密鍵を指定します

2019年3月23日土曜日

Ubuntu で LVS 入門

概要

LVS (Linux Virtual Server) は大抵の Linux ディストリビューションで使える負荷分散のためのソフトウェアです
L4ロードバランサになります
keepalived が LVS のラッパーで大抵は keepalived と組み合わせてロードバランサの冗長化構成などを実現します
今回は入門なので簡単な HTTP のロードバランサをしてみました

環境

  • Ubuntu 16.04
  • LVS (ipvsadm) 1.28
  • keepalived 1.2.24

インストール

  • sudo apt -y install ipvsadm keepalived

keepalived.conf の作成

Ubuntu でインストールすると設定ファイルのサンプルがあるのでそれをベースに設定すると簡単です

  • sudo cp /usr/share/doc/keepalived/samples/keepalived.conf.sample /etc/keepalived/keepalived.conf
  • sudo vim /etc/keepalived/keepalived.conf
! Configuration File for keepalived

global_defs {
   router_id LVS_DEVEL
}

vrrp_instance VI_1 {
    interface enp0s9
    virtual_router_id 50
    nopreempt
    priority 100
    advert_int 2
    virtual_ipaddress {
        192.168.99.220
    }
}

virtual_server 192.168.99.220 1080 {
    delay_loop 6
    lb_algo rr 
    lb_kind NAT
    persistence_timeout 50
    protocol TCP

    real_server 192.168.99.200 80 {
        weight 1
        HTTP_GET {
            url { 
              path /
            }
            connect_timeout 3
            nb_get_retry 3
            delay_before_retry 3
        }
    }
}

解説

ポイントとなる項目ごとに説明していきます

global_defs

今回はほぼ使っていません
router_id で識別子を設定しているだけです
メールなどを送信する設定をすると router_id の値を参照してメールに記載してくれるようです

vrrp_instance

VRRP (Virtual Router Redundancy Protocol) の設定をします
ルータとして動作しかつ冗長化するための virtual_ipaddress を設定するのがポイントです
この virtual_ipaddress が仮想 IP として interface に指定したインタフェースに自動的に振られます
DHCP では払い出されない範囲の IP などを指定しましょう

そしてこの IP にアクセスすることで配下にぶら下がっている実サーバ (real_server) にアクセスすることができます
virtual_router_id は仮想ルータの識別子です
他に vrrp_instance を定義する場合は ID が被らないようにしましょう

virtual_server

配下にぶら下げるサーバやロードバランシングの方式を設定します
delay_loop はヘルスチェックの間隔です
lb_algo, lb_kind がバランシングの方式です
今回は 上記を使っていますが現在は lvs_schedlvs_method に項目名が置き換えられています
persistence_timeout はセッションの保持時間です

virtual_server 192.168.99.200 1080 の 1080 の部分は fwmark と呼ばれています
LISTEN するポートではないのでご注意ください

real_server

実際にぶら下げるサーバを指定します
複数のサーバをぶら下げる場合は real_server の項目を追加します
アクセスするパスやメソッドを指定します
connect_timeout, nb_get_retry, delay_before_retry はそれぞれタイムアウト、リトライする回数、リトライするまでにどれだけウェイトするかになります

プロセス起動

設定ファイルの準備ができたらプロセスを起動しましょう

  • sudo systemctl start ipvsadm
  • sudo systemctl start keepalived

keepalived を起動すると virtual_server に設定した IP が interface enp0s9 に振られます

動作確認

適当に nginx でも立てておきます
そして virtual_server に指定した IP にアクセスすると nginx の画面が確認できると思います
今回であれば real_server の IP にアクセスしても nginx の画面が確認できます

参考サイト

2019年3月22日金曜日

rb_raise で指定可能なエラーの一覧

概要

rb_raise は ruby の C API で C 側から Ruby のエラーを発生させることができます
第一引数でエラーの種類を指定することができるのですが情報が見つからないので自分で出してみました
デフォルトで用意されているエラーの一覧になります

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

一覧

  • rb_eArgError -> ArgError
  • rb_eEOFError -> EOFError
  • rb_eFloatDomainError -> FloatDomainError
  • rb_eIOError -> IOError
  • rb_eIndexError -> IndexError
  • rb_eLoadError -> LoadError
  • rb_eNameError -> NameError
  • rb_eNoMemError -> NoMemError
  • rb_eNotImpError -> NotImpError
  • rb_eRangeError -> RangeError
  • rb_eRuntimeError -> RuntimeError
  • rb_eScriptError -> ScriptError
  • rb_eSecurityError -> SecurityError
  • rb_eStandardError -> StandardError
  • rb_eSyntaxError -> SyntaxError
  • rb_eSystemCallError -> SystemCallError
  • rb_eTypeError -> TypeError
  • rb_eZeroDivError -> ZeroDivError

どうやって出したの

RDoc に管理してる定数があったのでそこから引っ張ってきています
RDoc で管理されているおそらく正解だと思います、、

require 'rdoc'

RDoc::KNOWN_CLASSES.each { |k, v| puts "* #{k} -> #{v}" if k.include?('Error') }

2019年3月21日木曜日

Sidekiq でワーカーにパスワードを渡す場合は暗号化しよう

概要

Sidekiq でジョブをエンキューするときは基本的に平文です
Enterprise Sidekiq であれば Encryption という機能があるのですがフリー版にはありません
そこで Redis にエンキューする際に暗号化する方法を考えます
エンキューする側は Sinatra を想定しています

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

Base64 にする

かなり単純ですが Base64 にするだけでも一応暗号化できます
エンキューする側で Base64 にエンコードし受け取るワーカー側でデコードします

  • vim app.rb
require 'sinatra/base'
require 'base64'
require 'openssl'
require './worker.rb'

class App < Sinatra::Base
  get '/start' do
    pass = Base64.encode64('password123')
    Worker.perform_async(pass)
  end
end
  • vim worker.rb
require 'sidekiq'
require 'base64'

class Worker
  include Sidekiq::Worker

  def perform(pass)
    puts pass
    puts Base64.decode64(pass)
  end
end

結果的には以下のようにワーカー側で取得できます

cGFzc3dvcmQxMjM=
password123

AES-256-CBC を使う

共通鍵暗号方式です
鍵になる文字列 (key) を使って AES-256-CBC で暗号化したバイト列を作成します
Sidekiq の場合バイト列はワーカーに渡せないので、バイト列を Base64 で文字列にします

  • vim app.rb
require 'sinatra/base'
require 'base64'
require 'openssl'
require './worker.rb'

class App < Sinatra::Base
  get '/start' do
    key = "test_key"
    pass = 'password123'
    encrypted = encrypt(pass, key)
    b64pass = Base64.encode64(encrypted)
    Worker.perform_async(b64pass)
  end

  def encrypt(pass, key)
    enc = OpenSSL::Cipher.new("aes-256-cbc")
    enc.encrypt
    enc.pkcs5_keyivgen(key)
    enc.update(pass) + enc.final
  end
end
  • vim worker.rb
require 'sidekiq'
require 'base64'

class Worker
  include Sidekiq::Worker

  def perform(b64pass)
    key = "test_key"
    puts b64pass
    pass = Base64.decode64(b64pass)
    decrypted = decrypt(pass, key)
    puts decrypted
  end

  def decrypt(pass, key)
    dec = OpenSSL::Cipher.new("aes-256-cbc")
    dec.decrypt
    dec.pkcs5_keyivgen(key)
    dec.update(pass) + dec.final
  end
end

ワーカー側で受け取れる情報は以下の通りです

ZpZtwrsrz6u5XXVHrQiBoA==
password123

こっちのほうがよりセキュアにはなると思います

その他

今回の場合は元の文字列を復号できることが条件になります
なので不可逆なアルゴリズムは使えません
また今回は共通鍵暗号方式になります
Sidekiq のようにワーカーとエンキューする側がシステム内で閉じている場合などは共通鍵暗号方式でいいかなと思います
逆に不特定多数の第三者に対してエンキューする場合などは公開鍵暗号方式のほうがおすすめです

最後に

Sidekiq で Redis に格納する際に暗号化する方法を検討しました
とりあえずパスワードなどが平文で Redis に格納されないだけでも良いかなと思います

今回はエンキューする側が Sinatra のような Web アプリを想定して行いました
もしかするとリクエストにパスワードなどが乗ってくる場合があると思います
そのような場合はすでに暗号化されている可能性があるのでエンキューする側は特に暗号化はせずエンキューしてワーカー側で復号するだけという手段も取れるかなと思います

その辺はシステムの仕様なので好きなように決めればいいかなと思います

参考サイト

2019年3月20日水曜日

SSL verification error at depth 1: unable to get local issuer certificate (20)

概要

rubygems にアクセスする作業のときによく発生する証明書のエラーです
自分は bundle exec rake release 時に発生しました
対処方法を紹介します

環境

  • CentOS 7.5.1840
  • Ruby 2.5.0p0

背景

CentOS で RHSCL な Ruby を使ってる場合に出ました

解決方法

エラー内容は rubygems にアクセスする際の証明書がおかしい場合に発生します
単純に証明書が古い場合や参照している証明書のパスがおかしい場合に発生するようです
まず自分の環境の ruby がどのパスの証明書を参照しているか確認しましょう

  • bundle exec ruby -ropenssl -e "p OpenSSL::X509::DEFAULT_CERT_FILE"

これで証明書のパスが確認できます
自分は /etc/pki/tls/cert.pem でした
このファイルはシンボリックリンクで実体は /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem にありました

方法1: 証明書を更新してみる

まずは証明書を更新してみましょう
念の為バックアップを取得してから実施します

  • cp /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem{,.back}
  • curl "https://curl.haxx.se/ca/cacert.pem" -o /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem

こんな感じです
これで再度実行してエラーが解消されるか確認します

方法2: 証明書のパスを指定する

それでもダメな場合は SSL_CERT_FILE 環境変数を使って証明書のパスを指定してみましょう
以下のような感じです

  • export SSL_CERT_FILE=/etc/pki/tls/cert.pem && bundle exec rake release

これで実行してエラーが解決されるか確認します
自分はこれでエラーが解決しました

その他

あとは bundler のバージョンを上げたり、rake のバージョンを上げたりすると直ったという記事を見ました
おそらくそれは update したことで証明書が正しく認識されたんだと思います

2019年3月19日火曜日

rbvmomi で QueryChangedDiskAreas をコールしてみた

概要

過去に golang で QueryChangedDiskAreas をコールしました
今回は rbvmomi からコールしてみました

環境

  • CentOS 7.5.1840
  • Ruby 2.5.0p0
  • rbvmomi 2.0.1

サンプルコード

事前に changeTrackingEnabled は有効にしておきましょう
また snapshot も作成しておきます
vCenter などの接続情報は適宜変更してください

require 'rbvmomi'

vim = RbVmomi::VIM.connect(
  host: '192.168.100.30',
  user: 'administrator@vsphere.local',
  password: 'xxxxxxxxxx',
  insecure: 'true'
)

dc = vim.serviceInstance.find_datacenter('dc') || fail('datacenter not found')
vm = dc.find_vm('vm') || fail('VM not found')

params = {
  snapshot: RbVmomi::VIM.VirtualMachineSnapshot(vim, "snapshot-100"),
  deviceKey: 2000,
  startOffset: 0,
  changeId: "*"
}
ret = vm.QueryChangedDiskAreas(params)
puts ret.startOffset
puts ret.length
ret.changedArea.each { |area|
  puts "start => #{area.start}, length => #{area.length}"
}

少し解説

params の 4 つの項目はすべて必須です
スナップショットの moref と HDD のデバイス番号、探索を開始するオフセット、そしてスナップショットと差分を取得する開始地点の ID です
すべて mob から取得できます

QueryChangedDiskAreas の戻り値は RbVmomi::VIM::DiskChangeInfo になります
アクセス可能な属性は startOffset, length, changedArea になります
さらに changedAreaRbVmomi::VIM::DiskChangeExtent になっておりこれをループすることで実際にディスクの差分箇所を確認することができます

最後に

rbvmomi で QueryChangedDiskAreas を実行してみました
今回の手順の場合は事前に CBT とスナップショットを取得しておく必要があります
QueryChangedDiskAreas に渡らすパラメータの構造が少しわかりにくいくらいであとは簡単にコールできると思います

参考サイト

2019年3月18日月曜日

NativeExtensions な gem に rdoc のコメントを入れてドキュメントを生成する方法

概要

c のソースファイル上で普通に rdoc のタグが使えます
今回はいろいろ試して実際にドキュメントを作成してみました
またプロジェクトなどは前回までのものを流用しています

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

とりあえずコメントを入れてみる

例えば NativeExtensions として定義した C の関数に以下のようにコメントを入れてみます

  • vim ext/firstext/firstext.c
/*
 * Set data to struct members.
 */
static VALUE wrap_set(VALUE self, VALUE _index) {
  int index = FIX2INT(_index);
  MyStruct *s;
  Data_Get_Struct(self, MyStruct, s);
  char *name = (char *)"hawk1";
  if (index == 0) {
    s->name1 = name;
    s->age = 10;
  } else if (index == 1) {
    s->name2 = name;
    s->age = 20;
  } else {
    s->name3 = name;
    s->age = 30;
  }
  return Qnil;
}

ドキュメントを作成する

rdoc コマンドが使えるのでそれで生成します

  • bundle exec bundle rdoc ext/ lib/

ext/lib/ 配下を対象に rdoc を作成します
これを指定しないと Gemfile や README.md やらいろいろと含まれてしまうので対象のディレクトリを指定しています
今回は YARD ではなく rdoc の Darkfish を使います

成功すると doc/ ディレクトリができているのでその配下の index.html を開きましょう

  • open doc/index.html

こんな感じで関数に設定したコメントがドキュメントにも反映されていると思います
右側に「toggle a source」もあるので選択すると C のソースファイルを確認することもできます

クラスにコメントを入れる

NativeExtensions でクラスを追加 (rb_define_class) した場合に、そのクラスにコメントを入れる方法です

  • vim ext/firstext/firstext.c
/*
 * My first native extensions.
 */
void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 1);
  rb_define_method(fe, "show", wrap_show, 1);
  Init_secondext();
}

こんな感じで Init_ から始まる関数の前にコメントを入れれば OK です
rdoc の parser が rb_define_class で定義しているクラス名から Init_ が付くメソッドを自動で探してコメントを解析しているようです

ちなみに Init_ がない場合には Document-class: name という命令を使って好きな箇所にコメントを入れることでクラスの説明を追加することもできます

引数の説明や返り値の説明を入れる

NativeExtensions で作成した関数の場合、引数などの表示が p1 みたいな感じでポインタを表す文字だけになってしまうようです
これだとよくわからないので関数の引数や返り値に対して説明を追記してあげましょう
例えば以下のような感じです

/*
 * Debug struct members.
 *
 * call-seq:
 *   show(index) -> nil
 *
 * ===== Parameters
 * - index - Member's index.
 *
 * ===== Return
 * - nil
 */
static VALUE wrap_show(VALUE self, VALUE _index) {
  // ...
}

結論から言うと rdoc には引数や返り値を説明するための命令はありません
代わりに markup が用意されておりそれらを駆使してそれっぽい説明に見せる感じです
例えば上記の場合だと ===== は見出しようの markup です
- はリストを表示することができる markup でこれで各引数の説明を記載しています
これらはあくまでも自分のやり方になるので、こうじゃなくても OK です
再度説明しますが YARD のような @param@return のような命令が rdoc にはないので markup を駆使する感じになります

ただ rdoc にも call-seq: という命令がありこれで「関数をどう呼び出すか」を定義することができます
なのでデフォルトで引数が p1 となっていた部分を適切な引数名に変えることができるようになります
上記で生成したドキュメントは以下のようになります

Constants と VERSION を設定する

まず Constants ですがこれは定数がそもそもないと表示されません
rb_define_const で定義した定数に対してコメントを入れれば rdoc でも表示されます

/*
 * My first native extensions.
 */
void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 1);
  rb_define_method(fe, "show", wrap_show, 1);
  /*
   * Set max score with 100.
   */
  rb_define_const(fe, "SCORE", INT2FIX(100));
  Init_secondext();
}

こんな感じです
次に VERSION です
VERSION は lib/firstext/version.rb ですでに定義されている定数です
versions.rb でコメントしても OK ですが今回は C 側でコメントする方法を紹介します
先程の SCORE 同様 VERSION 定数を rb_define_const で定義してあげれば OK です

/*
 * My first native extensions.
 */
void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 1);
  rb_define_method(fe, "show", wrap_show, 1);
  /*
   * Set max score with 100.
   */
  rb_define_const(fe, "SCORE", INT2FIX(100));
  /* 
   * 1.0.0: The version of this package
   */
  rb_define_const(fe, "VERSION", rb_str_new2("1.0.0"));
  Init_secondext();
}

ちなみに値は version.rb が優先されます
これで以下のように表示されます

Pages と Table of Contents

よく Pages と Table of Contents が表示されている rdoc があると思います
これらは .rdoc ファイルや .md ファイルが直接表示されています
例えば rdoc 実行時に以下のように README.md を指定しましょう

  • bundle exec rdoc ext/ lib/ README.md

すると左ペインに Pages が追加されて README.md が項目にあると思います
それを選択すると Markdown がパースされて Table of Contents が自動的に生成されるというようになっています

最後に

NativeExtensions として開発した gem にコメントを入れてみました
今回は rdoc 形式なのでご注意ください
最近だと YARD 形式しか見ない気もします、、

rdoc を使う場合には参考にしてみてください

参考サイト

2019年3月17日日曜日

複数の .c ファイルを作成して NativeExtensions を開発する方法

概要

NativeExtensions でクラスやモジュールを複数定義した場合があると思います
構造体も異なる場合は別の .c ファイルを作成したほうが良い場合があると思います
今回はその方法を紹介します

環境

  • macOS 10.14.3
  • Ruby 2.5.1p57

.c ファイルを追加する

まずは別のクラスを管理する C のコードを追加しましょう

  • touch ext/firstext/secondext.c
  • vim ext/firstext/secondext.c
#include <ruby.h>

void Init_secondext(void) {
  VALUE se = rb_define_class("SecondExt", rb_cObject);
}

とりあえずクラスだけ定義しています
中身は空です

エントリーポイントとなる .c ファイルから参照する

NativeExtensions は Makefile 内で TARGET_ENTRY を指定しているのですが 1 つしか指定しません
なので、そのエントリーポイントから追加した secondext.c を参照します

  • vim ext/firstext/firstext.c
#... ↑ 省略

void Init_firstext(void) {
  VALUE fe = rb_define_class("FirstExt", rb_cObject);
  rb_define_alloc_func(fe, my_struct_alloc);
  rb_define_method(fe, "set", wrap_set, 0);
  rb_define_method(fe, "show", wrap_show, 0);
  Init_secondext();
}

最後の Init_secondext(); を 1 行追加しただけです
今回はヘッダファイル (.h) は作成しておらずソースファイル (.c) だけです
同一階層にソースファイルを追加した場合は include しなくても別ソースファイルの関数を参照できました

コンパイル&テスト

  • bundle exec rake clean
  • bundle exec rake compile

でいつも通り lib/firstext.bundle が作成されます
ちゃんと SecondExt が使えるかテストしましょう

RSpec.describe FirstExt do
  it "has a version number" do
    expect(FirstExt::VERSION).not_to be nil
  end

  it "create a instance" do
    cli = FirstExt.new
    cli.set
    cli.show
    cli = SecondExt.new
  end
end
  • bundle exec rake spec

でちゃんと SecondExt クラスが new できるようになっていると思います
クラスが見つからないというエラーが出る場合は再度コンパイルし直してみてください

最後に

NativeExtensions の開発時に複数のソースファイルに分けて開発する際のポイントを紹介しました
基本的にはクラスやモジュール単位でソースファイルも分かると良いかなと思います

今回はヘッダファイルを作成していません
お作法的にはちゃんとヘッダファイルを書くべきです
他に気になるワードとしては depend という機能がありこれに依存関係を書いておくと依存関係のあるファイルを自動でコンパイルしてくれます
rake-compiler を使っている場合はもしかしたら不要っぽかったので今回は使っていません

参考サイト