add support for AWS Organizations
endpoints covers so far: - create_organization - describe_organization - create_account - describe_account - list_accounts all tests passing. could use some advise from maintaners.
This commit is contained in:
parent
a1d095c14b
commit
edbc57e00d
@ -27,6 +27,7 @@ from .glacier import mock_glacier, mock_glacier_deprecated # flake8: noqa
|
||||
from .iam import mock_iam, mock_iam_deprecated # flake8: noqa
|
||||
from .kinesis import mock_kinesis, mock_kinesis_deprecated # flake8: noqa
|
||||
from .kms import mock_kms, mock_kms_deprecated # flake8: noqa
|
||||
from .organizations import mock_organizations # flake8: noqa
|
||||
from .opsworks import mock_opsworks, mock_opsworks_deprecated # flake8: noqa
|
||||
from .polly import mock_polly # flake8: noqa
|
||||
from .rds import mock_rds, mock_rds_deprecated # flake8: noqa
|
||||
|
@ -26,6 +26,7 @@ from moto.kinesis import kinesis_backends
|
||||
from moto.kms import kms_backends
|
||||
from moto.logs import logs_backends
|
||||
from moto.opsworks import opsworks_backends
|
||||
from moto.organizations import organizations_backends
|
||||
from moto.polly import polly_backends
|
||||
from moto.rds2 import rds2_backends
|
||||
from moto.redshift import redshift_backends
|
||||
@ -72,6 +73,7 @@ BACKENDS = {
|
||||
'kinesis': kinesis_backends,
|
||||
'kms': kms_backends,
|
||||
'opsworks': opsworks_backends,
|
||||
'organizations': organizations_backends,
|
||||
'polly': polly_backends,
|
||||
'redshift': redshift_backends,
|
||||
'rds': rds2_backends,
|
||||
|
6
moto/organizations/__init__.py
Normal file
6
moto/organizations/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
from .models import organizations_backend
|
||||
from ..core.models import base_decorator
|
||||
|
||||
organizations_backends = {"global": organizations_backend}
|
||||
mock_organizations = base_decorator(organizations_backends)
|
131
moto/organizations/models.py
Normal file
131
moto/organizations/models.py
Normal file
@ -0,0 +1,131 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.utils import unix_time
|
||||
from moto.organizations import utils
|
||||
|
||||
MASTER_ACCOUNT_ID = '123456789012'
|
||||
MASTER_ACCOUNT_EMAIL = 'fakeorg@moto-example.com'
|
||||
ORGANIZATION_ARN_FORMAT = 'arn:aws:organizations::{0}:organization/{1}'
|
||||
MASTER_ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{0}'
|
||||
ACCOUNT_ARN_FORMAT = 'arn:aws:organizations::{0}:account/{1}/{2}'
|
||||
|
||||
|
||||
class FakeOrganization(BaseModel):
|
||||
|
||||
def __init__(self, feature_set):
|
||||
self.id = utils.make_random_org_id()
|
||||
self.feature_set = feature_set
|
||||
self.master_account_id = MASTER_ACCOUNT_ID
|
||||
self.master_account_email = MASTER_ACCOUNT_EMAIL
|
||||
self.available_policy_types = [{
|
||||
'Type': 'SERVICE_CONTROL_POLICY',
|
||||
'Status': 'ENABLED'
|
||||
}]
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
return ORGANIZATION_ARN_FORMAT.format(self.master_account_id, self.id)
|
||||
|
||||
@property
|
||||
def master_account_arn(self):
|
||||
return MASTER_ACCOUNT_ARN_FORMAT.format(self.master_account_id, self.id)
|
||||
|
||||
def _describe(self):
|
||||
return {
|
||||
'Organization': {
|
||||
'Id': self.id,
|
||||
'Arn': self.arn,
|
||||
'FeatureSet': self.feature_set,
|
||||
'MasterAccountArn': self.master_account_arn,
|
||||
'MasterAccountId': self.master_account_id,
|
||||
'MasterAccountEmail': self.master_account_email,
|
||||
'AvailablePolicyTypes': self.available_policy_types,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FakeAccount(BaseModel):
|
||||
|
||||
def __init__(self, organization, **kwargs):
|
||||
self.organization_id = organization.id
|
||||
self.master_account_id = organization.master_account_id
|
||||
self.create_account_status_id = utils.make_random_create_account_status_id()
|
||||
self.account_id = utils.make_random_account_id()
|
||||
self.account_name = kwargs['AccountName']
|
||||
self.email = kwargs['Email']
|
||||
self.create_time = datetime.datetime.utcnow()
|
||||
self.status = 'ACTIVE'
|
||||
self.joined_method = 'CREATED'
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
return ACCOUNT_ARN_FORMAT.format(
|
||||
self.master_account_id,
|
||||
self.organization_id,
|
||||
self.account_id
|
||||
)
|
||||
|
||||
@property
|
||||
def create_account_status(self):
|
||||
return {
|
||||
'CreateAccountStatus': {
|
||||
'Id': self.create_account_status_id,
|
||||
'AccountName': self.account_name,
|
||||
'State': 'SUCCEEDED',
|
||||
'RequestedTimestamp': unix_time(self.create_time),
|
||||
'CompletedTimestamp': unix_time(self.create_time),
|
||||
'AccountId': self.account_id,
|
||||
}
|
||||
}
|
||||
|
||||
def describe(self):
|
||||
return {
|
||||
'Account': {
|
||||
'Id': self.account_id,
|
||||
'Arn': self.arn,
|
||||
'Email': self.email,
|
||||
'Name': self.account_name,
|
||||
'Status': self.status,
|
||||
'JoinedMethod': self.joined_method,
|
||||
'JoinedTimestamp': unix_time(self.create_time),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class OrganizationsBackend(BaseBackend):
|
||||
|
||||
def __init__(self):
|
||||
self.org = None
|
||||
self.accounts = []
|
||||
|
||||
def create_organization(self, **kwargs):
|
||||
self.org = FakeOrganization(kwargs['FeatureSet'])
|
||||
return self.org._describe()
|
||||
|
||||
def describe_organization(self):
|
||||
return self.org._describe()
|
||||
|
||||
def create_account(self, **kwargs):
|
||||
new_account = FakeAccount(self.org, **kwargs)
|
||||
self.accounts.append(new_account)
|
||||
return new_account.create_account_status
|
||||
|
||||
def describe_account(self, **kwargs):
|
||||
account = [account for account in self.accounts
|
||||
if account.account_id == kwargs['AccountId']][0]
|
||||
return account.describe()
|
||||
|
||||
def list_accounts(self):
|
||||
return dict(
|
||||
Accounts=[account.describe()['Account'] for account in self.accounts]
|
||||
)
|
||||
|
||||
|
||||
organizations_backend = OrganizationsBackend()
|
||||
|
||||
|
||||
|
48
moto/organizations/responses.py
Normal file
48
moto/organizations/responses.py
Normal file
@ -0,0 +1,48 @@
|
||||
from __future__ import unicode_literals
|
||||
import json
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import organizations_backend
|
||||
|
||||
|
||||
class OrganizationsResponse(BaseResponse):
|
||||
|
||||
@property
|
||||
def organizations_backend(self):
|
||||
return organizations_backend
|
||||
|
||||
@property
|
||||
def request_params(self):
|
||||
try:
|
||||
return json.loads(self.body)
|
||||
except ValueError:
|
||||
return {}
|
||||
|
||||
def _get_param(self, param, default=None):
|
||||
return self.request_params.get(param, default)
|
||||
|
||||
def create_organization(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.create_organization(**self.request_params)
|
||||
)
|
||||
|
||||
def describe_organization(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.describe_organization()
|
||||
)
|
||||
|
||||
def create_account(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.create_account(**self.request_params)
|
||||
)
|
||||
|
||||
def describe_account(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.describe_account(**self.request_params)
|
||||
)
|
||||
|
||||
def list_accounts(self):
|
||||
return json.dumps(
|
||||
self.organizations_backend.list_accounts()
|
||||
)
|
||||
|
10
moto/organizations/urls.py
Normal file
10
moto/organizations/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
from .responses import OrganizationsResponse
|
||||
|
||||
url_bases = [
|
||||
"https?://organizations.(.+).amazonaws.com",
|
||||
]
|
||||
|
||||
url_paths = {
|
||||
'{0}/$': OrganizationsResponse.dispatch,
|
||||
}
|
34
moto/organizations/utils.py
Normal file
34
moto/organizations/utils.py
Normal file
@ -0,0 +1,34 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
CHARSET=string.ascii_lowercase + string.digits
|
||||
ORG_ID_SIZE = 10
|
||||
ROOT_ID_SIZE = 4
|
||||
ACCOUNT_ID_SIZE = 12
|
||||
CREATE_ACCOUNT_STATUS_ID_SIZE = 8
|
||||
|
||||
|
||||
def make_random_org_id():
|
||||
# The regex pattern for an organization ID string requires "o-"
|
||||
# followed by from 10 to 32 lower-case letters or digits.
|
||||
# e.g. 'o-vipjnq5z86'
|
||||
return 'o-' + ''.join(random.choice(CHARSET) for x in range(ORG_ID_SIZE))
|
||||
|
||||
def make_random_root_id():
|
||||
# The regex pattern for a root ID string requires "r-" followed by
|
||||
# from 4 to 32 lower-case letters or digits.
|
||||
# e.g. 'r-3zwx'
|
||||
return 'r-' + ''.join(random.choice(CHARSET) for x in range(ROOT_ID_SIZE))
|
||||
|
||||
def make_random_account_id():
|
||||
# The regex pattern for an account ID string requires exactly 12 digits.
|
||||
# e.g. '488633172133'
|
||||
return ''.join([random.choice(string.digits) for n in range(ACCOUNT_ID_SIZE)])
|
||||
|
||||
def make_random_create_account_status_id():
|
||||
# The regex pattern for an create account request ID string requires
|
||||
# "car-" followed by from 8 to 32 lower-case letters or digits.
|
||||
# e.g. 'car-35gxzwrp'
|
||||
return 'car-' + ''.join(random.choice(CHARSET) for x in range(CREATE_ACCOUNT_STATUS_ID_SIZE))
|
0
tests/test_organizations/__init__.py
Normal file
0
tests/test_organizations/__init__.py
Normal file
24
tests/test_organizations/object_syntax.py
Normal file
24
tests/test_organizations/object_syntax.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""
|
||||
Temporary functions for checking object structures while specing out
|
||||
models. This module will go away.
|
||||
"""
|
||||
|
||||
import yaml
|
||||
import moto
|
||||
from moto import organizations as orgs
|
||||
|
||||
|
||||
# utils
|
||||
print(orgs.utils.make_random_org_id())
|
||||
print(orgs.utils.make_random_root_id())
|
||||
print(orgs.utils.make_random_account_id())
|
||||
print(orgs.utils.make_random_create_account_id())
|
||||
|
||||
# models
|
||||
my_org = orgs.models.FakeOrganization(feature_set = 'ALL')
|
||||
print(yaml.dump(my_org._describe()))
|
||||
#assert False
|
||||
|
||||
my_account = orgs.models.FakeAccount(my_org, AccountName='blee01', Email='blee01@moto-example.org')
|
||||
print(yaml.dump(my_account))
|
||||
#assert False
|
193
tests/test_organizations/test_organizations_boto3.py
Normal file
193
tests/test_organizations/test_organizations_boto3.py
Normal file
@ -0,0 +1,193 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import boto3
|
||||
import botocore.exceptions
|
||||
import sure # noqa
|
||||
import yaml
|
||||
import re
|
||||
import datetime
|
||||
|
||||
import moto
|
||||
from moto import mock_organizations
|
||||
from moto.organizations.models import (
|
||||
MASTER_ACCOUNT_ID,
|
||||
MASTER_ACCOUNT_EMAIL,
|
||||
ORGANIZATION_ARN_FORMAT,
|
||||
MASTER_ACCOUNT_ARN_FORMAT,
|
||||
ACCOUNT_ARN_FORMAT,
|
||||
)
|
||||
from .test_organizations_utils import (
|
||||
ORG_ID_REGEX,
|
||||
ROOT_ID_REGEX,
|
||||
ACCOUNT_ID_REGEX,
|
||||
CREATE_ACCOUNT_STATUS_ID_REGEX,
|
||||
)
|
||||
|
||||
EMAIL_REGEX = "^.+@[a-zA-Z0-9-.]+.[a-zA-Z]{2,3}|[0-9]{1,3}$"
|
||||
|
||||
|
||||
def validate_organization(response):
|
||||
org = response['Organization']
|
||||
sorted(org.keys()).should.equal([
|
||||
'Arn',
|
||||
'AvailablePolicyTypes',
|
||||
'FeatureSet',
|
||||
'Id',
|
||||
'MasterAccountArn',
|
||||
'MasterAccountEmail',
|
||||
'MasterAccountId',
|
||||
])
|
||||
org['Id'].should.match(ORG_ID_REGEX)
|
||||
org['MasterAccountId'].should.equal(MASTER_ACCOUNT_ID)
|
||||
org['MasterAccountArn'].should.equal(MASTER_ACCOUNT_ARN_FORMAT.format(
|
||||
org['MasterAccountId'],
|
||||
org['Id'],
|
||||
))
|
||||
org['Arn'].should.equal(ORGANIZATION_ARN_FORMAT.format(
|
||||
org['MasterAccountId'],
|
||||
org['Id'],
|
||||
))
|
||||
org['MasterAccountEmail'].should.equal(MASTER_ACCOUNT_EMAIL)
|
||||
org['FeatureSet'].should.be.within(['ALL', 'CONSOLIDATED_BILLING'])
|
||||
org['AvailablePolicyTypes'].should.equal([{
|
||||
'Type': 'SERVICE_CONTROL_POLICY',
|
||||
'Status': 'ENABLED'
|
||||
}])
|
||||
#
|
||||
#'Organization': {
|
||||
# 'Id': 'string',
|
||||
# 'Arn': 'string',
|
||||
# 'FeatureSet': 'ALL'|'CONSOLIDATED_BILLING',
|
||||
# 'MasterAccountArn': 'string',
|
||||
# 'MasterAccountId': 'string',
|
||||
# 'MasterAccountEmail': 'string',
|
||||
# 'AvailablePolicyTypes': [
|
||||
# {
|
||||
# 'Type': 'SERVICE_CONTROL_POLICY',
|
||||
# 'Status': 'ENABLED'|'PENDING_ENABLE'|'PENDING_DISABLE'
|
||||
# },
|
||||
# ]
|
||||
#}
|
||||
|
||||
def validate_account(org, account):
|
||||
sorted(account.keys()).should.equal([
|
||||
'Arn',
|
||||
'Email',
|
||||
'Id',
|
||||
'JoinedMethod',
|
||||
'JoinedTimestamp',
|
||||
'Name',
|
||||
'Status',
|
||||
])
|
||||
account['Id'].should.match(ACCOUNT_ID_REGEX)
|
||||
account['Arn'].should.equal(ACCOUNT_ARN_FORMAT.format(
|
||||
org['MasterAccountId'],
|
||||
org['Id'],
|
||||
account['Id'],
|
||||
))
|
||||
account['Email'].should.match(EMAIL_REGEX)
|
||||
account['JoinedMethod'].should.be.within(['INVITED', 'CREATED'])
|
||||
account['Status'].should.be.within(['ACTIVE', 'SUSPENDED'])
|
||||
account['Name'].should.be.a(str)
|
||||
account['JoinedTimestamp'].should.be.a(datetime.datetime)
|
||||
#'Account': {
|
||||
# 'Id': 'string',
|
||||
# 'Arn': 'string',
|
||||
# 'Email': 'string',
|
||||
# 'Name': 'string',
|
||||
# 'Status': 'ACTIVE'|'SUSPENDED',
|
||||
# 'JoinedMethod': 'INVITED'|'CREATED',
|
||||
# 'JoinedTimestamp': datetime(2015, 1, 1)
|
||||
#}
|
||||
|
||||
def validate_create_account_status(create_status):
|
||||
sorted(create_status.keys()).should.equal([
|
||||
'AccountId',
|
||||
'AccountName',
|
||||
'CompletedTimestamp',
|
||||
'Id',
|
||||
'RequestedTimestamp',
|
||||
'State',
|
||||
])
|
||||
create_status['Id'].should.match(CREATE_ACCOUNT_STATUS_ID_REGEX)
|
||||
create_status['AccountId'].should.match(ACCOUNT_ID_REGEX)
|
||||
create_status['AccountName'].should.be.a(str)
|
||||
create_status['State'].should.equal('SUCCEEDED')
|
||||
create_status['RequestedTimestamp'].should.be.a(datetime.datetime)
|
||||
create_status['CompletedTimestamp'].should.be.a(datetime.datetime)
|
||||
#'CreateAccountStatus': {
|
||||
# 'Id': 'string',
|
||||
# 'AccountName': 'string',
|
||||
# 'State': 'IN_PROGRESS'|'SUCCEEDED'|'FAILED',
|
||||
# 'RequestedTimestamp': datetime(2015, 1, 1),
|
||||
# 'CompletedTimestamp': datetime(2015, 1, 1),
|
||||
# 'AccountId': 'string',
|
||||
# 'FailureReason': 'ACCOUNT_LIMIT_EXCEEDED'|'EMAIL_ALREADY_EXISTS'|'INVALID_ADDRESS'|'INVALID_EMAIL'|'CONCURRENT_ACCOUNT_MODIFICATION'|'INTERNAL_FAILURE'
|
||||
#}
|
||||
|
||||
@mock_organizations
|
||||
def test_create_organization():
|
||||
client = boto3.client('organizations', region_name='us-east-1')
|
||||
response = client.create_organization(FeatureSet='ALL')
|
||||
#print(yaml.dump(response))
|
||||
validate_organization(response)
|
||||
response['Organization']['FeatureSet'].should.equal('ALL')
|
||||
#assert False
|
||||
|
||||
@mock_organizations
|
||||
def test_describe_organization():
|
||||
client = boto3.client('organizations', region_name='us-east-1')
|
||||
client.create_organization(FeatureSet='ALL')
|
||||
response = client.describe_organization()
|
||||
#print(yaml.dump(response))
|
||||
validate_organization(response)
|
||||
#assert False
|
||||
|
||||
|
||||
mockname = 'mock-account'
|
||||
mockdomain = 'moto-example.org'
|
||||
mockemail = '@'.join([mockname, mockdomain])
|
||||
|
||||
@mock_organizations
|
||||
def test_create_account():
|
||||
client = boto3.client('organizations', region_name='us-east-1')
|
||||
client.create_organization(FeatureSet='ALL')
|
||||
create_status = client.create_account(
|
||||
AccountName=mockname, Email=mockemail)['CreateAccountStatus']
|
||||
#print(yaml.dump(create_status, default_flow_style=False))
|
||||
validate_create_account_status(create_status)
|
||||
create_status['AccountName'].should.equal(mockname)
|
||||
#assert False
|
||||
|
||||
@mock_organizations
|
||||
def test_describe_account():
|
||||
client = boto3.client('organizations', region_name='us-east-1')
|
||||
org = client.create_organization(FeatureSet='ALL')['Organization']
|
||||
account_id = client.create_account(
|
||||
AccountName=mockname, Email=mockemail)['CreateAccountStatus']['AccountId']
|
||||
response = client.describe_account(AccountId=account_id)
|
||||
#print(yaml.dump(response, default_flow_style=False))
|
||||
validate_account(org, response['Account'])
|
||||
response['Account']['Name'].should.equal(mockname)
|
||||
response['Account']['Email'].should.equal(mockemail)
|
||||
#assert False
|
||||
|
||||
@mock_organizations
|
||||
def test_list_accounts():
|
||||
client = boto3.client('organizations', region_name='us-east-1')
|
||||
org = client.create_organization(FeatureSet='ALL')['Organization']
|
||||
for i in range(5):
|
||||
name = mockname + str(i)
|
||||
email = name + '@' + mockdomain
|
||||
client.create_account(AccountName=name, Email=email)
|
||||
response = client.list_accounts()
|
||||
#print(yaml.dump(response, default_flow_style=False))
|
||||
response.should.have.key('Accounts')
|
||||
accounts = response['Accounts']
|
||||
len(accounts).should.equal(5)
|
||||
for account in accounts:
|
||||
validate_account(org, account)
|
||||
accounts[3]['Name'].should.equal(mockname + '3')
|
||||
accounts[2]['Email'].should.equal(mockname + '2' + '@' + mockdomain)
|
||||
#assert False
|
||||
|
26
tests/test_organizations/test_organizations_utils.py
Normal file
26
tests/test_organizations/test_organizations_utils.py
Normal file
@ -0,0 +1,26 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sure # noqa
|
||||
import moto
|
||||
from moto.organizations import utils
|
||||
|
||||
ORG_ID_REGEX = r'o-[a-z0-9]{%s}' % utils.ORG_ID_SIZE
|
||||
ROOT_ID_REGEX = r'r-[a-z0-9]{%s}' % utils.ROOT_ID_SIZE
|
||||
ACCOUNT_ID_REGEX = r'[0-9]{%s}' % utils.ACCOUNT_ID_SIZE
|
||||
CREATE_ACCOUNT_STATUS_ID_REGEX = r'car-[a-z0-9]{%s}' % utils.CREATE_ACCOUNT_STATUS_ID_SIZE
|
||||
|
||||
def test_make_random_org_id():
|
||||
org_id = utils.make_random_org_id()
|
||||
org_id.should.match(ORG_ID_REGEX)
|
||||
|
||||
def test_make_random_root_id():
|
||||
org_id = utils.make_random_root_id()
|
||||
org_id.should.match(ROOT_ID_REGEX)
|
||||
|
||||
def test_make_random_account_id():
|
||||
account_id = utils.make_random_account_id()
|
||||
account_id.should.match(ACCOUNT_ID_REGEX)
|
||||
|
||||
def test_make_random_create_account_status_id():
|
||||
create_account_status_id = utils.make_random_create_account_status_id()
|
||||
create_account_status_id.should.match(CREATE_ACCOUNT_STATUS_ID_REGEX)
|
Loading…
Reference in New Issue
Block a user