def sanity_check(self): self.find_missing_env() if all(item is not NotSpecified for item in (self.params_json, self.params_yaml)): raise BadStack("Please don't have both params_json and params_yaml") if all(item is NotSpecified for item in (self.stack_json, self.stack_yaml)): raise BadStack("Could not find either stack_json or stack_yaml") # Hack for sanity check for var in self.nested_vars(): if hasattr(var, 'stack') and not isinstance(var.stack, six.string_types): if not var.stack.cloudformation.status.exists: var._resolved = "YYY_RESOLVED_BY_MISSING_STACK_YYY" elif var.stack.output_yet_to_be_deployed(var): var._resolved = "YYY_RESOLVED_BY_MISSING_OUTPUT_YYY" matches = re.findall("XXX_[A-Z0-9_]+_XXX", json.dumps(self.params_json_obj)) for var in self.nested_vars(): if hasattr(var, "_resolved"): var._resolved = None if matches: raise BadStack("Found placeholders in the generated params file", stack=self.name, found=matches) if self.cloudformation.status.failed: raise BadStack("Stack is in a failed state, this means it probably has to be deleted first....", stack=self.stack_name) self.validate_template_params()
def wait(self, timeout=1200, rollback_is_failure=False, may_not_exist=True): status = self.status if not status.exists and may_not_exist: return status last = datetime.datetime.now(pytz.utc) if status.failed: raise BadStack( "Stack is in a failed state, it must be deleted first", name=self.stack_name, status=status) for _ in hp.until(timeout, step=15): if status.exists and status.complete: break log.info("Waiting for %s - %s", self.stack_name, status.name) if status.exists and not status.complete: status = self.status else: break description = self.description() events = [] while True: try: with self.ignore_throttling_error(): response = self.conn.describe_stack_events( StackName=self.stack_name) events = response['StackEvents'] break except Throttled: log.info("Was throttled, waiting a bit") time.sleep(1) next_last = events[0]['Timestamp'] for event in events: if event['Timestamp'] > last: reason = event.get('ResourceStatusReason', '') log.info("%s - %s %s (%s) %s", self.stack_name, event['ResourceType'], event['LogicalResourceId'], event['ResourceStatus'], reason) last = next_last status = self.status if status.failed or (rollback_is_failure and status.is_rollback) or not status.complete: raise BadStack("Stack failed to complete", final_status=status) return status
def validate_template_params(self): """ Validate stack template and stack params against CloudFormation """ validation = self.validate_template() _defined_params = lambda obj: set([x['ParameterKey'] for x in obj]) template_params = _defined_params(validation.get('Parameters', [])) defaulted_params = _defined_params(x for x in validation.get('Parameters', []) if 'DefaultValue' in x) required_params = template_params - defaulted_params stack_params = _defined_params(json.loads(self.params_json_raw)) if stack_params > template_params: raise BadStack("Parameters not defined in template provided", stack=self.name, requires=list(template_params), additional=list(stack_params - template_params)) if required_params > stack_params: raise BadStack("Parameters required by template missing", stack=self.name, defined=list(required_params), missing=list(required_params - stack_params)) return validation
def create_or_update(self): """Create or update the stack, return True if the stack actually changed""" log.info("Creating or updating the stack (%s)", self.stack_name) status = self.cloudformation.wait(may_not_exist=True) tags = self.tags or None role = self.bespin.credentials.account_role_arn(self.role_name) if tags and type(tags) is not dict and hasattr(self.tags, "as_dict"): tags = tags.as_dict() if not status.exists: log.info("No existing stack, making one now") if self.bespin.dry_run: log.info("DRYRUN: Would create stack") log.info("Would use following stack:") print(self.dumped_stack_obj) else: return self.cloudformation.create(self.dumped_stack_obj, self.params_json_obj, tags, self.dumped_policy_obj, role, self.termination_protection) elif status.complete: log.info("Found existing stack, doing an update") if self.bespin.dry_run: log.info("DRYRUN: Would update stack") log.info("Would use following stack:") print(self.dumped_stack_obj) else: return self.cloudformation.update(self.dumped_stack_obj, self.params_json_obj, tags, self.dumped_policy_obj, role, self.termination_protection) else: raise BadStack("Stack could not be updated", name=self.stack_name, status=status.name) return False
def wait(self, environment): endpoint = self.endpoint().resolve() while endpoint.endswith("/"): endpoint = endpoint[:-1] while endpoint.endswith("."): endpoint = endpoint[:-1] while self.check_url.startswith("/"): self.check_url = self.check_url[1:] url = endpoint + '/' + self.check_url expected = self.expect.format(**environment) log.info("Asking server for version till we match %s", expected) for _ in hp.until(self.timeout_after, step=15): log.info("Asking %s", url) try: res = requests.get(url) result = res.text status = res.status_code except requests.exceptions.ConnectionError as error: log.warning("Failed to ask server\terror=%s", error) else: log.info("\tgot back (%s) '%s'", status, result) if fnmatch.fnmatch(result, expected): log.info("Deployment successful!") return raise BadStack( "Timedout waiting for the app to give back the correct version")