#
# Chamelenon Extentsions
#
import json
import os
import subprocess
import time
import uuid
import pprint
import datetime
from vcpcli.vault import VaultClient # noqa
from vcpsdk.plugins.ext import VcpExtResource
from vcpsdk.vcp_tools import VcpSDKError
DEFAULT_VCC_PORT = 443
DEFAULT_VAULT_PORT = 8443
# 最終的にTrue
VERIFY = True
BUILD_VERSION = "20200831"
[ドキュメント]
class VcpChameleonOpenrc(VcpExtResource):
"""
Chamelenon Extentsions の BASEクラス
(内部クラスであるためAPI上は無視してよい)
"""
version = BUILD_VERSION
def __init__(self, provider_name, config_dir, token, verbose=0):
super().__init__(provider_name, config_dir, token)
# debug output
self._verbose = verbose
# vaultはssl 固定とする
self._vcc_host = self._vcp_config["vcc"]["host"]
self._vcc_port = self._vcp_config["vcc"].get("vcc_port", DEFAULT_VCC_PORT)
self._vault_port = self._vcp_config["vcc"].get("vault_port", DEFAULT_VAULT_PORT)
# vault client 接続
self.vault_cli = VaultClient(
vcc_host=self._vcc_host, token=self._token, no_verify=False
)
def verbose(self, verbose):
""" Chameleon extension の verbose
:param verbose: > 0 でdebug出力
"""
self._verbose = verbose
def set_property(self, vpn_catalog):
""" Chameleon 用のコマンド実行するためのパラメータ設定
:param vpn_catalog: = vcpsdk.get_vpn_catalog() で取得できるVPNカタログ情報
"""
self._auth_url = vpn_catalog["chameleon_auth_url"]
self._region = vpn_catalog["chameleon_region"]
# TODO; interfaceの項目と値を確認
self._interface = vpn_catalog["chameleon_interface"]
self._identity_api_version = vpn_catalog["chameleon_identity_api_version"]
def local_exec(self, cmd, env=None):
""" Chameleon 用のコマンド実行
:param cmd: コマンドライン文字列
:param env: 環境変数
"""
# TODO maskする (ストレージアカウントKey)
if self._verbose:
print("--- command ---")
print(" ".join(cmd))
proc = subprocess.Popen(
args=cmd,
env=env,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
(stdout, stderr) = proc.communicate()
if proc.returncode != 0:
raise Exception(
"stdout = %s\nstderr = %s"
% (stdout.decode("utf-8"), stderr.decode("utf-8"))
)
if self._verbose:
try:
print(json.loads(stdout))
if stderr and len(stderr) > 0:
print(json.loads(stderr))
except Exception:
pprint.pprint(stdout)
pprint.pprint(stderr)
return (stdout, stderr)
# TODO: openstackでの動作確認
# blazar cli起動時の環境変数の設定
def cli_env(self):
""" Chameleon 用のコマンド実行時の環境変数設定
"""
child_env = os.environ.copy()
child_env["OS_AUTH_URL"] = self._auth_url
child_env["OS_AUTH_TYPE"] = "v3applicationcredential"
child_env["OS_REGION_NAME"] = self._region
child_env["OS_INTERFACE"] = self._interface
child_env["OS_IDENTITY_API_VERSION"] = self._identity_api_version
# vcp_config.yml から chameleon のクレデンシャル情報を取得
chameleon_config = self._vcp_config.get("chameleon")
child_env[
"OS_APPLICATION_CREDENTIAL_ID"
] = self.vault_cli.read_cloud_params_value(
chameleon_config["application_credential_id"]
)
child_env[
"OS_APPLICATION_CREDENTIAL_SECRET"
] = self.vault_cli.read_cloud_params_value(
chameleon_config["application_credential_secret"]
)
return child_env
def __str__(self):
child_env = self.cli_env()
text = """
OS_AUTH_URL: {OS_AUTH_URL}
OS_INTERFACE: {OS_INTERFACE}
OS_IDENTITY_API_VERSION: {OS_IDENTITY_API_VERSION}
OS_APPLICATION_CREDENTIAL_ID: {OS_APPLICATION_CREDENTIAL_ID}
OS_APPLICATION_CREDENTIAL_SECRET: ****""".format_map(
child_env
)
return text
[ドキュメント]
class VcpExtResourceChameleonNetwork(VcpChameleonOpenrc):
"""
chameleon上でvlanセグメントを予約し、ネットワークを構築する。sharedwan1 を使用する場合は不要。
サンプルコード
.. code-block:: python
# vpnカタログ情報を取得
vpn_catalog_name = "default"
vpn_catalog = vcpsdk.get_vpn_catalog("chameleon", catalog_name=vpn_catalog_name)
# Chameleon Network の予約用 extension を生成
network_ext = vcpsdk.get_ext("chameleon_network")
network_ext.setup(vpn_catalog)
# 期間 %Y-%m-d %H:%M (省略可) を指定して、Chameleon Network を予約
# start_date = start_dt.strftime("%Y-%m-%d %H:%M")
network_ext.create_network(start_date=start_date, end_date=end_date)
"""
[ドキュメント]
def setup(self, vpn_catalog):
""" chameleon のネットワークの予約情報を作成する
:param vpn_catalog: VPNカタログ情報
"""
self._vpn_catalog = vpn_catalog
self.set_property(vpn_catalog)
# TODO; VCコントローラID, VPNカタログ名
# network name + xxx (network_nameがほかVCコントローラとかぶらない前提)
self._network_lease_name = "{}-net-lease".format(self.network_name)
[ドキュメント]
def network_exists(self):
self.set_property(self._vpn_catalog)
network_name = self.network_name
cmd = ["openstack", "network", "list", "-f", "json"]
(stdout, stderr) = self.local_exec(cmd, env=self.cli_env())
network = json.loads(stdout)
return network_name in [x["Name"] for x in network]
@property
def network_name(self):
return self._vpn_catalog.get("chameleon_network_name")
[ドキュメント]
def create_network(self, start_date=None, end_date=None):
""" vlanセグメントを予約し、予約できた場合にネットワーク、サブネットを作成する。
"""
self.set_property(self._vpn_catalog)
network_name = self.network_name
if self.network_exists():
VcpSDKError("network {} already exists".format(network_name))
network_spec = "resource_type=network,network_name={}".format(network_name)
# vlanの予約
# TODO: VLAN IDの固定
# network_spec = 'resource_type=network,network_name={},resource_properies["==", "$segment_id","{}"]'.format(
# catalog["chameleon_network_name"], catalog["chameleon_network_segment_id"]
# )
date_opt = []
if start_date is not None:
date_opt.append("--start-date={}".format(start_date))
if end_date is not None:
date_opt.append("--end-date={}".format(end_date))
# TODO: 予約があってネットワークがないという自体を考慮して、予約が有るかチェック?
# 成功するとネットワークが作成される
cmd = (
["blazar", "lease-create", "-f", "json"]
+ date_opt
+ ["--reservation", network_spec, self._network_lease_name]
)
(stdout, stderr) = self.local_exec(cmd, env=self.cli_env())
# TODO; get reservation id
# TODO: networkのleaseのstatus確認
for _i in range(0, 20):
cmd = ["openstack", "network", "list", "-f", "json"]
(stdout, stderr) = self.local_exec(cmd, env=self.cli_env())
network = json.loads(stdout)
if network_name in [x["Name"] for x in network]:
break
time.sleep(10)
# openstackのサブネットの作成
# https://chameleoncloud.readthedocs.io/en/latest/technical/networks/networks_vlan.html?highlight=subnet%20create
# "--dhcp", # TODO if enabled in catalog
cmd = [
"openstack",
"subnet",
"create",
"-f",
"json",
"--subnet-range",
self._vpn_catalog.get("private_network_ipmask"),
"--network",
network_name,
self._vpn_catalog.get("chameleon_subnet_name"),
]
(stdout, stderr) = self.local_exec(cmd, env=self.cli_env())
# TODO; get lease_id
# TODO: 作成できたかどうかを返す
[ドキュメント]
class VcpExtResourceChameleonHostLeaseInfo(VcpChameleonOpenrc):
"""
Chameleon クラウドインスタンスの予約結果
サンプルコード
.. code-block:: python
# lease idを使って情報取得
info_ext = vcpsdk.get_extension("chameleon_host_lease_info")
info_ext.setup(lease_info.host_lease_id, vpn_catalog)
print(info_ext)
print("host_lease_id is {}".format(info_ext.host_lease_id))
print("reservation_id is {}".format(info_ext.reservation_id))
print("status is {}".format(info_ext.status))
# 予約の延長
info_ext.extend()
# 予約の削除
info_ext.delete()
# 予約の完了まで待つ
info_ext.wait()
"""
[ドキュメント]
def setup(self, host_lease_id, vpn_catalog):
""" chameleon のホストの予約情報を設定する
:param host_lease_id: ホストの予約情報識別子
:param vpn_catalog: VPNカタログ情報
"""
self._host_lease_id = host_lease_id
self._reservation_id = None
self._vpn_catalog = vpn_catalog
self.set_property(vpn_catalog)
self.update()
[ドキュメント]
def update(self):
"""
ホストの予約情報を再取得する。
"""
cmd = ["blazar", "lease-show", "-f", "json", self._host_lease_id]
(stdout, stderr) = self.local_exec(cmd, env=self.cli_env())
info = json.loads(stdout)
if self._verbose:
print(json.dumps(info, indent=2))
reservation_info = json.loads(info["reservations"])
self._reservation_id = reservation_info["id"]
self._start_date = info["start_date"]
self._end_date = info["end_date"]
self._status = info["status"]
self._updated_at = info["updated_at"]
def __str__(self):
return "lease_id: {}\n reservation_id: {}\n state: {}\nstart_date: {}\nend_date {}\nupdated_at:{}".format(
self._host_lease_id,
self._reservation_id,
self._status,
self._start_date,
self._end_date,
self._updated_at,
)
# getter
@property
def reservation_id(self):
"""
reservation IDを返す。CCIのreservationに指定する。
"""
return self._reservation_id
# getter
@property
def host_lease_id(self):
"""
lease IDを返す。
"""
return self._host_lease_id
# getter
@property
def status(self):
"""
lease状態を返す
:return: leaseの状態
- ``STARTING`` 予約情報を作成中
- ``PENDING`` 予約開始時刻が到来していない
- ``ACTIVE`` 予約開始時刻が到来した
- ``ERROR`` エラーが発生した
"""
return self._status
[ドキュメント]
def extend(self, weeks=None, days=None, hours=None):
"""
予約を延長する。
weeks, days, hours のどれか一つを選んで指定する。複数の引数が指定された場合、単位が長い方の指定を優先する。
引数を指定しない場合、1日予約を延長する
chameleonの仕様により、予約の延長ができるのは予約が切れる48時間まえ。
その時間外に呼ぶと予約延長操作は失敗する。
予約に失敗した場合は例外を送出する。
:param weeks: 延長する週
:param days: 延長する日数
:param hours: 延長する時間
"""
delta_str = ""
if weeks is not None:
delta_str = "{}w".format(weeks)
elif days is not None:
delta_str = "{}d".format(days)
elif hours is not None:
delta_str = "{}h".format(hours)
else:
# default
delta_str = "1d"
# 予約の切れる48時間前であるかチェック
now = datetime.datetime.utcnow()
end_date = datetime.datetime.strptime(self._end_date, "%Y-%m-%dT%H:%M:%S.%f")
if end_date - datetime.timedelta(hours=48) > now:
VcpSDKError(
"lease update avairable at {} ".format(
end_date - datetime.delta(hours=48)
)
)
# blazar lease-update --prolong-for "1d" my-first-lease
cmd = [
"blazar",
"lease-update",
"--prolong-for",
delta_str,
self._host_lease_id,
]
self.local_exec(cmd, env=self.cli_env())
[ドキュメント]
def delete(self):
"""
ホストの予約を削除する。
"""
cmd = ["blazar", "lease-delete", self._host_lease_id]
(stdout, stderr) = self.local_exec(cmd, env=self.cli_env())
self._host_lease_id = None
self._reservation_id = None
[ドキュメント]
def wait(self):
"""
leaseの状態(status)がACTIVEになるまで待つ
"""
if self._host_lease_id is None:
raise Exception("no lease found")
# TODO: config
for _i in range(0, 20):
self.update()
if self._status == "ACTIVE":
break
if self._status not in ["PENDING", "STARTING"]:
# reservation error
# TODO: エラーの理由を出力する blazarの出力の 最終行をひろう
# 例: ERROR: No hosts are available
raise Exception(
"host reservation is failed, unknown status {}".format(self._status)
)
time.sleep(10)
[ドキュメント]
class VcpExtResourceChameleon(VcpChameleonOpenrc):
"""
Chameleon クラウドインスタンスの予約を行うクラス
サンプルコード
.. code-block:: python
# vpnカタログ情報を取得
vpn_catalog_name = "default"
vpn_catalog = vcpsdk.get_vpn_catalog("chameleon", catalog_name=vpn_catalog_name)
host_ext = vcpsdk.get_extension("chameleon")
# 指定可能項目
node_type = "compute_skylake"
min_instances = 1
max_instances = 3
host_ext.setup(
node_type,
start_date=start_date,
end_date=end_date,
min_instances=min_instances,
max_instances=max_instances,
lease_name_prefix = "S003",
vpn_catalog=vpn_catalog,
)
# インスタンスの予約
lease_info = host_ext.reserve_host()
lease_info.wait()
print("host_lease_id is {}".format(lease_info.host_lease_id))
print("reservation_id is {}".format(lease_info.reservation_id))
print("status is {}".format(lease_info.status))
"""
[ドキュメント]
def setup(
self,
node_type,
start_date=None,
end_date=None,
min_instances=1,
max_instances=1,
vcpus=None,
memory_mb=None,
disk_gb=None,
lease_name_prefix=None,
vpn_catalog=None,
):
""" chameleon のホストの予約情報を作成する (実際の予約処理は reserve_host関数で行う)
:param node_type: chameleon 上での node_type
:param start_date: 予約の開始時刻 (タイムゾーンはUTCで ``YYYY-mm-dd HH:MM`` 形式) Noneを指定した場合は、呼び出し時の時刻が使われる。
:param end_date: 予約の終了時刻 (タイムゾーンはUTCで ``YYYY-mm-dd HH:MM`` 形式) Noneを指定した場合は、1日後
:param min_instances: 最小台数
:param max_instances: 最大台数
:param vcpus: VCPU数 future use
:param memory_mb: メモリ容量(MBytes) future use
:param disk_gb: Disk容量(GBytes) future use
:param lease_name_prefix: Chameleon のlease 名に付けるprefix
:param vpn_catalog: VPNカタログ情報
.. note::
vcpus, memory_mb, disk_gb の指定は現状の実装では、無視する。
"""
self._vpn_catalog = vpn_catalog
self.set_property(vpn_catalog)
self._node_type = node_type
self._start_date = start_date
self._end_date = end_date
self._min_instances = min_instances
self._max_instances = max_instances
# TODO: 調査
self._vcpus = vcpus
# TODO: 調査
self._memory_mb = memory_mb
# TODO: 調査
self._disk_gb = disk_gb
# TODO: VC id + unit名 でつける, 連想できたほうが良い
# 予約が先になるのでVC id, Unit名はつけられない
prefix = uuid.uuid4().hex[:10]
if lease_name_prefix is not None:
self._host_lease_name = "{}-{}-host".format(lease_name_prefix, prefix)
else:
self._host_lease_name = "{}-host".format(prefix)
self._reservation_id = None
self._host_lease_id = None
[ドキュメント]
def reserve_host(self):
"""
クラウドインスタンスの予約を実行する。
:return: クラウドインスタンスの予約結果 ( :class:`vcpsdk.plugins.chameleon_ext.VcpExtResourceChameleonHostLeaseInfo` )
"""
# TODO: hypervisor propertiesを引数で取るようにする $vcpu, $memory_mb, $disk_gb
resource_spec_detail = [["=", "$node_type", self._node_type]]
hypervisor_spec_detail = []
# 条件を指定しても効果が有るのか不明なためコメントアウト
# if self._vcpus is not None:
# # TODO; 不等号固定で良いか?
# hypervisor_spec_detail.append([">=", "$vcpu", str(self._vcpu)])
# if self._memory_mb is not None:
# # TODO; 不等号固定で良いか?
# hypervisor_spec_detail.append([">=", "$memory_mb", str(self._memory_mb)])
# if self._disk_gb is not None:
# disk_bytes = 1024 * 1024 * 1024 * self._disk_gb
# resource_spec_detail.append(
# [">=", "$storage_devices.0.size", str(disk_bytes)]
# )
# if self._disk_gb is not None:
# # TODO; 不等号固定で良いか?
# spec_detail.append(['>=', '$disk_gb', str(self._disk_gb)])
# TODO: andは3つ以上の条件を取れるか確認
# TODO: 取れない場合は、木にする
if len(hypervisor_spec_detail) == 0:
hypervisor_spec = ""
elif len(hypervisor_spec_detail) == 1:
hypervisor_spec = "hypervisor_properties={},".format(
json.dumps(hypervisor_spec_detail[0])
)
else:
# 複数条件をandで結合する
hypervisor_spec = "hypervisor_properties={},".format(
json.dumps(["and"] + hypervisor_spec_detail)
)
if len(resource_spec_detail) == 0:
resource_spec = ""
elif len(resource_spec_detail) == 1:
resource_spec = "resource_properties={}".format(
json.dumps(resource_spec_detail[0])
)
else:
# 複数条件をandで結合する
resource_spec = "resource_properties={}".format(
json.dumps(["and"] + resource_spec_detail)
)
chameleon_spec = "min={},max={},{}{}".format(
self._min_instances, self._max_instances, hypervisor_spec, resource_spec
)
date_opt = []
if self._start_date is not None:
date_opt.append("--start-date={}".format(self._start_date))
if self._end_date is not None:
date_opt.append("--end-date={}".format(self._end_date))
cmd = (
["blazar", "lease-create", "-f", "json",]
+ date_opt
+ ["--physical-reservation", chameleon_spec, self._host_lease_name]
)
(stdout, stderr) = self.local_exec(cmd, env=self.cli_env())
# Createdというjsonではない行(1行)を消す
stdout = "\n".join(stdout.decode("utf-8").split("\n")[1:])
lease = json.loads(stdout)
self._host_lease_id = lease["id"]
self._host_lease_info = VcpExtResourceChameleonHostLeaseInfo(
self._provider_name, self._config_dir, self._token
)
self._host_lease_info.setup(self._host_lease_id, self._vpn_catalog)
self._host_lease_info.update()
return self._host_lease_info
def __str__(self):
text = super().__str__()
text += """
node_type: {node_type}
start_date: {start_date}
end_date: {end_date}
min_instances: {min_instances}
max_instances: {max_instances}
vcpus: {vcpus}
memory_mb: {memory_mb} MB
disk_gb: {disk_gb} GB
host_lease_name: {host_lease_name}
reservation_id: {reservation_id}
host_lease_id: {host_lease_id}""".format(
node_type=self._node_type,
start_date=self._start_date,
end_date=self._end_date,
min_instances=self._min_instances,
max_instances=self._max_instances,
vcpus=self._vcpus,
memory_mb=self._memory_mb,
disk_gb=self._disk_gb,
host_lease_name=self._host_lease_name,
reservation_id=self._reservation_id,
host_lease_id=self._host_lease_id,
)
return text