from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from builtins import object
import os
import sys
import logging
import threading
from ..dag import walk, ThreadedWalker, UnlimitedSemaphore
from ..plan import Step, build_plan, build_graph
import botocore.exceptions
from stacker.session_cache import get_session
from stacker.exceptions import PlanFailed
from ..status import (
COMPLETE
)
from stacker.util import (
ensure_s3_bucket,
get_s3_endpoint,
)
logger = logging.getLogger(__name__)
# After submitting a stack update/create, this controls how long we'll wait
# between calls to DescribeStacks to check on it's status. Most stack updates
# take at least a couple minutes, so 30 seconds is pretty reasonable and inline
# with the suggested value in
# https://github.com/boto/botocore/blob/1.6.1/botocore/data/cloudformation/2010-05-15/waiters-2.json#L22
#
# This can be controlled via an environment variable, mostly for testing.
STACK_POLL_TIME = int(os.environ.get("STACKER_STACK_POLL_TIME", 30))
[docs]def build_walker(concurrency):
"""This will return a function suitable for passing to
:class:`stacker.plan.Plan` for walking the graph.
If concurrency is 1 (no parallelism) this will return a simple topological
walker that doesn't use any multithreading.
If concurrency is 0, this will return a walker that will walk the graph as
fast as the graph topology allows.
If concurrency is greater than 1, it will return a walker that will only
execute a maximum of concurrency steps at any given time.
Returns:
func: returns a function to walk a :class:`stacker.dag.DAG`.
"""
if concurrency == 1:
return walk
semaphore = UnlimitedSemaphore()
if concurrency > 1:
semaphore = threading.Semaphore(concurrency)
return ThreadedWalker(semaphore).walk
[docs]def plan(description, stack_action, context,
tail=None, reverse=False):
"""A simple helper that builds a graph based plan from a set of stacks.
Args:
description (str): a description of the plan.
action (func): a function to call for each stack.
context (:class:`stacker.context.Context`): a
:class:`stacker.context.Context` to build the plan from.
tail (func): an optional function to call to tail the stack progress.
reverse (bool): if True, execute the graph in reverse (useful for
destroy actions).
Returns:
:class:`plan.Plan`: The resulting plan object
"""
def target_fn(*args, **kwargs):
return COMPLETE
steps = [
Step(stack, fn=stack_action, watch_func=tail)
for stack in context.get_stacks()]
steps += [
Step(target, fn=target_fn) for target in context.get_targets()]
graph = build_graph(steps)
return build_plan(
description=description,
graph=graph,
targets=context.stack_names,
reverse=reverse)
[docs]def stack_template_key_name(blueprint):
"""Given a blueprint, produce an appropriate key name.
Args:
blueprint (:class:`stacker.blueprints.base.Blueprint`): The blueprint
object to create the key from.
Returns:
string: Key name resulting from blueprint.
"""
name = blueprint.name
return "stack_templates/%s/%s-%s.json" % (blueprint.context.get_fqn(name),
name,
blueprint.version)
[docs]def stack_template_url(bucket_name, blueprint, endpoint):
"""Produces an s3 url for a given blueprint.
Args:
bucket_name (string): The name of the S3 bucket where the resulting
templates are stored.
blueprint (:class:`stacker.blueprints.base.Blueprint`): The blueprint
object to create the URL to.
endpoint (string): The s3 endpoint used for the bucket.
Returns:
string: S3 URL.
"""
key_name = stack_template_key_name(blueprint)
return "%s/%s/%s" % (endpoint, bucket_name, key_name)
[docs]class BaseAction(object):
"""Actions perform the actual work of each Command.
Each action is tied to a :class:`stacker.commands.base.BaseCommand`, and
is responsible for building the :class:`stacker.plan.Plan` that will be
executed to perform that command.
Args:
context (:class:`stacker.context.Context`): The stacker context for
the current run.
provider_builder (:class:`stacker.providers.base.BaseProviderBuilder`,
optional): An object that will build a provider that will be
interacted with in order to perform the necessary actions.
"""
def __init__(self, context, provider_builder=None, cancel=None):
self.context = context
self.provider_builder = provider_builder
self.bucket_name = context.bucket_name
self.cancel = cancel or threading.Event()
self.bucket_region = context.config.stacker_bucket_region
if not self.bucket_region and provider_builder:
self.bucket_region = provider_builder.region
self.s3_conn = get_session(self.bucket_region).client('s3')
[docs] def ensure_cfn_bucket(self):
"""The CloudFormation bucket where templates will be stored."""
if self.bucket_name:
ensure_s3_bucket(self.s3_conn,
self.bucket_name,
self.bucket_region)
[docs] def stack_template_url(self, blueprint):
return stack_template_url(
self.bucket_name, blueprint, get_s3_endpoint(self.s3_conn)
)
[docs] def s3_stack_push(self, blueprint, force=False):
"""Pushes the rendered blueprint's template to S3.
Verifies that the template doesn't already exist in S3 before
pushing.
Returns the URL to the template in S3.
"""
key_name = stack_template_key_name(blueprint)
template_url = self.stack_template_url(blueprint)
try:
template_exists = self.s3_conn.head_object(
Bucket=self.bucket_name, Key=key_name) is not None
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == '404':
template_exists = False
else:
raise
if template_exists and not force:
logger.debug("Cloudformation template %s already exists.",
template_url)
return template_url
self.s3_conn.put_object(Bucket=self.bucket_name,
Key=key_name,
Body=blueprint.rendered,
ServerSideEncryption='AES256',
ACL='bucket-owner-full-control')
logger.debug("Blueprint %s pushed to %s.", blueprint.name,
template_url)
return template_url
[docs] def execute(self, *args, **kwargs):
try:
self.pre_run(*args, **kwargs)
self.run(*args, **kwargs)
self.post_run(*args, **kwargs)
except PlanFailed as e:
logger.error(str(e))
sys.exit(1)
[docs] def pre_run(self, *args, **kwargs):
pass
[docs] def run(self, *args, **kwargs):
raise NotImplementedError("Subclass must implement \"run\" method")
[docs] def post_run(self, *args, **kwargs):
pass
[docs] def build_provider(self, stack):
"""Builds a :class:`stacker.providers.base.Provider` suitable for
operating on the given :class:`stacker.Stack`."""
return self.provider_builder.build(region=stack.region,
profile=stack.profile)
@property
def provider(self):
"""Some actions need a generic provider using the default region (e.g.
hooks)."""
return self.provider_builder.build()
def _tail_stack(self, stack, cancel, retries=0, **kwargs):
provider = self.build_provider(stack)
return provider.tail_stack(stack, cancel, retries, **kwargs)