syndicate/core/resources/ebs_resource.py (201 lines of code) (raw):
"""
Copyright 2018 EPAM Systems, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from time import time
from uuid import uuid1
from pathlib import PurePath
from botocore.exceptions import ClientError
from syndicate.commons.log_helper import get_logger
from syndicate.core.build.meta_processor import S3_PATH_NAME
from syndicate.core.helper import unpack_kwargs
from syndicate.core.resources.base_resource import BaseResource
from syndicate.core.resources.helper import build_description_obj
_LOG = get_logger(__name__)
class EbsResource(BaseResource):
def __init__(self, ec2_conn, iam_conn, ebs_conn, sns_conn,
s3_conn, region, account_id, deploy_target_bucket) -> None:
self.ec2_conn = ec2_conn
self.iam_conn = iam_conn
self.ebs_conn = ebs_conn
self.sns_conn = sns_conn
self.s3_conn = s3_conn
self.region = region
self.account_id = account_id
self.deploy_target_bucket = deploy_target_bucket
def describe_ebs(self, name, meta, response=None):
arn = f'arn:aws:elasticbeanstalk:{self.region}:{self.account_id}' \
f':application/{name}'
if not response:
response = self.ebs_conn.describe_applications([name])
if not response:
return {}
return {
arn: build_description_obj(response, name, meta)
}
def create_ebs(self, args):
return self.create_pool(self._create_ebs_app_env_from_meta, args)
@unpack_kwargs
def _create_ebs_app_env_from_meta(self, name, meta):
from syndicate.core import CONFIG
response = self.ebs_conn.describe_applications([name])
if response:
_LOG.warn(f'{name} EBS app exists.')
return self.describe_ebs(name, meta, response[0])
env_settings = meta['env_settings']
topic_name = meta.get('notification_topic')
# check topic exists
if topic_name:
topic_arn = self.sns_conn.get_topic_arn(topic_name)
if topic_arn:
env_settings.append({
"OptionName": "Notification Topic ARN",
"Namespace": "aws:elasticbeanstalk:sns:topics",
"Value": topic_arn
})
else:
raise AssertionError('Cant find notification '
'topic {0} for EBS.'.format(topic_name))
# check key pair exists
key_pair_name = meta['ec2_key_pair']
if self.ec2_conn.if_key_pair_exists(key_pair_name):
env_settings.append({
"OptionName": "KeyName",
"ResourceName": "AWSEBAutoScalingLaunchConfiguration",
"Namespace": "aws:cloudformation:template:resource:property",
"Value": key_pair_name
})
else:
raise AssertionError('Specified key pair '
'does not exist: {0}.'.format(key_pair_name))
# check ec2 role exists
iam_role = meta['ec2_role']
if self.iam_conn.check_if_role_exists(iam_role):
env_settings.append({
"OptionName": "IamInstanceProfile",
"ResourceName": "AWSEBAutoScalingLaunchConfiguration",
"Namespace": "aws:autoscaling:launchconfiguration",
"Value": iam_role
})
else:
raise AssertionError(
'Specified iam role does not exist: {0}.'.format(iam_role))
# check service role exists
iam_role = meta['ebs_service_role']
if self.iam_conn.check_if_role_exists(iam_role):
env_settings.append({
"OptionName": "ServiceRole",
"Namespace": "aws:elasticbeanstalk:environment",
"Value": iam_role
})
else:
raise AssertionError(f'Specified iam role '
f'does not exist: {iam_role}.')
image_id = meta.get('image_id')
if image_id:
env_settings.append({
"OptionName": "ImageId",
"ResourceName": "AWSEBAutoScalingLaunchConfiguration",
"Namespace": "aws:autoscaling:launchconfiguration",
"Value": image_id
})
else:
_LOG.warn('Image id is not specified.')
# check that desired solution stack exists
stack = meta['stack']
available_stacks = self.ebs_conn. \
describe_available_solutions_stack_names()
if stack not in available_stacks:
raise AssertionError(f'No solution stack named {stack} found.'
f' Available:\n{available_stacks}')
vpc_id = next(
(option for option in env_settings if
option['OptionName'] == 'VPCId'),
None)
if not vpc_id:
vpc_id = self.ec2_conn.get_default_vpc_id()
_LOG.info('Default vpc id %s', vpc_id)
if vpc_id:
_LOG.debug('Will use vpc %s', vpc_id)
subnets = self.ec2_conn.list_subnets(filters=[{
'Name': 'vpc-id',
'Values': [vpc_id]
}])
_LOG.debug(f'Found subnets for {vpc_id} vpc: {subnets}')
if subnets:
_LOG.info(f'Will attach default {vpc_id} vpc to env')
self._add_subnets_info(env_settings, subnets, vpc_id)
sg_id = self.ec2_conn.get_sg_id(group_name='default',
vpc_id=vpc_id)
if sg_id:
_LOG.debug(f'Found default sg with id {sg_id}')
env_settings.append({
"OptionName": "SecurityGroups",
"Namespace": "aws:autoscaling:launchconfiguration",
"Value": sg_id
})
env_name = meta["env_name"] + str(int(time()))
start = time()
end = start + 180
while end > time():
describe_app_result = self.ebs_conn.describe_applications([name])
if not describe_app_result:
break
# create APP
response = self.ebs_conn.create_application(name,
tags=meta.get('tags'))
_LOG.info(f'Created EBS app {name}.')
# create ENV
self.ebs_conn.create_environment(app_name=name,
env_name=env_name,
option_settings=env_settings,
tier=meta['tier'],
solution_stack_name=stack,
tags=meta.get('tags'))
key = meta[S3_PATH_NAME]
key_compound = PurePath(CONFIG.deploy_target_bucket_key_compound,
key).as_posix()
if not self.s3_conn.is_file_exists(self.deploy_target_bucket,
key_compound):
raise AssertionError(f'Deployment package does not exist in '
f'{self.deploy_target_bucket} bucket')
# create VERSION
version_label = env_name + str(uuid1())
self.ebs_conn.create_app_version(app_name=name,
version_label=version_label,
s3_bucket=self.deploy_target_bucket,
s3_key=key_compound,
tags=meta.get('tags'))
_LOG.debug(f'Waiting for beanstalk env {env_name}')
# wait for env creation
start = time()
status = {}
end = start + 360 # end in 6 min
while end > time():
status = self.ebs_conn.describe_environment_health(
env_name=env_name,
attr_names=[
'Status'])
if status['Status'] == 'Ready':
_LOG.info('Launching env took %s.', time() - start)
break
if status['Status'] != 'Ready':
_LOG.error(f'Env status: {status}. Failed to create env.')
# deploy new app version
self.ebs_conn.deploy_env_version(name, env_name, version_label)
_LOG.info('Created environment for %s.', name)
return self.describe_ebs(name, meta, response)
@staticmethod
def _add_subnets_info(env_settings, subnets, vpc_id):
env_settings.append({
"OptionName": "VPCId",
"Namespace": "aws:ec2:vpc",
"Value": vpc_id
})
subnets = ",".join(subnet['SubnetId'] for subnet in subnets)
env_settings.append({
"OptionName": "Subnets",
"Namespace": "aws:ec2:vpc",
"Value": subnets
})
def remove_ebs_apps(self, args):
return self.create_pool(self._remove_ebs_app, args)
@unpack_kwargs
def _remove_ebs_app(self, arn, config):
app_name = config['resource_name']
try:
self.ebs_conn.remove_app(app_name, log_not_found_error=False)
_LOG.info(f'EBS app {app_name} was removed.')
return {arn: config}
except ClientError as e:
if e.response['Error']['Code'] == 'ResourceNotFoundException':
_LOG.warn(f'EBS app {app_name} is not found')
return {arn: config}
else:
raise e