ebcli/controllers/create.py (455 lines of code) (raw):
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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.
import argparse
import os
import time
from ebcli.core import io, fileoperations, hooks
from ebcli.core.abstractcontroller import AbstractBaseController
from ebcli.lib import elasticbeanstalk, utils, iam
from ebcli.objects.exceptions import (
AlreadyExistsError,
InvalidOptionsError,
RetiredPlatformBranchError,
NotFoundError,
)
from ebcli.objects.platform import PlatformVersion, PlatformBranch
from ebcli.objects.solutionstack import SolutionStack
from ebcli.objects.requests import CreateEnvironmentRequest
from ebcli.objects.tier import Tier
from ebcli.operations import (
commonops,
composeops,
createops,
envvarops,
platformops,
platform_branch_ops,
saved_configs,
solution_stack_ops,
shared_lb_ops,
spotops,
statusops,
)
from ebcli.operations.tagops import tagops
from ebcli.resources.strings import strings, prompts, flag_text, alerts
from ebcli.resources.statics import elb_names, platform_branch_lifecycle_states
class CreateController(AbstractBaseController):
class Meta:
label = 'create'
usage = AbstractBaseController.Meta.usage.replace('{cmd}', label)
description = strings['create.info']
epilog = strings['create.epilog']
arguments = [
(['environment_name'], dict(
action='store', nargs='?', default=None,
help=flag_text['create.name'])),
(['-m', '--modules'], dict(nargs='*', help=flag_text['create.modules'])),
(['-g', '--env-group-suffix'], dict(help=flag_text['create.group'])),
(['-c', '--cname'], dict(help=flag_text['create.cname'])),
(['-t', '--tier'], dict(help=flag_text['create.tier'])),
(['-i', '--instance_type'], dict(
help=flag_text['create.itype'])),
(['-p', '--platform'], dict(help=flag_text['create.platform'])),
(['-s', '--single'], dict(
action='store_true', help=flag_text['create.single'])),
(['--sample'], dict(
action='store_true', help=flag_text['create.sample'])),
(['-d', '--branch_default'], dict(
action='store_true', help=flag_text['create.default'])),
(['-ip', '--instance_profile'], dict(
help=flag_text['create.iprofile'])),
(['-sr', '--service-role'], dict(
help=flag_text['create.servicerole'])),
(['--version'], dict(help=flag_text['create.version'])),
(['-k', '--keyname'], dict(help=flag_text['create.keyname'])),
(['--scale'], dict(type=int, help=flag_text['create.scale'])),
(['-nh', '--nohang'], dict(
action='store_true', help=flag_text['create.nohang'])),
(['--timeout'], dict(type=int, help=flag_text['general.timeout'])),
(['--tags'], dict(help=flag_text['create.tags'])),
(['--envvars'], dict(help=flag_text['create.envvars'])),
(['--cfg'], dict(help=flag_text['create.config'])),
(['--source'], dict(help=flag_text['create.source'])),
(['--elb-type'], dict(help=flag_text['create.elb_type'])),
(['-ls', '--shared-lb'], dict(help=flag_text['create.shared_lb'])),
(['-lp', '--shared-lb-port'], dict(help=flag_text['create.shared_lb_port'])),
(['-es', '--enable-spot'], dict(action='store_true', help=flag_text['create.enable_spot'])),
(['-sm', '--spot-max-price'], dict(help=flag_text['create.maxprice'])),
(['-it', '--instance-types'], dict(help=flag_text['create.instance_types'])),
(['-sb', '--on-demand-base-capacity'], dict(help=flag_text['create.on_demand_capacity'])),
(['-sp', '--on-demand-above-base-capacity'], dict(help=flag_text['create.on_demand_above_base_percent'])),
(['-im', '--min-instances'], dict(help=flag_text['create.min_instances'])),
(['-ix', '--max-instances'], dict(help=flag_text['create.max_instances'])),
(['-db', '--database'], dict(
action="store_true", help=flag_text['create.database'])),
# Hidden RDS commands
(
['-db.user', '--database.username'],
dict(
dest='db_user',
help=argparse.SUPPRESS
)
),
(['-db.pass', '--database.password'],
dict(dest='db_pass', help=argparse.SUPPRESS)),
(['-db.i', '--database.instance'],
dict(dest='db_instance', help=argparse.SUPPRESS)),
(['-db.version', '--database.version'],
dict(dest='db_version', help=argparse.SUPPRESS)),
(['-db.size', '--database.size'],
dict(type=int, dest='db_size', help=argparse.SUPPRESS)),
(['-db.engine', '--database.engine'],
dict(dest='db_engine', help=argparse.SUPPRESS)),
(['--vpc'], dict(action='store_true',
help=flag_text['create.vpc'])),
(['--vpc.id'], dict(dest='vpc_id', help=argparse.SUPPRESS)),
(['--vpc.ec2subnets'], dict(
dest='vpc_ec2subnets', help=argparse.SUPPRESS)),
(['--vpc.elbsubnets'], dict(
dest='vpc_elbsubnets', help=argparse.SUPPRESS)),
(['--vpc.elbpublic'], dict(
action='store_true', dest='vpc_elbpublic',
help=argparse.SUPPRESS)),
(['--vpc.publicip'], dict(
action='store_true', dest='vpc_publicip',
help=argparse.SUPPRESS)),
(['--vpc.securitygroups'], dict(
dest='vpc_securitygroups', help=argparse.SUPPRESS)),
(['--vpc.dbsubnets'], dict(
dest='vpc_dbsubnets', help=argparse.SUPPRESS)),
(['-pr', '--process'], dict(
action='store_true', help=flag_text['create.process'])),
]
def do_command(self):
env_name = self.app.pargs.environment_name
modules = self.app.pargs.modules
if modules and len(modules) > 0:
self.compose_multiple_apps()
return
group = self.app.pargs.env_group_suffix
cname = self.app.pargs.cname
tier = self.app.pargs.tier
itype = self.app.pargs.instance_type
platform = self.app.pargs.platform
single = self.app.pargs.single
iprofile = self.app.pargs.instance_profile
service_role = self.app.pargs.service_role
label = self.app.pargs.version
branch_default = self.app.pargs.branch_default
key_name = self.app.pargs.keyname
sample = self.app.pargs.sample
nohang = self.app.pargs.nohang
tags = self.app.pargs.tags
envvars = self.app.pargs.envvars
scale = self.app.pargs.scale
timeout = self.app.pargs.timeout
cfg = self.app.pargs.cfg
elb_type = self.app.pargs.elb_type
shared_lb = self.app.pargs.shared_lb
shared_lb_port = self.app.pargs.shared_lb_port
source = self.app.pargs.source
process = self.app.pargs.process
enable_spot = self.app.pargs.enable_spot
spot_max_price = self.app.pargs.spot_max_price
instance_types = self.app.pargs.instance_types
on_demand_base_capacity = self.app.pargs.on_demand_base_capacity
on_demand_above_base_capacity = self.app.pargs.on_demand_above_base_capacity
max_instances = self.app.pargs.max_instances
min_instances = self.app.pargs.min_instances
interactive = False if env_name else True
provided_env_name = env_name
if sample and label:
raise InvalidOptionsError(strings['create.sampleandlabel'])
if single and scale:
raise InvalidOptionsError(strings['create.singleandsize'])
if (max_instances or min_instances) and scale:
raise InvalidOptionsError(strings['create.scaleandminmax'])
if (max_instances or min_instances) and single:
raise InvalidOptionsError(strings['create.singleandminmax'])
if single and elb_type:
raise InvalidOptionsError(strings['create.single_and_elb_type'])
if single and shared_lb:
raise InvalidOptionsError(alerts['create.can_not_use_options_together'].format("--single", "--shared-lb"))
if (shared_lb or shared_lb_port) and elb_type != 'application':
raise InvalidOptionsError(alerts['sharedlb.wrong_elb_type'])
if shared_lb_port and not shared_lb:
raise InvalidOptionsError(alerts['sharedlb.missing_shared_lb'])
if cname and tier and Tier.looks_like_worker_tier(tier):
raise InvalidOptionsError(strings['worker.cname'])
if cname and not elasticbeanstalk.is_cname_available(cname):
raise AlreadyExistsError(
strings['cname.unavailable'].replace('{cname}', cname)
)
if tier and Tier.looks_like_worker_tier(tier):
if self.app.pargs.vpc_elbpublic or self.app.pargs.vpc_elbsubnets or self.app.pargs.vpc_publicip:
raise InvalidOptionsError(strings['create.worker_and_incompatible_vpc_arguments'])
if (not tier or Tier.looks_like_webserver_tier(tier)) and single:
if self.app.pargs.vpc_elbpublic or self.app.pargs.vpc_elbsubnets:
raise InvalidOptionsError(strings['create.single_and_elbpublic_or_elb_subnet'])
if (spot_max_price or on_demand_base_capacity or on_demand_above_base_capacity) and not enable_spot:
raise InvalidOptionsError(strings['create.missing_enable_spot'])
if instance_types == "":
raise InvalidOptionsError(strings['spot.instance_types_validation'])
if itype and instance_types:
raise InvalidOptionsError(strings['create.itype_and_instances'])
if service_role and not iam.role_exists(service_role):
raise InvalidOptionsError(f"The specified service role '{service_role}' does not exist. Please use a role that exists or create a new role .")
platform = _determine_platform(platform, iprofile)
app_name = self.get_app_name()
tags = tagops.get_and_validate_tags(tags)
envvars = get_and_validate_envars(envvars)
process_app_version = fileoperations.env_yaml_exists() or process
template_name = get_template_name(app_name, cfg)
tier = get_environment_tier(tier)
env_name = provided_env_name or get_environment_name(app_name, group)
cname = cname or get_environment_cname(env_name, provided_env_name, tier)
key_name = key_name or commonops.get_default_keyname()
vpc = self.form_vpc_object(tier, single)
elb_type = elb_type or get_elb_type_from_customer(interactive, single, tier)
shared_lb = get_shared_load_balancer(interactive, elb_type, platform, shared_lb, vpc)
shared_lb_port = shared_lb_port or shared_lb_ops.get_shared_lb_port_from_customer(interactive, shared_lb)
enable_spot = enable_spot or spotops.get_spot_request_from_customer(interactive)
instance_types = instance_types or spotops.get_spot_instance_types_from_customer(interactive, enable_spot)
database = self.form_database_object()
if not timeout and database:
timeout = 15
env_request = CreateEnvironmentRequest(
app_name=app_name,
env_name=env_name,
group_name=group,
cname=cname,
template_name=template_name,
platform=platform,
tier=tier,
instance_type=itype,
version_label=label,
instance_profile=iprofile,
service_role=service_role,
single_instance=single,
key_name=key_name,
sample_application=sample,
tags=tags,
scale=scale,
database=database,
vpc=vpc,
elb_type=elb_type,
shared_lb = shared_lb,
shared_lb_port = shared_lb_port,
enable_spot=enable_spot,
instance_types=instance_types,
spot_max_price=spot_max_price,
on_demand_base_capacity=on_demand_base_capacity,
on_demand_above_base_capacity=on_demand_above_base_capacity,
min_instances=min_instances,
max_instances=max_instances)
env_request.option_settings += envvars
createops.make_new_env(env_request,
branch_default=branch_default,
process_app_version=process_app_version,
nohang=nohang,
interactive=interactive,
timeout=timeout,
source=source)
def form_database_object(self):
create_db = self.app.pargs.database
username = self.app.pargs.db_user
password = self.app.pargs.db_pass
engine = self.app.pargs.db_engine
size = self.app.pargs.db_size
instance = self.app.pargs.db_instance
version = self.app.pargs.db_version
if create_db or username or password or engine or size \
or instance or version:
db_object = dict()
if not username:
io.echo()
username = io.get_input(prompts['rds.username'],
default='ebroot')
if not password:
password = io.get_pass(prompts['rds.password'])
db_object['username'] = username
db_object['password'] = password
db_object['engine'] = engine
db_object['size'] = str(size) if size else None
db_object['instance'] = instance
db_object['version'] = version
return db_object
else:
return {}
def form_vpc_object(self, tier, single):
vpc = self.app.pargs.vpc
vpc_id = self.app.pargs.vpc_id
ec2subnets = self.app.pargs.vpc_ec2subnets
elbsubnets = self.app.pargs.vpc_elbsubnets
elbpublic = self.app.pargs.vpc_elbpublic
publicip = self.app.pargs.vpc_publicip
securitygroups = self.app.pargs.vpc_securitygroups
dbsubnets = self.app.pargs.vpc_dbsubnets
database = self.app.pargs.database
if vpc:
io.echo()
vpc_id = vpc_id or io.get_input(prompts['vpc.id'])
if not tier or tier.is_webserver():
publicip = publicip or io.get_boolean_response(text=prompts['vpc.publicip'])
ec2subnets = ec2subnets or io.get_input(prompts['vpc.ec2subnets'])
if (not tier or tier.is_webserver()) and not single:
elbsubnets = elbsubnets or io.get_input(prompts['vpc.elbsubnets'])
elbpublic = elbpublic or io.get_boolean_response(text=prompts['vpc.elbpublic'])
securitygroups = securitygroups or io.get_input(prompts['vpc.securitygroups'])
if database:
dbsubnets = dbsubnets or io.get_input(prompts['vpc.dbsubnets'])
if vpc_id or vpc:
vpc_object = dict()
vpc_object['id'] = vpc_id
vpc_object['ec2subnets'] = ec2subnets
if (not tier or tier.is_webserver()) and not single:
vpc_object['elbsubnets'] = elbsubnets
vpc_object['elbscheme'] = 'public' if elbpublic else 'internal'
else:
vpc_object['elbsubnets'] = None
vpc_object['elbscheme'] = None
if not tier or tier.is_webserver():
vpc_object['publicip'] = 'true' if publicip else 'false'
else:
vpc_object['publicip'] = None
vpc_object['securitygroups'] = securitygroups
vpc_object['dbsubnets'] = dbsubnets
return vpc_object
else:
return {}
def compose_multiple_apps(self):
module_names = self.app.pargs.modules
group = self.app.pargs.env_group_suffix or 'dev'
nohang = self.app.pargs.nohang
timeout = self.app.pargs.timeout
root_dir = os.getcwd()
version_labels = []
grouped_env_names = []
app_name = None
for module in module_names:
if not os.path.isdir(os.path.join(root_dir, module)):
io.log_warning(strings['create.appdoesntexist'].replace('{app_name}',
module))
continue
os.chdir(os.path.join(root_dir, module))
if not fileoperations.env_yaml_exists():
io.log_warning(strings['compose.noenvyaml'].replace('{module}',
module))
continue
io.echo('--- Creating application version for module: {0} ---'.format(module))
hooks.set_region(None)
hooks.set_ssl(None)
hooks.set_profile(None)
commonops.set_group_suffix_for_current_branch(group)
if not app_name:
app_name = self.get_app_name()
process_app_version = fileoperations.env_yaml_exists()
version_label = commonops.create_app_version(app_name, process=process_app_version)
version_labels.append(version_label)
environment_name = fileoperations.get_env_name_from_env_yaml()
if environment_name is not None:
commonops.set_environment_for_current_branch(environment_name.
replace('+', '-{0}'.
format(group)))
grouped_env_names.append(environment_name.replace('+', '-{0}'.
format(group)))
os.chdir(root_dir)
if len(version_labels) > 0:
composeops.compose(app_name, version_labels, grouped_env_names, group,
nohang, timeout)
else:
io.log_warning(strings['compose.novalidmodules'])
def get_environment_cname(env_name, provided_env_name, tier):
"""
Returns the CNAME for the environment that will be created. Suggests to customer a name based
of the environment name, which the customer is free to disregard.
:param env_name: Name of the environment determined by `create.get_environment_name`
:param provided_env_name: True/False depending on whether or not the customer passed an environment name
through the command line
:return: Unique CNAME for the environment which will be created by `eb create`
"""
if tier and tier.is_worker():
return
if not provided_env_name:
return get_cname_from_customer(env_name)
def get_environment_name(app_name, group):
"""
Returns:
- environment name is present in the env.yaml file if one exists, or
- prompts customer interactively to enter an environment name
If using env.yaml to create an environment with, `group` must be passed through
the `-g/--env-group-suffix/` argument.
:param app_name: name of the application associated with the present working
directory
:param group: name of the group associated with
:return: Unique name of the environment which will be created by `eb create`
"""
env_name = None
if fileoperations.env_yaml_exists():
env_name = fileoperations.get_env_name_from_env_yaml()
if env_name:
if env_name.endswith('+') and not group:
raise InvalidOptionsError(strings['create.missinggroupsuffix'])
elif not env_name.endswith('+') and group:
raise InvalidOptionsError(strings['create.missing_plus_sign_in_group_name'])
else:
env_name = env_name[:-1] + '-' + group
return env_name or io.prompt_for_environment_name(get_unique_environment_name(app_name))
def get_environment_tier(tier):
"""
Set the 'tier' for the environment from the raw value received for the `--tier`
argument.
If a configuration template corresponding to `template_name` is also resolved,
and the tier corresponding to the configuration template is a 'worker' tier,
any previously set value for 'tier' is replaced with the value from the saved
config.
:return: A Tier object representing the environment's tier type
"""
if tier:
tier = Tier.from_raw_string(tier)
return tier
def get_unique_cname(env_name):
"""
Derive a unique CNAME for a new environment based on the environment name
:param env_name: name of the environment
directory
:return: A unique CNAME for a new environment
"""
cname = env_name
tried_cnames = []
while not elasticbeanstalk.is_cname_available(cname):
tried_cnames.append(cname)
_sleep(0.5)
cname = utils.get_unique_name(cname, tried_cnames)
return cname
def get_unique_environment_name(app_name):
"""
Derive a unique name for a new environment based on the application name
to suggest to the customer
:param app_name: name of the application associated with the present working
directory
:return: A unique name for a new environment
"""
default_name = app_name + '-dev'
current_environments = elasticbeanstalk.get_all_environment_names()
return utils.get_unique_name(default_name, current_environments)
def get_cname_from_customer(env_name):
"""
Prompt customer to specify the CNAME for the environment.
Selection defaults to the Environment's name when provided with blank input.
:param env_name: name of the environment whose CNAME to configure
:return: CNAME chosen for the environment
"""
cname = get_unique_cname(env_name)
while True:
cname = io.prompt_for_cname(default=cname)
if cname and not elasticbeanstalk.is_cname_available(cname):
io.echo('That cname is not available. Please choose another.')
else:
break
return cname
def get_elb_type_from_customer(interactive, single, tier):
"""
Prompt customer to specify the ELB type if operating in the interactive mode and
on a load-balanced environment.
Selection defaults to 'application' when provided with blank input.
:param interactive: True/False depending on whether operating in the interactive mode or not
:param single: False/True depending on whether environment is load balanced or not
:param region: AWS region in which in load balancer will be created
:param tier: the tier type of the environment
:return: selected ELB type which is one among ['application', 'classic', 'network']
"""
if single or (tier and not tier.is_webserver()):
return
elif not interactive:
return elb_names.APPLICATION_VERSION
io.echo()
io.echo('Select a load balancer type')
result = utils.prompt_for_item_in_list(
[elb_names.CLASSIC_VERSION, elb_names.APPLICATION_VERSION, elb_names.NETWORK_VERSION],
default=2
)
elb_type = result
return elb_type
def get_shared_load_balancer(interactive, elb_type, platform, shared_lb=None, vpc=None):
if shared_lb:
shared_lb = shared_lb_ops.validate_shared_lb_for_non_interactive(shared_lb)
else:
shared_lb = shared_lb_ops.get_shared_lb_from_customer(interactive, elb_type, platform, vpc)
return shared_lb
def get_and_validate_envars(environment_variables_input):
"""
Returns a list of environment variables as option settings from the raw environment variables
string input provided by the customer
:param environment_variables_input: a string of the form "KEY_1=VALUE_1,...,KYE_N=VALUE_N"
:return: the list of option settings derived from the key-value pairs in `environment_variables_input`
"""
environment_variables = envvarops.sanitize_environment_variables_from_customer_input(
environment_variables_input
)
environment_variable_option_settings, options_to_remove = envvarops.create_environment_variables_list(
environment_variables
)
return environment_variable_option_settings
def get_template_name(app_name, cfg):
"""
Returns the name of the saved configuration template:
- specified by the customer stored in S3
- identified as 'default' present locally
For more information, please refer to saved_configs.resolve_config_name
:param app_name: name of the application associated with this directory
:param cfg: saved config name specified by the customer
:return: normalized
"""
if not cfg:
if not saved_configs.resolve_config_location('default'):
return
else:
cfg = 'default'
return saved_configs.resolve_config_name(app_name, cfg)
def _determine_platform(platform_string=None, iprofile=None):
platform = None
if not platform_string:
platform_string = platformops.get_configured_default_platform()
if platform_string:
platform = platformops.get_platform_for_platform_string(
platform_string)
else:
platform = platformops.prompt_for_platform()
if isinstance(platform, SolutionStack):
if platform.language_name == 'Multi-container Docker' and not iprofile:
io.log_warning(prompts['ecs.permissions'])
if isinstance(platform, PlatformVersion):
platform.hydrate(elasticbeanstalk.describe_platform_version)
if platform.platform_branch_lifecycle_state == platform_branch_lifecycle_states.RETIRED:
raise RetiredPlatformBranchError(alerts['platformbranch.retired'])
statusops.alert_platform_status(platform)
if 'Multi-container Docker' in platform.platform_name and not iprofile:
io.log_warning(prompts['ecs.permissions'])
return platform
def _sleep(seconds):
time.sleep(seconds)