AnsibleでNetApp ONTAP Storageを設定

**2020/10 いくつか最近のバージョンでの注意点を追記しました。記事を書いてから2年以上経っていて内容が古くなっていますので近いうちに2.10版として更新したいと思っています**
**Ansible 2.10 + NetApp ONTAP Collection 20.10.0 を使った [SSLの Client認証使うときの設定](https://qiita.com/yasuoohno/items/86d4c4f7dcef9dbece9e)について記事を書きましたので、新しい環境の方はこちらも見ていただければと思います**

# AnsibleでNetApp ONTAP Storageを設定 #

Ansible 2.6以後、NetAppストレージ用のモジュールが追加されました。

この記事では NetApp ONTAP9を搭載したストレージの Clusterが出来た状態から初めて、
NFSでアクセス可能な領域＝ボリュームをクライアントに公開するところまでを行うPlayBookを作っています。

## お約束 ##

この記事で書かれている PlayBookや Roleは動作を保証されたものではありません。
各環境に合わせてテスト・確認をしたうえで自己責任にて利用していただければと思います。

## Ansibleの環境 ##

- Ubuntu 18.04
- Ansible 2.7.6
- [netapp-lib 2018.11.13](https://pypi.org/project/netapp-lib/)

## 前提とする ONTAPクラスタの状態 ##

- クラスタが組まれている事
- クラスタ管理IP・ノード管理IPに Ansibleを動かすホストからネットワークが届く事

## 下準備

### ONTAP cluster で HTTPによるアクセスを有効化します

```bash
# enable http api access
ssh admin@ONTAP_IP_ADDRESS 'set -privilege advanced; system services web modify -http-enabled true;'
```

### pythonの netapp-libを追加

```bash
sudo apt-get install -y python-pip
pip install netapp-lib
```

## Roleと変数とInventory ##

ONTAPの Ansibleモジュールは かなり細かい粒度(API粒度)で用意されており、
タスク毎に認証情報が必要な事等から普通に書くとかなり冗長な感じになってしまいます。

これを緩和するために一部Roleを使って書いています。

また、認証情報は PlayBookに書かず、変数を利用するようにしていますが
クラスタで一つのファイルにしておきたいので Inventoryを階層的に定義しています。

## Role ##

今回用意したRoleのは以下の２＋１個です。

- VLAN I/Fの作成
- Broadcastドメインの作成

- Factを集めて表示する

Roleのメンテナンスが面倒なので、再利用・ループが必要無いようなところは普通にベタっと書いています。

### Role:VLAN I/Fの作成 ###

各ノードに対応した host_varsでデータ用インターフェースの一覧を書いておき、
必要に応じて このRoleでvlanインターフェースを作る様にしています。

``` yaml:roles/datavlan/tasks/main.yml
- na_ontap_net_vlan:
    state=present
    vlanid={{ vlan_id }}
    node={{ inventory_hostname }}
    parent_interface={{ item }}
    username={{ cluster_username }}
    password={{ cluster_password }}
    hostname={{ cluster_hostname }}
  loop: "{{ data_interfaces }}"
```

### Role:Broadcastドメインの作成 ###

クラスタ内で同一のvlan idを持っているI/Fを集めてブロードキャストドメインを作ります。
vlanのroleと ノードのデータinterface一覧から パラメータを作っているのでちょっと長いです。

``` yaml:roles/bcdomain/tasks/main.yml
- set_fact:
    vlan_interface_list: >-
      {%- set templist = [] -%}
      {%- for i in hostvars[inventory_hostname]['groups'][inventory_hostname+'-nodes'] -%}
      {%-   if hostvars[i]['vlan_interface_dict'] is defined -%}
      {%-     if hostvars[i]['vlan_interface_dict'][vlan_id] is defined -%}
      {%-       set _ = templist.append(hostvars[i]['vlan_interface_dict'][vlan_id]|join(",")) -%}
      {%-     endif -%}
      {%-   endif -%}
      {%- endfor -%}
      {{ templist }}
- name: create broadcast domain for vlan {{ vlan_id }}
  na_ontap_broadcast_domain:
    state=present
    username={{ cluster_username }}
    password={{ cluster_password }}
    hostname={{ cluster_hostname }}
    broadcast_domain={{ bc_name }}
    mtu=9000
    ipspace={{ ipspace }}
    ports={{ vlan_interface_list|join(",")}}
```

### Role:Factの収集 ###

**2020/10 2.9時点で na_ontap_gather_factsは Deprecatedになっています。2.13で削除される予定です。_info 系の利用が推奨されます**

Ansibleから Storageを管理するためには、代理となるサーバからStorage上にAPIを発行します。
そのままでは Storageの Factを集める事が出来ませんので、ここもモジュールで対応する事になります。

簡単なので普通に書いても良いのですが、いつも使うので Roleにしておきました。

``` yaml:roles/ontap_facts/tasks/main.yml
- name: Get ONTAP Info
  na_ontap_gather_facts:
    state: info
    username: "{{ cluster_username }}"
    password: "{{ cluster_password }}"
    hostname: "{{ cluster_hostname }}"
- debug: var=ontap_facts
```

## 変数 ##

クラスタの認証情報と接続先をグループ変数で設定します。

なお、グループ/ホスト変数双方とも ansible_hostに local_hostを指定しています。
playbookを実行するホスト自身に netapp-lib(APIを叩くためのライブラリ)が
インストールされている必要があります。
ストレージの操作を他のホストで実行したい場合は ansible_hostを適宜変更する必要があります。

### グループ変数 ###

クラスタ毎の認証情報を含めて作成します。ansible-vaultで暗号化すると良いかと思います。

``` yaml:group_vars/クラスタ名-cluster.yml
---
ansible_host: localhost
cluster_name:     "クラスタ名"
cluster_username: "admin"
cluster_password: "パスワード"
cluster_hostname: "IPアドレス/ホスト名"
```

**2020/10 最近のVersionで ansible_host側での Python3が必要な場合があるようです。`pip3 install netapp-lib` 及び 下記の様にInterpreterの指定の追加が必要な場合がありました。**

```yaml
ansible_python_interpreter: "/usr/bin/python3"
```

### ホスト変数 ###

ノード毎の情報です。

``` yaml:host_vars/ノード名.yml
---
node_hostname: "<ノード管理IPアドレス>"
data_interfaces:
  - e0e
  - e0g
```

data_interfacesにデータインターフェースとして利用する予定のインターフェース名を列挙しています。
これは VLAN I/F作成Roleとブロードキャストドメイン作成Roleで利用していますので
それらのRoleが不要であれば特に設定する必要はありません。

## Inventory ##

実際にネットワーク上に存在するのは以下ですが、これを階層的にします。

- AFF クラスタ
- AFF-01 ノード
- AFF-02 ノード
- svm01 ストレージ仮想マシン
- svm02 ストレージ仮想マシン...

グループ階層はこんな形になります。

`AFF-cluster グループ`

  - AFF
  - `AFF-nodes グループ`
    - AFF-01
    - AFF-02...
  - `AFF-svms グループ`
    - svm01
    - svm02...

インベントリファイルは以下のようにしています。

ノード名とクラスタ名は実機の設定に完全一致させる必要がありますので
実機の設定通りに大文字小文字等を変えないようにしてください。

``` yaml:lab_aff.yml
---
#
# AFF-clusterグループ
#
AFF-cluster:
  hosts:
    AFF:
  children:
    AFF-nodes:
    AFF-svms:
#
# AFFクラスタのノード一覧
#
AFF-nodes:
  hosts:
    AFF-01:
    AFF-02:
#
# AFFクラスタのSVM一覧
#
AFF-svms:
  hosts:
    svm01:
    svm02:
```


## タスク・ロールの設定 ##

実際に構成タスクを書いていきます。

各タスクでどこを対象に(hosts指定)しているかを気にしながら見ていただければと思います。

### Factの収集 ###

「クラスタに対して」Roleを使ってFactを集めてDebugで表示させます。

``` yaml
#
# ONTAPクラスタのFactsを集める
#
- hosts: AFF
  gather_facts: no
  roles:
    - { role: ontap_facts }
```

### Licenseの登録 ###

「クラスタに対して」ライセンスを登録します。

ロール定義してループで回してやるべきかなと思いつつ今はベタっと書いてます。
以下はbaseだけですが、同様にnfs/iscsi/fcp等も登録します。

``` yaml
#
# ライセンスの登録
#
- hosts: AFF
  gather_facts: no
  tasks:
    - name: base license
      na_ontap_license:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        license_codes: XXXXXXXXXXXXXXAAAAAAAAAAAAAA
        license_names: base
```

### Aggregateの作成 ###

「クラスタに対して」各ノードでAggregateを作ります。

指定はディスク本数とディスクタイプ、名前を指定します。

``` yaml
#
# ディスクカウントとタイプ、名前を指定してAggregateを作ります。
#
- hosts: AFF
  gather_facts: no
  tasks:
    - name: create aggregate aggr01
      na_ontap_aggregate:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        disk_count: 22
        disk_type: SSD
        name: aggr01
        nodes: AFF-01

    - name: create aggregate aggr02
      na_ontap_aggregate:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        disk_count: 22
        disk_type: SSD
        name: aggr02
        nodes: AFF-02
```

### Interface Groupの作成 ###

「クラスタ内の全ノードに対して」LACP等で物理NICのチームを作ります。

各ノードに対してすべて同じ設定をしています。

以下の例では e0cとe0d を Default bcdomainから抜いたあとに、
ifgrp a0aをlacpで作成して 各ポートを所属させます。

注意：一度インターフェースを作成したあとに、この設定ファイルのmode等を変更して
再度PlayBookを走らせてもifgrpの設定は変更されません。
一度明示的にifgrpを消したあとに実施してください。

``` yaml
#
# インターフェースグループの作成
#
- hosts: AFF-nodes
  gather_facts: no
  tasks:
    - name: remove ports from default bcdomain.
      na_ontap_broadcast_domain_ports:
        state: absent
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        broadcast_domain: Default
        ports: "{{ inventory_hostname }}:e0c"

    - name: remove ports from default bcdomain.
      na_ontap_broadcast_domain_ports:
        state: absent
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        broadcast_domain: Default
        ports: "{{ inventory_hostname }}:e0d"

    - name: create or add port to ifgrp for e0c
      na_ontap_net_ifgrp:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        mode: multimode_lacp
        distribution_function: port
        node: "{{ inventory_hostname }}"
        name: a0a
        port: e0c

    - name: add port to ifgrp for e0d
      na_ontap_net_ifgrp:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        mode: multimode_lacp
        distribution_function: port
        node: "{{ inventory_hostname }}"
        name: a0a
        port: e0d
```

### VLAN I/Fを作成 ###

「クラスタ内の全ノードに対して」データインターフェース上にVLAN I/Fを作ります。

データインターフェースと書いていますが、
host_vars内で設定したインターフェースの事です。

``` yaml
#
# vlan i/fの作成
#
- hosts: AFF-nodes
  gather_facts: no
  roles:
    - { role: datavlan, vlan_id: 1011 }
    - { role: datavlan, vlan_id: 1012 }
```

### Broadcast Domainの作成 ###

「クラスタに対して」VLAN I/Fに対応した ブロードキャスト ドメインを作ります。

``` yaml
- hosts: AFF
  gather_facts: no
  roles:
    - { role: bcdomain, vlan_id: 1011, ipspace: 'Default' }
    - { role: bcdomain, vlan_id: 1012, ipspace: 'Default' }
```

ipspaceの作成はAnsible 2.7から サポートされています。

### SVMを作る ###

「クラスタに対して」SVMを作成します。

```yaml

#
# create a svm.
#
- hosts: AFF
  gather_facts: no
  tasks:
    - name: create svm
      na_ontap_svm:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        name: svm01
        aggr_list: aggr01,aggr02
        root_volume: volroot
        root_volume_aggregate: aggr01
        root_volume_security_style: unix
    - name: create svm
      na_ontap_svm:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        name: svm02
        aggr_list: aggr01,aggr02
        root_volume: volroot
        root_volume_aggregate: aggr02
        root_volume_security_style: unix
```

### LIFを作る ###

「クラスタに対して」SVMを指定してLIFを作成します。

```
#
# create lif for node a300-01
#
- hosts: AFF
  gather_facts: no
  tasks:
    - name: create svm lif.
      na_ontap_interface:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        vserver: svm01
        interface_name: lif_svm01
        home_node: AFF-01
        home_port: a0a
        role: data
        protocols: nfs,cifs
        address: xxx.xxx.xxx.xxx
        netmask: 255.255.255.0
```

### Export Policyを作る ###

「クラスタに対して」SVMを指定してExport Policyを作成します。

``` yaml
#
# create export policy.
#
- hosts: AFF
  gather_facts: no
  tasks:
    - name: create export policy.
      na_ontap_export_policy_rule:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        vserver: svm01
        client_match: 0.0.0.0/0
        policy_name: default
        protocol: any
        ro_rule: any
        rw_rule: any
        super_user_security: any
```

### NFSサービスをEnableにする ###

「クラスタに対して」SVMを指定してNFSサービスをEnableにします。

``` yaml
#
# enable nfs service.
#
- hosts: AFF
  gather_facts: no
  tasks:
    - name: enable nfs service.
      na_ontap_nfs:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        vserver: svm01
        service_state: started
        nfsv3: enabled
        nfsv4: enabled
        nfsv41: enabled
```

### ボリュームを作る ###

「クラスタに対して」SVMを指定してボリュームを作成します。

``` yaml
#
# make volume.
#
- hosts: AFF
  gather_facts: no
  tasks:
    - name: make volume.
      na_ontap_volume:
        state: present
        hostname: "{{ cluster_hostname }}"
        username: "{{ cluster_username }}"
        password: "{{ cluster_password }}"
        vserver: svm01
        name: vol_data1
        size: 200
        size_unit: gb
        space_guarantee: none
        aggregate_name: aggr01
        volume_security_style: unix
        type: rw
        is_online: yes
        junction_path: /data1
```

## まとめ ##

ここまでで ONTAPの初期構築に必要な最低限の作業として

- aggregate
- network
- svm
- lif
- service
- volume

が作成出来るかと思います。便利ですので是非試してみてもらえればと思います。

## 改善すべき点 ##

以下は改善すべき点かと思ってます。

- 安全のため svm内で完結するオペレーションは svmスコープで作業すべき(今はCluster Scope)
- ライセンス登録なんかは繰り返し書くのはダルいのでRole化