Contents
TKG(Tanzu Kubernetes Grid)は、インターネットから隔離された環境(プロキシ環境を含む)では内部ネットワークに個別のコンテナレジストリを構築する必要があります。この場合、Kubernetesクラスターは外部のコンテナレジストリを参照せず、内部のコンテナレジストリを参照します。Kubernetesクラスターを作るよりもこの環境を準備するほうがよっぽどややこしく面倒です。
環境
ホスト
Harbor
最初にHarborを作ります。HarborはVMwareが開発したプライベートコンテナイメージレポジトリです。
Photon OS
Harborを稼働させるOSはPhoton OSを使用します。Photon OSも同じくVMwareが開発しています。Harbor(内部でDockerレジストリを使います)は特にOSを問いませんが、VMware環境ではPhoton OSは最初からDockerが組み込まれたりしていて使い勝手が良いです。
イメージの入手
https://github.com/vmware/photon/wiki/Downloading-Photon-OS
上記のリンクからダウンロード可能です。VMware環境で使用するならOVAで良いですが、ISOやその他のパブリッククラウド用のイメージも入手できます。本稿ではOVA(photon-ova_uefi-4.0-ca7c9e9330.ova)を使用します。
インストール
OVAなのでそのままインストールします。
パスワード変更
最初はコンソールでログインします。ユーザー名:root、パスワード:changemeです。初回ログイン時にパスワード変更が求められるので変更します。
SSHログイン
起動後はDHCPでIPアドレスを取得しているので、ifconfigでIPアドレスを確認し、とりあえずSSHで接続しましょう。意図していないネットワークアダプタ等の場合は適宜修正して再起動して下さい。
ネットワーク設定
固定IPアドレスを設定します。インターフェイス名はeth0になっていると思いますが、違う場合は適宜読み替えて下さい。
[root@photonos ~]# vim /etc/systemd/network/10-static-eth0.network [Match] Name=eth0 [Network] Address=192.168.95.10/24 Gateway=192.168.95.2 DNS=192.168.95.2 [root@photonos ~]# chmod 644 /etc/systemd/network/10-static-eth0.network [root@photonos ~]# systemctl restart systemd-networkd
systemd-networkdの再起動ではネットワークが切れるのでコンソールから実施するか、OS再起動のほうが良いと思います。
プロキシ参照設定
Harbor自体もプロキシを経由したインターネット接続の場合はプロキシ参照設定が必要です。保存先は任意です。
[root@photonos ~]# vim /root/set_proxy.sh #!/bin/bash HTTP_PROXY=$1 NO_PROXY_NETS=$2 export HTTP_PROXY=${HTTP_PROXY} export HTTPS_PROXY=${HTTP_PROXY} export NO_PROXY_NODE="localhost,${NO_PROXY_NETS}" export NO_PROXY_RUNTIME="localhost,100.64.0.0/13,100.96.0.0/11" echo "export http_proxy=${HTTP_PROXY}" >> /etc/environment echo "export https_proxy=${HTTPS_PROXY}" >> /etc/environment echo "export no_proxy=${NO_PROXY_NODE}" >> /etc/environment echo "export no_proxy_runtime=${NO_PROXY_RUNTIME}" >> /etc/profile echo "export no_proxy_runtime=${NO_PROXY_RUNTIME}" >> /etc/environment echo "export http_proxy=${HTTP_PROXY}" >> /etc/profile echo "export https_proxy=${HTTPS_PROXY}" >> /etc/profile echo "export no_proxy=${NO_PROXY_NODE}" >> /etc/profile source /etc/profile mkdir -p /etc/systemd/system/docker.service.d/ cat <<EOF > /etc/systemd/system/docker.service.d/http_proxy.conf [Service] Environment="HTTP_PROXY=$HTTP_PROXY" Environment="HTTPS_PROXY=$HTTP_PROXY" Environment="NO_PROXY=localhost,$NO_PROXY_NETS" EOF systemctl daemon-reload systemctl restart docker.service echo "setup proxy done" [root@photonos ~]# chmod u+x /root/set_proxy.sh [root@photonos ~]# ./set_proxy.sh http://192.168.95.11:3128 "192.168.0.0/16,si1230.com" [root@photonos ~]# source /etc/profile
set_proxy.shの引数は、①プロキシサーバー、②プロキシ除外ネットワークです。
プロキシ参照設定の動作を確認します。
[root@photonos ~]# curl https://www.vmware.com --head
OSアップデート
OSをアップデートします。Photon OSではtdnfコマンドになります。割とどうでもいいですがdnfをPhoton OS用にカスタマイズしたもののようです。
[root@photonos ~]# tdnf makecache [root@photonos ~]# tdnf update [root@photonos ~]# reboot
tdnfのバージョンを確認します。参考URLではversion 3.1.0 or laterとあります。
[root@photonos ~]# tdnf --version
viのビジュアルモード無効化
OSアップデート後、viの設定が変わりビジュアルモードになりうっとうしいので無効化します。root以外で作業する場合は各ホームディレクトリで同じようにして下さい。
[root@photonos ~]# echo "set mouse-=a" > /root/.vimrc
iptables
Harborで8043を使用する為、ファイアウォールを開けておきます。
※DockerコンテナではiptablesのINPUTルールは意味を持たない為、不要です。Dockerは対象のコンテナがポートフォワードを指定した場合にPREROUTINGルールに動的に追加する為です。
[root@photonos ~]# vim /etc/systemd/scripts/ip4save -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT -A INPUT -p tcp -m tcp --dport 8043 -j ACCEPT [root@photonos ~]# systemctl restart iptables.service
必要なソフトウェアのインストール
必要なソフトウェアをインストールします。
wget, tar, openssl-c_rehash
[root@photonos ~]# tdnf install wget tar openssl-c_rehash
imgpkg
[root@photonos ~]# wget https://github.com/vmware-tanzu/carvel-imgpkg/releases/download/v0.12.0/imgpkg-linux-amd64 -O /usr/local/bin/imgpkg [root@photonos ~]# chmod 755 /usr/local/bin/imgpkg
yq
[root@photonos ~]# wget https://github.com/mikefarah/yq/releases/download/v4.9.1/yq_linux_amd64 -O /usr/local/bin/yq [root@photonos ~]# chmod 755 /usr/local/bin/yq
jq
[root@photonos ~]# wget wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -O /usr/local/bin/jq [root@photonos ~]# chmod 755 /usr/local/bin/jq
vethカーネルモジュールの有効化とDocker起動
vethカーネルモジュールの状態を確認します。
[root@photonos ~]# lsmod | grep veth
無効の場合はvethカーネルモジュールを有効にして再度確認します。
[root@photonos ~]# modprobe veth [root@photonos ~]# lsmod | grep veth
OS起動時も有効になるように設定を追加します。
[root@photonos ~]# echo "veth" > /etc/modules-load.d/20-veth.conf
Dockerを起動します。
[root@photonos ~]# systemctl enable --now docker.service [root@photonos ~]# systemctl status docker.service
ディスクの追加
後述のDockerレジストリの同期でディスク容量が不足(OVAインストール時はサイズ指定ができない)する為、ディスクを追加します。
一旦VMを停止し、100GBのディスク領域を追加、起動します。
パーティションを作成します。fdisk -lで追加したディスク名を確認してから対象のディスクにfdiskで接続します。
[root@photonos ~]# fdisk -l [root@photonos ~]# fdisk /dev/sdb Welcome to fdisk (util-linux 2.36). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. Device does not contain a recognized partition table. Created a new DOS disklabel with disk identifier 0xddefec31. Command (m for help): n Partition type p primary (0 primary, 0 extended, 4 free) e extended (container for logical partitions) Select (default p): Enter Partition number (1-4, default 1): Enter First sector (2048-209715199, default 2048): Enter Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-209715199, default 209715199): Enter Created a new partition 1 of type 'Linux' and of size 100 GiB. Command (m for help): p Disk /dev/sdb: 100 GiB, 107374182400 bytes, 209715200 sectors Disk model: VMware Virtual S Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xddefec31 Device Boot Start End Sectors Size Id Type /dev/sdb1 2048 209715199 209713152 100G 83 Linux Command (m for help): w The partition table has been altered. Calling ioctl() to re-read partition table. Syncing disks.
ext4でフォーマットします。
[root@photonos ~]# mkfs.ext4 /dev/sdb1
Dockerのディレクトリを追加ディスクに移動します。
[root@photonos ~]# systemctl stop docker.service [root@photonos ~]# mkdir /docker [root@photonos ~]# mount /dev/sdb1 /docker [root@photonos ~]# mv /var/lib/docker/* /docker [root@photonos ~]# rmdir /var/lib/docker [root@photonos ~]# ln -s /docker /var/lib [root@photonos ~]# systemctl start docker.service [root@photonos ~]# systemctl status docker.service
OS起動時にマウントするように追記します。再起動して確認しましょう。
[root@photonos ~]# vim /etc/fstab /dev/sdb1 /docker ext4 defaults 0 0
SSL証明書
[root@photonos ~]# mkdir -p /etc/docker/certs.d/harbor.si1230.com:8043 [root@photonos ~]# cp server.cert /etc/docker/certs.d/harbor.si1230.com:8043 [root@photonos ~]# cp server.key /etc/docker/certs.d/harbor.si1230.com:8043 [root@photonos ~]# cp ca.crt /etc/docker/certs.d/harbor.si1230.com:8043
[root@photonos ~]# cp /etc/docker/certs.d/harbor.si1230.com:8043/ca.crt /etc/ssl/certs/cacert.pem [root@photonos ~]# rehash_ca_certificates.sh
Harbor
docker-compose
[root@photonos ~]# wget https://github.com/docker/compose/releases/download/1.27.4/docker-compose-Linux-x86_64 -O /usr/local/bin/docker-compose [root@photonos ~]# chmod a+x /usr/local/bin/docker-compose
Harbor
[root@photonos ~]# mkdir /harbor [root@photonos ~]# cd /harbor [root@photonos ~]# wget https://github.com/goharbor/harbor/releases/download/v2.1.2/harbor-offline-installer-v2.1.2.tgz [root@photonos ~]# tar -zxf harbor-offline-installer-v2.1.2.tgz
[root@photonos ~]# cd /harbor/harbor [root@photonos ~]# cp harbor.yml.tmpl harbor.yml [root@photonos ~]# vim /harbor/harbor/harbor.yml hostname: harbor.si1230.com http: port: 8001 https port: 8043 certificate: /etc/docker/certs.d/harbor.si1230.com:8043/server.cert private_key: /etc/docker/certs.d/harbor.si1230.com:8043/server.key
[root@photonos ~]# vim /harbor/harbor/preparegoharbor/prepare:v2.1.2 prepare $@vmwtec.jfrog.io/registry/goharbor/prepare:v2.1.2 prepare $@
[root@photonos ~]# /harbor/harbor/prepare [root@photonos ~]# /harbor/harbor/install.sh
認証情報はharbor.ymlに記載があり、デフォルトはユーザー名:admin、パスワード:Harbor12345です。
[root@photonos ~]# docker login harbor.si1230.com:8043/registry
[root@photonos ~]# vim /etc/systemd/system/harbor.service [Unit] Description=Harbor After=docker.service systemd-networkd.service systemd-resolved.service Requires=docker.service Documentation=http://github.com/vmware/harbor [Service] Type=simple Restart=on-failure RestartSec=5 ExecStart=/usr/local/bin/docker-compose -f /harbor/harbor/docker-compose.yml up ExecStop=/usr/local/bin/docker-compose -f /harbor/harbor/docker-compose.yml down [Install] WantedBy=multi-user.target [root@photonos ~]# chmod 644 /etc/systemd/system/harbor.service [root@photonos ~]# systemctl daemon-reload [root@photonos ~]# systemctl enable --now harbor.service [root@photonos ~]# systemctl status harbor.service
Harborにブラウザからログインして下記を変更します(https://harbor.si1230.com:8043)。
新しいプロジェクト:registryを作成します。アクセスレベルはPublicにします。
デフォルトのプロジェクト:libraryを削除します。
Bootstrap
Tanzu CLIとその他のツール
https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/1.4/vmware-tanzu-kubernetes-grid-14/GUID-install-cli.html
これはbootstrapにインストールします。
Tanzu用ディレクトリ
Tanzu用ディレクトリを作成します。
[root@bootstrap ~]# mkdir /tanzu
tanzu-cli-bundle-v1.4.0-linux-amd64.tar
tanzu-cli-bundle-v1.4.0-linux-amd64.tarを/tanzuに配置し、展開、インストールします。
[root@bootstrap ~]# cd /tanzu [root@bootstrap ~]# tar -xvf tanzu-cli-bundle-v1.4.0-linux-amd64.tar [root@bootstrap ~]# cd /tanzu/cli [root@bootstrap ~]# install core/v1.4.0/tanzu-core-linux_amd64 /usr/local/bin/tanzu
Tanzu CLIプラグインのインストール
問題なければlistオプションで一覧が表示されます。
[root@bootstrap ~]# tanzu plugin clean [root@bootstrap ~]# cd /tanzu [root@bootstrap ~]# tanzu plugin install --local cli all [root@bootstrap ~]# tanzu plugin list
kubectl-linux-v1.21.2+vmware.1.gz
[root@bootstrap ~]# cd /tanzu [root@bootstrap ~]# gzip -d kubectl-linux-v1.21.2+vmware.1.gz [root@bootstrap ~]# install kubectl-linux-v1.21.2+vmware.1 /usr/local/bin/kubectl
Carvel Toolsのインストール
[root@bootstrap ~]# cd /tanzu/cli
yttのインストール
[root@bootstrap ~]# gunzip ytt-linux-amd64-v0.34.0+vmware.1.gz [root@bootstrap ~]# chmod ugo+x ytt-linux-amd64-v0.34.0+vmware.1 [root@bootstrap ~]# mv ./ytt-linux-amd64-v0.34.0+vmware.1 /usr/local/bin/ytt
kappのインストール
[root@bootstrap ~]# gunzip kapp-linux-amd64-v0.37.0+vmware.1.gz [root@bootstrap ~]# chmod ugo+x kapp-linux-amd64-v0.37.0+vmware.1 [root@bootstrap ~]# mv ./kapp-linux-amd64-v0.37.0+vmware.1 /usr/local/bin/kapp
kbldのインストール
[root@bootstrap ~]# gunzip kbld-linux-amd64-v0.30.0+vmware.1.gz [root@bootstrap ~]# chmod ugo+x kbld-linux-amd64-v0.30.0+vmware.1 [root@bootstrap ~]# mv ./kbld-linux-amd64-v0.30.0+vmware.1 /usr/local/bin/kbld
imgpkgのインストール
[root@bootstrap ~]# gunzip imgpkg-linux-amd64-v0.10.0+vmware.1.gz [root@bootstrap ~]# chmod ugo+x imgpkg-linux-amd64-v0.10.0+vmware.1 [root@bootstrap ~]# mv ./imgpkg-linux-amd64-v0.10.0+vmware.1 /usr/local/bin/imgpkg
その他必要なソフトウェアのインストール
yq
yqは後述の”./gen-publish-images.sh > publish-images.sh”の実行に必要です。また、本稿の執筆時点ではyqの必須バージョンが4.9.2です。
[root@bootstrap ~]# wget https://github.com/mikefarah/yq/releases/download/v4.9.2/yq_linux_amd64 -O /usr/local/bin/yq [root@bootstrap ~]# chmod 755 /usr/local/bin/yq
SSHキーペアを作成
[root@bootstrap ~]# ssh-keygen -t rsa -b 4096 -C "root@si1230.com"
マネジメントクラスタ
プロキシ環境では失敗しますが、これによりBOMファイルを入手できます。
[root@bootstrap ~]# tanzu management-cluster create
tanzu management-cluster createの実施後はbootstrapの下記のディレクトリにyamlが作成されます。ファイル名はランダムの文字列となっているので、分かりやすい名前に変更しましょう。
[root@bootstrap ~]# vim /root/.config/tanzu/tkg/clusterconfigs/mgmt-cluster-config.yaml
Publish Images
https://docs.vmware.com/en/VMware-Tanzu-Kubernetes-Grid/1.4/vmware-tanzu-kubernetes-grid-14/GUID-mgmt-clusters-airgapped-environments.html
ルート証明書を信頼します。
[root@bootstrap ~]# cp ca.crt /etc/pki/ca-trust/source/anchors [root@bootstrap ~]# update-ca-trust extract
ルート証明書をbase64でエンコードし、出力を控えます。
[root@bootstrap ~]# base64 -w 0 ca.crt
bootstrapで環境変数を設定します。最後のコマンドはルート証明書をbase64でエンコードしたものです。
[root@bootstrap ~]# export TKG_CUSTOM_IMAGE_REPOSITORY="harbor.si1230.com:8043/registry" [root@bootstrap ~]# export TKG_IMAGE_REPO="projects.registry.vmware.com/tkg" [root@bootstrap ~]# export TKG_CUSTOM_IMAGE_REPOSITORY_CA_CERTIFICATE=LS0t[...]tLS0tLQ==
gen-publish-images.sh
[root@bootstrap ~]# vim /root/gen-publish-images.sh #!/bin/bash # Copyright 2021 VMware, Inc. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 set -eo pipefail TANZU_BOM_DIR=${HOME}/.config/tanzu/tkg/bom INSTALL_INSTRUCTIONS='See https://github.com/mikefarah/yq#install for installation instructions' TKG_CUSTOM_IMAGE_REPOSITORY=${TKG_CUSTOM_IMAGE_REPOSITORY:-''} TKG_IMAGE_REPO=${TKG_IMAGE_REPO:-''} echodual() { echo "$@" 1>&2 echo "#" "$@" } if [ -z "$TKG_CUSTOM_IMAGE_REPOSITORY" ]; then echo "TKG_CUSTOM_IMAGE_REPOSITORY variable is required but is not defined" >&2 exit 1 fi if [ -z "$TKG_IMAGE_REPO" ]; then echo "TKG_IMAGE_REPO variable is required but is not defined" >&2 exit 2 fi if ! [ -x "$(command -v imgpkg)" ]; then echo 'Error: imgpkg is not installed.' >&2 exit 3 fi if ! [ -x "$(command -v yq)" ]; then echo 'Error: yq is not installed.' >&2 echo "${INSTALL_INSTRUCTIONS}" >&2 exit 3 fi function imgpkg_copy() { flags=$1 src=$2 dst=$3 echo "" echo "imgpkg copy $flags $src --to-repo $dst" } if [ -n "$TKG_CUSTOM_IMAGE_REPOSITORY_CA_CERTIFICATE" ]; then echo $TKG_CUSTOM_IMAGE_REPOSITORY_CA_CERTIFICATE > /tmp/cacrtbase64 base64 -d /tmp/cacrtbase64 > /tmp/cacrtbase64d.crt function imgpkg_copy() { flags=$1 src=$2 dst=$3 echo "" echo "imgpkg copy $flags $src --to-repo $dst --registry-ca-cert-path /tmp/cacrtbase64d.crt" } fi echo "set -eo pipefail" echodual "Note that yq must be version above or equal to version 4.9.2 and below version 5." actualImageRepository="$TKG_IMAGE_REPO" # Iterate through TKG BoM file to create the complete Image name # and then pull, retag and push image to custom registry. list=$(imgpkg tag list -i "${actualImageRepository}"/tkg-bom) for imageTag in ${list}; do tanzucliversion=$(tanzu version | head -n 1 | cut -c10-15) if [[ ${imageTag} == ${tanzucliversion}* ]]; then TKG_BOM_FILE="tkg-bom-${imageTag//_/+}.yaml" imgpkg pull --image "${actualImageRepository}/tkg-bom:${imageTag}" --output "tmp" > /dev/null 2>&1 echodual "Processing TKG BOM file ${TKG_BOM_FILE}" actualTKGImage=${actualImageRepository}/tkg-bom:${imageTag} customTKGImage=${TKG_CUSTOM_IMAGE_REPOSITORY}/tkg-bom imgpkg_copy "-i" $actualTKGImage $customTKGImage # Get components in the tkg-bom. # Remove the leading '[' and trailing ']' in the output of yq. components=(`yq e '.components | keys | .. style="flow"' "tmp/$TKG_BOM_FILE" | sed 's/^.//;s/.$//'`) for comp in "${components[@]}" do # remove: leading and trailing whitespace, and trailing comma comp=`echo $comp | sed -e 's/^[[:space:]]*//' | sed 's/,*$//g'` get_comp_images="yq e '.components[\"${comp}\"][] | select(has(\"images\"))|.images[] | .imagePath + \":\" + .tag' "\"tmp/\"$TKG_BOM_FILE"" flags="-i" if [ $comp = "tkg-standard-packages" ]; then flags="-b" fi eval $get_comp_images | while read -r image; do actualImage=${actualImageRepository}/${image} image2=$(echo "$image" | cut -f1 -d":") customImage=$TKG_CUSTOM_IMAGE_REPOSITORY/${image2} imgpkg_copy $flags $actualImage $customImage done done rm -rf tmp echodual "Finished processing TKG BOM file ${TKG_BOM_FILE}" echo "" fi done # Iterate through TKR BoM file to create the complete Image name # and then pull, retag and push image to custom registry. list=$(imgpkg tag list -i ${actualImageRepository}/tkr-bom) for imageTag in ${list}; do if [[ ${imageTag} == v* ]]; then TKR_BOM_FILE="tkr-bom-${imageTag//_/+}.yaml" echodual "Processing TKR BOM file ${TKR_BOM_FILE}" actualTKRImage=${actualImageRepository}/tkr-bom:${imageTag} customTKRImage=${TKG_CUSTOM_IMAGE_REPOSITORY}/tkr-bom imgpkg_copy "-i" $actualTKRImage $customTKRImage imgpkg pull --image ${actualImageRepository}/tkr-bom:${imageTag} --output "tmp" > /dev/null 2>&1 # Get components in the tkr-bom. # Remove the leading '[' and trailing ']' in the output of yq. components=(`yq e '.components | keys | .. style="flow"' "tmp/$TKR_BOM_FILE" | sed 's/^.//;s/.$//'`) for comp in "${components[@]}" do # remove: leading and trailing whitespace, and trailing comma comp=`echo $comp | sed -e 's/^[[:space:]]*//' | sed 's/,*$//g'` get_comp_images="yq e '.components[\"${comp}\"][] | select(has(\"images\"))|.images[] | .imagePath + \":\" + .tag' "\"tmp/\"$TKR_BOM_FILE"" flags="-i" if [ $comp = "tkg-core-packages" ]; then flags="-b" fi eval $get_comp_images | while read -r image; do actualImage=${actualImageRepository}/${image} image2=$(echo "$image" | cut -f1 -d":") customImage=$TKG_CUSTOM_IMAGE_REPOSITORY/${image2} imgpkg_copy $flags $actualImage $customImage done done rm -rf tmp echodual "Finished processing TKR BOM file ${TKR_BOM_FILE}" echo "" fi done list=$(imgpkg tag list -i ${actualImageRepository}/tkr-compatibility) for imageTag in ${list}; do if [[ ${imageTag} == v* ]]; then echodual "Processing TKR compatibility image" actualImage=${actualImageRepository}/tkr-compatibility:${imageTag} customImage=$TKG_CUSTOM_IMAGE_REPOSITORY/tkr-compatibility imgpkg_copy "-i" $actualImage $customImage echo "" echodual "Finished processing TKR compatibility image" fi done list=$(imgpkg tag list -i ${actualImageRepository}/tkg-compatibility) for imageTag in ${list}; do if [[ ${imageTag} == v* ]]; then echodual "Processing TKG compatibility image" actualImage=${actualImageRepository}/tkg-compatibility:${imageTag} customImage=$TKG_CUSTOM_IMAGE_REPOSITORY/tkg-compatibility imgpkg_copy "-i" $actualImage $customImage echo "" echodual "Finished processing TKG compatibility image" fi done [root@bootstrap ~]# chmod +x /root/gen-publish-images.sh
publish-images.sh
[root@bootstrap ~]# cd /root [root@bootstrap ~]# ./gen-publish-images.sh > publish-images.sh [root@bootstrap ~]# cat publish-images.sh [root@bootstrap ~]# chmod +x publish-images.sh [root@bootstrap ~]# docker login ${TKG_CUSTOM_IMAGE_REPOSITORY} [root@bootstrap ~]# ./publish-images.sh
https://customerconnect.vmware.com/en/downloads/details?downloadGroup=TKG-140&productId=988&rPId=73652