pyvmomiでVMクローンをする際にLinux Prepを実行してみる

pyvmomiで[VMのクローン](http://qiita.com/sky_jokerxx/items/75b3ef7aec4c879d3c4f)を以前はやってみましたが、今回はLinux Prepを加えたクローンするスクリプトを作ってみました。

# 実行環境

| 項目                                         | バージョン           |
|:---------------------------------------------|:---------------------|
| vCenter                                      | 6.5.0 ビルド 4602587 |
| [pyvmomi](https://github.com/vmware/pyvmomi) | 6.5.0.2017.5.post1   |
| python                                       | 3.4.5                |

# 検証方針

* `centos` という仮想マシンを元に `TEST` という仮想マシンを作成する。
* クローン先は `esxi-03.local` というホストを指定します。
* クローンタスク完了後に `TEST` の状態を確認して設定した内容に間違いが無いか調べます。

# Linux Prepについて

VMwareのLinux Prepを使うことで、クローン時にホスト名やネットワーク設定を同時に行うことが可能です。

# ソース

ここでは `Network settings.` にネットワーク設定をしています。
以下は、クローンでLinux Prepを実行するスクリプト例です。

```python
#!/usr/bin/env python3
from pyVim.connect import SmartConnect, Disconnect
from pyVmomi import vim, vmodl
from getpass import getpass
import ssl
import atexit
import argparse
import time

# Network settings.
ipaddr = '10.1.0.1'
netmask = '255.255.255.0'
gateway = '10.1.0.254'
dns = ['8.8.8.8','10.1.0.1']

def options():
    """
    コマンドラインオプション設定

    :rtype: class
    :return: argparse.Namespace
    """
    parser = argparse.ArgumentParser(prog='example_clone_script',
                                     add_help=True,
                                     description='VMクローンでLinux Prepを使うスクリプト例')
    parser.add_argument('--host', '-vc',
                        type=str, required=True,
                        help='vCenterのIP又はホスト名')
    parser.add_argument('--username', '-u',
                        type=str, default='administrator@vsphere.local',
                        help='vCenterのログインユーザー名(default:administrator@vsphere.local)')
    parser.add_argument('--password', '-p',
                        type=str,
                        help='vCenterのログインユーザーパスワード')
    parser.add_argument('--targetvm', '-tvm',
                        type=str, required=True,
                        help='クローン元の仮想マシン名を指定')
    parser.add_argument('--targethost', '-th',
                        type=str, required=True,
                        help='仮想マシンのクローン先ホスト名を指定')
    parser.add_argument('--vmname', '-vn',
                       type=str, required=True,
                       help='作成する仮想マシンのホスト名を指定')
    args = parser.parse_args()

    if(not(args.password)):
        args.password = getpass()

    return args

def get_mob_info(content, mob, target=''):
    """
    Management Objectを取得する。
    targetが指定されていない場合はContainerView(https://goo.gl/WXMfJK)で返す。

    :type content: vim.ServiceInstanceContent
    :param content: ServiceContent(https://goo.gl/oMtVFh)

    :type mob: Management Object
    :param mob: 取得する対象のManagement Objectを指定

    :type target: str
    :param target: 返すmobの名前を指定

    :rtype: Management Object
    :return: 指定したManagement Object又はContainerViewを返す
    """
    r = content.viewManager.CreateContainerView(content.rootFolder,
                                                [mob],
                                                True)

    # 返すmobを名前で指定する場合
    if(target):
        for i in r.view:
            if(i.name == target):
                r = i

    return r

def login(args):
    """
    ServiceContentオブジェクトを返す。

    :rtype: class
    :return: pyVmomi.VmomiSupport.vim.ServiceInstanceContent
    """
    # SSL証明書対策
    context = None
    if hasattr(ssl, '_create_unverified_context'):
        context = ssl._create_unverified_context()

    # 接続
    si = SmartConnect(host = args.host,
                      user = args.username,
                      pwd = args.password,
                      sslContext = context)

    # 処理完了時にvCenterから切断
    atexit.register(Disconnect, si)

    # ServiceContent(Data Object)を取得
    content = si.content

    return content

def task_check(content, task):
    """
    実行したタスクのステータスを確認してタスク終了時に実行結果を返す。
    ステータス参考

    runnin: タスク実行中
    success: タスクが成功
    error: タスクが失敗

    :rtype: str
    :return: タスク実行結果
    """
    while True :
        r = content.taskManager.recentTask
        if(task in r):
            task_r = [ x for x in r if task == x ]
            if(task_r[0].info.state == vim.TaskInfo.State.success):
                return task_r[0].info.state
                break
            elif(task_r[0].info.state == vim.TaskInfo.State.error):
                return task_r[0].info.state
                break
        else:
            return "Not found task: " + task
        time.sleep(5)

def main(args):
    # ServiceContent.
    content = login(args)

    # Create Linux Prep.
    customization_linux_prep = vim.vm.customization.LinuxPrep()
    linux_host_name = vim.vm.customization.FixedName()
    linux_host_name.name = args.vmname
    customization_linux_prep.hostName = linux_host_name
    customization_linux_prep.hwClockUTC = True
    customization_linux_prep.timeZone = 'Asia/Tokyo'

    # Create customizationIPSettings.
    nic = vim.vm.customization.AdapterMapping()
    nic.adapter = vim.vm.customization.IPSettings()
    nic.adapter.dnsDomain = ''
    nic.adapter.gateway = gateway
    nic.adapter.ip = vim.vm.customization.FixedIp()
    nic.adapter.ip.ipAddress = ipaddr
    nic.adapter.subnetMask = netmask

    # Create CustomizationSpec.
    customization_spec = vim.vm.customization.Specification()
    customization_spec.globalIPSettings = vim.vm.customization.GlobalIPSettings()
    customization_spec.globalIPSettings.dnsServerList = dns
    customization_spec.nicSettingMap = [nic]
    customization_spec.identity = customization_linux_prep

    # Create RelocateSpec.
    relocate_spec = vim.vm.RelocateSpec()
    relocate_spec.host = get_mob_info(content, vim.HostSystem, args.targethost)

    # Create CloneSpec.
    clone_spec = vim.vm.CloneSpec()
    clone_spec.customization = customization_spec
    clone_spec.location = relocate_spec
    clone_spec.powerOn = True
    clone_spec.template = False

    # Execute clone task.
    v = get_mob_info(content, vim.VirtualMachine, args.targetvm)
    task = v.CloneVM_Task(folder = v.parent, name = args.vmname, spec = clone_spec)

    # Task check.
    r = task_check(content, task)
    print("Task結果: " + r)

if __name__ == '__main__':
    args = options()
    main(args)
```

処理の流れは以下のようになっています。
スクリプトで一時的にカスタマイズ仕様を作ってクローンメソッドに食わせています。

* [LinuxPrep](https://vdc-download.vmware.com/vmwb-repository/dcr-public/6b586ed2-655c-49d9-9029-bc416323cb22/fa0b429a-a695-4c11-b7d2-2cbc284049dc/doc/vim.vm.customization.LinuxPrep.html)を作成
  * ホスト名やたいむゾーンを設定するオブジェクト
* [customizationIPSettings](https://vdc-download.vmware.com/vmwb-repository/dcr-public/6b586ed2-655c-49d9-9029-bc416323cb22/fa0b429a-a695-4c11-b7d2-2cbc284049dc/doc/vim.vm.customization.IPSettings.html)を作成
  * ネットワークを設定するオブジェクト
* [RelocateSpec](https://vdc-download.vmware.com/vmwb-repository/dcr-public/6b586ed2-655c-49d9-9029-bc416323cb22/fa0b429a-a695-4c11-b7d2-2cbc284049dc/doc/vim.vm.RelocateSpec.html)を作成
  * 仮想マシンの移動先を設定するオブジェクト
* [CloneSpec](https://vdc-download.vmware.com/vmwb-repository/dcr-public/6b586ed2-655c-49d9-9029-bc416323cb22/fa0b429a-a695-4c11-b7d2-2cbc284049dc/doc/vim.vm.CloneSpec.html)を作成
  * 仮想マシンのクローン仕様を設定するオブジェクト
* クローン処理実行
* 実行したクローン処理のタスクを確認
* クローン処理の結果を出力

# 実行

(1) スクリプトを実行してみます。

```bash
$ ./example_clone_script.py -vc vcenter01.local -tvm centos -vn TEST -th esxi-03.local
Password:
Task結果: success
```

問題なく終了したら `success` が表示されます。

(2) クローンした仮想マシンにログインしてホスト名やネットワーク設定が出来ていることを確認します。

```bash
[root@TEST ~]# hostnamectl
   Static hostname: TEST
(snip)
[root@TEST ~]# ip a s ens192 | grep inet
    inet 10.1.0.1/24 brd 10.1.0.255 scope global ens192
[root@TEST ~]# cat /etc/resolv.conf
(snip)
nameserver 8.8.8.8
nameserver 10.1.0.1
```

問題なく設定されていました :-)
vNICが複数ある場合は設定を `customization_spec.nicSettingMap` の配列に数分入れる必要があります。