ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【python】unittestでmockを使ってテストケースを書く

 以前の記事でテストケースを書く方法を紹介しました。

 簡単なプログラムであれば上記の内容だけである程度テストはできると思いますが、他のクラスからの呼び出しだったり、対象の関数が何回呼ばれているかを確かめたり、まだ実装してないクラスを呼び出していたりという場合には対応できません。

 そういう時に便利なのがモックと言われるもので、要はダミーのクラスや関数を設定して戻り値を固定させたり呼び出し回数をカウントしたりするためのものです。

 今回はpythonに最初から含まれているunittestを使ってそんなモックを使ったテストケースを書いた時の備忘録です。


 では、始めます。


1:unittestにおけるモックの書き方
 モックの書き方を簡単にですが説明していきます。基本的には以下の部分がわかっていれば大体の場合で問題はないかと思います。

・呼び出す関数をモックにしたい場合

from unittest import (TestCase, skip)
from unittest import mock
from target import Target # テスト対象ファイル読み込み

class TestTarget(TestCase):
    def _mock_function(self):
        # モックとしての処理を書く

    @mock.patch("Hoge.function", new=_mock_function)
    def test_hoge(self):
        hoge = Hoge()
        hoge.function() # ここの関数で_mock_functionが呼ばれる

if __name__ == '__main__':
    unittest.main()

 テストを行う関数test_hogeの前にある「@mock.patch("Hoge.function", new=_mock_function)」という記述でHogeクラスのfunction関数を自分で定義したモック関数である「_mock_function」に置き換えています。ちなみにこの書き方だとモック関数のselfはモック先のインスタンスになるので注意が必要です。

・呼び出すクラス自体をモックにしたい場合

# -*- coding:utf-8 -*-
from unittest import (TestCase, skip)
from unittest import mock

from target import Target # テスト対象ファイル読み込み

# 例えば以下のようにHogeの内部で
# 別のクラスのインスタンスを持っている場合
# class Hoge():
#     def __init__(self):
#         self.hogehoge = HogeHoge()

class TestTarget(TestCase):
    def test_hoge(self):
        hoge = Hoge()

        # hogeが持っているhogehogeインスタンスをモック用インスタンスに置き換え
        hoge.hogehoge = mock.MagicMock()

        # モックインスタンスが呼ばれているので何も行われない
        hoge.hogehoge.function()

        # モックインスタンスでfunction関数が呼ばれた回数を確認
        self.assertEqual(1, hoge.hogehoge.function.call_count)


if __name__ == '__main__':
    unittest.main()

 モックにしたいHogeHogeクラスをmock.MagicMock()というクラスで置き換えています。これはモック用のクラスで関数が呼ばれた回数を数えたりすることができる便利なクラスです。詳しい内容は参考資料に挙げているページ様や公式ドキュメントを参照してください。


2:実際にテストするコードの準備
 書き方だけ説明されてもどう使えばいいのかわからないと思うので、実際にモックを使ったテストコードを書いていきます。

 今回は以下のコードをテストする前提で進めていきます。そのまま使いたい場合はコピペして2つのファイルを同じ階層に保存してください。

・target.py

# -*- coding:utf-8 -*-
#
# target.py
#
from sumClass import SumClass

class Target():
    # コンストラクタ
    def __init__(self):
        self.sum1 = SumClass()
        self.sum2 = SumClass()

    # 初期化関数
    def init(self, sum1_add, sum1_loop, sum2_add, sum2_loop):
        # sum1を設定
        self.sum1.setAdd(sum1_add)
        self.sum1.setLoopTimes(sum1_loop)
        # sum2を設定
        self.sum2.setAdd(sum2_add)
        self.sum2.setLoopTimes(sum2_loop)

    # main関数
    def main(self):
        self.sum1.sumLoop()
        self.sum2.sumLoop()
        result_str = self.sumResultsToString()
        print(result_str)

    # 結果を文字列にする関数
    def sumResultsToString(self):
        result_str = "[result] sum1:{}, sum2:{}"
        return result_str.format(int(self.sum1.result), int(self.sum2.result))

if __name__ == "__main__":
    target = Target()
    target.init(3, 5, 2, 10)
    target.main()

・sumClass.py

# -*- coding:utf-8 -*-
#
# sumClass.py
#
class SumClass():
    # コンストラクタ
    def __init__(self):
        self.result = 0
        self.add = 0
        self.loop_times = 0

    # 足す数を設定
    def setAdd(self, add):
        self.add = add

    # ループ回数を設定
    def setLoopTimes(self, loopTimes):
        self.loop_times = loopTimes

    # 足し算を繰り返す関数
    def sumLoop(self):
        count = 1
        while count <= self.loop_times:
            self.result = self.mySum(self.result, self.add)
            count += 1

    # 足し算を行う関数
    def mySum(self, add1, add2):
        return add1 + add2

 今回はこの2つのファイルのうちの「target.py」のテストコードを書いていきます。


3:テストコードの実装
 2つのファイルと同じ階層に「tests」フォルダを作成し、その中に「test_target.py」ファイルを作成します。
f:id:rikoubou:20201126172647p:plain
f:id:rikoubou:20201126172723p:plain

・test_target.py

# -*- coding:utf-8 -*-
# target.pyファイルがある階層で以下のコマンドでテストを実行する
# python -m unittest tests.test_target

from unittest import (TestCase, skip)
from unittest import mock

from sumClass import SumClass
from target import Target # テスト対象ファイル読み込み

# モック用クラス
class MockSumClass(mock.MagicMock):
    init_count = 0
    result_count = 0

    # SumClassのコンストラクタモック用
    def _mock_SumClass_init(self):
        self.init_count += 1

    # sumResultsToString関数のモック用
    def _mock_sumResultsToString(self):
        self.result_count += 1


class TestTarget(TestCase):
    _mock_sum = MockSumClass() # モッククラスのインスタンス

    # コンストラクタのテスト
    @mock.patch("sumClass.SumClass.__init__", new=_mock_sum._mock_SumClass_init)
    def test_01__init__(self):
        Target() # 実行

        # SumClassのコンストラクタが呼ばれた回数を確認
        self.assertEqual(2, self._mock_sum.init_count)


    # init関数のテスト
    def test_02_init(self):
        target = Target()

        # モッククラスに置き換え
        target.sum1 = mock.MagicMock()
        target.sum2 = mock.MagicMock()

        target.init(1, 2, 3, 4) # 実行

        # 呼び出し回数の確認
        self.assertEqual(1, target.sum1.setAdd.call_count)
        self.assertEqual(1, target.sum1.setLoopTimes.call_count)
        self.assertEqual(1, target.sum2.setAdd.call_count)
        self.assertEqual(1, target.sum2.setLoopTimes.call_count)


    # main関数のテスト(全てモックの場合)
    @mock.patch("target.Target.sumResultsToString", new=_mock_sum._mock_sumResultsToString)
    def test_03_main_01(self):
        self._mock_sum.result_count = 0
        target = Target()

        # モッククラスに置き換え
        target.sum1 = mock.MagicMock()
        target.sum2 = mock.MagicMock()

        target.main() # 実行

        # 呼び出し回数の確認
        self.assertEqual(1, target.sum1.sumLoop.call_count)
        self.assertEqual(1, target.sum2.sumLoop.call_count)
        self.assertEqual(1, self._mock_sum.result_count)


    # main関数のテスト(計算結果を見る場合)
    @mock.patch("target.Target.sumResultsToString", new=_mock_sum._mock_sumResultsToString)
    def test_04_main_02(self):
        self._mock_sum.result_count = 0
        target = Target()

        target.init(1, 2, 3, 4)
        target.main() # 実行

        # 計算結果の確認
        self.assertEqual(2, target.sum1.result)
        self.assertEqual(12, target.sum2.result)
        # 呼び出し回数の確認
        self.assertEqual(1, self._mock_sum.result_count)


    # sumResultsToString関数のテスト
    def test_05_sumResultsToString_01(self):
        target = Target()

        result = target.sumResultsToString() # 実行

        # 確認
        self.assertEqual("[result] sum1:0, sum2:0", result)


    # sumResultsToString関数のテスト2
    def test_06_sumResultsToString_02(self):
        target = Target()
        target.init(1, 2, 3, 4)
        target.main()

        result = target.sumResultsToString() # 実行

        # 確認
        self.assertEqual("[result] sum1:2, sum2:12", result)


if __name__ == '__main__':
    unittest.main()

 テストコードの実行は「target.py」ファイルと同じ階層で以下のコマンドで実行できます。

$ python -m unittest tests.test_target

..None
.None
..[result] sum1:2, sum2:12
.
----------------------------------------------------------------------
Ran 6 tests in 0.006s

OK

 あまり良いテストコードではない気がしますが、それぞれ呼び出し回数やモック関数の呼び出しがちゃんとできています。

 また、個人的にはモック用のクラスを作成してそれを呼び出すという形にした方が呼び出し回数など色々制御しやすいので楽なのではないかと思いそういう形にしています。


 以上がunittestでmockを使う方法です。

 正直まだ自分自身もmockに慣れていないので、もうちょっと良い書き方等があればコメントしていただけると幸いです。


・参考資料

【Wi-Fiルータ】回線速度改善のためにv6プラス対応のルータに置き換えてみた

 自分が家で使っているインターネット固定回線はVDSLという古のものなのですが、昨今の情勢からかただでさえ遅い回線速度が致命的なまでに遅くなっていました。
 どれぐらい遅いかというとこちらのページで計測した結果、ひどい時で以下のような感じでした。
f:id:rikoubou:20201117181529p:plain

 具体的にはYoutubeのようなIPv6での接続ならそれなりに快適なのですが、IPv4での接続であるTwitterの画像ですらクリックして読み込むのに時間がかかると言った具合でした。

 流石にどうにかしたいと自分なりに調べたり知り合いに相談したりしたところ、「v6プラス」というサービスが利用できれば回線速度が速くなる可能性があるということでした。

 ということで、今回はv6プラス対応のルータに置き換えてみた時の備忘録というか日記みたいな内容になります。


 では、始めます。


1:v6プラスのサービスを有効にする
 v6プラスの提供有無や有効化の方法はプロバイダによって異なりますが、基本的にはプロバイダに電話をして有効にしてもらうか、会員ページのようなところで操作をして有効にする場合が多いと思います。詳しいやり方は各自で調べて行ってください。

 自分の場合はSo-net光なのですが、マイページの「ご契約サービスのご利用状況」の中にv6プラスの項目で有効になっていることを確認できます。
f:id:rikoubou:20201119230417p:plain

 v6プラスが有効になっていないと、対応しているルータを購入しても使えないのでちゃんと有効にしておくようにしましょう。


2:v6プラス対応のWi-Fiルータを入手する
 v6プラスが有効になっていることを確認したら、次はv6プラスに対応しているルータを購入またはレンタルします。

 v6プラスを提供しているプロバイダの多くは対応ルータのレンタルを行っているかと思います。例えばSo-net光だと以下のような対応ルータのレンタルページが存在します。

 別途月額料金がかかったり違約金が発生したりと値段が高いと思うので、個人的には「ルータは購入する」のをオススメします。

 ではどのルータを買えばいいのかという話ですが、レンタルページに型番が掲載されているのでその型番と同じ商品を購入するのが一番確実で楽です。例えば先のSo-net光のレンタルルータの型番で調べてみると価格.comで以下の商品ページが出てきます。

 大体4000円台で買えるようなので、1年以上使うのであれば購入した方がお得かもしれません。

 この他にも例えば「BUFFALO」にはv6プラスに対応したルータ商品一覧があるので、ここから探してもいいかもしれません。他のメーカーにもおそらく同様のページがあるはずです。

 いろいろとv6プラス対応ルータがある中で自分が購入したのは以下の商品です。

 購入理由としては、今まで使っていたルータがBUFFALO製品だったのと価格が購入時点で4000円以下と安かったからという2点です。

 とにかくv6プラスに対応していればなんでもいい、という人は安いルータを探して購入しましょう。


3:既存のWi-Fiルータと置き換える
 v6プラス対応ルータに置き換える作業ですが、購入した「BUFFALO WiFi 無線LAN ルーター WSR-1166DHPL2/N」の説明書通りにやれば自分はすんなり置換ができました。
 既存ルータの設定の引越し機能があり、新しいルータで設定しなおすという手間が省けてとても便利でした。

 ただし、デフォルトでv6プラスの設定が有効にはなっていないので、ルータ内にログインして自分で設定する必要があります。


4:ルータのv6プラス設定を有効にする
 ルータを置き換えてインターネットに繋がることが確認できたら、ルータ側でv6プラス設定を有効にする必要があります。

 基本的には以下のページの方法に沿ってやっていけばOKです。


「BUFFALO WiFi 無線LAN ルーター WSR-1166DHPL2/N」の場合、以下のアドレスをブラウザに入力することでルータのログイン画面を表示できます。

 アクセスすると以下のようなログイン画面が表示されるので、購入した箱の中にあるユーザ名とパスワードを入力してログインします。
f:id:rikoubou:20201119232835p:plain

 ログインするとホーム画面が表示されるので「詳細設定」をクリックします。
f:id:rikoubou:20201119233125p:plain

 左側のメニューにある「Internet」→「Internet」をクリックして「v6プラスを使用する」にチェックを入れます。
f:id:rikoubou:20201119233545p:plain

「v6プラスを使用する」にチェックが入った状態で「設定」ボタンをクリックします。
f:id:rikoubou:20201119234009p:plain

 するとインターネットが一度切断され、v6プラスで接続できるかどうかの確認をした後にv6プラスでインターネットに接続されます。この処理には数分かかる場合があるので「接続成功」の表示が出るまで気長に待ちます。

 一度設定すればPCやスマホ側でインターネットを切断、再接続をしてもv6プラスでインターネットに接続してくれます。

 ちなみにv6プラスを有効にした結果、夜でも以下のようにIPv6とほぼ変わらない速度が出るようになりました。しかもアップロードに関してはIPv6よりも速くなっていたりしています。
f:id:rikoubou:20201119234551p:plain


 以上がv6プラス対応のルータに置き換えた内容になります。

 IPv4での速度がストレスに感じていたので、ルータを買い換えるだけで快適になって本当に良かったです。


おまけ:v6プラス(IPv4 over IPv6)について
「v6プラス」という名称の他に「IPv6オプション」や「transix」というようにプロバイダによって色々呼び方は違いますが、やっていることは同じで「IPv4 over IPv6」というものです。

 IPv4IPv6では接続経路が違っており、一般的にIPv4の方が混雑、IPv6の方が空いているというような状況です。一番最初に挙げているあの画像がまさしくその通りの速度で、IPv4は混雑しているから遅い、IPv6は空いているから速いということになります。IPv4が一般道、IPv6が高速道路みたいなイメージです。

 このIPv4の速度を改善するために出てきたIPv4 over IPv6というサービスですが、簡単に言うと「IPv6でいけるところまで行ってIPv4でないといけないところだけIPv4を使う」という感じです。要は目的地まで一般道しか通らないのが従来のIPv4での接続経路でしたが、IPv4 over IPv6では途中できる限り高速道路であるIPv6の経路を通って行くというイメージです。

 これにより回線速度向上が見込めるということです(ただしVDSLは上限100Mbpsなのでそれ以上の速度は出ませんが…)。


・参考資料

【Windows】Reactの環境を構築する

 今までWeb系の方には手を出していなかったのですが、そろそろ挑戦してもいいかもしれないと思うようになってきました。

 開発環境は色々ありますが、Reactを使ってみようと思ったのでその環境構築の備忘録です。
 ちなみにブラウザ上でReactのコードを書くこともできますが、ローカル環境構築になります。
 基本的には公式ページのチュートリアルのままなので、詳しいことを知りたい方はそちらを参照してください。


 では、始めます。


1:node.jsのインストール
 Reactを使うためにはNode.jsをインストールしておく必要があります。

 以下の公式のダウンロードページを開きます。

 環境に合ったインストーラを選択してクリックし、ファイルをダウンロードします(今回はLTSのWindows 64bit msiを選択しました)。
f:id:rikoubou:20201112135438p:plain

 ダウンロードしたインストーラを起動させると、インストールできるかどうかのチェックが走るので少し待ちます。
f:id:rikoubou:20201112135606p:plain

 チェックが終わるとインストールを進められるようになるので「Next」をクリックします。
f:id:rikoubou:20201112135643p:plain

 ライセンスが表示されるので読み、同意するのところにチェックを入れて「Next」をクリックします。
f:id:rikoubou:20201112135840p:plain

 インストール場所が表示されるので、デフォルトのままで「Next」をクリックします。
f:id:rikoubou:20201112140010p:plain

 カスタムインストールもデフォルトのままで「Next」をクリックします。
f:id:rikoubou:20201112140208p:plain

 Nativeモジュールの追加が尋ねられますが、デフォルトでチェックを外したままで「Next」をクリックします。
f:id:rikoubou:20201112140718p:plain

 インストールの準備ができたので「Install」をクリックします。
f:id:rikoubou:20201112140801p:plain

 PCに変更を加えるか尋ねられるので「はい」を選択します。

 インストールが始まるので終わるまで待ちます。
f:id:rikoubou:20201112140925p:plain

 インストールが終わったら「Finish」をクリックします。
f:id:rikoubou:20201112141120p:plain

 次にちゃんとNode.jsがインストールされているかを確認していきます。

 コマンドプロンプトPowerShellを起動させて以下のコマンドを実行します。

$ node --version

 以下のようにバージョンが表示されていればNode.jsのインストールは完了です。
f:id:rikoubou:20201112141712p:plain


2:Reactのチュートリアル
 Node.jsのインストールができたのでReactを使ってチュートリアルにある空の3×3のマスを表示させてみます。

 まずはどこでもいいのでReact用の空フォルダを作成します。自分はドキュメント配下に「react」という空フォルダを作成しました。

 コマンドプロンプトPowerShellを立ち上げて以下のコマンドでそのフォルダに移動します。

$ cd [React用空フォルダパス]

 フォルダに移動したら、以下のコマンドを実行します。

$ npx create-react-app my-app

 必要なパッケージのインストールやテンプレートのダウンロードが開始されるので、終わるまで待ちます。

 ちなみにコマンドが終了すると「my-app」というフォルダが作成されます。
f:id:rikoubou:20201112143445p:plain

 先ほどのコマンドが終了したら、以下の4つのコマンドを順に実行します。

$ cd my-app # my-appフォルダへ移動
$ cd src # srcフォルダへ移動
$ del * # 現在いるのフォルダ内をすべて削除
$ cd .. # my-appフォルダへ戻る

 srcフォルダ内をすべて削除したら、そのsrcフォルダ内に以下のファイルを新規作成します。

・index.css

body {
    font: 14px "Century Gothic", Futura, sans-serif;
    margin: 20px;
  }
  
  ol, ul {
    padding-left: 30px;
  }
  
  .board-row:after {
    clear: both;
    content: "";
    display: table;
  }
  
  .status {
    margin-bottom: 10px;
  }
  
  .square {
    background: #fff;
    border: 1px solid #999;
    float: left;
    font-size: 24px;
    font-weight: bold;
    line-height: 34px;
    height: 34px;
    margin-right: -1px;
    margin-top: -1px;
    padding: 0;
    text-align: center;
    width: 34px;
  }
  
  .square:focus {
    outline: none;
  }
  
  .kbd-navigation .square:focus {
    background: #ddd;
  }
  
  .game {
    display: flex;
    flex-direction: row;
  }
  
  .game-info {
    margin-left: 20px;
  }

・index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

 この2つのファイルの作成ができたら以下のコマンドを実行して起動させます。

$ npm start

 ちなみに実行すると以下のようになります。
f:id:rikoubou:20201112145429p:plain

 起動後になんでも良いのでブラウザを開き「http://localhost:3000/」にアクセスすると、以下のような3×3のマス目が表示されます。
f:id:rikoubou:20201112145545p:plain

 終了させたい場合は、「npm start」コマンドを実行している画面で「Ctrl + C」を連打していると「バッチジョブを終了しますか (Y/N)?」と尋ねられるので「y」と入力してEnterキーを押下することで終了できます。
f:id:rikoubou:20201112150013p:plain

 終了させた状態で再度「http://localhost:3000/」にアクセスしても、マス目は表示されなくなります。
f:id:rikoubou:20201112150151p:plain


 以上がWindowsでのReactの環境構築方法になります。

 まだ環境を導入したというだけなので、これからちょっとずつでもReactを勉強していきたいですね…。


・参考資料

【https化】このブログをhttps化してみました

 かなり前から「このはてなブログhttps化をしないとなぁ」と思いながらも面倒そうなのでやってませんでした。

 しかし最近になって記事単体のページから編集できなくなったり、はてなのトップページ以外からログインできなくなったりと不便になってきたので、ついにhttps化をする決意をしました。

 が、実際にやってみるとかなり簡単だった上に、懸念だったリンクの貼り替えもどうやら発生しなかったようでした。

 https化は一度やってしまえばやらなくていいものですが、一応備忘録として残しておこうとおもった次第です。


 では、始めます。


1:バックアップを取る
 ほとんどの場合、特に問題は発生しないと思いますが、https化をすると元に戻せなくなるのでブログのバックアップを取っておきます。

 管理画面の「設定」→「詳細設定」を開きます。
f:id:rikoubou:20201111160437p:plain

 開いたページをスクロースした先にある「記事のバックアップと製本サービス」をクリックします。
f:id:rikoubou:20201111160618p:plain

 エクスポートするボタンがあるので、それをクリックします。

 しばらくすると以下のような表示になるので「ダウンロードする」をクリックします。
f:id:rikoubou:20201111160746p:plain

 するとブログのバックアップファイルがダウンロードされます。


2:https化をする
 https化をするために、もう一度管理画面の「設定」→「詳細設定」を開きます。
f:id:rikoubou:20201111160437p:plain

 項目の中に「https配信」があり、その項目が「無効」となってるはずなので、その横にある「HTTPS配信の状況を確認する」をクリックします。
f:id:rikoubou:20201111161117p:plain

 以下のような画面になるので「有効にする」をクリックします。
f:id:rikoubou:20201111161551p:plain

 chromeの場合、以下のようなポップアップが出てくるので「OK」をクリックします。
f:id:rikoubou:20201111161645p:plain

 これでブログのhttps化は完了です。


 以上がはてなブログhttps化の手順になります。

 https化した後色々確認したのですが、自分の別記事をリンクしたもの(記述内容はhttp)でも問題なくhttpsに変更されたページを開くことができます。
 また画像に関してもhttpでアップロードされたものでも表示はできており、その画像のみのリンクを開くとhttpとなっているというだけでエラーで表示されないといったことはないようでした。画像は恐らく2016年の8月以前まではhttpとなっていたらしいので、気になる方は画像をダウンロードして再度アップロードしてリンクを貼り直せばhttpsに差し替えることができます。

 自分のブログは割と記事の数が多いので全部チェックということまではできないので、https化したことで何かおかしな表示などがありましたらコメント等いただければと思います。


・参考資料

【Raspberry Pi】Raspberry PiにSambaを入れてNASにする

 最近作った自作PCのデータ保存容量を増やそう思い内蔵HDDを購入しました。ですが「組んだPCをまた開けてHDDを増設するのが面倒だし、他のPCと共通で使えるようにしたい」と考え何か良い方法はないかと思っていたところNAS(Network Attached Storage)」というものがあると知りました。NASは簡単に言うと同じネットワーク内で接続できるファイルサーバみたいなものです。特に自宅などのWi-Fiネットワーク内にNASを構築すれば、無線でファイルサーバにアクセスできるのでケーブル要らずで便利です。さらに調べていくとRaspberry Piで簡単にNASを作れるということもわかりました。

 前置きが長々となりましたが、そんな理由で今回はRaspberry Piを使ってNASを作ってみた備忘録になります。


 では、始めます。


0:Raspberry PiとHDDの準備
 Raspberry Piを普通に使うためにSDカードにOSを書き込んで最低限の環境を構築しておきます。以下の記事の内容を行っている前提で進めていきます。

 またIPアドレスを固定していた方が何かと便利なので、以下の記事に沿ってIPアドレスを固定しておきます。

 Raspberry Piに繋げるHDDはexFATでフォーマットしたいので、以下の記事に沿ってexFATでフォーマットしておきます。


1:マウント用ディレクトリの作成とHDDの認識確認
 最初にHDDのマウント用ディレクトリを作成しておきます。

「/mnt」ディレクトリ配下であればなんでもよいと思いますが、自分の場合は「hdd01」というディレクトリ名にして作成しました。

$ mkdir /mnt/hdd01

 
 以降は「/mnt/hdd01」に外付けHDDをマウントするという前提で進めます。

 次にHDDをUSBケーブルでRaspberry Piに接続した状態で以下のコマンドを実行します。

$ sudo fdisk -l

 このコマンドを実行すると色々出てきますが「/dev/sda1」や「/dev/sda2」と書かれているものが認識されているHDDになります。

 以降は「/dev/sda1」のHDDをファイルサーバにするという前提で進めていきます。


2:exFATのHDDをマウントする
 Raspberry PiでマウントさせたいHDDがexFATでフォーマットされたもの場合、そのままマウントコマンドを実行してもエラーになります。なので以下のコマンドを実行してexFATをマウントできるようにします。

$ sudo apt-get install exfat-fuse exfat-utils

 上記コマンド実行後に以下のマウントコマンドを実行するとエラーなくマウントされます。

$ sudo mount /dev/sda1 /mnt/hdd01

 これで「/mnt/hdd01」ディレクトリ配下にファイルを作成するとHDDに保存されるようになります。

 HDDをラズパイから取り外したいときは、以下のコマンドでアンマウントさせてから取り外します。

$ sudo umount /mnt/hdd01


3:Sambaのインストール
 いよいよ本題のファイルサーバの準備に入ります。

 ファイルサーバにするために、今回はSambaをインストールします。

 Sambaのインストールは以下のコマンドを実行します。

$ sudo apt install samba samba-common-bin

 インストール途中で以下のような画面が出てきますが「いいえ」を選択します(参考ページより引用)。
f:id:rikoubou:20201021053758j:plain

 これでSambaのインストールは完了です。


4:Sambaの設定ファイルの編集
 最初に以下のコマンドでデフォルトのSamba設定ファイルのバックアップを取っておきます。

$ sudo cp /etc/samba/smb.conf /etc/samba/smb.conf_backup

 エディタはなんでもいいですが、今回はnanoを使って設定ファイルを編集していきます。

$ sudo nano /etc/samba/smb.conf

 以下の記述をファイルの最後に追記します。

[nas_hdd1]
comment = nas
path = /mnt/hdd01
public = no
read only = no
browsable = yes
force user = pi

 少し解説しておくと、各項目は以下のような意味になっています。
[nas_hdd1]:外部からアクセスした時に表示されるディレクトリ名。括弧の中の文字列は任意。
comment:アクセスした時に表示されるディレクトリ名にマウスカーソルを合わせた時に出てくる説明。
path:ファイルサーバにするディレクトリパス。
public:yesだとパスワードなしのゲストユーザでも利用可能。
read only:yesだと読み込みのみ。noだと書き込み可能。
browsable:yesだと利用可能な共有の一覧に表示。noだと非表示。
force user:ファイル操作をする時のUNIX上でのユーザ。

 詳しい説明は以下のページを参照してください。

 編集が完了したらファイルを上書き保存してエディタを終了させます。

 次に以下のコマンドでSambaを再起動させ、更新した設定ファイルを有効にします。

$ sudo systemctl restart smbd


5:Sambaのユーザとパスワード設定
 Samba用のユーザとパスワードを設定します。

「pi」という名前のユーザでSambaのパスワードを設定するコマンドは以下の通りです。これはSamba用のユーザとパスワードなのでRaspberry Piのユーザとパスワードとは関係ありません。なので別のパスワードにしておくのが良いです。

$ sudo smbpasswd -a pi

 パスワードを設定したらSambaを再起動させます。

$ sudo systemctl restart smbd

 パスワードが設定されているユーザ一覧は以下のコマンドで確認できます。今回であれば「pi:1000:」というように表示されるはずです。

$ sudo pdbedit -L

 これでSambaの設定は全て終了です。

 ちなみにRaspberry Piが起動すると自動的にSambaも起動されます。

 Sambaが起動しているかどうかは以下のコマンドで確認できます。

$ ps aux | grep smbd
root       679  7.3  3.8  44888 16824 ?        Ss   07:17   0:01 /usr/sbin/smbd --foreground --no-process-group
root       724  0.0  1.2  41964  5720 ?        S    07:17   0:00 /usr/sbin/smbd --foreground --no-process-group
root       726  0.0  1.1  41956  5272 ?        S    07:17   0:00 /usr/sbin/smbd --foreground --no-process-group
root       739  0.0  1.3  44888  6016 ?        S    07:17   0:00 /usr/sbin/smbd --foreground --no-process-group
pi         814  0.0  0.4   3900  2096 pts/0    S+   07:17   0:00 grep --color=auto smbd

 ちなみにSambaを止めるコマンドは以下の通りです。

$ sudo systemctl stop smbd


6:Macからのアクセス
  まずSambaを起動させた状態のRaspberry Piと同じWi-FiMacのPCを接続します。

 その後Finderを立ち上げて「移動」→「サーバへ接続」を選択します。
f:id:rikoubou:20201021162340p:plain

「サーバへ接続」のウインドウが表示されるので「smb://IPアドレス」を入力して「接続」ボタンをクリックします。
f:id:rikoubou:20201021163659p:plain

 ユーザ名とパスワードを要求されるので「Samba用のユーザとパスワード」を入力して「接続」ボタンをクリックします。ちなみにSambaの設定ファイルで「public = no」と設定しているのでゲストユーザでの接続はできません。
f:id:rikoubou:20201021163818p:plain

 ボリューム一覧が表示されるので、今回設定した「nas_hdd1」を選択してOKをクリックします。
f:id:rikoubou:20201021164319p:plain

 これでMacから普通のフォルダのようにアクセスできるようになります。


7:Windowsからのアクセス
  まずSambaを起動させた状態のRaspberry Piと同じWi-FiWindowsのPCを接続します。

 エクスプローラーを開いてネットワークをクリックします。
f:id:rikoubou:20201022102632p:plain

 初めてネットワークを開くと以下のように表示されますが「OK」をクリックします。
f:id:rikoubou:20201022102738p:plain

 するとエクスプローラーの上の方に赤枠で囲ったメッセージが表示されるので、この部分をクリックします。
f:id:rikoubou:20201022102843p:plain

「ネットワーク探索とファイル共有の有効化」をクリックします。
f:id:rikoubou:20201022102948p:plain

 以下の表示が出るので「はい」の方を選択します。
f:id:rikoubou:20201022103359p:plain

 エクスプローラーの右上にある更新ボタンをクリックすると現在のネットワーク上から見える機器が表示されるので、その中にある「RASPBERRYPI」をダブルクリックします。
f:id:rikoubou:20201022105305p:plain

 Macの時と同じようにユーザ名とパスワードを要求されるので「Samba用のユーザとパスワード」を入力して「OK」ボタンをクリックします。
f:id:rikoubou:20201022105502p:plain

 アクセスできるフォルダ一覧が出てくるので、今回設定した「nas_hdd1」をダブルクリックすれば普通のフォルダと同じように使うことができます。
f:id:rikoubou:20201022110340p:plain


 以上がRaspberry PiにSambaを入れてNASにする方法です。


 割と長い内容になってしまいましたが、基本的には「Sambaを入れて、ファイルサーバにするディレクトリの設定をして、ユーザを作成して、Sambaを起動させる」だけです。
 初めてSambaを触ったので戸惑った部分もありますが、コマンドを叩ける程度の知識の自分でも簡単にNASを構築できました。

 自分はRaspberry Pi Zero Wを使っており、上りは1.5MB/s、下りは4MB/sぐらいなのでちょっと遅い気もしますが、そんなに重くないファイルであれば問題なく使えます。
 もう少し速度にこだわりたいならRaspberry Pi 4を使用するのもいいかもしれません。


・参考資料

【Raspberry Pi】IPアドレスの固定

 Raspberry Pisshなどを有効にしている場合、IPアドレスを確認してからリモートログインしますが、IPアドレスを固定していないと毎回IPアドレスが変更してないかチェックしてから接続するという手間が発生します。Raspberry PiIPアドレスを固定させておけば、そのチェックの手間を減らすことができます。

 なので、今回はRaspberry PiIPアドレスの固定方法の備忘録になります。

 基本的には参考資料に挙げたページ様の通りになるので、詳しい方法はそちらを参照してください。

 またIPアドレスがぶつかっていると色々と問題が起こるので、固定したいIPアドレスが使われていないことを確認してから行うようにしてください。

 では、始めます。


1:同一ネットワーク内で使われているIPアドレスを確認する
 IPアドレスを固定する前に現在接続しているネットワーク内で使われているIPアドレス一覧を出して確認します。

 MacWindows両方とも以下のコマンドを実行すると確認できます。

$ arp -a

 以下はMacでの例です。

$ arp -a
? (192.168.1.1) at 64:3:bd:35:2f:c0 on en0 ifscope [ethernet]
? (192.168.1.3) at 7c:22:fb:d3:7d:4f on en0 ifscope permanent [ethernet]
? (220.0.0.251) at 2:0:5e:0:0:ff on en0 ifscope permanent [ethernet]
? (220.0.0.252) at 2:0:5e:0:0:ff on en0 ifscope permanent [ethernet]
? (230.255.255.250) at 8:0:5e:7f:ff:ff on en0 ifscope permanent [ethernet]

 ここに出てくる「192.168.〜」のものがローカルで使用されているIPアドレスになります。なので固定したいIPアドレスは「192.168.〜」で表示されていないものから任意に選択します。


2:Raspberry PiIPアドレス固定方法
 まずはRaspberry Piの今のIPアドレスを確認します。

$ ip a

 実行すると以下のように表示されます。
f:id:rikoubou:20201021065917p:plain

 色々出てきますが、Wi-Fiで通信している場合は「wlan0」の「inet」と書かれている「192.168.0.0/24」などと書かれている部分です。この場合はIPアドレスは「192.168.0.0」になります。

 IPアドレスが確認できたら、以下のコマンドを実行してエディタを開きます。

$ sudo nano /etc/dhcpcd.conf

 ファイルの末尾に以下の記述を追加します(以下のIPアドレスは例であり各自のものに合わせてください)。

interface wlan0
static ip_address=192.168.1.5/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1

「interface wlan0」はwlan0の設定を表しています。static ip_addressは固定したいIPアドレス、static routersはルータのIPアドレス、static domain_name_serversはDNSサーバのIPアドレスを記述します。
 記述できたらファイルを上書き保存してエディタを終了させます。

 ちなみにWindowsでルータとDNSサーバのIPアドレスを確認したい場合は、コマンドプロンプトを起動させて以下のコマンドを入力すれば確認できます。

$ ipconfig /all

 このコマンドを実行した結果にある、デフォルトゲートウェイがstatic routersのIPアドレスDHCPサーバーがstatic domain_name_serversのIPアドレスになります。

 IPアドレスの固定の設定が終了したらRaspberry Piを再起動させ、もう一度以下のコマンドを実行します。

$ ip a

 これで固定したIPアドレスになっていることが確認できます。


 以上がRaspberry PiIPアドレスを固定する方法です。

 IPアドレスが固定されているので、Raspberry Piの起動さえ確認できればディスプレイなしでもsshで接続できるので何かと便利です。


・参考資料

【Windows】HDDをexFATでフォーマットする方法

 最近データ保存用にHDDを購入しました。自分はMacWindowsどちらも使うので両方のOSに対応したフォーマットをする必要があります。

 調べると両方のOSに対応しているフォーマットは「exFAT」形式だとわかったので、今回はWindows 10でのexFATのフォーマット方法の備忘録になります。


 では、始めます。


Windows 10でexFATでフォーマットする方法
 まず最初にフォーマットしたいHDDをWindow 10のPCに接続して認識させます。

 HDDを認識した状態で、Windowsのスタートアイコンを右クリックして「ディスクの管理」または「コンピューターの管理」をクリックします。
f:id:rikoubou:20201021050346p:plain

「コンピューターの管理」を開いた場合は記憶域の中にある「ディスクの管理」をクリックします。これでディスクの管理画面が表示されます。
f:id:rikoubou:20201021050431p:plain

 新品のHDDの場合は「未割り当て」というディスクが表示されているはずなので、そこにマウスカーソルを合わせた状態で右クリックして「新しいシンプルボリューム」を選択します。
f:id:rikoubou:20201021050659p:plain

 ウィザード画面が立ち上がるので「次へ」をクリックします。
f:id:rikoubou:20201021050849p:plain

 ボリュームサイズは最大がデフォルトで設定されているはずなので、最大サイズでフォーマットする場合はそのままで「次へ」をクリックします。
f:id:rikoubou:20201021051019p:plain

 今回は外付けHDDとして使用したいので「ドライブ文字またはドライブパスを割り当てない」を選択して「次へ」をクリックします。
f:id:rikoubou:20201021051325p:plain

 ここでフォーマットが選択できるので、ファイルシステムで「exFAT」を選択します。ボリュームラベルは名前なので適当につけておきます。またクイックフォーマットにチェックを入れるのを忘れないでください。チェックを外すとフォーマットに数時間かかる場合があります。設定したら「次へ」をクリックします。
f:id:rikoubou:20201021051814p:plain

 これでフォーマットが開始されます。

 フォーマットが終了すると以下のような画面になるので「完了」をクリックして終了させます。
f:id:rikoubou:20201021052112p:plain

 未割り当てだった部分がフォーマットされ、ちゃんと「exFAT」になっていることが確認できたら終了です。
f:id:rikoubou:20201021052258p:plain


 以上がWindowsでHDDをexFATでフォーマットする方法です。

 これでMacWindows両方で使えるHDDとなったので、便利に使えそうです。


おまけ:
 すでに使用しているHDDの場合、EFIというシステム領域などが残っている場合があります。その場合は以下のページ様にあるように「Win+R」キーで「diskpart」を起動させてから削除させて容量を確保します。


・参考資料