Write your blackbird plugin #002

今回はBlackbird pluginを書こう第二弾ってことで実際に簡単なpluginの書き方を紹介します！
各メソッドの細かい説明は第三弾に委ねるってことで、第一弾、第二弾、第三弾とそれぞれかぶっちゃうこともあるかもですが、
みなさん生ぬるい目で見守ってくださいまし。

outline
-------

ざっくりした手順としては、

1. pluginの基底クラスを入手
2. 具象クラスを作成
3. Mainプロセスから生成されたThread上で実行されるメソッドの実装
4. Queueに入れるアイテムの形式
5. コンフィグのValidation設定

こんな感じです。


1. pluginの基底クラスを入手
------------------------

基底クラスはblackbird本体に内包されていて、

```bash
pip install blackbird
```

で簡単に入手できます。


2. 具象クラスを作成

っで、あなたが作成するプラグイン側で

```python

# blackbird.plugins.base moduleをimport
import blackbird.plugins.base


# JobBase抽象クラスを継承
class ConcreteJob(blackbird.plugins.base.JobBase):
    pass
```

と指定すれば、具象クラスの作成は完了です。
注意するポイントとしては、**クラス名はConcreteJobで固定**なので、違うクラス名にすると動かないです。


3. Mainプロセスから生成されたThread上で実行されるメソッドの実装
------------------------------------------------------

アイテムをQueueに入れるとき、Mainのプロセスは**ConcreteJob.build_items**を呼び出せることを期待しています。
コード的には

```python

import YOUR_MODULE

YOUR_MODULE.ConcreteJob.build_items()
```

が実行できないと実際にアイテムはQueueにはつまれません。そのため**必ずbuild_itemsという名前でメソッドを実装してください**。
その際、基底クラス側にはenqueueメソッドを実装してあるので、build_itemsの中で親クラスのenqueueメソッドを実行するか、
具象クラス側でenqueueメソッドを実装したい場合は、overrideしちゃうか、_\_enqueue_メソッドを実装してください。
今までの話をまとめると、

```python

import blackbird.plugins.base


class ConcreteJob(blackbird.plugins.base.JobBase):

    def build_items(self):
        # Queueに入れるアイテム名とQueueにはついては後述
        items = {
            'HOGEHOGE': 'FUGAFUGA'
        }
```

こんな感じになるかと。


4. Queueに入れるアイテムの形式
--------------------------

っで、実際に入れるQueueのアイテムの形式を合わせてやれば概ね機能としては実装完了です。
Queueに入れるアイテムはすごいシンプルでPythonのdict形式を採用してます。具体的には

```python

ITEM = {
    'key': 'ANYTHING_OK',
    'value': '1.0000',
    'host': 'HOSTNAME_ON_ZABBIX_SERVER',
    'clock': datetime.datetime()
}
```

こんな感じになってます。

### key

_key_はzabbixのテンプレート上のkeyで、たとえば使い慣れてるものだと`agent.ping`みたいなものをイメージしてもらえればと思います。
なので、あまりabstractなkey名を指定しちゃうとかぶっちゃう可能性高いのでちょっと冗長になったり長くなっちゃうかもですが、
識別しやすいものが吉かと。

### value

この値はsenderが送り先に(いまんとこzabbix serverだけですが)送る実際の値です。
これは説明する必要ってあんまりないとは思うんですが、1 or 0なflag系の値でもいいですし、
1.234567みたいな小数点でも、文字列でもなんでもいい感じです。ただいろんなインテグレーション考えると、浮動小数にしとくのが無難かと思います。

### host

いまんとこzabbix serverが送り先なことを想定してるのでhostはZabbix Server上でのHostの名前です。
これでどのHostに対して値を送りたいかを指定します。
ただ、外から注入するような仕組みを作りにくいと思うので、後述するconfigのValidationと
連携してconfigに指定したhostnameの値がここの値に入ります。

### clock

clockはPythonの`datetime.datetime`インスタンスです。
これも、いちいちPlugin側でインテグレーションするのめんどくさかったので、基底クラス側で、
インスタンス生成時の時刻を`clock`のvalueとして入れるように設定しました。


4.5. 実はItemも自分でインテグレーションしなくていいですwww
--------------------------------------------------

_4._のところにも書いてたので、だいたいご察しかと思いますが、Itemはdict objectをreturnするような`ItemBase`クラスを定義してあります。
なので、

* clockの実装
    + 取得時刻を設定した`datetime.datetime`インスタンスを作成する必要はなし
* hostの実装
    + 後述しますが、`self.config`にはdict形式でconfigファイルに書いた内容が格納されてます。

はplugin開発者側で実装する必要はないです。

### blackbird.plugins.base.ItemBase クラスについて

このクラスは先程もちょろっと触れましたが、dict型オブジェクトをreturnするような抽象クラスです。
特に難しいことはやってなくって、`__init__`で`_generate`を呼び出してて、その中でdictを生成してます。
_returnするような_と書いたのは、実際にはdataっていう名前のgetterメソッドを``@property`して、`ConcreteItem.data`でdictをreturnするようにしてます。

ちょっと細かく書きすぎましたかね.....この辺の細かすぎる話は次の第三弾でkwsk!!

っでここまでの話をまとめると、

```python
import blackbird.plugins.base

class ConcreteJob(blackbird.plugins.base.JobBase):

    def build_items(self):
        item=ConcreteItem(
           key='isPublished',
           value=1,
           #self.configについてはValidationと一緒に後述しますね。
           host=self.config.get('hostname')
        )
        self.enqueue(
            item=item
        )

class ConcreteItem(blackbird.plugins.base.ItemBase):

    def __init__(self, key, value, host):
        super(ConcreteItem, self).__init__(key, value, host)

        self.__data = dict()
        self._generate()

    @property
    def data(self):
        return self.__data

    def _generate(self):
        """
        例えば、keyに特定のprefixをつけたければ、こんな感じで
        self.__data['key'] = 'YOUR_PREFIX.' + self.key
        ここに書けばおｋ
        """
        self.__data['key'] = self.key
        self.__data['value'] = self.value
        self.__data['host'] = self.host
        self.__data['clock'] = self.clock

```

こんな感じでConcreteItemを生成してQueueに入れて、zabbix\_sender pluginに送信させることができます。


5. コンフィグのValidation設定
--------------------------

Validationは実は設定しなくてもおｋなんです。
コンフィグファイルに書いちゃえばそのkeyとvalueがそのまま`blackbird.plugins.base.JobBase.config`に
dict型オブジェクトとして格納されます。たとえば、

```ini
[MY_BLACKBIRD_PLUGIN]
hostname = hogehoge.com
module = MODULE_NAME
```

って書けば、それだけで`self.config['hostname']`とかすればconfig内の値にアクセスできます。
ただ、必須の値とか欲しいじゃないですか、そこでValidationです。
Validationオブジェクトも基底クラス側でインテグレーションしてありまして、 文字列を使ってValidationの設定を書く感じです。

実際に書いてみると、

```python
class Validator(blackbird.plugins.base.ValidatorBase):
    def __init__(self):
        self.__spec = None

    @property
    def spec(self):
        self.__spec = (
            "[0]".format(__name__),
            "required_parameter = string()",
            "not_required_paramter = string(default='DEFAULT_STRING')"
        )
        return self.__spec
```

こんな感じです。**class名はValidator固定**です。ちょっといろいろ詰め込んでてややこしくなっちゃったんですが、
細かいところは第三弾に任せるとしまして、ざっくりとポイントだけ。

* class名はValidator固定
* specは`Validator.spec`でアクセスできる必要がある
* `__spec`の`[]`で囲まれたセクション名はplugin module自身の名前である必要がある
    + 直接文字列で指定してもいいとは思うんですが、`__name__`の使用をrecommendです
* `string()`みたいな指定だとdefault値がないために必須のparameterになる
* `string(default='HOGEHOGE')`みたいな指定だとdefault値がHOGEHOGEっていう文字列になるためoptionedになる
* 型指定はstring、integer、float、listの指定が可能
    + integerは最低値と最高値を範囲指定可能
        - e.g: `port = integer(0, 65535, default=80)`
    + listはカンマ区切りだよ
* docstringでも`__spec`を指定できるけど、formatメソッドのintegrationとか考えるとtupleで文字列渡して複数行文字列の方が楽だよ


まとめ
------

ここまでの内容をそのまま実装すると、実はplugin完成です。

```python
# blackbirdからplugin用の抽象クラスを含んだmoduleをimportする
import blackbird.plugins.base

# JobBase抽象クラスを継承したConcreteJobを作成する
# class名はConcreteJob固定
class ConcreteJob(blackbird.plugins.base.JobBase):

# build_itemsメソッドがMainのプロセスから呼ばれるので、build_itemsは名前固定です
    def build_items(self):
        item = self.generate_items()
        self.enqueue(
            item=item
        )

    def generate_items(self)
        return ConcreteItem(
           key='isPublished',
           value=1,
           host=self.config.get('hostname')
        )

class ConcreteItem(blackbird.plugins.base.ItemBase):

    def __init__(self, key, value, host):
        super(ConcreteItem, self).__init__(key, value, host)

        self.__data = dict()
        self._generate()

    @property
    def data(self):
        return self.__data

    def _generate(self):
        self.__data['key'] = self.key
        self.__data['value'] = self.value
        self.__data['host'] = self.host
        self.__data['clock'] = self.clock

class Validator(blackbird.plugins.base.ValidatorBase):
    def __init__(self):
        self.__spec = None

    @property
    def spec(self):
        self.__spec = (
            "[0]".format(__name__),
            "required_parameter = string()",
            "not_required_paramter = string(default='DEFAULT_STRING')"
        )
        return self.__spec


if __name__ == '__main__':
    OPTIONS = {
        'required_parameter': 'HOGEHOGE'
    }
    JOB = ConcreteJob(options=OPTIONS)
    import json
    print(json.dumps(JOB.generate_items))

```

まとめるとこんな感じですかね=3