from typing import Any
from idem_vra.helpers.mapper import add_properties
from idem_vra.helpers.mapper import omit_properties
from idem_vra.helpers.models import StateReturn
__contracts__ = ["resource"]
TREQ = {"present": {"require": []}, "absent": {"require": ["vra.iaas.project.absent"]}}
[docs]async def present(
hub,
ctx,
name: str,
cloudAccountProperties: Any,
regions: Any,
cloudAccountType: Any,
privateKey: Any,
privateKeyId: Any,
**kwargs,
):
"""
:param object cloudAccountProperties: (required in body) Cloud Account specific properties supplied in as name value pairs
:param array regions: (required in body) A set of regions to enable provisioning on.Refer to /iaas/api/cloud-
accounts/region-enumeration.
'regionInfos' is a required parameter for AWS, AZURE, GCP, VSPHERE,
VMC, VCF cloud account types.
:param string cloudAccountType: (required in body) Cloud account type
:param string privateKey: (required in body) Secret access key or password to be used to authenticate with the
cloud account. In case of AAP pass a dummy value.
:param string name: (required in body) A human-friendly name used as an identifier in APIs that support this
option.
:param string privateKeyId: (required in body) Access key id or username to be used to authenticate with the cloud
account
:param string apiVersion: (optional in query) The version of the API in yyyy-MM-dd format (UTC). For versioning
information refer to /iaas/api/about
:param string validateOnly: (optional in query) If provided, it only validates the credentials in the Cloud Account
Specification, and cloud account will not be created.
:param array associatedCloudAccountIds: (optional in body) Cloud accounts to associate with this cloud account
:param string description: (optional in body) A human-friendly description.
:param Any certificateInfo: (optional in body)
:param array tags: (optional in body) A set of tag keys and optional values to set on the Cloud Account
:param boolean createDefaultZones: (optional in body) Create default cloud zones for the enabled regions.
:param object customProperties: (optional in body) Additional custom properties that may be used to extend the Cloud
Account. In case of AAP, provide environment property here.Example:
"customProperties": {
"environment": "aap"
}
:param object associatedMobilityCloudAccountIds: (optional in body) Cloud Account IDs and directionalities create associations to other
vSphere cloud accounts that can be used for workload mobility. ID
refers to an associated cloud account, and directionality can be
unidirectional or bidirectional. Only supported on vSphere cloud
accounts.
"""
try:
state = CloudaccountStateImpl(hub, ctx)
return await state.present(
hub,
ctx,
name,
cloudAccountProperties,
regions,
cloudAccountType,
privateKey,
privateKeyId,
**kwargs,
)
except Exception as error:
hub.log.error("Error during enforcing present state: cloudaccount")
hub.log.error(str(error))
raise error
[docs]async def absent(hub, ctx, name: str, **kwargs):
"""
:param string p_id: (required in path) The ID of the Cloud Account
:param string apiVersion: (optional in query) The version of the API in yyyy-MM-dd format (UTC). For versioning
information refer to /iaas/api/about
"""
"""
:param string name: (required) name of the resource
"""
try:
state = CloudaccountStateImpl(hub, ctx)
return await state.absent(hub, ctx, name, **kwargs)
except Exception as error:
hub.log.error("Error during enforcing absent state: cloudaccount")
hub.log.error(str(error))
raise error
[docs]async def describe(hub, ctx):
try:
state = CloudaccountStateImpl(hub, ctx)
return await state.describe(hub, ctx)
except Exception as error:
hub.log.error("Error during describe: cloudaccount")
hub.log.error(str(error))
raise error
[docs]def is_pending(hub, ret: dict, state: str = None, **pending_kwargs):
try:
state = CloudaccountStateImpl(hub, None)
return state.is_pending(hub, ret, state, **pending_kwargs)
except Exception as error:
hub.log.error("Error during is_pending: cloudaccount")
hub.log.error(str(error))
raise error
[docs]class CloudaccountState:
def __init__(self, hub, ctx):
self.hub = hub
self.ctx = ctx
[docs] async def present(
self,
hub,
ctx,
name: str,
cloudAccountProperties: Any,
regions: Any,
cloudAccountType: Any,
privateKey: Any,
privateKeyId: Any,
**kwargs,
):
search_result = (await self.paginate_find(hub, ctx))["ret"]
for s in search_result.content:
if name == s["name"] and True:
hub.log.info(
f'Returning resource cloudaccount "{s["name"]}" due to existing resource "{name}"'
)
s = await self.remap_resource_structure(hub, ctx, s)
return StateReturn(
result=True,
comment=f"Resource cloudaccount {name} already exists.",
old=s,
new=s,
)
res = (
await hub.exec.vra.iaas.cloudaccount.create_cloud_account_async(
ctx,
cloudAccountProperties,
regions,
cloudAccountType,
privateKey,
name,
privateKeyId,
**kwargs,
)
)["ret"]
res = await self.remap_resource_structure(hub, ctx, res)
return StateReturn(
result=True,
comment=f"Creation of cloudaccount {name} success.",
old=None,
new=res,
)
[docs] async def absent(self, hub, ctx, name: str, **kwargs):
search_result = (await self.paginate_find(hub, ctx))["ret"]
resource = None
for s in search_result.content:
if name == s["name"] and True:
hub.log.info(
f'Found resource cloudaccount "{s["name"]}" due to existing resource "{name}"'
)
s = await self.remap_resource_structure(hub, ctx, s)
resource = s
if resource:
# it exists!
delete_kwargs = {}
delete_kwargs["p_id"] = resource.get("id")
hub.log.debug(
f"cloudaccount with name = {resource.get('name')} already exists"
)
await hub.exec.vra.iaas.cloudaccount.delete_cloud_account(
ctx, **delete_kwargs
)
return StateReturn(
result=True,
comment=f"Resource with name = {resource.get('name')} deleted.",
old=resource,
new=None,
)
return StateReturn(
result=True,
comment=f"Resource with name = {name} is already absent.",
old=None,
new=None,
)
[docs] async def describe(self, hub, ctx):
result = {}
res = await self.paginate_find(hub, ctx)
for obj in res.get("ret", {}).get("content", []):
# Keep track of name and id properties as they may get remapped
obj_name = obj.get("name", "unknown")
obj_id = obj.get("id", "unknown")
obj = await self.remap_resource_structure(hub, ctx, obj)
# Define props
props = [{key: value} for key, value in obj.items()]
# Build result
result[f"{obj_name}-{obj_id.split('-')[-1]}"] = {
"vra.iaas.cloudaccount.present": props
}
return result
[docs] async def paginate_find(self, hub, ctx, **kwargs):
"""
Paginate through all resources using their 'find' method.
"""
res = await hub.exec.vra.iaas.cloudaccount.get_cloud_accounts(ctx, **kwargs)
numberOfElements = res.get("ret", {}).get("numberOfElements", 0)
totalElements = res.get("ret", {}).get("totalElements", 0)
initialElements = numberOfElements
if numberOfElements != totalElements and totalElements != 0:
while initialElements < totalElements:
hub.log.debug(
f"Requesting cloudaccount with offset={initialElements} out of {totalElements}"
)
pres = await hub.exec.vra.iaas.cloudaccount.get_cloud_accounts(
ctx, skip=initialElements
)
initialElements += pres.get("ret", {}).get("numberOfElements", 0)
aggO = res.get("ret", {}).get("content", [])
aggN = pres.get("ret", {}).get("content", [])
res["ret"]["content"] = [*aggO, *aggN]
res["ret"]["numberOfElements"] = initialElements
return res
[docs] def is_pending(self, hub, ret: dict, state: str = None, **pending_kwargs):
"""
State reconciliation
"""
hub.log.debug(f'Running is_pending for resource: {ret.get("__id__", None)}...')
is_pending_result = False
hub.log.debug(
f'is_pending_result for resource "{ret.get("__id__", None)}": {is_pending_result}'
)
return is_pending_result
[docs] async def remap_resource_structure(self, hub, ctx, obj: dict) -> dict:
schema_mapper = {
"add": [
{
"key": "certificateInfo.certificate",
"value": "jsonpath:cloudAccountProperties.certificate",
},
{
"key": "regions",
"value": "jsonpath:$",
"source": "lambda",
"lambda": "return [{\n 'externalRegionId': region.get('externalRegionId'),\n 'name': region.get('name'),\n 'regionId': region.get('id')\n} for region in resource.get('enabledRegions')] if resource.get('enabledRegions') else []\n",
"kwargs": {"resource": "jsonpath:$"},
},
{
"key": "associatedCloudAccountIds",
"value": "jsonpath:$",
"source": hub.states.vra.iaas.cloudaccount.remap_associated_cloudaccount_ids,
"kwargs": {"resource": "jsonpath:$"},
"hub": hub,
"ctx": ctx,
},
{
"key": "privateKeyId",
"value": "jsonpath:cloudAccountProperties.privateKeyId",
},
{"key": "privateKey", "value": "<private key / password>"},
],
"omit": [
"orgId",
"createdAt",
"updatedAt",
"owner",
"_links",
"healthy",
"inMaintenanceMode",
"customProperties",
"cloudAccountProperties.privateKeyId",
"cloudAccountProperties.certificate",
"cloudAccountProperties.supportTagging",
"cloudAccountProperties.supportDatastores",
"enabledRegions",
],
}
# Perform resource mapping by adding properties and omitting properties.
# Property renaming is addition followed by omission.
if schema_mapper:
resource_name = "cloudaccount"
hub.log.debug(f"Remapping resource {resource_name}...")
obj = await add_properties(obj, schema_mapper.get("add", []))
obj = omit_properties(obj, schema_mapper.get("omit", []))
return obj
# ====================================
# State override
# ====================================
import time
from idem_vra.helpers.models import FunctionReturn
import json
[docs]class CloudaccountStateImpl(CloudaccountState):
[docs] async def present(
self,
hub,
ctx,
name: str,
cloudAccountProperties: Any,
privateKey: Any,
regions: Any,
cloudAccountType: Any,
privateKeyId: Any,
**kwargs,
):
# ==================================
# Handle account that already exists
# ==================================
hub.log.debug(f"we are searching for a name {name}")
search_result = (await self.paginate_find(hub, ctx))["ret"]
for s in search_result.content:
if name == s["name"] and True:
hub.log.info(
f'Returning resource cloudaccount "{s["name"]}" due to existing resource "{name}"'
)
hub.log.debug(f'search_result: "{json.dumps(s)}')
enumeration_task_state = s.get("customProperties", {}).get(
"enumerationTaskState", None
)
image_enumeration_task_state = s.get("customProperties", {}).get(
"imageEnumerationTaskState", None
)
# Check for datacollection
if (
enumeration_task_state == "FINISHED"
and image_enumeration_task_state == "FINISHED"
):
hub.log.debug(f'Enumeration finished: "{json.dumps(s)}')
s = await self.remap_resource_structure(hub, ctx, s)
# return StateReturn(
# result=True,
# comment=f"Creation of cloudaccount {name} success.",
# old=s,
# new=s
# )
s.update({"account_creation_in_progress": False})
hub.log.debug(f"proper result location for cloud account {name}")
return {
"result": True,
"name": name,
"comment": f"Creation of cloudaccount {name} success.",
"old_state": {},
"new_state": s,
}
elif (
enumeration_task_state == "FAILED"
or image_enumeration_task_state == "FAILED"
):
# return StateReturn(
# result=False,
# comment=f"Unable to data collect cloud account {name}.",
# old=None,
# new=None
# )
return {
"result": False,
"name": name,
"comment": f"Unable to data collect cloud account {name}.",
"old_state": {},
"new_state": {},
}
# Set account_creation_in_progress flag if data collection is not finished
elif (
enumeration_task_state != "FINISHED"
or image_enumeration_task_state != "FINISHED"
):
hub.log.debug(f'Enumeration not finished: "{json.dumps(s)}')
s = await self.remap_resource_structure(hub, ctx, s)
s.update({"account_creation_in_progress": True})
# return StateReturn(
# result=False,
# comment=f"Waiting for datacolection on {name}.",
# old=s,
# new=s
# )
return {
"result": False,
"name": name,
"comment": f"Waiting for datacolection on {name}.",
"old_state": s,
"new_state": s,
}
s = await self.remap_resource_structure(hub, ctx, s)
# return StateReturn(
# result=True,
# comment=f"Resource cloudaccount {name} already exists.",
# old=s,
# new=s
# )
s.update({"account_creation_in_progress": False})
return {
"result": True,
"name": name,
"comment": f"Resource cloudaccount {name} already exists",
"old_state": s,
"new_state": s,
}
# ==================================
# Handle new account
# ==================================
# Do the async create
res = (
await hub.exec.vra.iaas.cloudaccount.create_cloud_account_async(
ctx,
cloudAccountProperties,
privateKey,
regions,
cloudAccountType,
name,
privateKeyId,
**kwargs,
)
)["ret"]
# Monitor the create flow (wait for 10 seconds)
iteration = 0
interval = 1
max_iterations = 10
while res.get("status", None) == "INPROGRESS" and iteration < max_iterations:
hub.log.debug(f"Cloud account creation is in progress... for {name}")
time.sleep(interval)
res = await hub.tool.vra.authed.get(ctx, res.get("selfLink"))
res.update({"name": name})
res.update({"account_creation_in_progress": True})
iteration += 1
hub.log.debug(f"after successfull creation search again for {name}")
search_result = (await self.paginate_find(hub, ctx))["ret"]
for s in search_result.content:
if name == s["name"] and True:
s.update({"account_creation_in_progress": True})
s = await self.remap_resource_structure(hub, ctx, s)
return {
"result": True,
"name": name,
"comment": f"Creation of cloudaccount {name} is success, waiting for other stuff",
"old_state": {},
"new_state": s,
}
# return StateReturn(
# result=True,
# comment=f"Creation of cloudaccount {name} in progress...",
# old=None,
# new=res,
# )
hub.log.debug(f"Returning with rerun_data: {name} {res}")
return {
"result": False,
"name": name,
"comment": f"Creation of cloudaccount {name} in progress...",
"rerun_data": res.get("selfLink"),
"old_state": {},
"new_state": {},
}
[docs] def is_pending(self, hub, ret: dict, state: str = None, **pending_kwargs) -> bool:
"""
State reconciliation for cloud accounts
This method enables state specific implementation of is_pending logic,
based on resource specific attribute(s).
Usage 'idem state <sls-file> --reconciler=basic', where the reconciler attribute
can be missed.
:param hub: The Hub into which the resolved callable will get placed.
:param ret: The returned dictionary of the last run.
:param state: The name of the state.
:param pending_kwargs: (dict, Optional) May include 'ctx' and 'reruns_wo_change_count'.
:return: True | False
"""
hub.log.debug(f'Running is_pending for resource: {ret.get("__id__", None)}...')
hub.log.debug(f"ret: {json.dumps(ret)}")
# Limiting reconciliation to up to 600 consecutive times w/o change
if pending_kwargs and pending_kwargs.get("reruns_wo_change_count", 0) >= 600:
hub.log.debug("is_pending: False")
return False
if (
ret.get("new_state") != None
and ret.get("new_state").get("status", None) == "FAILED"
):
hub.log.debug("is_pending: False")
hub.log.debug(f'message: {ret.get("new_state").get("message", None)}')
return False
if (
ret.get("new_state") != None
and ret.get("new_state").get("account_creation_in_progress", None) == True
):
hub.log.debug("is_pending: True")
return True
if ret["result"]:
hub.log.debug(f"found result breaking is_pending {ret}")
return False
# hub.log.debug('is_pending: False')
hub.log.debug(f"why ret{ret}")
return True
[docs]async def remap_associated_cloudaccount_ids(hub, ctx, resource):
associated_account_ids = []
associated_accounts = resource.get("_links", {}).get(
"associated-cloud-accounts", None
)
# Output accociated cloud account ids only for vsphere cloud accounts
if resource.get("cloudAccountType", None) == "vsphere" and associated_accounts:
associated_account_ids = [
account.split("/")[-1] for account in associated_accounts.get("hrefs", [])
]
return FunctionReturn(ret=associated_account_ids)