"""State module for managing Amazon Organizations Accounts."""
import copy
from typing import Any
from typing import Dict
from typing import List
__contracts__ = ["resource"]
SERVICE = "organizations"
TREQ = {
"absent": {
"require": [
"aws.organizations.policy_attachment.absent",
"aws.organizations.policy.absent",
],
},
"present": {
"require": [
"aws.organizations.organization_unit.present",
],
},
}
[docs]async def present(
hub,
ctx,
name: str,
email: str,
role_name: str = None,
iam_user_access_to_billing: str = "ALLOW",
resource_id: str = None,
parent_id: str = None,
tags: List[Dict[str, Any]] or Dict[str, Any] = None,
) -> Dict[str, Any]:
"""Creates an AWS account that is automatically a member of the organization whose credentials made the request.
Args:
name(str):
The name of the member account.
email(str):
The email address of the owner to assign to the new member account.
This email address must not already be associated with another Amazon Web Services account.
You must use a valid email address to complete account creation.
role_name(str, Optional):
The name of an IAM role that Organizations automatically preconfigures in the new member account.
This role trusts the management account, allowing users in the management account to assume the role,
as permitted by the management account administrator. The role has administrator permissions in the new member account.
If you don't specify this parameter, the role name defaults to ``OrganizationAccountAccessRole``.
iam_user_access_to_billing(str, Optional):
If set to ``ALLOW``, the new account enables IAM users to access account billing information if they have the required permissions.
If set to ``DENY``, only the root user of the new account can access account billing information.
If you don't specify this parameter, the value defaults to ``ALLOW``.
resource_id(str, Optional):
The ID of the member account in Amazon Web Services.
parent_id(str, Optional):
Parent Organizational Unit ID or Root ID for the account. Defaults to the Organization default Root ID.
tags(dict or list, Optional):
Dict in the format of ``{tag-key: tag-value}`` or List of tags in the format of
``[{"Key": tag-key, "Value": tag-value}]`` to associate with the member account.
* Key (*str*):
The key identifier, or name, of the tag.
* Value (*str*):
The string value that's associated with the key of the tag.
Request Syntax:
.. code-block:: sls
[idem_test_aws_organizations_account]:
aws.organizations.account.present:
- name: 'string'
- resource_id: 'string'
- email: 'string'
- role_name: 'string'
- iam_user_access_to_billing: 'ALLOW|DENY'
- parent_id: 'string'
- tags:
- Key: 'string'
Value: 'string
Returns:
Dict[str, Any]
Examples:
.. code-block:: sls
idem_test_aws_organizations_account:
aws.organizations.account.present:
- name: 'idem_test_account'
- email: 'xyz@email.com'
- role_name: 'idem_test_role'
- iam_user_access_to_billing: 'ALLOW'
- parent_id: 'o-parent-id'
- tags:
- Key: 'provider'
Value: 'idem'
"""
result = dict(comment=[], name=name, result=True, old_state=None, new_state=None)
before = None
update_tag = False
update_parent = False
if resource_id:
before = await hub.exec.aws.organizations.account.get(
ctx, resource_id=resource_id, name=name
)
if not before["result"]:
result["result"] = before["result"]
result["comment"] = before["comment"]
return result
if isinstance(tags, List):
tags = hub.tool.aws.tag_utils.convert_tag_list_to_dict(tags)
if before and before["ret"]:
# Account exists , update
try:
result["old_state"] = before["ret"]
plan_state = copy.deepcopy(result["old_state"])
# we need to list parents to check if move is required in case the new_parent_id is not equal to current_
# parent_id
current_parent_id = before["ret"]["parent_id"]
if current_parent_id and parent_id and current_parent_id != parent_id:
if not ctx.get("test", False):
move_account_result = (
await hub.tool.aws.organizations.account.move_account(
ctx, resource_id, result, parent_id
)
)
if move_account_result and not move_account_result["result"]:
result["comment"] += [
f"Could not update parent for aws.organizations.account {name}"
]
result["result"] = False
return result
update_parent = True
if update_parent:
plan_state["parent_id"] = parent_id
result["comment"] += [
f"Would update parent for aws.organizations.account {name}"
]
# update tags
if tags is not None and tags != result["old_state"].get("tags"):
update_tags_ret = await hub.tool.aws.organizations.tag.update_tags(
ctx, resource_id, result["old_state"].get("tags"), tags
)
if not update_tags_ret["result"]:
result["comment"] += update_tags_ret["comment"]
result["result"] = update_tags_ret["result"]
return result
result["comment"] += [
f"Updated tags on aws.organizations.account '{name}'."
]
update_tag = True
if ctx.get("test", False):
if update_tags_ret["ret"]:
plan_state["tags"] = update_tags_ret["ret"]
else:
plan_state["tags"] = result["old_state"]["tags"]
except hub.tool.boto3.exception.ClientError as e:
result["comment"] += [f"{e.__class__.__name__}: {e}"]
result["result"] = False
else:
# Account not present , create
if ctx.get("test", False):
result["new_state"] = hub.tool.aws.test_state_utils.generate_test_state(
enforced_state={},
desired_state={
"email": email,
"name": name,
"parent_id": parent_id,
"role_name": role_name,
"iam_user_access_to_billing": iam_user_access_to_billing,
"tags": tags,
},
)
result["comment"] = hub.tool.aws.comment_utils.would_create_comment(
resource_type="aws.organizations.account", name=name
)
return result
try:
create_account_ret = (
await hub.exec.boto3.client.organizations.create_account(
ctx,
Email=email,
AccountName=name,
RoleName=role_name,
IamUserAccessToBilling=iam_user_access_to_billing,
Tags=hub.tool.aws.tag_utils.convert_tag_dict_to_list(tags)
if tags
else None,
)
)
result["result"] = create_account_ret["result"]
if not result["result"]:
result["comment"] += create_account_ret["comment"]
return result
account_status_id = create_account_ret["ret"]["CreateAccountStatus"]["Id"]
# Call a custom waiter to wait on account's creation.
acceptors = [
{
"matcher": "path",
"expected": "SUCCEEDED",
"state": "success",
"argument": "CreateAccountStatus.State",
},
{
"matcher": "path",
"expected": "IN_PROGRESS",
"state": "retry",
"argument": "CreateAccountStatus.State",
},
{
"matcher": "path",
"expected": "FAILED",
# Failure is also mapped with success to catch the error message
"state": "success",
"argument": "CreateAccountStatus.State",
},
]
account_waiter = hub.tool.boto3.custom_waiter.waiter_wrapper(
name="AccountCreated",
operation="DescribeCreateAccountStatus",
argument=["CreateAccountStatus.State"],
acceptors=acceptors,
client=await hub.tool.boto3.client.get_client(ctx, SERVICE),
matcher="path",
delay=10,
max_tries=10,
)
await hub.tool.boto3.client.wait(
ctx,
SERVICE,
"AccountCreated",
account_waiter,
CreateAccountRequestId=account_status_id,
)
account_status = await hub.exec.boto3.client.organizations.describe_create_account_status(
ctx, CreateAccountRequestId=account_status_id
)
if account_status["result"]:
create_account_status = account_status["ret"]["CreateAccountStatus"]
account_state = create_account_status["State"]
if account_state == "FAILED":
result["result"] = False
result["comment"] += [create_account_status["FailureReason"]]
return result
elif account_state == "SUCCEEDED":
resource_id = create_account_status["AccountId"]
result["comment"] = hub.tool.aws.comment_utils.create_comment(
resource_type="aws.organizations.account", name=name
)
if resource_id is not None and parent_id is not None:
parents = (
await hub.exec.boto3.client.organizations.list_parents(
ctx, ChildId=resource_id
)
)
result["result"] = result["result"] and parents["result"]
if not result["result"]:
result["comment"] += parents["comment"]
return result
if parents:
current_parent_id = parents["ret"]["Parents"][0]["Id"]
if current_parent_id != parent_id:
move_account_result = await hub.tool.aws.organizations.account.move_account(
ctx, resource_id, result, parent_id
)
if (
move_account_result
and not move_account_result["result"]
):
result["comment"] += [
f"Could not update parent for aws.organizations.account {name}"
]
result["result"] = False
return result
else:
result["result"] = False
result["comment"] += account_status["comment"]
return result
except hub.tool.boto3.exception.ClientError as e:
result["comment"] += [f"{e.__class__.__name__}: {e}"]
result["result"] = False
if ctx.get("test", False):
result["new_state"] = plan_state
elif not before or update_parent or update_tag and resource_id:
try:
after = await hub.exec.aws.organizations.account.get(
ctx, resource_id=resource_id, name=name
)
if after and after.get("ret"):
result["new_state"] = after["ret"]
else:
result["result"] = result["result"] and after["result"]
if not result["result"]:
result["comment"] += after["comment"]
return result
except Exception as e:
result["comment"] += [str(e)]
result["result"] = False
else:
result["new_state"] = copy.deepcopy(result["old_state"])
return result
[docs]async def absent(hub, ctx, name: str, resource_id: str = None) -> Dict[str, Any]:
"""Removes the specified account from the organization.
The removed account becomes a standalone account that isn't a member of any organization.
It's no longer subject to any policies and is responsible for its own bill payments.
The organization's management account is no longer charged for any expenses accrued by
the member account after it's removed from the organization.
This operation can be called only from the organization's management account.
Args:
name(str):
The name of the member account.
resource_id(str, Optional):
The ID of the member account in Amazon Web Services.
Request syntax:
.. code-block:: sls
[idem_test_aws_organizations_account]:
aws.organizations.account.absent:
- name: 'string'
- resource_id: 'string'
Returns:
Dict[str, Any]
Examples:
.. code-block:: sls
idem_test_aws_organizations_account:
aws.organizations.account.absent:
- name: 'idem_test_account'
- resource_id: '123456789012'
"""
result = dict(comment=[], old_state=None, new_state=None, name=name, result=True)
if not resource_id:
result["comment"] = hub.tool.aws.comment_utils.already_absent_comment(
resource_type="aws.organizations.account",
name=name,
)
return result
before = await hub.exec.aws.organizations.account.get(
ctx, resource_id=resource_id, name=name
)
if not before["result"]:
result["result"] = before["result"]
result["comment"] = before["comment"]
return result
if not before["ret"]:
result["comment"] = hub.tool.aws.comment_utils.already_absent_comment(
resource_type="aws.organizations.account",
name=name,
)
elif ctx.get("test", False):
result["comment"] = hub.tool.aws.comment_utils.would_delete_comment(
resource_type="aws.organizations.account",
name=name,
)
else:
try:
ret = await hub.exec.boto3.client.organizations.remove_account_from_organization(
ctx, AccountId=resource_id
)
result["result"] = ret["result"]
if not result["result"]:
result["comment"] += ret["comment"]
return result
result["comment"] = hub.tool.aws.comment_utils.delete_comment(
resource_type="aws.organizations.account", name=name
)
except hub.tool.boto3.exception.ClientError as e:
result["comment"] += [f"{e.__class__.__name__}: {e}"]
result["result"] = False
if before:
result["old_state"] = before["ret"]
return result
[docs]async def describe(hub, ctx) -> Dict[str, Dict[str, Any]]:
"""Describes AWS Organizations Accounts in a way that can be recreated/managed with the corresponding "present" function.
Returns:
Dict[str, Dict[str, Any]
Examples:
.. code-block:: bash
$ idem describe aws.organizations.account
"""
result = {}
describe_ret = await hub.exec.aws.organizations.account.list(ctx)
if not describe_ret["ret"]:
hub.log.warning(f"Could not list accounts {describe_ret['comment']}")
return {}
accounts = describe_ret["ret"]
for account in accounts:
account_id = account["resource_id"]
result[account_id] = {
"aws.organizations.account.present": [
{parameter_key: parameter_value}
for parameter_key, parameter_value in account.items()
]
}
return result