RyuによるOpenFlow入門 (2)

前回の続き.今回はMACアドレス学習機能のある,スイッチングハブを作成する.
前回同様,OpenFlowのバージョンは1.0を使用します.

スイッチングハブ

ソースコードは以下の通り.

L2のフレームをスイッチの他のポートに送信するという基本機能は前回実装したリピータハブと変わらない.
そのため,プログラムの大枠は同じである.

リピータハブとスイッチングハブの違いとして,スイッチングハブではMACアドレスの学習機能があるという点がある.
このため,一度MACアドレスの学習をした後は,そのMACアドレス宛のフレームはその機器が接続されているポートのみに送信するようになる.
(リピータハブでは,全てのフレームを全てのポート(フレームが入ってきたポート以外)に送信する)

では,プログラムを見てみる.
なお,リピータハブのプログラムと似たような部分は省略します.

L2Switch クラスのインスタンスを作成する際に,MACアドレスとポートの対応付けをするためのディクショナリ( mac_to_port )を作成しておく(19行目).
MACアドレスを学習したら,この辞書にポートとの対応付けを登録していく.

def __init__(self, *args, **kwargs):
    super(L2Switch, self).__init__(*args, **kwargs)
    self.mac_to_port = {}

MACアドレスの学習は,Packet Inでスイッチからのデータを受け取ったときに行う.
まず,スイッチから入ってきたフレームのMACアドレスとポートの対応付けを mac_to_port に登録する(63行目).

self.mac_to_port[dpid][src] = msg.in_port

その後,出力ポートを決定するために,宛先MACアドレスの対応付け情報が mac_to_port の中に存在するかどうかを確認する(66行目〜70行目).
もし存在していたら,見つかったポートを出力ポートとしてPacket Outメッセージを作成する.
もし存在していなかったら,全てのポートを出力ポートとしてPacket Outメッセージを作成する.

ofproto = dp.ofproto
if dst in self.mac_to_port[dpid]:
    out_port = self.mac_to_port[dpid][dst]
else:
    out_port = ofproto.OFPP_FLOOD

加えて,もし宛先MACアドレスの対応付け情報が存在していた場合,同じ宛先MACアドレスのフレームが再度コントローラに来るのを防ぐため,FlowModメッセージを使ってスイッチにフローエントリを書き込む(76行目〜77行目).
FlowModを使うのは,毎回コントローラにフレームが来てしまうとその分パフォーマンスが落ちてしまうため,それを避けるため.

if out_port != ofproto.OFPP_FLOOD:
    self.add_flow(dp, msg.in_port, dst, src, actions)

RyuによるOpenFlow入門 (1)

Ryuを使って簡単なL2スイッチを作ってみた.
Ryuについての理解が浅い状態でいきなりMACアドレスの学習機能を持ったL2スイッチを作成するのは難しいかと思ったので,まずMACアドレス学習機能のない単純なL2スイッチ(リピータハブ)を作成し,その後スイッチングハブを実装してみる.

リピータハブ

ソースコードは以下の通り.

OpenFlowスイッチに入ってきたパケットは,スイッチ内にあるフローテーブルを参照し,テーブル内にマッチするエントリがなければOpenFlowコントローラへPacket Inメッセージを送出する.
リピータハブでは,入ってきたパケットをそれ以外のポート全てにそのまま送信する(フラッディング)ので,Packet Inで入ってきたデータをそのまますべてのポートに向けて送ってあげれば良い.

コントローラからPacket Outメッセージを送ることで,OpenFlowスイッチからパケットを送出することができる.
これを利用して,データのフラッディングを行う.

Packet Outメッセージの作成部分は次のようになる.

actions = [ofp_parser.OFPActionOutput(ofp.OFPP_FLOOD)]
out = ofp_parser.OFPPacketOut(
    datapath=dp, buffer_id=msg.buffer_id, in_port=msg.in_port,
    actions=actions
)

Packet Outメッセージでは,アクションを指定することで,パケットの出力先を指定することができる.
今回は入力ポート以外全てに送りたいため, OFPActionOutput の引数として ofp.OFPP_FLOOD を指定する.

作成したアクション等の情報を OFPPacketOut の引数として与えてあげ,Packet Outメッセージを作成する.
指定した引数は次のような感じ.

  • datapath : データパス.OpenFlowスイッチを表す(みたい).それぞれのOpenFlowスイッチはユニークなID(Datapath ID)を持つ.
  • buffer_id : OpenFlowスイッチにバッファされているデータのID.
  • in_port : パケットの受信ポート.
  • actions : 上で作ったアクションを指定する.

最後に,作成したメッセージを send_msg() で送信してあげれば完了.

mininetを使って動作確認を行ってみる.

$ ryu-manager dumb_l2_switch.py

$ mn --switch ovs --controller remote
...
*** Starting CLI:
mininet> pingall
*** Ping: testing ping reachability
h1 -> h2
h2 -> h1
*** Results: 0% dropped (2/2 received)

ちゃんと疎通しているみたい.

スイッチングハブについては違う記事に分けて書きます.

SharePoint REST APIを使ってみた

SharePoint OnlineでサイトにアップロードしているExcelファイルを毎日自動でダウンロードしたいという要望をもらったので,
公開されているAPIを使ってやってみた.

環境

  • OS X El Capitan v10.11.6
  • Python 3.5.2
  • Office 365 Enterprise E3

Office 365 APIを使う

認証情報を取得する

Office 365 APIを使用するには,まずアクセストークンと呼ばれる認証情報を取得する必要がある.
次の手順でアクセストークンを取得する.

なお,今回はOffice 365 Enterprise E3を使用し,すでにSharePointでサイトを作成していることとする.

  1. 新Azure PortalにOffice 365の管理者アカウントでログインする.
  2. 左側のメニューから「Azure Active Direcotory」を選択する.
  3. メニューから「アプリの登録」を選択する.
  4. ページ上部にある「追加」を選択して新規アプリケーションを作成する.
    • 名前: 好きなものを
    • アプリケーションの種類: Webアプリ/API
    • サインオンURL: 作成したアプリケーションを動かすURL
  5. アプリケーションの作成後,作成したアプリケーションを選択して,以下の情報をメモしておく.
    • アプリケーションID
    • ホームページ(サインオンURLで指定したものになっているはず)
  6. 右側の設定メニューから「キー」を選択して新しくキーを作成する.
    キーの説明を入力して期間をドロップダウンリストから選択し,保存をクリックするとキーが生成される.
    この時キーを 必ず メモしておくこと(この画面から移動した後はキーを再確認できない).
  7. 設定メニューから「必要なアクセス許可」を選択してアプリケーションに対して必要な権限を付与する.
    今回は,SharePointの機能を使いたいので,上部にある「追加」をクリック後,表示されるサービス名の中から
    「Office 365 SharePoint Online」を選択し,必要な権限を追加する.
  8. アクセストークンを生成するために必要な code を取得する.次のURLにGETでリクエストを送信する.
    • URL: https://login.windows.net/common/oauth2/authorize?response_type=code&client_id=<client_id>&resource=<resource>&redirect_uri=<redirect_uri>
    • パラメータ
      • client_id : アプリケーションID
      • resource : https://<テナント名>.sharepoint.com/
        (例: テナント名が testtenanthttps://testtenant.sharepoint.com/
      • redirect_uri : サインオンURL
  9. 正しくリクエストを送れていればログイン画面が表示され,ログインが成功すると redirect_uri で指定したURIにリダイレクトされ,パラメータに code がセットされる.
  10. 取得した code を用いてアクセストークンを取得する.次のURLにPOSTでリクエストを送信する.
    • URL: https://login.windows.net/common/oauth2/token
    • HTTPヘッダに追加: Content-Type: application/x-www-form-urlencoded
    • リクエストボディ: grant_type=authorization_code&code=<code>&client_id=<client_id>&client_secret=<client_secret>&redirect_uri=<redirect_uri>
    • パラメータ
      • code : 先程取得したもの
      • client_secret : アプリケーション作成時に生成したキー
  11. 正しくリクエストを送れていれば, access_token を含むJSONが返却される.

APIを叩く

上記の手順で取得したアクセストークンを使用して実際にAPIを使用した.
APIを叩くときには,HTTPヘッダに取得したアクセストークンを以下のようなフォーマットで加える必要がある.

Authorization: Bearer <access_token>

今回はSharePointのサイト上にアップロードしているExcelファイルをダウンロードするのが目的なので,
それっぽいAPIをリファレンスから探し出して叩いてみた.

SharePoint 2013 REST API リファレンスによれば,
Fileというリソースがサイト内のファイルを表していて,そのファイルを取得するには次のようなURIを指定すれば良い.

http://<site_url>/_api/web/getfilebyserverrelativeurl('/<folder>/<file>')

また,ファイル自体をダウンロードするためには, $value というODataのクエリオプションを付加する.

というわけで,Pythonで簡単にAPIを叩く.なお,以下のプログラムではRequestsという外部ライブラリを使用しているので注意.

import requests
import urllib.parse

def main():
    # ファイル自体を取得するため,$valueを付加
    uri = "https://<tenant>.sharepoint.com/_api/web/getfilebyserverrelativeurl('<file-path>')/$value"
    access_token = sys.argv[1]
    headers = {
        'accept': 'application/json;odata=verbose',
        'Content-Type': 'application/json;odata=verbose',
        'Authorization': 'Bearer ' + access_token,
    }
    res = requests.get(urllib.parse.quote(uri, safe=':/'), headers=headers, stream=True)
    with open('file.xlsx', 'wb') as f:
        f.write(res.raw.read())


if __name__ == '__main__':
    main()

これを実行すると,URIに指定したファイルが file.xlsx という名前でダウンロードされる.

まだ未完成なところ

  • 現時点では code を取得する際にブラウザを使う必要がある
    • Requestsモジュールの Session を使えばできるようなので後で修正する

参考にしたところ

PythonからMySQLを使う

DjangoでデータベースにMySQLを使用するときはmysqlclientを使用することが推奨されている.Djangoが勝手にデータベースに接続などの処理をしてくれるのでモジュールの使い方は知らなくても使うことは可能だが,せっかくなら使い方もわかるほうが良いので調べてみた.

基本的に,Python標準ライブラリのsqlite3と使い方は同じ.
まずコネクションオブジェクトを作成し,そこからカーソルオブジェクトを作る.できたカーソルオブジェクトを使って様々なクエリを実行する.

インストール

$ pip install mysqlclient

MySQLに接続する

conn = MySQLdb.connect(
    user='username',
    passwd='password',
    host='host',
    db='dbname'
)

返り値はコネクションオブジェクト.userpasswd は名前の通り.MySQLに登録されているユーザー情報を記述する. host はデータベースの置いてある場所を指定する.ローカルのMySQLに接続する場合は localhost を指定する. db には使用するデータベース名を指定する.

カーソルオブジェクトの作成

c = conn.cursor()

MySQLdb.connect で作成したオブジェクトを使ってカーソルオブジェクトを作成する.

クエリの実行

c.execute(query)

query に指定したクエリを実行する.

プレースホルダ

クエリ中に %s を記述すると,プレースホルダとして扱える.ここに値を埋め込む場合は,与えたい値を execute() の第2引数にタプルで渡す.

c.execute('select * from test where id = %s', (2,))

レコードの取得

execute() でselect文を実行した後,レコードを得るためには以下のいずれかを使用する.

  • fetchone() : レコードを1件取得
  • fetchmany(n) : レコードをn件取得
  • fetchall() : レコードをすべて取得

データベースへの変更を保存

conn.commit()

このメソッドを呼び出すことで,変更を保存できる. これを呼び出し忘れると,追加・削除などの変更が破棄される ので注意.

このメソッドはカーソルオブジェクトではなく,コネクションオブジェクトが持っていることにも注意.

サンプルコード

# coding: utf-8

import MySQLdb


def main():
    conn = MySQLdb.connect(
        user='testuser',
        passwd='testuser',
        host='192.168.33.3',
        db='testdb'
    )
    c = conn.cursor()

    # テーブルの作成
    sql = 'create table test (id int, content varchar(32))'
    c.execute(sql)
    print('* testテーブルを作成\n')

    # テーブル一覧の取得
    sql = 'show tables'
    c.execute(sql)
    print('===== テーブル一覧 =====')
    print(c.fetchone())

    # レコードの登録
    sql = 'insert into test values (%s, %s)'
    c.execute(sql, (1, 'hoge'))  # 1件のみ
    datas = [
        (2, 'foo'),
        (3, 'bar')
    ]
    c.executemany(sql, datas)    # 複数件
    print('\n* レコードを3件登録\n')

    # レコードの取得
    sql = 'select * from test'
    c.execute(sql)
    print('===== レコード =====')
    for row in c.fetchall():
        print('Id:', row[0], 'Content:', row[1])
    
    # レコードの削除
    sql = 'delete from test where id=%s'
    c.execute(sql, (2,))
    print('\n* idが2のレコードを削除\n')

    # レコードの取得
    sql = 'select * from test'
    c.execute(sql)
    print('===== レコード =====')
    for row in c.fetchall():
        print('Id:', row[0], 'Content:', row[1])

    # データベースへの変更を保存
    conn.commit()
    
    c.close()
    conn.close()


if __name__ == '__main__':
    main()

実行結果

* testテーブルを作成

===== テーブル一覧 =====
('test',)

* レコードを3件登録

===== レコード =====
Id: 1 Content: hoge
Id: 2 Content: foo
Id: 3 Content: bar

* idが2のレコードを削除

===== レコード =====
Id: 1 Content: hoge
Id: 3 Content: bar

Python3にOpenCVをインストールした

環境はOS X Yosemite 10.10.5.

Python3はHomebrewからインストールしたものを使用した.

Homebrewが入っていない場合はHomebrew公式の「インストール」の部分に書いてあるコマンドを実行してインストールしておく.

HomebrewからOpenCVをインストール

下記コマンドを実行してOpenCVをインストールする.

$ brew tap homebrew/science
$ brew install opencv3 --with-python3

インストール後,Homebrewの指示に従ってパスを通す.(<username>の箇所は自分のユーザー名に置き換える)

echo /usr/local/opt/opencv3/lib/python2.7/site-packages >> /usr/local/lib/python2.7/site-packages/opencv3.pth
mkdir -p /Users/<username>/.local/lib/python3.5/site-packages
echo 'import site; site.addsitedir("/usr/local/lib/python2.7/site-packages")' >> /Users/<username>/.local/lib/python3.5/site-packages/homebrew.pth

ここまで完了したら,下記コマンドで確認.バージョンが出ればインストール完了.

$ python -c 'import cv2; print(cv2.__version__)'
3.1.0

※Homebrewでは上記のように出てきたが,自分の環境では実際に実行してもOpenCVのパスは通らなかった.上のコマンドの代わりに下記のように実行するとうまく行った.

echo /usr/local/opt/opencv3/lib/python3.5/site-packages >> /usr/local/lib/python3.5/site-packages/opencv3.pth
mkdir -p /Users/<username>/.local/lib/python3.5/site-packages
echo 'import site; site.addsitedir("/usr/local/lib/python3.5/site-packages")' >> /Users/<username>/.local/lib/python3.5/site-packages/homebrew.pth