2020年9月30日水曜日

Gitlab のバックアップデータを自動で S3 に保存する方法

概要

過去に gitlab のデータをバックアップする方法を紹介しました
今回はバックアップしたデータを自動で S3 にアップロードする方法を紹介します
また gitlab のバージョンも 13 系になったのでコマンドが変わったのでそのあたりも紹介します

環境

  • GitLab Enterprise Edition 13.3.5-ee

フルバックアップコマンド

  • gitlab-backup create

これですべてのデータベースやリポジトリのバックアップが取得できます
ビルドの成果物や git lfs のファイルも含まれるのでファイルがかなり大きくなるケースがあるので注意しましょう
デフォルトだとバックアップファイルは /var/opt/gitlab/backups/ に保存されます

ただこのコマンドにはセンシティブの設定ファイルやシークレットファイルが含まれていません

gitlab.rb と gitlab-secrets.json をバックアップする

実際に別サーバに移行するとなった場合にはこれらのファイルも必要です
先程の gitlab-backup create コマンドにはこれらは含まれません
なので別途バックアップする必要があります
gitlab 12.3 以上であれば専用のコマンドがありこれで tar ファイルを作成してくれるのでこれを使いましょう

  • gitlab-ctl backup-etc

これで /etc/gitlab/config_backup/ に tar ファイルが作成されます

本題: S3 に自動アップロードする方法

本題の自動アップロード方法です
gitlab.rb に S3 のバケット名と認証情報を設定するだけです
なので事前にバケットの作成と認証情報の確認をしておきましょう

  • vim /etc/gitlab/gitlab.rb
gitlab_rails['backup_upload_connection'] = {
  'provider' => 'AWS',
  'region' => 'us-east-2',
  'aws_access_key_id' => 'AKIxxxxxxxxxxxxxxxxxxx',
  'aws_secret_access_key' => 'xxxxxxxxxxxxxxxxxxxxx',
  # # If IAM profile use is enabled, remove aws_access_key_id and aws_secret_access_key
  'use_iam_profile' => false
}
gitlab_rails['backup_upload_remote_directory'] = 'your-bucket-name'

設定できたら reconfigure を実行します

  • gitlab-ctl reconfigure

再度バックアップを実行して動作確認

では再度バックアップを実行してどうなるか確認してみます
まずフルバックアップしてみましょう

  • gitlab-backup create

Uploading backup archive to remote storage ... というログが表示されるようになると思います

バケット側を確認するとちゃんと tar ファイルがアップロードされていることも確認できると思います

一応ローカル側にも tar ファイルは残っているようです
/var/opt/gitlab/backups/1601361216_2020_09_29_13.3.5-ee_gitlab_backup.tar
不要な場合は削除しましょう

backup_keep_time

どうやら gitlab.rb の backup_keep_time が経過したらローカル側は自動で削除してくれるようです
試しに 1 秒にしてみたらすぐにローカルの tar ファイルは削除されました

gitlab_rails['backup_keep_time'] = 1

Excon::Error::Socket: Broken pipe (Errno::EPIPE)

自分は S3 のバケットがリージョン名が違っていたために発生しました
基本的にこのエラーが出た場合は gitlab.rb を見直して認証情報やリージョン情報など S3 の設定が間違っていないか確認しましょう

設定ファイルはどうなるか

次に設定ファイルのバックコマンドを実行して S3 にアップロードされるか確認してみました

  • gitlab-ctl backup-etc

こちらは S3 にアップロードされることはなくローカルに保存されるだけでした
おそらくセキュリティ的な面と /etc/gitlab 配下のサイズが大きくならないので、わざわざ S3 にアップロードする必要がないという判断なのかなと思います

最後に

gitlab のバックアップファイルを S3 に自動にアップロードする方法を紹介しました
gitlab.rb に S3 の情報を記載するだけで OK なので簡単にできると思います
また注意する点としては設定ファイルはフルバックアップに含まれないので別途手動でバックアップする必要があるということがわかりました

参考サイト

2020年9月29日火曜日

Sinatra の map を使ってルーティングを定義する方法

概要

Sinatra でルーティングを定義する際にはいろいろな方法があります
過去に Rack::URLMap を使って定義する方法を紹介しました
今回は map を使って定義する方法を紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • sinatra 2.0.7

app.rb

require 'sinatra/base'

class MyApp < Sinatra::Base
  get '/test' do
    'test'
  end
end

class MyApp2 < Sinatra::Base
  get '/test2' do
    'test2'
  end
end

config.ru

require './app'

map('/') do
  use MyApp
  run MyApp2
end

Rack の run は 1 度しか呼べないので別のアプリケーションを読み込む場合は use を使います

最後に

app.rb で定義するアプリケーションのサブルーティングの定義をもう少しキレイに管理する方法はないだろうか

2020年9月28日月曜日

Sinatra の動的ルーティングを試してみた

概要

Sinatra には動的にルーティングを定義する機能があります
今回はそれを使ってデータからルーティングを定義する方法を紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • sinatra 2.0.7

基本

動的に Sinatara アプリケーションを作成する場合は Sinatra.new を使います
あとは作成した Sinatra インスタンスに対して run を実行します

require 'sinatra/base'
my_app = Sinatra.new { get('/') { "hi" } }
my_app.run!

データファイルを元にアプリケーションを作成

例えば json ファイルを作成してそれを元にアプリケーションを作成したりできます

  • vim data.json
[
  {
    "path": "/msg",
    "response": "hello"
  }
]
  • vim app.rb
require 'sinatra/base'

refs = JSON.load(File.read('data.json'))
my_app = Sinatra.new do 
  refs.each do |ref|
    get(ref['path']) do
      ref['response']
    end
  end
end
my_app.run!

これで /msg/user にアクセスすることができます
もし別のパスを追加したい場合は Ruby ファイルではなく json ファイルを更新するだけで追加できます

データベースを元にアプリケーションを作成することも可能

同じようにファイルではなく data.json のような値をデータベースに持ってそれをもとにルーティングを定義することもできます

config.ru で定義する場合は

rackup で起動することも可能です
controller は別ファイルで定義してもいいかもしれません

  • vim config.ru
require 'sinatra/base'

refs = JSON.load(File.read('./data.json'))

controller = Sinatra.new do
end

map('/') do
  app = Sinatra.new(controller) do
    refs.each do |ref|
      get(ref['path']) { ref['response'] }
    end
  end
  run app
end

Siantra::Reloader と組み合わせたがったがうまくいかなかった

require 'sinatra/base'
require 'sinatra/reloader'

refs = JSON.load(File.read('./data.json'))

my_app = Sinatra.new do 
  register Sinatra::Reloader
  also_reload 'data.json'

  refs.each do |ref|
    get(ref['path']) do
      ref['response']
    end
  end

end

my_app.run!

最後に

Sinatra.new を使うと動的にルーティングを定義できるのでデータなどからアプリケーションを作成することができるようになります
今回はレスポンスが固定であることを想定しているのでレスポンスも json に含めていますがレスポンスは基本的に動的に変化するのでそこはロジックを組むかそこもデータとして管理したいのであれば eval などを使えば実現できるかなと思います

参考サイト

2020年9月25日金曜日

Sinatra で attachment を使ってブラウザにファイルのダウンロードをさせる方法

概要

Sinatra でクライアント側にコンテンツをファイルとしてをダウンロードさせるには attachment が便利です
使い方と attachment の仕組みを紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • sinatra 2.0.7

準備

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install

テキストファイルをダウンロードさせる

attachment を使います
content_type を使ってちゃんとファイルの形式を指定してあげるとクライアントにもやさしいので指定しましょう
以下の場合だと「ok」と記載された data.txt がダウンロードできます

require 'sinatra/base'

class App < Sinatra::Base
  get '/download' do
    content_type "text/plain"
    attachment "data.txt"
    "ok"
  end
end

Content-Disposition ヘッダにファイル名が記載されている

実は attachment は Content-Disposition を操作しているだけのようです (参考)
クライアント側、例えばブラウザや curl, wget などに対して「このパスへのアクセスはファイルに保存してね」というのを教えて上げているだけになります

  • Content-Disposition: attachment; filename="data.txt"

なのでクライアント側はちゃんとヘッダを参照していない場合は単純に「ok」を表示するだけになります

ブラウザで動作確認

Chrome などのブラウザは当然ヘッダを見るようになっているのでファイルとしてダウンロードできます
localhost:9292/download にアクセスするとちゃんと data.txt がダウンロードできるのが確認できると思います

curl で動作確認

curl でも Content-Disposition を見てファイルに保存することができます
-J でヘッダを参照し -O でファイルに保存することができます
もしヘッダに記載のファイル名で保存したい場合は -J-O を一緒に指定しましょう

  • curl -J -O localhost:9292/download

HEAD だけ確認したい

事前にどんなファイル名になるのか確認したい場合があると思います
そんな場合は HEAD メソッドを使って一度ヘッダの情報のみ参照しましょう
curl の場合は -I オプションを使います (-XHEAD ではないのでご注意 (参考))

  • curl -I localhost:9292/download
HTTP/1.1 200 OK
Content-Type: text/plain;charset=utf-8
Content-Disposition: attachment; filename="data.txt"
Content-Length: 2
X-Content-Type-Options: nosniff
Connection: keep-alive
Server: thin

2020年9月24日木曜日

pytest の monkeypatch を threading を使って遅延パッチする方法

概要

例えばステータスを監視するようなコードがある場合にステータスが非同期で変わります
ただそのステータスのレスポンス情報を monkeypatch してしまうと一定のレスポンス情報しか返らなくなりステータスを監視するループから抜け出せなくなります
そんな場合に threading を使って monkeypatch を遅延して適用する方法を使えば monkeypatch を使ってもうまくループを抜け出せます

環境

  • macOS 10.15.6
  • Python 3.8.5

ステータスを監視するコード

例えば以下のようなステータスを監視するコードを考えます
これは while ループで 5 秒ごとにステータス情報をチェックし stopped 以外に慣ればループを抜けます

  • vim app.py
import time

def get_status():
    return "stopped"

def check_status():
    while get_status() == "stopped":
        print("status is stopped")
        time.sleep(5)
    return "status is running"

この状態だと get_status は常に stopped を返すためループを抜けれません

monkeypatch を使って別のステータスを返却するようにする

上記のコードを monkeypatch を使ってテストしループを抜け出せるようにしてみます

  • vim test_app.py
from app import check_status

def test_check_status(monkeypatch):
    monkeypatch.setattr('app.get_status', lambda: "running")
    result = check_status()
    thread1.join()
    assert (result == "status is running")
  • pipenv run pytest ./test_app.py -s

こんな感じで monkeypatch すれば簡単にループを抜けるテストが書けます
しかしここで問題なのはループ内の処理を一度も実行しないためカバレッジが上がりません
なので一回以上ループ内の処理をさせるように monkeypatch を遅延して適用したくなります

monkeypatch を遅延させるために threading を使う

monkeypatch を当てるための簡単なスレッドを作成すれば解決できました
テストのコードを以下のように書き換えます

import threading
import time
from app import check_status

def patch(monkeypatch):
    time.sleep(15)
    monkeypatch.setattr('app.get_status', lambda: "running")


def test_check_status(monkeypatch):
    thread1 = threading.Thread(target=patch, args=(monkeypatch,))
    thread1.start()
    result = check_status()
    thread1.join()
    assert (result == "status is running")

15 秒後に monkeypatch が当たるように変更してみます
threading は必ず start -> join してメインのスレッドに戻ってくるようにしましょう
これで実行すると「status is stopping」が 3 回表示された後にちゃんとループを抜けてテストが正常終了するのが確認できると思います

最後に

monkeypatch + threading で遅延パッチする方法を紹介しました
非同期な処理をパッチする場合には使えるテクニックかなと思います

2020年9月23日水曜日

pytest で celery のタスクを monkeypatch する方法

概要

pytest の monkeypatch を使って celery の各タスクにパッチを当てる方法を紹介します
これを行うことでタスクのプロセスが実行していない状態でもテストすることができます

環境

  • macOS 10.15.6
  • Python 3.8.5
    • celery 4.4.7

タスク

import time
from celery import Celery

app = Celery('sub_tasks', backend='redis://localhost', broker='redis://localhost')

@app.task
def add(x, y):
    return x + y

メイン

from sub_tasks import add

def main():
    ret = add.delay(100, 1).get()
    return ret

テストコード

import pytest
from celery import Celery

from main import main
from sub_tasks import add, app

class DummyAsyncResult():
    def get(self):
        return 100

def test_main(monkeypatch):
    monkeypatch.setattr('sub_tasks.add.delay', lambda x,y: DummyAsyncResult())
    result = main()
    assert (result == 100)

ポイント

  • delay を monkeypatch すること
  • delay の返り値をダミーの AsyncResult にすること

参考サイト

2020年9月22日火曜日

QIDI Tech X-Smart でプリントを一時中断して後から再開する方法 (レジューム)

環境

  • QIDI Tech X-Smart

一時中断する

プリント中の場合以下のような画面になります
中央の四角ボタンを押しましょう

ブレークポイントを保存する

"Save the progress so that you can print from the break point next ti me?"

と表示されるので「Yes」を選択します

再開する gcode を選択

トップメニューの「Print」からプリントする一覧を表示します
そして再開する gcode を選択しましょう

再開ボタンを押す

いつも通り印刷物のプレビューが表示されるので三角ボタンを押して再開します

ブレークポイントから再開する

"Whether from the breakpoint to print?"

と表示されたら「Yes」を選択しましょう
これで途中から再開できます
ここで「No」を押すと最初から始まってしまうのでもし最初から印刷する場合は一時中断した印刷物をちゃんと外してから行いましょう

注意1: 電源断してしまうとブレークポイントの情報が失われてしまう

試したのですが一度プリンタの電源を落としてしまうとブレークポイントが失われて上記の確認画面が表示されず初めからの印刷しかできませんでした
なのでレジュームする場合は電源を入れっぱなしにしておきましょう

注意2: 再開時にノズルや印刷物にフィラメントの残りがないようにする

残りがあるとブレークポイントの部分が歪んだりします
必ずブレークポイントの部分とノズルにフィラメントの残りかすがないようにしてから再開しましょう

注意3: 操作を間違えないようにする

Yes/No の選択を間違えるとブレークポイントが設定されずにレビュームできなくなるので注意しましょう
文章はすべて英語で表示されるので冷静に操作しましょう

2020年9月21日月曜日

celery.chain を monkeypatch する方法

概要

celery で chain を使っている場合に chain 自体を monkeypatch する方法を紹介します

環境

  • macOS 10.15.6
  • Python 3.8.5
    • celery 4.4.7

タスクを定義するコード

  • vim sub_tasks.py
import time
from celery import Celery

app = Celery('sub_tasks', backend='redis://localhost', broker='redis://localhost')

@app.task
def add(x, y):
    return x + y

@app.task
def multi(x, y):
    return x * y

@app.task
def total(ary):
    return sum(ary)

chain するメインのコード

  • vim chain_test.py
import celery
from sub_tasks import add, multi

def chain_test():
    tasks = celery.chain(
        add.s(1, 2),
        add.s(4),
        add.s(5),
        add.s(6),
        add.s(7),
        multi.s(10)
    )
    result = tasks.apply_async()
    return result.as_list()

テストコード

  • vim test_chain_test.py
import pytest
import sub_tasks

from celery import Celery
from chain_test import chain_test

app = Celery('sub_tasks', backend='redis://localhost', broker='redis://localhost', task_always_eager=True)

class DummyResult():
    def as_list(self):
        return ["hoge"]

class DummyChain():
    def apply_async(self, args=None, kwargs=None, task_id=None, **options):
        return DummyResult()

def test_chain_test(monkeypatch):
    monkeypatch.setattr('celery.chain', lambda self, *tasks, **options: DummyChain())
    monkeypatch.setattr(sub_tasks, 'app', app)
    result = chain_test()
    assert (result == ["hoge"])

ポイント

  • sub_tasks.py で定義している Celery オブジェクトを必ず上書きすること
  • 上書きする際は task_always_eager=True にすること
  • chain を呼び出しているメインの処理は import celery して celery.chain として chain を呼び出すこと
  • chain を monkeypatch して DummyChain のクラスを返却するようにすること
  • DummyChain クラスは apply_async メソッドを持ち DummyResult を返却すること

参考サイト

2020年9月20日日曜日

pytest でコンストラクタを mokeypatch したい

概要

pytest の monkeypatch でコンストラクタをパッチしたい場合があると思います
そんな場合の対処方法を紹介します

環境

  • macOS 10.15.6
  • Python 3.8.5

答え: クラスを差し替える

メイン

class User():
    def __init__(self, name, age):
        self.name = name
        self.age = age

def hello():
    u = User("hawksnowlog", 10)
    return u.name

テストコード

from user import hello

class DummyUser():
    def __init__(self, name, age):
        self.name = "dummy"
        self.age = 9999

def test_hello(monkeypatch):
    monkeypatch.setattr("user.User", DummyUser)
    assert(hello() == "dummy")

2020年9月19日土曜日

pytest でデコレータに対して monkeypatch を適用する方法

概要

Python でデコレータを使う方法を過去に紹介しました
デコレートされている関数を pytest でテストする場合にデコレータは必ず実行されてしまいます
デコレータを monkeypatch したいケースがあったのでコツや回避方法を紹介します

環境

  • macOS 10.15.6
  • Python 3.8.5

テストするコード

以下のようなデコレータを持つコードをテストすることを想定しています

def first_deco(msg):
    def _first_deco(func):
        def wrapper(*args, **kwargs):
            message = generate_message(msg)
            return func(*args, **kwargs) + msg
        return wrapper
    return _first_deco

@first_deco(msg="World")
def hello():
    return "Hello"

デコレータ自体のパッチはできないっぽい

いきなり結論なのですが自分が試した感じだと直接デコレータ自体を monkeypatch することはできませんでした
例えば以下のように直接デコレータに対して monkeypatch をしてもパッチした処理はコールされませんでした

import pytest

import deco
from deco import hello

def test_first_deco(monkeypatch):
    monkeypatch.setattr(deco, 'first_deco', lambda msg: "Python")
    result = hello()
    assert (result == "HelloPython")

また以下のようにパッチする関数をテスト側で定義してもダメでした
ただ個別で monkeypatch した関数をコールするとちゃんと monkeypatch は当たっている感じでしたが、なぜかデコレートした元の関数をコールするとパッチされた関数は呼ばれません

deco.first_deco(msg="Python")(hello)()

import pytest

import deco
from deco import hello

def dummy_deco(msg):
    def _first_deco(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs) + "Python"
        return wrapper
    return _first_deco

def test_first_deco(monkeypatch):
    monkeypatch.setattr(deco, 'first_deco', dummy_deco)
    result = hello()
    assert (result == "HelloPython")

というのを踏まえて対処方法を紹介します

デコレータの内部処理をパッチする

これが一番現実的かなと思います
デコレータの内部で読んでいる処理を外部の関数にしてその関数をパッチする感じです
調べてもこの方法が王道っぽいです

以下では generate_message をデコレータの外に定義してその関数を monkeypatch することで対処しています

def generate_message(msg):
    return msg

def first_deco(msg):
    def _first_deco(func):
        def wrapper(*args, **kwargs):
            message = generate_message(msg)
            return func(*args, **kwargs) + message
        return wrapper
    return _first_deco

@first_deco(msg="World")
def hello():
    return "Hello"

テスト側では generate_message をパッチします

import pytest

from deco import hello, generate_message

def test_first_deco(monkeypatch):
    monkeypatch.setattr(deco, 'generate_message', lambda msg: "Python")
    result = hello()
    assert (result == "HelloPython")

undecorated を使う

そもそもデコレータが邪魔で処理自体が不要という場合は undecorated という便利なライブラリがあるのでこれを使うのも有効です
デコレータの処理は完全にスルーすることになるので注意が必要です

  • pipenv install undecorated
import pytest

from undecorated import undecorated
from deco import hello

def test_first_deco(monkeypatch):
    org_hello = undecorated(hello)
    result = org_hello()
    assert (result == "Hello")

最後に

いろいろ調べましたが直接デコレータを monkeypatch することはできないっぽいです
対処として

  • デコレータの内部処理を外部の関数として定義してそれをパッチする
  • undecorated を使ってデコレータの処理自体をスルーさせる

の 2 パターンかなと思います

2020年9月18日金曜日

pytest で組み込みライブラリが monkeypatch できない場合にチェックするポイント

概要

例えば socket モジュール内のメソッドを monkeypatch したい場合に「なぜか monkeypatch できない」という現象が発生したとします
そんな場合にチェックするべきポイントを紹介します

環境

  • macOS 10.15.6
  • Python 3.8.5

準備

  • pipenv install pytest

monkeypatch できないコード

まずは monkeypatch できないコードを紹介します
一見できそうなのですが以下で紹介しているテストコードでテストしようとすると普通に DNS サーバに問い合わせてしまいます

  • vim app.py
from socket import gethostbyname

def resolve(hostname):
    return gethostbyname(hostname)

テストコード

こちらがテストコードです
monkeypatch で socket の gethostbyname をパッチしています
リターンは適当な IP アドレスにしています

import socket
from app import resolve

def test_resolve(monkeypatch):
    monkeypatch.setattr(socket, 'gethostbyname', lambda hostname: "192.168.100.1")
    result = resolve('test.local')
    print(result)
    assert (result == "192.168.100.1")

テストの実行は以下のようにします

  • pipenv run pytest

すると socket.gaierror: [Errno 8] nodename nor servname provided, or not known というエラーが発生します
これは monkeypatch できずに実際に test.local を DNS に問い合わせているために名前解決できずエラーになっています

なぜ monkeypatch できないのか

どうやらロジック側で import している方法とテスト側で import しているやり方を統一しないとダメなようです
今回だと from socket import gethostbyname ではダメだということです
以下のようにロジック側を修正します

  • vim app.py
import socket

def resolve(hostname):
    return socket.gethostbyname(hostname)

これで再度テストを実行すると正常に終了するのが確認できると思います

最後に

monkeypatch で組み込みモジュールをテストする場合はロジック側とテスト側の import 方法を合わせる必要があるということがわかりました
なかなか気づきにくい仕様かなと思います
しかし知らないとハマるので知っておいて損はないと思います

2020年9月17日木曜日

Ruby waterfall を使ってフロー処理を実装してみる

概要

apneadiving/waterfall は Ruby でフロー処理を実現するためのライブラリです
今回は簡単なサンプルを作成して挙動を確認してみました

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • waterfall 1.3.0

簡単なサンプルコード

まずは成功する簡単なサンプルコードを作成しました
waterfall は chain を使って処理を数珠つなぎにすることでフロー処理を実現します
またメインの処理は Waterfall モジュールを include しクラスにすることで管理しやすくしています
また call メソッドを実装することでオブジェクトが生成された際に自動的に処理を開始することができます

結果を受け取る場合は chain にシンボルで引数を与えその名前のシンボルに結果が格納されるようにします

フローを開始する場合は Flow.new します
最後まで成功した場合は最後の chain で結果を受け取ることができます

require 'waterfall'

class MyClass
  include Waterfall

  def initialize(value)
    @value = value
  end

  def call
    self.chain(:value) do
      @value ** 2
    end
  end
end

f = Flow.new
ret = f.chain(value1: :value) do
  MyClass.new(2)
end
.chain(value2: :value) do
  MyClass.new(3)
end
.chain do |result|
  puts result.value1
  puts result.value2
end

失敗した場合

次に途中でフローが失敗した場合のサンプルです
フローが失敗した場合には dam に流すことでフローを失敗にすることができます
前の chain の結果がどうだったか判定するのに when_falsy を使います
このブロック内の結果が false だっと場合に .dam が実行されてエラー処理に流れます

Flow.new 側では dam に流れた処理を受け取るために .on_dam を実装します
dam で返却された内容とエラーの箇所がブロックの引数として渡ってきます

require 'waterfall'

class MyClass
  include Waterfall

  def initialize(value)
    @value = value
  end

  def call
    chain do
      begin
        @value / 0
      rescue ZeroDivisionError
        @value = false
      end
    end
    when_falsy do
      @value
    end
      .dam do
        "ZeroDivisionError"
      end
    chain(:value) do
      @value ** 2
    end
  end
end

f = Flow.new
ret = f.chain(value1: :value) do
  MyClass.new(2)
end
.chain(value2: :value) do
  MyClass.new(3)
end
.chain do |result|
  puts "SUCCESS"
  puts result.value1
  puts result.value2
end
.on_dam do |error, context| 
  puts "ERROR"
  puts error
  puts context
end

halt_chain

dam に流したあとに続けて別のエラー処理をしたい場合に使います
例えば以下のようにエラーになって dam に流れた場合は @value にエラーコード的な値を設定することができます

require 'waterfall'

class MyClass
  include Waterfall

  def initialize(value)
    @value = value
  end

  def call
    chain do
      begin
        @value / 0
      rescue ZeroDivisionError
        @value = false
      end
    end
    .when_falsy do
      @value
    end
      .dam do
        "ZeroDivisionError"
      end
      .halt_chain do |outflow, error_pool, error_pool_context|
        outflow.value = 9999 if error_pool
      end
    chain(:value) do
      @value ** 2
    end
  end
end

f = Flow.new
ret = f.chain(value1: :value) do
  MyClass.new(2)
end
.chain(value2: :value) do
  MyClass.new(3)
end
.chain do |result|
  puts "SUCCESS"
  puts result.value1
  puts result.value2
end
.on_dam do |error, error_pool_context, outflow, waterfall|
  puts "ERROR"
  p waterfall.outflow.value1
end

最後に

Ruby の waterfall を使ってフロー処理を実現してみました
今回は丁寧に do ... end で書きましたが見やすくするために {} で 1 行ブロックで書いてもいいかもしれません
フロー間の引数のやり取りが少し面倒なのでそれに慣れないと使いこなすのは難しいかもしれません
Sidekiq などを使ってもフロー処理は実現できますがブローカが必要だったりするので簡単なフロー処理を実現したいのであれば waterfall でもいいかもしれません

2020年9月16日水曜日

Sinatra の Rack::Protection で有効になっているパストラバーサルの機能を無効にしてどのような影響があるのか確認してみた

概要

パストラバーサル は脆弱性の一つでリクエストに相対パスや絶対パスを入れることで意図しないファイルにアクセスされてしまう脆弱性です
Sinatra にもかつでパストラバーサルの脆弱性があり現在では対応されています
今回はあえてパストラバーサルの保護機能を無効化しどのような影響があるのか確認してみました

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • sinatra 2.0.7

Rack::Protection::PathTraversal の効果

試してみた感じ request.path_info を見て %2e のドットの URL エンコードした文字をデコードするかしないかくらいの効果しかなさそうです
以下で例を紹介します

パストラバーサルを無効にしたコード

とりあえず動作確認してみました
Sinatra でパストラバーサルを無効にする方法は set :protection, :except => :path_traversal を追記するだけです

  • vim config.ru
require './app'
run App
  • vim app.rb
require 'sinatra/base'

class App < Sinatra::Base
  set :protection, except: :path_traversal

  get '/test' do
    env['PATH_INFO']
  end
end

set の部分をコメントアウト/インして動作確認してみます

テストコード/動作確認

以下のような curl を実行するスクリプトを作成しました

curl "localhost:9292/test"
curl "localhost:9292/test/../test"
curl "localhost:9292/test/%2e%2e/test"

これを使ってパストラバーサルが有効なときと無効なときで動作確認してみました
結果は以下の通りでした

  • パストラバーサル有効・・・すべて 200 でアクセスできる
  • パストラバーサル無効・・・1, 2 番目が 200 でアクセスできる、3 番目は 404 になる

という感じでした
要するにパストラバーサルが無効になっている場合は %2e -> . に変換していないことがわかります
また . を直接指定した場合はパストラバーサルの有効/無効に関わらず畳み込みをしてくれるようです

そもそもパストラバーサルに脆弱なサンプルコード

ちょっと話は逸れますがそもそもパストラバーサルに脆弱なコードを紹介します
パストラバーサルを無効にし脆弱性のあるコードをあえて作成します
指定されたファイルを読み込んで返却するようなアプリケーションになっています

  • vim app.rb
require 'sinatra/base'

class App < Sinatra::Base
  set :protection, :except => :path_traversal

  get '/test' do
    File.read(params["path"])
  end
end
  • vim config.ru
require './app'
run App

脆弱性の確認

例えばアプリケーションのカレントディレクトリにある hoge.txt というファイルを読み込む場合には以下のようにリクエストすると思います

  • curl "localhost:9292/test?path=hoge.txt"

app.rb と同じディレクトリに hoge.txt があれば問題なく読み込みレスポンスとしてファイルの内容を返却します
ただこのコードには脆弱性がありフルパスなどを入れるとアプリケーションの上位のファイルも簡単に参照できてしまいます

  • curl "localhost:9292/test?path=/etc/hosts"
  • curl "localhost:9292/test/../open?path=../../../../../etc/hosts"

これで hosts ファイルの内容を簡単に確認できてしまいます
簡単なサンプルですがこれがパストラバーサルに脆弱なサンプルコードになります

Sinatra の Rack::Protection::PathTraversal では上記のようなコードは対処できない

もうここまでくればわかると思いますが params などのリクエストを使ってファイルなどを参照する場合は Rack::Protection::PathTraversal が使えないことがわかると思います
というか Rack::Protection::PathTraversal はほぼ何もしていない感じなのでパストラバーサル対策は自信のアプリケーション側でしっかり行う必要があります

ちなみに Sinatra の Rack::Protection::PathTraversal で対処できそうな脆弱なコード

こんな感じのコードをもし書いていたとすれば path_traversal を有効にするだけで対応はできそうです

require 'sinatra/base'

class App < Sinatra::Base
  set :protection, except: :path_traversal

  get '/*' do
    File.read(env['PATH_INFO'].gsub('%2e', '.'))
  end
end
  • curl "localhost:9292/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/hosts"

そもそもデフォルトの Webrick で動作させていると上記のリクエストは Bad Request で弾かれます
thin を使った場合は弾かれないので path_traversal を使うことが有効ですがそもそもこんなコードは書かないかなと思います

最後に

Siantra のパストラバーサル防御機能の動作確認をしてみました
デフォルトは有効なので気にする必要はないのですが Rack::Protection::PathTraversal だけではパストラバーサルの脆弱性を完全に防げているわけではないので注意が必要だと言うことがわかりました

参考サイト

2020年9月15日火曜日

Ruby から docker-compose を操作してみた

概要

docker-compose.yml を作成して CLI からではなく Ruby からコンテナの作成などを行ってみました
ただ使った gem のバージョンが古くあまり使えなさそうな印象です

環境

  • macOS 10.15.6
  • docker 19.03.12
  • Ruby 2.7.1p83
    • docker-compose-api 1.1.8

docker-compose.yml の作成

何でも OK です
今回は nginx が立ち上がるだけの YAML ファイルを作成しました

version: '3'
services:
  web:
    image: nginx
    ports:
      - 80:80

Ruby からコンテナを立ち上げてみる

では Ruby から docker-compose.yml を使ってコンテンを立ち上げてみます

  • vim app.rb
require 'docker-compose'

compose = DockerCompose.load('./docker-compose.yml')
ret = compose.start
puts ret

これで nginx のコンテナが立ち上がります

コンテナの一覧を確認する

containers を使います
ハッシュで返ってきます

require 'docker-compose'

compose = DockerCompose.load('./docker-compose.yml')
ret = compose.containers
pp ret

コンテナを停止する

stop を使うだけです
ラベルと名前でコンテナを指定することができます

require 'docker-compose'

compose = DockerCompose.load('./docker-compose.yml')
# compose.start
ret = compose.stop
puts ret

別プロセスになるとなぜかコンテナに振られるインデックスが変わるのでその場合は start した同じプロセス内で停止しましょう

Tips: バージョンの表示など

puts DockerCompose.version
puts DockerCompose.docker_client

Tip: AttributeError: ‘ComposeVersion’ object has no attribute ‘version’

なぜか Ruby で実行すると CLI 側で docker-compose を実行できなくなりました
おそらくバージョンの違いや Ruby 側だと処理が足りていないためだと思います

AttributeError: 'ComposeVersion' object has no attribute 'version'
[2692] Failed to execute script docker-compose

最後に

既存の docker-compose.yml があればそれを簡単に Ruby から呼び出すことはできそうです

ただ残念ですが現在は gem のバージョンが古くメンテナンスもされていないようです
docker-compose v3 に対応していませんでした
また docker-compose.yml なしで docker-compose 的なことをするのはこの gem ではできないようです

2020年9月14日月曜日

Flask-Marshmallow でデータベースに格納されている配列の文字列データを配列にする方法

概要

データベースに文字列で配列の情報が格納されている場合にレスポンスには文字列ではなく配列として返却したい場合があります
Flask-Marshmallow で文字列から配列にコンバートして返却する方法を紹介します

環境

  • macOS 10.15.5
  • MySQL 8.0.19
  • Python 3.8.3
    • Flask-Migrate 2.5.3
    • Flask-SQLAlchemy 2.5.3
    • Flask-Marshmallow 0.13.0

やり方

ma.Function とラムダ式を使って実現できます
引数にレコードのオブジェクトが入ってくるのでそこから指定のカラムの情報を抜き出して処理します

class UserCustomSchema(ma.Schema):
    my_id = ma.Integer(attribute="id")
    my_name = ma.Function(lambda obj: json.loads(obj.name))

例えばデータベースの name カラムのフィールドには ["hawk","snowlog"] という文字列情報が入っている場合は json.loads を使いましょう
単純に文字列で CSV が入っている場合は .split などを使いましょう

2020年9月11日金曜日

grape-swagger で Swagger ドキュメントを自動生成する方法

概要

前回は grape を使って RESTful API を構築してみました
grape には grape-swagger がありこれを使うと swagger のドキュメントを自動生成してくれます
今回は導入方法と使い方のポイントを紹介します
また説明は前回のアプリをそのまま使います

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • grape 1.4.0
    • grape-swagger 1.3.0

とりあえず使ってみる

既存の grape アプリがあれば簡単に導入できます
まずは gem を追加しましょう

  • vim Gemfile
gem "grape"
gem "grape-swagger"

そしてアプリに add_swagger_documentation を追加するだけです

  • vim app.rb
require 'grape'
require 'grape-swagger'

module Test
  class API < Grape::API
    format :json
    prefix :api
    articles = []
    @username = ""

    resource :blog do
      desc 'Return an article.'
      get :article do
        articles
      end

      desc 'Create new article.'
      params do
        requires :title, type: String, desc: 'An article title'
        requires :body, type: String, desc: 'An article body'
      end
      post do
        articles.append({
          title: params[:title],
          body: params[:body]
        })
      end

      desc 'Delete an article.'
      params do
        requires :index, type: Integer, desc: 'An article index'
      end
      delete ':index' do
        error!("Does not found index", 404) if articles[params[:index]].nil?
        articles.delete_at(params[:index])
      end

      desc 'Update an article.'
      params do
        requires :index, type: Integer, desc: 'An article index'
        requires :title, type: String, desc: 'An article title'
        requires :body, type: String, desc: 'An article body'
      end
      put ':index' do
        error!("Does not found index", 404) if articles[params[:index]].nil?
        articles[params[:index]] = {
          title: params[:title],
          body: params[:body]
        }
      end
    end

    add_swagger_documentation # New!!

  end
end

あとは普通にアプリを起動しましょう

  • bundle exec rackup config.ru

すると localhost:9292/api/swagger_doc にアクセスするだけで swagger の JSON が返っくることが確認できると思います

CORS に対応する

SwaggerUI を使って確認する場合に CORS に対応しておかないと JSON にアクセスできません
わざわざファイルに落として読み込ませるのも面倒なので CORS に対応しておきます

  • vim Gemfile
gem "rack-cors"
gem "grape"
gem "grape-swagger"
  • vim config.ru
require 'rack/cors'

use Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [ :get, :post, :put, :delete, :options ]
  end
end

require './app'

Test::API.compile!
run Test::API

これで SwaggerUI から確認してみましょう

SwaggerUI から確認してみる

grape-swagger は単純に swagger 定義の JSON を出力してくれるだけで SwaggerUI の機能はありません
なので確認する場合は別途 SwaggerUI を使いましょう

  • docker run -d -p 8080:8080 swaggerapi/swagger-ui

あとは localhost:8080 にアクセスして grape-swagger で出力される JSON の URL を入力すれば OK です

こんな感じで API の定義を確認することができます

レスポンスのサンプルを追加してみる

このままでも十分といえば十分ですがレスポンスにボディがある場合にそれを表示することができません
そんな場合は grape-entity を使います

  • vim Gemfile
gem "rack-cors"
gem "grape"
gem "grape-swagger"
gem "grape-entity"
gem "grape-swagger-entity"

まずは Entity を追加します
必ず Grape::Entity クラスを継承して作成します
expose を使うことでフィールドとして登録することができます

  • vim app.rb
require 'grape'
require 'grape-swagger'
require 'grape-entity'
require 'grape-swagger-entity'

module Test
  module Entities
    class Article < Grape::Entity
      expose :title, documentation: { type: 'string', desc: 'Blog article title.', required: true }
      expose :body, documentation: { type: 'string', desc: 'Blog article body', required: true }
    end
  end
  # 省略...
end

次に定義した Entity を登録します
これも簡単で既存の grape アプリで定義した desc の引数に entity: ハッシュとして追加するだけです

  • vim app.rb
desc 'Create new article.',
  entity: Test::Entities::Article
params do
  requires :title, type: String, desc: 'An article title'
  requires :body, type: String, desc: 'An article body'
end
post do
  articles.append({
    title: params[:title],
    body: params[:body]
  })
end

これで OK です
あとはアプリを起動して localhost:9292/api/swagger_doc を確認するとちゃんとレスポンスサンプルとして含まれていることが確認できると思います

パラメータとしても使える

既存の grape アプリの params の代替にもなります
desc の引数のハッシュに params: として追加するだけで OK です

desc 'Create new article.',
  entity: Test::Entities::Article,
  params: Test::Entities::Article.documentation
post do
  articles.append({
    title: params[:title],
    body: params[:body]
  })
end

こちらのほうが管理的にもきれいになるので良いかなと思います

全体のソースコード

一応紹介しておきます

  • app.rb
require 'grape'
require 'grape-swagger'
require 'grape-entity'
require 'grape-swagger-entity'

module Test
  module Entities
    class Article < Grape::Entity
      expose :title, documentation: { type: 'string', desc: 'Blog article title.', required: true }
      expose :body, documentation: { type: 'string', desc: 'Blog article body', required: true }
    end
  end

  class API < Grape::API
    format :json
    prefix :api
    articles = []
    @username = ""

    resource :blog do
      desc 'Return an article.'
      get :article do
        articles
      end

      desc 'Create new article.',
        entity: Test::Entities::Article,
        params: Test::Entities::Article.documentation
      post do
        articles.append({
          title: params[:title],
          body: params[:body]
        })
      end

      desc 'Delete an article.'
      params do
        requires :index, type: Integer, desc: 'An article index'
      end
      delete ':index' do
        error!("Does not found index", 404) if articles[params[:index]].nil?
        articles.delete_at(params[:index])
      end

      desc 'Update an article.'
      params do
        requires :index, type: Integer, desc: 'An article index'
        requires :title, type: String, desc: 'An article title'
        requires :body, type: String, desc: 'An article body'
      end
      put ':index' do
        error!("Does not found index", 404) if articles[params[:index]].nil?
        articles[params[:index]] = {
          title: params[:title],
          body: params[:body]
        }
      end
    end

    add_swagger_documentation

  end
end
  • config.ru
require 'rack/cors'

use Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [ :get, :post, :put, :delete, :options ]
  end
end

require './app'

Test::API.compile!
run Test::API
  • vim Gemfile
source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rack-cors"
gem "grape"
gem "grape-swagger"
gem "grape-entity"
gem "grape-swagger-entity"

最後に

grape-swagger を使って grape のアプリから swagger のドキュメントを自動生成する方法を紹介しました
手動で swagger ファイルを書くよりかは重複を避けられたり管理が楽になので良いかなと思います

定義した Entity をレスポンスのサンプルにしたり実際に API で扱うパラメータにする方法も紹介しました
今回は調査しなかったのですが作成した Entity をデータベースのモデルにしてオブジェクトを生成しフィールドにアクセスすることができれば更に便利な使い方ができるかなと思います

参考サイト

2020年9月10日木曜日

certbot の standalone プラグインで証明書を取得する

概要

DNS を使った manual プラグインでの取得方法を過去に紹介しました
今回は standalone プラグインを使った方法を紹介します
standalone プラグインは DNS ではなく HTTP/80 ポートを使って証明書の取得を行います

環境

  • Ubuntu18.04
  • certbot 1.7.0

事前準備: A レコードを登録する

取得するドメイン or サブドメインの A レコードを DNS に登録しましょう
standalone プラグインは証明書取得時に指定したドメインの 80 番ポートにアクセスするため DNS で引けることが必須条件になります

certbot-auto インストール

  • cd /usr/local/bin
  • sudo git clone https://github.com/certbot/certbot.git
  • sudo ln -s /usr/local/bin/certbot/certbot-auto /usr/local/bin/

HTTP/80 ポートを停止

Nginx なり Apache Httpd なりですでに 80 番ポートを LISTEN している場合は停止しましょう
docker の場合はコンテナを停止しておきます

  • docker-compose stop proxy

証明書取得

では証明書を取得します
今回はインタラクティブモードで取得します

  • sudo certbot-auto certonly --standalone -t

メールアドレスと取得する証明書のドメインを入力します
問題なければ証明書が取得できます
取得後に証明書が配置されるパスは /etc/letsencrypt/live/domain.name になります
fullchain.pemprivkey.pem がシンボリックされているのでこれを使います

トラブルシュート

主な原因としては以下が考えられると思います

  • Let’sEncrypt から指定のドメインの 80 番ポートにアクセスできない
    • ファイアウォールの設定を見直しましょう
  • A レコードが引けない
    • 少し待ってから再度実施しましょう
    • dig コマンドなどで A レコードが引けるか確認しましょう

HTTP/80 ポートの再起動

止めていた Nginx や httpd を再起動しましょう

  • docker-compose start proxy

もし証明書のパスが変わって docker-compose.yml などを書き換えた場合はコンテナを再作成しましょう

  • docker-compose rm proxy
  • docker-compose create proxy
  • docker-compose start proxy

Tips: 古い証明書の削除

今回は standalone プラグインを使って新規で証明書を作成しました
もし manual プラグインを使って別環境で同一ドメインの証明書を取得していた場合は manual プラグイン側の証明書はもう使わないので削除しましょう

  • sudo certbot-auto delete --cert-name domain.name

Let’sEncrypt はすでに同一ドメインで証明書を発行していても別プラグインを使えば取得できてしまいます
なので一時的に同一ドメインの複数証明書ができてしまいます
管理用のメールアドレスなども同一のものが使えてしまうので混乱しないように使わなくなったプラグイン側の証明書は削除しておきましょう

このあとは自動更新をする

基本は renew コマンドで OK です

  • sudo certbot-auto renew

証明書を新規で作成したばかりだと証明書の更新の必要がないというエラーになるので更新できません
また standalone プラグインを使った更新は結局 80 番ポートの停止が必要なので renew 前に同じように停止し更新後に再起動が必要になります

自動更新の方法はまた別記事でまとめる予定ですが --pre-hook--post-hook オプションを使って自動更新することになると思います

最後に

certbot の standalone プラグインを使って証明書の取得を行ってみました
一瞬 http が止まるので 80 番ポートで動作しているサービスの場合は前に紹介した manual プラグインや webroot プラグインを使ったほうが良いかなと思います
ただ自動更新まで考えるとやはり一番簡単なのは standalone プラグインなんじゃないかなと思います

参考サイト

2020年9月9日水曜日

Ruby の Paint を使ってコンソール表示をカラフルにする

概要

Paint は ANSI カラーコードを使った文字列を簡単に生成することができるライブラリです
コンソール表示をカラフルにしてロギングなどを見やすくすることができます
今回は簡単な使い方を紹介します

環境

  • macOS 10.15.6
  • Ruby 2.7.1p83
    • Paint 2.2.0

インストール

  • bundle init
  • vim Gemfile
gem "paint"
  • bundle install

赤色にする

まずは単純に文字を赤色にしてみます

require 'paint'

painted_str = Paint['hoge', :red]
puts painted_str

Paint.[] というクラスメソッドを使うだけで指定の文字を ANSI カラーコード付きの文字に変換することができます
返り値が String クラスのオブジェクトなので単純に puts すれば色付き文字が表示されます

背景色を設定する

引数を 1 つ増やすだけで OK です
文字色を赤、背景をシアンにする場合は以下のようにします

require 'paint'

puts Paint['hoge', :red, :cyan]

色を反転する

文字色と背景色を反転するには :inverse を指定します

require 'paint'

puts Paint['hoge', :red, :cyan, :inverse]

HTML カラーコードを使う

デフォルトの設定であれば HTML のカラーコードをを指定することもできます

require 'paint'

puts Paint['hoge', "#dc143c"]

:inverse もそのまま使えます

puts Paint['hoge', "#dc143c", :inverse]

モードを設定する

Paint にはモードがありデフォルトは 16777215 色使えます
モードを確認するには Paint.mode を参照します
また設定する場合は 256 or 16 or 8 or 0 を設定するだけです

require 'paint'

puts Paint.mode
Paint.mode = 16
puts Paint['hoge', "#dc143c"] # 強制的に赤色になる

環境によってはフルカラー使えない環境もあるので一度使える色の種類を確認すると良いと思います

おまけ: logger をカスタムして使う

例えば Ruby 標準の logger を Paint でカスタムして使うこともできます
コンソールにだけ表示する分にはこれだけで見やすくなると思います

require 'paint'
require 'logger'

class PaintedLogger < Logger
  def info(str)
    super(Paint[str, :red])
  end
end

plogger = PaintedLogger.new(STDOUT)
plogger.info("hoge")

最後に

Ruby の Paint を使って簡単に ANSI カラーコードを使う方法を紹介しました
今回は紹介していませんが :bright (太字)、Paint.random (ランダムカラー)、Paint.unpaint (カラーコードの削除) など他にも便利な機能が多くあります
コンソールに表示するロガーは Paint してファイルに出力するログはプレーンテキストにするなどすると使い勝手も良くなるかなと思います

ANSI カラーコードを直接記載するとごちゃごちゃになるので Ruby が使える環境であれば /etc/motd などでカラフルなログインメッセージを表示したりするのに使えると思います