Ansible で QNAP の qcli系の設定をする

QNAPのQTSは、いろんな設定をするのにqcli系のコマンドを使う。qcliを引数なしで実行すると、いろんなコマンドがあることを教えてくれる。

```
[~] # qcli

  -v  --version,                             display the version of QCLI and exit.
  -h  --help,                                print this help.
  -l  --login,                               login to check authentication.

  qcli_admin,                                admin operations.
  qcli_volume,                               volume operations.
  qcli_pool,                                 pool operations.
  qcli_raid,                                 RAID operations.
  qcli_hdd,                                  HDD operations.
  qcli_cache,                                cache operations.

  qcli_iscsi,                                iSCSI operations.
  qcli_iscsiacl,                             iSCSI ACL operations.
  qcli_iscsibackup,                          iSCSI backup operations.
  qcli_virtualdisk,                          virtual disk operations.

  qcli_power,                                power operations.
  qcli_network,                              network operations.
  qcli_log,                                  log operations.
  qcli_backuprestore,                        backup/restore operations.
  qcli_firmwareupdate,                       firmware update operations.

  qcli_sharedfolder,                         shared folder operations.
  qcli_quota,                                quota operations.

  qcli_networkservice,                       network service operations.
  qcli_encrypt,                              encrypt operations.
  qcli_rsyncserver,                          rsync server operations.
  qcli_timemachine,                          time machine operations.
  qcli_nastonas,                             nas to nas operations.
  qcli_rsync,                                rsync operations.
  qcli_networkrecyclebin,                    network recycle bin operations.
  qcli_timezone,                             time zone operations.
  qcli_domainsecurity,                       domain security operations.
  qcli_users,                                users operations.
  qcli_usergroups,                           usergroups operations.
  qcli_ntp,                                  NTP service operations.
  qcli_hardware,                             hardware operations.
  qcli_systemstatus,                         system status operations.
  qcli_externaldevice,                       external device operations.
  qcli_mysqlserver,                          mysqlserver operations.

  qcli_volumesnapshot,                       volume snapshot operations.
  qcli_iscsisnapshot,                        iSCSI snapshot operations.

  qcli_domaincontroller,                     domain controller operations.

  qcli_snapreplica,                          SnapReplica operations.
  qcli_snapshotvault,                        Snapshot Vault operations.

QCLI 4.4.2 20200413, QNAP Systems, Inc.
```
これをAnsibleからcommandで叩けば……と思ったけれど、事はそれほど単純ではなかった。

# qcli の実行手順
qcli系のコマンドを使うには、いったんログインしなくてはならず、そこで返ってくるSIDを各コマンドで入力しなくてはならない。

```
[~] # qcli -l user=admin pw=PASSWORD
Authentication success!
sid is 12345678
[~] # qcli_hardware -B sid=12345678
system_operation Disabled
system_events    Disabled
```

Ansibleのcommandをこの順番で実行して、結果を保存して……とやるよりは、モジュールを作る方が早い(本当にAnsibleのモジュールは簡単なので、whenとかregisterとかいろいろ考えるよりはモジュールの方がいいと思う)。

# モジュールを作る
qcli_login でログインして、 qcli でコマンドを実行するようにする。例によって未テスト・ドキュメントなし・詰めは甘い。

```python:qcli_login.py
#!/usr/bin/python

import re

ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'iwatam'
}

DOCUMENTATION = '''
'''

EXAMPLES = '''
'''

RETURN = '''
'''

from ansible.module_utils.basic import AnsibleModule

def main():
    module = AnsibleModule(
        argument_spec=dict(
            user=dict(type='str',required=False,default='admin'),
            password=dict(type='str',required=False,no_log=True)
        ),
        supports_check_mode=False
    )
    user=module.params['user']
    password=module.params['password']

    rc,out,err=module.run_command(['qcli','-l','user='+user,'pw='+password])
    m=re.search('sid is (\\w+)',out)
    if m is None:
        module.fail_json(msg='Commmand parse error or login failed.',
                         out=out)
    sid=m.groups()[0]

    module.exit_json(changed=False,
                     sid=sid,
                     output=out,
                     ansible_facts=dict(ansible_qcli_sid=sid))

if __name__ == '__main__':
    main()
```
user と password を渡して実行すると、ログインしてansible_qcli_sidという名前でfactとしてsidを格納する。必ずChanged=Falseで返る。

```python:qcli.py
#!/usr/bin/python

import re

ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'iwatam'
}

DOCUMENTATION = '''
'''

EXAMPLES = '''
'''

RETURN = '''
'''

from ansible.module_utils.basic import AnsibleModule

def main():
    module = AnsibleModule(
        argument_spec=dict(
            query_cmd=dict(type='str',required=True),
            expects=dict(type='str',required=True),
            modify_cmd=dict(type='str',required=True),
            sid=dict(type='str',required=False)
        ),
        supports_check_mode=False
    )
    query_cmd=module.params['query_cmd']
    expects=module.params['expects']
    modify_cmd=module.params['modify_cmd']
    sid=module.params['sid']
    sidopt=" sid="+sid
    rc, out, err=module.run_command(query_cmd+sidopt,check_rc=True)
    changed=False
    if not re.fullmatch(expects,out):
        rc, out2, err=module.run_command(modify_cmd+sidopt,check_rc=True)
        changed=True
    module.exit_json(changed=changed,
                     sid=sid,
                     original_output=out,
                     ansible_facts=dict(ansible_qcli_sid=sid),
                     output='')

if __name__ == '__main__':
    main()
```
query_cmd, expects, modify_cmd, sid の4つを指定する。最初にquery_cmdを(後ろにsid=XXXXを付けて)実行して、その結果がexpectsで指定した正規表現と完全にマッチするならOK、しないならmodify_cmdを実行する。

Ansible のplaybookは以下のようになる。

```yaml
- name: login to qcli
  qcli_login:
      password: "{{ password }}"

- name: add user
  qcli:
    query_cmd: "qcli_users -c username=iwatam"
    expects: 'User\s+name\s+exist!\s*'
    modify_cmd: "qcli_users -a username=iwatam password=PASSWORD passwordVerify=PASSWORD"
    sid: "{{ ansible_qcli_sid }}"
```

個人的なヤツなので、passwordが平文で書かれるのがどうとか面倒くさいことは言わない。

### イケてないところ
* いちいちsidを渡さないといけない。(モジュール呼び出しの間で値を共有する機構を発見できなかった)
* passwordが平文で書かれるのはどうなのか
* コマンドごとにモジュールを作るのが一番カッコイイんだろうけど、個人でやるには面倒すぎる。
* というか、そういうモジュールは既にあるんじゃないかと思うんだけど。