Nervesで起動時に`Node.connect()`するようにした

これまでNervesで頑張っていたのは、Nerves起動時に`Node.connect()`したかったからなのでした。ようやくできるようになりました＼(^o^)／


これは「[Nervesのネットワーク設定を`mix firmware`する時に指定できるようにした](https://qiita.com/nishiuchikazuma/items/449f19ec64ea33d6f8e2)」の続編になります。

# 更新情報
<font color="red">こちらの記事のほうが新しいので興味があればぜひ。</font>

- [nerves_pack（vintage_net含む）を使ってNerves起動時に`Node.connect()`するようにした](https://qiita.com/nishiuchikazuma/items/f68d2661959197d0765c)


# やりたいこと
Nerves起動時にOSXに`Node.connect()`して、接続できるようにしたい。


# 環境

## Nervesプロジェクト名
- Exineris001

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


## ネットワーク
- Mac OSX
  - 有線LAN→メンテナンス用、固定IPアドレス（192.168.5.100/24）
  - 無線LAN→インターネット接続用、DHCP（本記事では192.168.46.6）

- ラズパイ
  - 有線LAN（eth0）→メンテナンス用、固定IPアドレス（192.168.5/24の範囲）
  - 無線LAN（wlan0）→インターネット接続用、DHCP（192.168.46/24から自動割当）
  - 192.168.5.240(default)は後述

![ネットワーク構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51020/95e29daf-01e9-ac3b-3e5e-3d4448db4f19.png)


# こんな風に書きました

## mix.exs

お約束で`defp deps`に必要なライブラリを記載して、`def release`にあらかじめ決めておきたいCookieを記載しました。

- `defp deps`に必要なライブラリを記載
- `def release`にあらかじめ決めておきたいCookieを記載

```elixir:mix.exs
defp deps do
  [
    (略)
    {:nerves_network, "~> 0.5", targets: @all_targets},
    {:nerves_firmware_ssh, "~> 0.3", targets: @all_targets},
    {:ex_string_util, "~> 0.1.0"}
  ]
end

def release do
[
  cookie: "comecomeeverybody"    # セットしたいCookieを設定
]
end
```

## config/target.exs

`:nerves_init_gadget`やIPアドレス等のネットワークの設定は`config/network.exs`にまとめて記載するようにしました。SSHの鍵の登録を別途する場合はここで登録します。
（参考） [NervesのSSH環境を整えた　〜upload.shを使ってファームウェア更新も〜](https://qiita.com/nishiuchikazuma/items/6773d8f1d351d9355738)

- `:nerves_init_gadget`をコメントアウト
- `import_config("network.exs")`を記載して、IPアドレス等のネットワーク設定を別ファイルに分離

```elixir:config/target.exs
# config :nerves_init_gadget,
#   ifname: "usb0",
#   address_method: :dhcpd,
#   mdns_domain: "nerves.local",
#   node_name: node_name,
#   node_host: :mdns_domain

import_config("network.exs")
```


## config/network.exs

ここで無線（wlan0）と有線（eth0）のネットワーク設定をおこないます。
`:nerves_config_gadget`に記載している`node_name@node_host`という名前でNodeが起動します。

``` elixir:config/network.exs
import Config

settings = [
  networks: [
    [ssid: "SSIDを記載", psk: "PSKを記載", key_mgmt: :"WPA-PSK", priority: 0,
      ipv4_address_method: :dhcp,
      nameservers: ["8.8.8.8", "8.8.4.4"]
    ]
  ]
]

config :nerves_network, default: [
  wlan0: settings,
  eth0: [
    ipv4_address_method: :static,
    ipv4_address: "192.168.5.５５",
    ipv4_subnet_mask: "255.255.255.0"
  ]
]

config :nerves_init_gadget,
  ifname: "wlan0",
  address_method: :dhcp,
  node_name: "exineris001",
  node_host: :ip
```


## lib/exineris001/application.ex

アプリケーション起動時の引数として、OSXのノード名@ホスト名（`kazumambp@192.168.46.6`）を渡すようにしました。
ホスト名といいつつIPアドレスを記載しています。ここ、DNSサーバたてて解決できるかどうかが次の課題かなぁと思ってるところです。

```elixir:lib/exineris001/application.ex
def children(_target) do
  {Exineris001.Bootexineris, ["kazumambp@192.168.46.6"]}    # OSXのWiFiのIPアドレス
end
```


## lib/exineris001/bootexineris.ex

Nervesとしては以下の順で処理がすすむのですべての項目がクリア（今回は`:true（trueでも同じ）`）にならないと`Node.connect()`しないようにしました。

1. wlan0インターフェース作成　→　`is_wlan0_ready()`で確認
1. WiFi設定を有効化後、DHCPでIPアドレス取得　→　`is_ipv4_ready()`で確認
1. nodeの設定　→　`is_nodeconn_ready()`で確認


※`ExStringUtil.is_ip!()`はIPアドレスのチェックがかなりかなり甘いので注意して使うことが必要です。

```elixir:lib/exineris001/bootexineris.ex
defmodule Exineris001.Bootexineris do

  use GenServer
  require Logger

  def start_link(node \\ []) do
    GenServer.start_link(__MODULE__, node, name: __MODULE__)
  end

  def init(node) do
    conn = do_nodeconn(node)
    {:ok, conn}
  end

  def do_nodeconn(node) do
    nodeconn_status = get_nodeconn_status()
    case nodeconn_status do
      [:true, :true, :true] ->
        conn = Node.connect(:"#{node}")
        case conn do
          :true  -> Logger.info("===== conn => #{conn} =====")
          _      -> do_nodeconn(node)
        end
      [_, _, _] ->
        do_nodeconn(node)
    end
  end

  def get_nodeconn_status() do
    [is_nodeconn_ready(), is_ipv4_ready(), is_wlan0_ready()]
  end

  def is_nodeconn_ready() do
    node = Node.self()
    case node do
      :nonode@nohost -> :false
      _              -> :true
    end
  end

  def is_ipv4_ready() do
    case Nerves.NetworkInterface.settings("wlan0") do
      {:ok, list} -> Map.get(list, :ipv4_address) |> ExStringUtil.is_ip!()   # return: true or false
      {_, _}      -> :false
    end
  end

  def is_wlan0_ready() do
    wlan0 = Nerves.NetworkInterface.interfaces() |> Enum.find(& &1 == "wlan0")
    case wlan0 do
      "wlan0" -> :true
      _       -> :false
    end
  end

end
```

# 実行→確認

準備として、OSXのターミナルから名前とCookieを指定してプロセスを起動しておきます。
Nerves起動前は`Node.list()`しても空のリストが返ってきますが、Nervesを起動するとConnectしているNodeのリストが返ってきます。

- OSXのターミナルから名前とCookieを指定してプロセスを起動
- Nerves起動前は`Node.list()`しても空のリストとなる
- Nerves起動後は接続先のリストが返ってくる


```bash:bash(OSX)
$ mix deps.get
$ mix firmware
$ ./upload.sh 192.168.5.55

$ iex --name kazumambp@192.168.46.5 --cookie comecomeeverybody
iex(kazumambp@192.168.46.5)> Node.list()
[]

＜＜＜ ここでNerves起動 ＞＞＞

iex(kazumambp@192.168.46.5)> Node.list()
[:"exineris001@192.168.46.6"]
```

```elixir:iex(exineris001@192.168.46.5)
iex(exineris001@192.168.46.6)> Node.list()
[:"kazumambp@192.168.46.5"]
```

# まとめ
Nerves起動時に`Node.connect()`できるようになりました！
感想：ながかったー

## 足跡
- [Elixirのバージョン管理環境をasdfを使って作った](https://qiita.com/nishiuchikazuma/items/b9d319732ddb540fd990)
- [Nervesのネットワーク環境を整えた](https://qiita.com/nishiuchikazuma/items/1341b32c4ce997fc362d)
- [NervesのSSH環境を整えた　〜upload.shを使ってファームウェア更新も〜](https://qiita.com/nishiuchikazuma/items/6773d8f1d351d9355738)
- [Nervesのnode_nameとnode_hostとcookieを設定した](https://qiita.com/nishiuchikazuma/items/1ae314c47b635d9a1ce8)
- [NervesでShellに落ちた](https://qiita.com/nishiuchikazuma/items/ceafee4bd265cfc50501)
- [Nervesのネットワーク設定を`mix firmware`する時に指定できるようにした](https://qiita.com/nishiuchikazuma/items/449f19ec64ea33d6f8e2)

@kikuyuta さんが [はじめてNerves(0) ElixirによるIoTフレームワークNervesがとにかく動くようになるためのリンク集](https://qiita.com/kikuyuta/items/9dee85c32004c8e827e7) に色々とまとめてくれてるのでこちらもどうぞ。