tech::hexagram

personal note for technical issue.

任意で渡ってきた変数が特定のStructかどうか判定する方法

is_struct みたいな便利メソッドがないようなので以下のように判定する必要があった。 割と面倒だったのでまとめておく。

例として、任意で受け取った引数 argsUser のStructかどうかを判定する。

StructはMapの拡張になっている

まずStructは Mapを拡張した拡張であるので、 is_map(args) がtrueになる。

Structs are extensions on top of maps that bring default values, compile-time guarantees and polymorphism into Elixir.

ref: Structs - Elixir

iex(1)> user_struct = %User{}
%User{...}
iex(2)> string = "test"
"test"
iex(3)> is_map(user_struct)
true
iex(4)> is_map(string)
false

Structは __struct__ というkeyを保持している

In the example above, pattern matching works because underneath structs are bare maps with a fixed set of fields. As maps, structs store a “special” field named struct that holds the name of the struct:

次に、Structの場合は __struct__ というkeyを保持しているので、「MapであるがStructでない変数」の場合は Map.has_key?(args, :__struct) で弾くことが出来る。

iex(5)> map = %{hoge: "huga"}
%{hoge: "huga"}
iex(6)> map |> Map.has_key?(:__struct__)
false
iex(7)> user_struct |> Map.has_key?(:__struct__)
true

__struct__ にはStructの名前が入っている

args.__struct__User であればtrueになるので、あとはこれを比較すれば良い。

iex(8)> not_user_struct = %NotUser{}
%NotUser{...}

iex(9)> user_struct.__struct__ == User
true
iex(10)> not_user_struct.__struct__ == User
false

まとめると

def is_user?(args) do
  is_map(args) && Map.has_key?(args, :__struct__) && args.__struct__ == User
end 

ansibleでrubyのArray#zipみたいなことをやる

varsで定義した2つの同じ要素数のリストをrubyのArray#zipみたいに結合してよしなに何かする場合のサンプル。

template module で利用するJinja2というpythonのテンプレートエンジンが、yamlでも展開して利用できる。

- name: "hogehoge"
  vars:
    list1:
      - "1"
      - "2"
      - "3"
    list2:
      - "a"
      - "b"
      - "c"
    list3: |
       {% set o = [] %}
       {% for i in list1 %}
       {% set _ = o.append({ 'hoge': i, 'huga': list2[loop.index0]}) %}
       {% endfor %}
       {{ o }}
# list3 = [ {hoge: 1, huga: a}, {hoge: 2, huga: b}, {hoge: 3, huga: c} ] のようになる

参考

Template Designer Documentation — Jinja2 Documentation (2.8-dev)

ほげめも: Ansible の Jinja2 を活用する

PhoenixのテンプレートではMap |> Enum.eachでイテレートを回した中身が表示されない

ERBと同じように書こうとしたら詰まったのでメモ。

簡単な例で書いてみる。

Ruby

@grouped_users = User.all.group_by(&:group)

とcontrollerで定義しておくと、erbだと以下のようにループを回せる。

<% @grouped_users.each do |group, users| %>
  <!-- 何か処理 -->
  <% users.each do |user| %>
    <!-- 何か処理 -->
  <% end %>
<% end %>

Elixir

NG

@grouped_users =
User
|> Repo.all
|> Enum.group_by(&(&1.group))
<%= @grouped_users |> Enum.each(fn(group, users) -> %>
  <!-- 何か処理 -->
  <%= for user <- users do %>
    <!-- 何か処理 -->
  <% end %>
<%= end) %>

こう書いても、テンプレート上では ok としか出ない。 Enum.each の返り値を表示しようとしているのでこうなっているのだと思われる。

OK

@grouped_users =
User
|> Repo.all
|> Enum.group_by(&(&1.group))
|> Enum.map(fn {key, value} -> {key, value} end)
<%= for {group, users} <- @grouped_users do %>
  <!-- 何か処理 -->
  <%= for user <- users do %>
    <!-- 何か処理 -->
  <% end %>
<% end %>

一度keyword listに変換してあげて、for文で回せば問題なく表示される。

Mac OSXで立ち上げたVagrant内で、dockerのcontainerを立ち上げる時の小技

課題

$ docker run -p 8080:80 --name some_container -it some_env:latest /bin/bash

このように記述すると、containerを立ち上げた後に、開発用のユーザアカウントを用意してSSH接続する想定の場合は、sshdの起動の設定をする必要がある。

2回目以降の起動時に、このcontainerを docker start しようとすると、特定のユーザのアカウントでMac -> vagrant -> dockerとSSH接続するときに不便。

解決策

monit というツールを利用すると、docker start時にsshdを自動で立ち上がるようにすることができる。

Dockerでは、内部で動かせるサービスは1つだけなので、 docker run [options] /bin/bash と指定すると起動時にbashしか立ち上げることが出来ない。

monit は、本来様々なサービスやプロセスの死活監視に利用されているが、 docker から動かすサービスに指定すると各サービスを動かすためのコンテナとして利用することが出来る。

なお、今回使うconatinerは ubuntu/trusty64 を前提として話を進める。

事前準備

monitのインストール

/bin/bashを起動プロセスに指定したsome_container内で、以下を実行してmonitをインストールする。

[docker]
$ sudo apt-get install monit
confの設定

monitインストール後のデフォルトの状態では、監視したいサービスの設定は /etc/monit/conf.d/ 配下を読み込むようになっている。 このため、以下の内容でconfファイルを追加する。

[docker]
$ sudo emacs /etc/monit/conf.d/sshd.conf
---
check process sshd with pidfile /var/run/sshd.pid
  start program = "/etc/init.d/ssh start"
  stop  program = "/etc/init.d/ssh stop"
ここまでの内容をsome_envにcommit

一旦dockerから抜けて、以下をVagrant内で実行する。

[vagrant]
$ docker stop some_container
$ docker commit some_container some_env

containerの立ち上げ

ここまで事前準備が終わったら、一度some_containerを捨てて、起動プロセスにmonit を指定して docker run でcontainerを立ち上げ直す。 -I オプションを付けておかないとバックグラウンドで monit が立ち上がり、docker上でフォアグラウンドで起動しているタスクがなくなってしまうため起動後即終了してしまう点に注意。

[vagrant]
$ docker rm some_container
$ docker run -p 8081:80 --name some_container -it some_env:latest /usr/bin/monit -I -c /etc/monit/monitrc

containerの終了

Mac OSXの電源を落とすときに、このcontainerを落としておかないと、次回以降同じcontainerを立ち上げ直すことができなくなる。 これはvagrant上で docker stop some_container を行わずに電源が落ちると、monitのPIDが残るらしく、以下の様なエラーが出ることがあることが原因の模様。

monit daemon with PID 1 awakened

これを避けるには、Mac OSXの電源を落とすときに必ず docker stop some_container を行うようにすると良い。 ただ毎度手動でやるのは手間であるのと忘れることもあるため、以下で自動化すると楽なのでお勧め。

/Library/StartupItems/配下に自動起動するデーモンを追加する

必要なのは以下の2つのファイル。

  • /Library/StartupItems/SomeDaemon/SomeDaemon : スクリプト本体
  • /Library/StartupItems/SomeDaemon/StartupParameters.plist : スクリプトに関する説明を記述するためのplistファイル

追加方法は以下を参考にすると良さそう。

VagrantのCentOS7の仮想環境はcentos/7のほうが良さそう

centosから公式のimageと思われるboxが5ヶ月前くらいにリリースされたらしく、こちらを利用したらsftpのpathが正しい状態になっていた。

というわけでこの件は解決した!