AnsibleでOSのディストリビューションによって挙動を変えるモジュールの実装例

AnsibleでOSによって挙動を変えるモジュールの実装例について説明します。

モジュールの作成方法については以下のページを参照してください。

* [Developing Modules](http://docs.ansible.com/developing_modules.html)
* [Ansible モジュール作成のイロハ | Backlogブログ](http://www.backlog.jp/blog/2013/11/ansible-module-1.html)

## OSによって挙動を変えるモジュール

まず、ディストリビューションではなくOSによって挙動を変える実装について説明します。

例として[userモジュール](http://docs.ansible.com/user_module.html)の[ソース](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user)を見ていきます。

このモジュール内では[Userクラス](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L196)とそのサブクラスとして [FreeBsdUserクラス](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L595)、[OpenBSDUserクラス](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L773)、[NetBSDUserクラス](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L923)、[SunOSクラス](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L1077)、[AIXクラス](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L1247)が定義されています。

サブクラスではcreate_user()、remove_user()、modify_user()メソッドをオーバーライドしています。

[Userクラスの__new__メソッド](https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L216-L217)は以下の様な定義になっています。

```py
    def __new__(cls, *args, **kwargs):
        return load_platform_subclass(User, args, kwargs)
```

[load_platform_subclass関数](https://github.com/ansible/ansible/blob/v1.4.1/lib/ansible/module_utils/basic.py#L135-L157)の実装は以下のようになっています。

```py
def load_platform_subclass(cls, *args, **kwargs):
    '''
    used by modules like User to have different implementations based on detected platform.  See User
    module for an example.
    '''

    this_platform = get_platform()
    distribution = get_distribution()
    subclass = None

    # get the most specific superclass for this platform
    if distribution is not None:
        for sc in cls.__subclasses__():
            if sc.distribution is not None and sc.distribution == distribution and sc.platform == this_platform:
                subclass = sc
    if subclass is None:
        for sc in cls.__subclasses__():
            if sc.platform == this_platform and sc.distribution is None:
                subclass = sc
    if subclass is None:
        subclass = cls

    return super(cls, subclass).__new__(subclass)
```

呼び出したクラスのサブクラスのうち、ディストリビューションとプラットフォームが一致する場合はそのサブクラス、ディストリビューションが未定義な場合 (get_distributionがNoneを返す場合) はプラットフォームが一致するサブクラス、一致するものがない場合は呼び出したクラスの__new__を呼ぶようになっています。

あとは、
https://github.com/ansible/ansible/blob/v1.4.1/library/system/user#L1438

```py
    user = User(module)
```

のように呼び出すことで、実行環境のOSに応じたクラスのインスタンスが生成されるというわけです。

## ディストリビューションによって挙動を変えるモジュール

ディストリビューションによって挙動を変えるモジュールの例としては、Ansible 1.4から追加された[hostnameモジュール](http://docs.ansible.com/hostname_module.html)があります。ソースは https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname です。

実はhostnameモジュールは私の[プルリクエスト](https://github.com/ansible/ansible/pull/3940)が取り込まれたものです。

userモジュールのUserクラスと同様に、
[Hostnameクラス](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L68)とそのサブクラスとして [DebianHostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L170)、[UbuntuHostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L175)、[RedHat5Hostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L224)、[RedHatHostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L229)、[CentOSHostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L234)、[FedoraHostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L282)、[OpenSUSEHostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L287)、[ArchHostname](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L292)が定義されています。

ただ、DebianとUbuntuのようにホスト名の設定方法が同じディストリビューションもあるので実装を共通化したいと考えました。

通常の継承ベースの考え方ならHostnameのサブクラスにDebianとUbuntuの共通用のクラスを作りたいところです。が、load_platform_subclassは上に引用したような実装になっていて、子孫クラスではなく直下のサブクラスしか探してくれませんし、サブクラスのget_distribution()の戻り値と比較するのでディストリビューションごとにサブクラスを分ける必要があります。

そこで、ホスト名の設定方法については別クラスにすることにしました。[UnimplementedStrategy](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L42)、[GenericStrategy](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L102)、[DebianStrategy](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L140)、[RedHatStrategy](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L182)、[FedoraStrategy](https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L241)が定義されています。

これらのクラスは
https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L170-L178

```py
class DebianHostname(Hostname):
    platform = 'Linux'
    distribution = 'Debian'
    strategy_class = DebianStrategy

class UbuntuHostname(Hostname):
    platform = 'Linux'
    distribution = 'Ubuntu'
    strategy_class = DebianStrategy
```

のようにHostnameの各サブクラスで利用するものを設定していて、Hostnameクラスの実装で
https://github.com/ansible/ansible/blob/v1.4.1/library/system/hostname#L90-L100

```py
    def get_current_hostname(self):
        return self.strategy.get_current_hostname()

    def set_current_hostname(self, name):
        self.strategy.set_current_hostname(name)

    def get_permanent_hostname(self):
        return self.strategy.get_permanent_hostname()

    def set_permanent_hostname(self, name):
        self.strategy.set_permanent_hostname(name)
```

のように呼び出すことによって実装を切り替えています。

## 汎用的なモジュールを作ったらプルリクエストを送りましょう

AnsibleはPythonで書かれているだけではなく、Pythonの"[batteries included](http://www.python.org/about/#python-is-powerful-and-fast)"の思想も受け継いでいます。http://www.ansibleworks.com/docs/#modules

>  Ansible follows a “batteries included” philosophy, so you have a lot of great modules for all manner of IT tasks in the core distribution.

ですので、汎用的なモジュールは喜んで取り込んでもらえます。

2014-05-24追記 サイト更新で上記の記述は無くなっています。Batteries Includedについては[Ansible for Configuration Management](http://www.ansible.com/configuration-management)の最後に書かれています。

> Ansible features close to 200 modules in the core distribution, providing a great base to build automation upon. From services and databases to cloud providers, with Ansible you don't have to start from scratch.

https://github.com/ansible/ansible/blob/devel/CONTRIBUTING.md#sharing-a-feature-idea と https://github.com/ansible/ansible/blob/devel/CONTRIBUTING.md#contributing-code-features-or-bugfixes によると、アイディアについてはgithubでチケットを作るかメーリングリストやIRCで相談して、コードをレビューしてもらうにはプルリクエストを送るのがよいです。

hostnameモジュールを作った時も、メーリングリストでMichael DeHaanさんに
https://groups.google.com/d/msg/ansible-project/komqS4WdqHU/cRfWiMnCR3cJ

> The right way to get this reviewed would be to submit a pull request to the github project.

と言われてプルリクエストを送りました。当初はDebian, Ubuntu, RHEL6, CentOSぐらいしか対応していませんでしたが、他のディストリビューションでの動作報告を受けて対応ディストリビューションが増えています。

最初は多少不完全でもレビューを受けて改善していけば取り込んでもらえます。

みなさんも汎用的なモジュールを作ったら、ぜひプルリクエストを送りましょう！