StackStorm : st2sdkでpackを開発する

StackStormで機能を拡張するには、主にpackを作ることになります。
ここでは、StackStormの公式リポジトリにある `st2sdk` を使って、pack開発と簡単なテストを行います。

https://github.com/StackStorm/st2sdk

# 最初にまとめ

## st2sdkでできること

* packの最低限の作成を簡単に行うことができます。
* StackStormに登録するpackとして、必要最低限のテストを回すことができます。

## st2sdkでできないこと

* packの機能テストはできません(あくまでpackを作り、python的に問題ないことを確認し、そしてStackStormで問題なく読み込めることが確認できるだけ)。従って、そもそも作成しているpackが要件を満たしているかを確認することができません。

## st2sdkのメリット

いちいちStackStorm自体の環境を作る必要がありません(とはいえMongoDBとRabbitMQは必要ですが)。
環境をガッツリ作らずに、packの開発を進めることができるのは便利です。

今回はさらに簡単に環境を用意できる、Dockerfile一式を用意しています。

## st2sdkのデメリット

基本的に使う分にはデメリットはないと思います。
特にscaffold(初期pack作成)は頻繁に使うのではないでしょうか。

ただ、デメリットはないといえばないのですが、そもそも使うほどまであるのだろうか？というコマンドは存在します…。

# st2sdkを利用する

以下に、簡単に`st2sdk`を利用するための環境一式をDockerで用意しました。

https://github.com/tkit/st2sdk-dockerfiles

この後は、上記を使って実際に動かしてみます。
ここからは、公式の[st2sdk](https://github.com/StackStorm/st2sdk)のREADMEにあるコマンドを1つずつ確認してみます。

## 初期pack作成

```bash
$ st2sdk bootstrap <作成したいpackファイルのパス>
```

多分最もよく使うコマンドで、これから開発対象となるpack一式を作成してくれます。

```bash:pack_testというpackを作成
$ st2sdk bootstrap packs/pack_test
```

以下のような構成のものが簡単に作成されます。

```
packs/pack_test
├── README.md
├── actions
├── config.yaml
├── pack.yaml
└── sensors
```

## yamlのフォーマットチェック(バリデーション)

```bash
$ st2-check-validate-yaml-file <yamlファイルのパス>
```

yamlファイルが、正しくyaml形式であるかをチェックします。

```bash:packs/pack_test/pack.yamlのバリデーション
$ st2-check-validate-yaml-file packs/pack_test/pack.yaml
Validating YAML syntax for file packs/pack_test/pack.yaml...
$ echo $?
0
```

行っていることはものすごく単純で、単にPythonのpyyamlを使って、ファイルをyaml形式として読み込めるかどうかを見ているだけです。
注意しなければならないのが、yaml形式であるかどうかを見ているだけなので、このyamlがStackStorm的にどうなのかというチェックは一切行っていません。

## jsonのフォーマットチェック(バリデーション)

```bash
$ st2-check-validate-json-file <jsonファイルのパス>
```

jsonファイルが、正しくjson形式であるかをチェックします。

```bash:packs/pack_test/test.jsonのバリデーション
$ echo '{ "hoge": "fuga" }' > packs/pack_test/test.json
$ st2-check-validate-json-file packs/pack_test/test.json
Validating JSON syntax for file packs/pack_test/test.json...
$ echo $?
0
```

こちらもyamlと同じく、単純にjson形式であるかどうかだけを見ています。

## packメタデータ存在チェック

```bash
$ st2-check-validate-pack-metadata-exists <packのパス>
```

packにメタデータとなる`pack.yaml`が存在するかどうかをチェックします。
通常、packは直下に`pack.yaml`が存在しなければなりません。また、そのファイルにはそのpackの概要が記述されることになります。

```bash:メタデータの存在チェック
$ st2-check-validate-pack-metadata-exists packs/pack_test
$ echo $?
0
```

ただこのコマンドは、本当に`pack.yaml`が存在するかどうかだけしか見ません。`pack.yaml`にStackStormとして必要な記述がなされていなかったとしても何も起こらないどころか、yamlとして正常でなかったとしてもコマンドは通ります。
このコマンド、意味があるのだろうか…。

## packの登録チェック

```bash
$ st2-check-register-pack-resources <packのパス>
```

実際にStackStormに登録できるかどうかを見ます。内容に不備があるとエラーになります。このコマンドのために、実際にStackStormが動作する環境を用意する必要はありません。代わりに、以下が必要です。

* StackStorm用のRabbitMQ(初期状態でOK)
* StackStorm用のMongoDB(初期状態でOK)
* このコマンドで利用する`st2.tests.conf`ファイルで、上記リソースにアクセスするための設定が書かれていること
    * `/usr/bin` や `/usr/local/bin` 配下に上記confファイルがあるようです。(confなのに)
* 公式[st2](https://github.com/StackStorm/st2)リポジトリがcloneされており、 `ST2_REPO_PATH`環境変数がそこを向いていること

これらの設定は、今回用意したDocker環境では全て設定済みですので、興味があれば[Dockerfile](https://github.com/tkit/st2sdk-dockerfiles/blob/master/dev/Dockerfile)をご覧ください。

このコマンドは、初回実行時にst2リポジトリの`requirements.txt`を読んで、必要なモジュールをインストールするため、初回実行時は時間がかかります。

```bash:公式st2contribのcsvpackを登録テストする
$ git clone https://github.com/StackStorm/st2contrib.git
Cloning into 'st2contrib'...
remote: Counting objects: 21369, done.
remote: Compressing objects: 100% (269/269), done.
remote: Total 21369 (delta 157), reused 0 (delta 0), pack-reused 21096
Receiving objects: 100% (21369/21369), 6.39 MiB | 658.00 KiB/s, done.
Resolving deltas: 100% (13351/13351), done.
Checking connectivity... done.
$ cd st2contrib/
$ st2-check-register-pack-resources packs/csv
Registering content from pack csv
2016-10-09 17:09:04,853 INFO [-] Connecting to database "st2" @ "mongo:27017" as user "None".
2016-10-09 17:09:04,972 INFO [-] =========================================================
2016-10-09 17:09:04,972 INFO [-] ############## Registering triggers #####################
2016-10-09 17:09:04,973 INFO [-] =========================================================
2016-10-09 17:09:04,980 INFO [-] Registered 0 triggers.
2016-10-09 17:09:04,980 INFO [-] =========================================================
2016-10-09 17:09:04,980 INFO [-] ############## Registering sensors ######################
2016-10-09 17:09:04,980 INFO [-] =========================================================
2016-10-09 17:09:04,980 INFO [-] Registered 0 sensors.
2016-10-09 17:09:04,981 INFO [-] =========================================================
2016-10-09 17:09:04,981 INFO [-] ############## Registering actions ######################
2016-10-09 17:09:04,981 INFO [-] =========================================================
2016-10-09 17:09:05,219 INFO [-] Registered 2 actions.
2016-10-09 17:09:05,219 INFO [-] =========================================================
2016-10-09 17:09:05,219 INFO [-] ############## Registering rules ########################
2016-10-09 17:09:05,219 INFO [-] =========================================================
2016-10-09 17:09:05,230 INFO [-] Registered 0 rules.
2016-10-09 17:09:05,230 INFO [-] =========================================================
2016-10-09 17:09:05,230 INFO [-] ############## Registering aliases ######################
2016-10-09 17:09:05,230 INFO [-] =========================================================
2016-10-09 17:09:05,230 INFO [-] Registered 0 aliases.
2016-10-09 17:09:05,230 INFO [-] =========================================================
2016-10-09 17:09:05,231 INFO [-] ############## Registering policy types #################
2016-10-09 17:09:05,231 INFO [-] =========================================================
2016-10-09 17:09:05,252 INFO [-] Registered 3 policy types.
2016-10-09 17:09:05,253 INFO [-] =========================================================
2016-10-09 17:09:05,253 INFO [-] ############## Registering policies #####################
2016-10-09 17:09:05,254 INFO [-] =========================================================
2016-10-09 17:09:05,254 INFO [-] Registered 0 policies.
2016-10-09 17:09:05,254 INFO [-] =========================================================
2016-10-09 17:09:05,254 INFO [-] ############## Registering configs ######################
2016-10-09 17:09:05,254 INFO [-] =========================================================
2016-10-09 17:09:05,255 INFO [-] Registered 0 configs.
```

### 注意

#### `st2sdk bootstrap` で作成した直後のpackをテストするとエラーになる

タイトルの通りなのですが、下記の通り、 `st2sdk bootstrap`コマンドで作成したpackを、何の変更もせずにテストにかけると、途中でエラーになります。

```bash
st2-check-register-pack-resources packs/pack_test
Registering content from pack pack_test
2016-10-09 16:30:04,399 INFO [-] Connecting to database "st2" @ "mongo:27017" as user "None".
2016-10-09 16:30:08,357 INFO [-] =========================================================
2016-10-09 16:30:08,358 INFO [-] ############## Registering triggers #####################
2016-10-09 16:30:08,358 INFO [-] =========================================================
2016-10-09 16:30:08,377 ERROR [-] Failed to register pack "pack_test"
Traceback (most recent call last):
  File "/tmp/st2/st2common/st2common/bootstrap/base.py", line 117, in register_pack
    pack_db, _ = self._register_pack(pack_name=pack_name, pack_dir=pack_dir)
  File "/tmp/st2/st2common/st2common/bootstrap/base.py", line 132, in _register_pack
    pack_db = self._register_pack_db(pack_name=pack_name, pack_dir=pack_dir)
  File "/tmp/st2/st2common/st2common/bootstrap/base.py", line 164, in _register_pack_db
    pack_db = Pack.add_or_update(pack_db)
  File "/tmp/st2/st2common/st2common/persistence/base.py", line 163, in add_or_update
    model_object = cls._get_impl().add_or_update(model_object)
  File "/tmp/st2/st2common/st2common/models/db/__init__.py", line 252, in add_or_update
    instance.save()
  File "/usr/local/lib/python2.7/site-packages/mongoengine/document.py", line 304, in save
    self.validate(clean=clean)
  File "/usr/local/lib/python2.7/site-packages/mongoengine/base/document.py", line 413, in validate
    raise ValidationError(message, errors=errors)
ValidationError: ValidationError (PackDB:None) (Field is required: ['description'])
2016-10-09 16:30:08,378 INFO [-] Registered 0 triggers.
2016-10-09 16:30:08,379 INFO [-] =========================================================
2016-10-09 16:30:08,379 INFO [-] ############## Registering sensors ######################
2016-10-09 16:30:08,379 INFO [-] =========================================================
2016-10-09 16:30:08,385 INFO [-] Registered 0 sensors.
2016-10-09 16:30:08,385 INFO [-] =========================================================
2016-10-09 16:30:08,385 INFO [-] ############## Registering actions ######################
2016-10-09 16:30:08,386 INFO [-] =========================================================
2016-10-09 16:30:08,550 INFO [-] Registered 0 actions.
2016-10-09 16:30:08,551 INFO [-] =========================================================
2016-10-09 16:30:08,551 INFO [-] ############## Registering rules ########################
2016-10-09 16:30:08,551 INFO [-] =========================================================
2016-10-09 16:30:08,639 INFO [-] Registered 0 rules.
2016-10-09 16:30:08,639 INFO [-] =========================================================
2016-10-09 16:30:08,639 INFO [-] ############## Registering aliases ######################
2016-10-09 16:30:08,639 INFO [-] =========================================================
2016-10-09 16:30:08,641 INFO [-] Registered 0 aliases.
2016-10-09 16:30:08,641 INFO [-] =========================================================
2016-10-09 16:30:08,641 INFO [-] ############## Registering policy types #################
2016-10-09 16:30:08,641 INFO [-] =========================================================
2016-10-09 16:30:08,714 INFO [-] Registered 3 policy types.
2016-10-09 16:30:08,715 INFO [-] =========================================================
2016-10-09 16:30:08,715 INFO [-] ############## Registering policies #####################
2016-10-09 16:30:08,715 INFO [-] =========================================================
2016-10-09 16:30:08,722 INFO [-] Registered 0 policies.
2016-10-09 16:30:08,722 INFO [-] =========================================================
2016-10-09 16:30:08,722 INFO [-] ############## Registering configs ######################
2016-10-09 16:30:08,723 INFO [-] =========================================================
2016-10-09 16:30:08,725 INFO [-] Registered 0 configs.
$ echo $?
0 # 戻り値0...?
```

読んで字のごとしなのですが、`description`が空になっているため、エラーになります。

```yaml:packs/pack_test/pack.yaml
---
name: packs/pack_test
description: # ここが空欄になっているからエラー
version: 0.1
author: John Doe
email: john.doe@example.com
```

じゃあそれ`st2-check-validate-yaml-file`コマンド側でバリデーションしてくれればいいんじゃないの？と思わなくもないです。
あと最大の問題は、これが戻り値0(つまり正常終了扱い)であることです。

#### 必ず、コマンド実行時にカレントディレクトリ配下に`packs/<パック名>` という構成で実行しなければならない

例えば、テストしたい対象のpackが`/root/st2contrib/packs/csv` というパスに存在するものだった場合、**必ず `/root/st2contrib/` に移動してから**コマンドを実行する必要があります。つまり、カレントディレクトリの直下に`packs`というディレクトリが存在する位置にいなければなりません。

つまり、以下のようになります。

```bash:うまく動く例
$ cd /root/st2contrib
$ st2-check-register-pack-resources packs/csv
```

カレントディレクトリがこれ以外だと(actionを書いている場合)必ず失敗します。

```bash:うまく動かない例
$ cd /tmp
$ st2-check-register-pack-resources /root/st2contrib/packs/csv
Registering content from pack csv
2016-10-09 17:30:03,078 INFO [-] Connecting to database "st2" @ "mongo:27017" as user "None".
2016-10-09 17:30:03,169 INFO [-] =========================================================
2016-10-09 17:30:03,169 INFO [-] ############## Registering triggers #####################
2016-10-09 17:30:03,169 INFO [-] =========================================================
2016-10-09 17:30:03,176 INFO [-] Registered 0 triggers.
2016-10-09 17:30:03,176 INFO [-] =========================================================
2016-10-09 17:30:03,176 INFO [-] ############## Registering sensors ######################
2016-10-09 17:30:03,176 INFO [-] =========================================================
2016-10-09 17:30:03,177 INFO [-] Registered 0 sensors.
2016-10-09 17:30:03,177 INFO [-] =========================================================
2016-10-09 17:30:03,177 INFO [-] ############## Registering actions ######################
2016-10-09 17:30:03,177 INFO [-] =========================================================
2016-10-09 17:30:03,389 WARNING [-] Failed to register actions: Content pack "csv" is not found or doesn't contain actions directory. Searched in: packs/
Traceback (most recent call last):
  File "/tmp/st2/st2common/bin/st2-register-content", line 22, in <module>
    sys.exit(content_loader.main(sys.argv[1:]))
  File "/tmp/st2/st2common/st2common/content/bootstrap.py", line 354, in main
    register_content()
  File "/tmp/st2/st2common/st2common/content/bootstrap.py", line 313, in register_content
    register_actions()
  File "/tmp/st2/st2common/st2common/content/bootstrap.py", line 195, in register_actions
    raise e
st2common.exceptions.apivalidation.ValueValidationException: Content pack "csv" is not found or doesn't contain actions directory. Searched in: packs/
Registering resources for pack csv failed
$ echo $?
1 # 戻り値も1...
```

どうやら、action登録時に、actionはカレントディレクトリ直下の`packs`を見に行っているためのようです。これは、`st2.tests.conf`に以下の記載があるためです。

```
[content]
system_packs_base_path =
packs_base_paths = packs/ # この記述のせい
```

もちろんこれを変更すれば動きます。

## pythonファイルのlint(静的解析)

```bash
$ st2-check-pylint-pack <packのパス>
```

[pylint](https://www.pylint.org/)をかけて、コードの静的解析を行います。
ただこれを動かすためには、以下の条件があります。

* virtualenv環境であること、しかも環境名が`virtualenv`であること
* lintの設定ファイルは`./lint-configs/python/.pylintrc` に格納されていること

前者は、[ここ](https://github.com/StackStorm/st2sdk/blob/master/scripts/st2-check-pylint-pack#L48)にソースがあるのですが、がっつり以下のように書かれているため、環境名が`virtualenv`に縛られます。

```bash:普通は環境名/bin/pythonではないのかな…
if [[ ${PYTHON_BINARY} != *"virtualenv/bin/python" ]]; then
    echo "Script must run under a virtual environment which is created in the Make target"
    exit 2
fi
```

後者も[ソースの記述](https://github.com/StackStorm/st2sdk/blob/master/scripts/st2-check-pylint-pack#L99)によるもので、st2contribはその構成ではあるのですが、なんでそこに固定するのか…。

```bash:pylintrcの位置が固定されている
if [ ${ACTION_PYTHON_FILES_COUNT} -gt 0 ]; then
    find ${PACK_PATH}/actions -name "*.py" -print0 | xargs -0 ${PYTHON_BINARY} -m pylint -E --rcfile=./lint-configs/python/.pylintrc && echo "--> No pylint issues found in actions." || exit $?
fi
```

これらは、今回作ったDockerfileでも対応していません。一応、st2contribに対して動かすには以下のようにすれば動きます。

```
$ virtualenv virtualenv
New python executable in /root/virtualenv/bin/python2
Also creating executable in /root/virtualenv/bin/python
Installing setuptools, pip, wheel...done.
$ source virtualenv/bin/activate
(virtualenv) $ cd st2contrib/
(virtualenv) $ st2-check-pylint-pack packs/csv
Running pylint on pack: csv
--> No pylint issues found in actions.
```


## テストのカバレッジ計測 

```bash
$ st2-check-print-pack-tests-coverage <packのパス>
```

作成したpack中のactionに対応するテストが存在するか、カバレッジを計測します。

```
$ st2-check-print-pack-tests-coverage st2contrib/packs/csv

====== st2contrib/packs/csv ======

Python Actions missing tests:
	format.py (st2contrib/packs/csv/tests/test_action_format.py not found)

Python Tests with no actions:

Python Test Coverage:
	Actions: 1/2
```

ただこれは、`test_action_<action名>.py`というファイルが存在するかどうかしか見ていません。
中のテストがなんであっても、最悪空ファイルであったとしても、カバレッジとしてカウントされてしまいます。
命名規則自体は[こちら](https://docs.stackstorm.com/development/pack_testing.html)に従っているものではあるので正しいのですが、とはいえカバレッジってそういうものなの…？という気がしてなりません。

# 最後に所感など

とにかく提供されるコマンドが、内部にパスを埋め込みまくりで、開発だからといって自由なパスにpackを置いてしまうと基本的に動きません。これが、`st2sdk`の`README.md` にも書かれていないせいで、謎のエラーで止まることが何度も何度も何度も…。本当に辛かった…。

コマンドを提供してくれること自体はありがたいのですが、これ使うの？使えるの？といういまいち重要度が謎なものもいっぱいあり、scaffold以外は「とりあえず」的な位置付けであることは否めません。
CIにこれらのコマンドをとりあえず突っ込んでおくくらいがいいのかもしれません。