OpenBlocks IoTの自動キッティングスクリプトを書いてみた

IoTゲートウェイ向け超小型サーバ **OpenBlocks** を大量にキッティングすることになった。
![bx1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/0c2e056e-a6f1-54bc-9dbe-0768726f9411.jpeg)
OpenBlocks IoTファミリには、ぷらっとホーム社が提供する専用のGUIアプリケーションである Web UI がインストールされている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236222/9a9ad4e3-01d6-4e31-8f81-ff7d8bdfd21c.png)
ぷらっとホーム社の[FAQ](https://openblocks.plathome.co.jp/products/obs_iot/bx1/faq3.html)によると、ネットワークの設定など Web UI から設定可能な項目に関しては Web UI 上から設定することになっていて、CUI（Linuxのコマンド）で設定してはいけないらしい。
アプリケーションプログラムのscp転送と実行まで含めると、1台あたり15分はかかってしまう。とかく面倒なGUI操作を数十回も繰り返すよりは、最初から自動化プログラムを作った方がトータルで工数を削減できるため、Pythonでキッティングスクリプトを書くことにした。工数削減というと聞こえはいいが、実際のところ、単純作業をやりたくなかったというのが本音である。

# Web UI の操作を自動化
Seleniumでブラウザのオペレーションを自動化した。シンプルなWebアプリケーションなので、GETリクエスト、テキストボックスの入力、保存ボタンのクリックだけで、特別高度なことをしているわけでは無い。

```python:Python
"""
OpenBlocks IoT BX1 キッティングスクリプト stage1
"""
__author__  = "MindWood"
__version__ = "1.00"
__date__    = "13 Mar 2020"

from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import time
import sys

admin_id   = 'adminUser'
admin_pw   = 'adminPass'
passphrase = 'passPhrase'
id_rsa     = 'ssh-rsa XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== mw'
domain     = 'example.com'

options = webdriver.ChromeOptions()
options.add_argument('--no-sandbox')
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(10)

# OpenBlocks IoT の Web UI に接続
driver.get('http://192.168.254.254:880')

# 最初に同意ボタンがあれば初めて使う機器と判定する
ar = driver.find_elements_by_id('eula_agree')
if len(ar) == 0:
    # ログイン画面
    driver.find_element_by_name('name').send_keys(admin_id)
    driver.find_element_by_name('password').send_keys(admin_pw)
    driver.find_element_by_name('_act').click()  # ログイン
else:
    # 管理者アカウント設定および同意画面
    driver.find_element_by_id('eula_agree').click()  # 同意する
    time.sleep(1)
    driver.find_element_by_name('_name').send_keys(admin_id)
    driver.find_element_by_name('_password').send_keys(admin_pw)
    driver.find_element_by_name('_password_confirm').send_keys(admin_pw)
    driver.find_element_by_id('save').click()  # 保存する
time.sleep(1)

# システム >> シリアル画面
driver.get('http://192.168.254.254:880/system/serial.php')
sq = driver.find_element_by_class_name('sq')  # シリアル番号取得
serial_num = sq.find_element_by_xpath('pre').text.strip()
print('シリアル番号[' + serial_num + ']')

# サービス >> 基本画面
driver.get('http://192.168.254.254:880/apps/m2miot/basic.php')
time.sleep(1)
driver.find_element_by_id('enable_bluetooth').click()  # Bluetooth許可
driver.find_element_by_id('save').click()  # 保存する
time.sleep(1)

# サービス >> 状態画面
driver.get('http://192.168.254.254:880/apps/m2miot/status.php')
time.sleep(1)
sq = driver.find_element_by_class_name('sq')
hcconfig = sq.find_element_by_xpath('pre').text
p = hcconfig.find('BD Address: ')
bd_adrs = hcconfig[p+12:p+29].replace(':', '')  # BDアドレス取得
print('BDアドレス[' + bd_adrs + ']')

# システム >> フィルター画面
driver.get('http://192.168.254.254:880/system/ipfilter.php')
time.sleep(1)

this_checkbox = driver.find_element_by_id('filter_hold_true')
if not this_checkbox.is_selected(): this_checkbox.click()  # チェックされていなければチェックする
driver.find_element_by_id('mode_ssh_true').click()  # SSH有効化
driver.find_element_by_id('save').click()  # 保存する
time.sleep(1)

# システム >> SSH関連画面
driver.get('http://192.168.254.254:880/system/sshd_config.php')
time.sleep(1)
driver.find_element_by_id('deny_passwd_login').click()  # パスワード認証禁止

elm = driver.find_element_by_id('pubkey')
elm.clear()
elm.send_keys(id_rsa)

driver.find_element_by_id('save').click()  # 保存する
time.sleep(1)

# システム >> マイページ画面
driver.get('http://192.168.254.254:880/system/mypage.php')
time.sleep(1)
driver.find_element_by_id('password').send_keys(admin_pw)
driver.find_element_by_id('save').click()  # 保存する
time.sleep(1)

# ネットワーク >> 基本画面
driver.get('http://192.168.254.254:880/network/basic.php')
time.sleep(1)

elm = driver.find_element_by_id('domainname')
elm.clear()
elm.send_keys(domain)  # ドメイン

elm = driver.find_element_by_id('wlan0_ssid')
elm.clear()
ssid = serial_num + '_MindWood'
elm.send_keys(ssid)  # SSID

elm = driver.find_element_by_id('wlan0_passphrase')
elm.clear()
elm.send_keys(passphrase)  # パスフレーズ

driver.find_element_by_id('enable_ppp0').click()  # PPPを使用する
time.sleep(1)

driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')  # 最下行までスクロール
time.sleep(1)

# SORACOM データ専用SIM の設定
driver.find_element_by_id('ppp0_apn').send_keys('soracom.io')  # APN
driver.find_element_by_id('ppp0_user').send_keys('sora')  # ユーザ名
driver.find_element_by_id('ppp0_pass').send_keys('sora')  # パスワード
driver.find_element_by_id('ppp0_autotype_false').click()  # 自動接続しない
driver.find_element_by_id('ppp0_set_reconnect_false').click()  # 定期再接続しない
driver.find_element_by_id('save').click()  # 保存する
time.sleep(1)

# システム >> パスワード画面
driver.get('http://192.168.254.254:880/system/passwd.php')
time.sleep(1)
driver.find_element_by_id('password').send_keys(admin_pw)
driver.find_element_by_id('password_confirm').send_keys(admin_pw)
driver.find_element_by_id('save').click()  # 保存する

# メンテナンス >> 再起動画面
driver.get('http://192.168.254.254:880/maintenance/shutdown.php')
time.sleep(1)
sq = driver.find_element_by_class_name('sq')
sq.find_element_by_xpath('//input[@onclick="pre_reboot()"]').click()  # 再起動
driver.find_element_by_id('reboot_btn').click()  # 実行
time.sleep(2)

alert = driver.switch_to.alert
alert.accept()  # アラートにOKを応答
sys.exit()
```

# ファイル転送とsshコマンドの実行を自動化
paramikoとscpパッケージを使い、アプリケーション配備シェルの転送とその実行までを自動化した。
手作業でやろうとすると、鍵の設定だけでも面倒くさい。

```python:Python
"""
OpenBlocks IoT BX1 キッティングスクリプト stage2
"""
__author__  = "MindWood"
__version__ = "1.00"
__date__    = "13 Mar 2020"

import paramiko
import scp
import time

def exec_cmd(cmd, ssh):
    """sshでコマンドを実行する"""
    print('# ' + cmd)
    stdin, stdout, stderr = ssh.exec_command(cmd)
    for out_line in stdout:
        print(out_line.strip('\n'))
    for err_line in stderr:
        print(err_line.strip('\n'))

with paramiko.SSHClient() as ssh:
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    pkey = paramiko.RSAKey.from_private_key_file('bx1_rsa.key')
    ssh.connect(hostname='192.168.254.254', port=22, username='root', password='adminPass', pkey=pkey)

    exec_cmd('rm -f write_firmware.sh; ls -l', ssh)

    with scp.SCPClient(ssh.get_transport()) as scp:
        scp.put('write_firmware.sh', 'write_firmware.sh')

    time.sleep(1)

    exec_cmd('chmod 755 write_firmware.sh; ./write_firmware.sh', ssh)
    time.sleep(2)

    exec_cmd('reboot', ssh)
```

以上、参考になれば！