nerves_pack（vintage_net含む）を使ってNerves起動時に`Node.connect()`するようにした

vintage_netを含むnerves_packを使ってこのようなことをしていました。今回はNerves起動時に`Node.connect()`するようにしました。これは [Nervesで起動時に`Node.connect()`するようにした](https://qiita.com/nishiuchikazuma/items/f6b0ef85fbd5257bcb19) のvintage_net版になります。個人的にはきっとこれからこっちを使うことになると思ってます。

- [nerves_pack（vintage_net含む）を使ってNervesのネットワーク設定をした〜SSHログインまで〜](https://qiita.com/nishiuchikazuma/items/01f5df5472ec0feceb5f)」
- [nerves_pack（vintage_net含む）を使ってNervesのネットワーク設定をした〜SSHのポート番号変更〜](https://qiita.com/nishiuchikazuma/items/0312d2e264128a10edb0)



# 追記
`mix local.nerves`してnerves_bootstrap 1.8.0にアップデートしたら暗黙の動作が増えたようなのでバグ対応を含め`bootexineris.ex`ファイルを修正しました。



# 環境
ハードウェアとネットワークはこのような構成です。
今回もホスト名flamboise03を操作することにします。
`nerves_pack`のインストールと設定は「[nerves_pack（vintage_net含む）を使ってNervesのネットワーク設定をした〜SSHログインまで〜](https://qiita.com/nishiuchikazuma/items/01f5df5472ec0feceb5f)」をみてください。

## ハードウェア
- Mac OSX
- NervesをRaspberryPi 3で稼働

## ネットワーク
- Mac OSX
  - 有線LAN→メンテナンス用、固定IPアドレス（192.168.5.100/24）

- RaspberryPi 3(flamboise03)
  - 有線LAN（eth0）→メンテナンス用、固定IPアドレス（192.168.5.57/24）
  - 無線LAN（wlan0）→インターネット接続用、DHCP（192.168.46/24から自動割当）
![ネットワーク構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51020/95e29daf-01e9-ac3b-3e5e-3d4448db4f19.png)


# このようにしました

IPアドレスの設定は完了しているとして、次のファイルをそれぞれ追記、作成します。

- lib/exineris002/application.ex：nerves起動時の設定を追記
- lib/exineris002/bootexineris.ex：Node.connectするプログラム

前回の [Nervesで起動時に`Node.connect()`するようにした](https://qiita.com/nishiuchikazuma/items/f6b0ef85fbd5257bcb19) の時はNerves起動時に接続した後、再接続する仕組みがなかったので今回は再接続の仕組みを追加しています。


## lib/exineris002/application.ex
`Bootexineris`モジュールにリストを渡して起動します。
リストは左から「自分のnode_name」「クッキー（共通鍵）」「接続先」としています。

``` elixir:lib/exineris002/application.ex
  def children(_target) do
    [
      # {Bootexineris, [node_name, cookie, conn_node]}
      {Bootexineris, ["exima", "comecomeeverybody", "kazumambp@192.168.46.100"]}
    ]
  end
```



## lib/exineris002/bootexineris.ex

- Nerves起動時の動作（キーワード：`:wakeup`, `nodeconn/2`）
 - Nerves起動後1秒おきに接続しようとする
 - `wlan0`インタフェースにDHCPよりIPアドレスが割り当てられるのを確認後、`Node.connect/1`の手続きを実施
 - 接続完了後、接続確認の動作に遷移
- 接続確認の動作（キーワード：`:alive`, `re_nodeconn/2`）
 - 60秒おきに接続を確認
 - 正常時 → 再度60秒接続を確認を実施　（`Node.list`に接続先が存在している && `Node.ping`で応答がある場合 → `:node_alive`）
 - 接続が切れた後に接続先が復帰した場合 → 再接続を実施し失敗した場合は1秒おきに再接続を実施　（`Node.list`に接続先が存在していない && `Node.png`で応答がある場合 → `:node_re_conn`）
 - 接続が切れている場合 → 1秒おきに再接続を実施　（`Node.list`に接続先が存在していない && `Node.ping`で応答がない場合　→　`:node_down`）


``` elixir:lib/exineris002/bootexineris.ex
defmodule Bootexineris do
  use GenServer
  require Logger

  @interval_init_ms 1_000
  @interval_wakeup_ms 1_000
  @interval_alive_ms 60_000
  @interval_alive_false_ms 1_000

  def start_link(node_option \\ []) do
    # to init/1
    GenServer.start_link(__MODULE__, node_option, name: __MODULE__)
  end

  def init(node_option) do
    set_interval(:init, @interval_init_ms)
    {:ok, node_option}
  end

  def set_interval(msg, ms) do
    # to handle_info/2
    Process.send_after(self(), msg, ms)
  end

  def handle_info(:init, node_option) do
    init_nodeconn(wlan0_ready?(), node_option)
    {:noreply, node_option}
  end

  def handle_info(:wakeup, node_option) do
    nodeconn(node_option)
    {:noreply, node_option}
  end

  def handle_info(:alive, node_option) do
    re_nodeconn(conn_node_alive?(node_option), node_option)
    {:noreply, node_option}
  end

  defp init_nodeconn(true, [node_name, cookie, _]) do
    node_host = get_ipaddr_wlan0()
    System.cmd("epmd", ["-daemon"])
    Node.start(:"#{node_name}@#{node_host}")
    Node.set_cookie(:"#{cookie}")

    Logger.info("=== Node.start -> #{node_name}@#{node_host} ===")
    Logger.info("=== Node.set_cookie -> #{cookie} ===")

    case [node_start?(), node_set_cookie?()] do
      [true, true] ->
        Logger.info("=== init_nodeconn -> success! Node.start & Node.set ===")
        set_interval(:wakeup, @interval_wakeup_ms)

      [_, _] ->
        Logger.info("=== init_nodeconn -> false, node_start(#{inspect(node_start?())}), node_set_cookie(#{inspect(node_set_cookie?())}) ===")
        set_interval(:init, @interval_init_ms)
    end
  end

  defp init_nodeconn(false, [_, _, _]) do
    Logger.info("=== init_nodeconn -> false, wlan0_ready(#{inspect(wlan0_ready?())}) ===")
    set_interval(:init, @interval_init_ms)
  end

  defp nodeconn([_, _, conn_node]) do
    conn = Node.connect(:"#{conn_node}")
    Logger.info("=== Node.connect -> try connect to #{conn_node} ===")

    case conn do
      true ->
        Logger.info("=== nodeconn -> #{conn} ===")
        set_interval(:alive, @interval_alive_ms)

      _ ->
        set_interval(:wakeup, @interval_wakeup_ms)
    end
  end

  defp re_nodeconn(:node_alive, _) do
    set_interval(:alive, @interval_alive_ms)
  end

  defp re_nodeconn(:node_re_conn, [_, _, conn_node]) do
    conn = Node.connect(:"#{conn_node}")
    Logger.info("=== re_nodeconn Node.connect -> #{conn_node} ===")

    case conn do
      true ->
        Logger.info("=== re_nodeconn -> #{conn} ===")
        set_interval(:alive, @interval_alive_ms)

      _ ->
        set_interval(:alive, @interval_alive_false_ms)
    end
  end

  defp re_nodeconn(:node_down, [_, _, conn_node]) do
    Logger.debug("=== re_nodeconn -> false... try connect to #{conn_node} ====")
    set_interval(:alive, @interval_alive_false_ms)
  end

  def node_start?() do
    case Node.self() do
      :nonode@nohost -> false
      _ -> true
    end
  end

  def node_set_cookie?() do
    case Node.get_cookie() do
      :nocookie -> false
      _ -> true
    end
  end

  def conn_node_alive?([_, _, conn_node]) do
    case [conn_node_list_find?(conn_node), conn_node_ping?(conn_node)] do
      [true, true] -> :node_alive
      [false, true] -> :node_re_conn
      [_, _] -> :node_down
    end
  end

  def conn_node_list_find?(conn_node) do
    case Node.list() |> Enum.find(fn x -> x == :"#{conn_node}" end) do
      nil -> false
      _ -> true
    end
  end

  def conn_node_ping?(conn_node) do
    case Node.ping(:"#{conn_node}") do
      :pang -> false
      :pong -> true
    end
  end

  def wlan0_ready?() do
    case get_ipaddr_wlan0() do
      nil -> false
      _ -> true
    end
  end

  def get_ipaddr_wlan0() do
    case VintageNet.get_by_prefix(["interface", "wlan0", "addresses"]) do
      [] ->
        nil

      [list_int_wlan0_addr] ->
        list_int_wlan0_addr
        |> (fn {_, list_settings} -> list_settings end).()
        |> hd()
        |> Map.get(:address)
        |> VintageNet.IP.ip_to_string()
    end
  end
end
```


# 確認
OSXのターミナルで`iex`を起動します。

``` shell:osx
$ iex --name kazumambp@192.168.46.100 --cookie comecomeeverybody

iex(kazumambp@192.168.46.100)> Node.list
[]
```

次にNervesを起動します。
`bootexineris.ex`がうまく動いていれば自動的に`Node.connect`して、OSXとNervesの`Node.list`にお互いが登録されているはずです。

``` shell:nerves
$ ssh 192.168.5.55

iex> …①
iex(exima@192.168.46.11)> …②
iex(exima@192.168.46.11)> Node.list …③
[:"kazumambp@192.168.46.100"]

①Nerves起動直後はNode.startしてないのでこの表示
②wlan0がIPアドレスをDHCPサーバより取得するとNode.startしてNode.set_cookie後、Node.connectする
③Node.listして接続の確認
```

Nervesで`Node.connect`できていることを確認できたのでOSXでも確認します。

``` shell:osx
iex(kazumambp@192.168.46.100)> Node.list
[:"exima@192.168.46.11"]
```

`Node.connect`していることを確認できました！！

結果は省きますが、以下の機能もあります。

- `Node.connect`先のOSXがNervesより後に起動した場合の接続
- `Node.connect`先のOSXがダウンして再起動した場合の再接続


# まとめ
Nerves起動時に`Node.connect()`できるようになりました！
vintage_netを使った今回の方が [前回](https://qiita.com/nishiuchikazuma/items/f6b0ef85fbd5257bcb19) より関連するファイルが少なく導入が楽なのかなぁという印象です。安定しててほしいなぁ。

`def`と`defp`の使い分けが適当になってきてるので整理したいなー。