Skip to content

Commit

Permalink
Merge pull request #306 from remind101/self_parse_user_data
Browse files Browse the repository at this point in the history
Self parse user data
  • Loading branch information
ttaub authored Jan 30, 2017
2 parents 027ce93 + 8f52dd2 commit c3fbc36
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 2 deletions.
74 changes: 73 additions & 1 deletion stacker/blueprints/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import copy
import hashlib
import logging
import string
from stacker.util import read_value_from_path

from troposphere import (
Parameter,
Ref,
Template,
Template
)

from ..exceptions import (
Expand All @@ -14,6 +16,7 @@
UnresolvedVariables,
ValidatorError,
VariableTypeRequired,
InvalidUserdataPlaceholder
)
from .variables.types import (
CFNType,
Expand Down Expand Up @@ -216,7 +219,59 @@ def resolve_variable(var_name, var_def, provided_variable, blueprint_name):
return value


def parse_user_data(variables, raw_user_data, blueprint_name):
"""Parse the given user data and renders it as a template
It supports referencing template variables to create userdata
that's supplemented with information from the stack, as commonly
required when creating EC2 userdata files.
For example:
Given a raw_user_data string: 'open file'
And a variables dictionary with: {'file': 'test.txt'}
parse_user_data would output: open file test.txt
Args:
variables (dict): variables available to the template
raw_user_data (str): the user_data to be parsed
blueprint_name (str): the name of the blueprint
Returns:
str: The parsed user data, with all the variables values and
refs replaced with their resolved values.
Raises:
InvalidUserdataPlaceholder: Raised when a placeholder name in
raw_user_data is not valid.
E.g ${100} would raise this.
MissingVariable: Raised when a variable is in the raw_user_data that
is not given in the blueprint
"""
variable_values = {}

for key in variables.keys():
if type(variables[key]) is CFNParameter:
variable_values[key] = variables[key].to_parameter_value()
else:
variable_values[key] = variables[key]

template = string.Template(raw_user_data)

res = ""

try:
res = template.substitute(variable_values)
except ValueError as exp:
raise InvalidUserdataPlaceholder(blueprint_name, exp.args[0])
except KeyError as key:
raise MissingVariable(blueprint_name, key)

return res


class Blueprint(object):

"""Base implementation for rendering a troposphere template.
Args:
Expand Down Expand Up @@ -398,6 +453,23 @@ def render_template(self):
version = hashlib.md5(rendered).hexdigest()[:8]
return (version, rendered)

def read_user_data(self, user_data_path):
"""Reads and parses a user_data file.
Args:
user_data_path (str):
path to the userdata file
Returns:
str: the parsed user data file
"""
raw_user_data = read_value_from_path(user_data_path)

variables = self.get_variables()

return parse_user_data(variables, raw_user_data, self.name)

@property
def rendered(self):
if not self._rendered:
Expand Down
15 changes: 15 additions & 0 deletions stacker/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ def __init__(self, lookup, *args, **kwargs):
super(UnknownLookupType, self).__init__(message, *args, **kwargs)


class InvalidUserdataPlaceholder(Exception):

def __init__(self, blueprint_name, exception_message, *args, **kwargs):
message = exception_message + ". "
message += "Could not parse userdata in blueprint \"%s\". " % (
blueprint_name)
message += "Make sure to escape all $ symbols with a $$."
super(InvalidUserdataPlaceholder, self).__init__(
message, *args, **kwargs)


class UnresolvedVariables(Exception):

def __init__(self, blueprint_name, *args, **kwargs):
Expand Down Expand Up @@ -102,18 +113,22 @@ def __init__(self, cls, error, *args, **kwargs):


class StackDidNotChange(Exception):

"""Exception raised when there are no changes to be made by the
provider.
"""


class CancelExecution(Exception):

"""Exception raised when we want to cancel executing the plan."""


class ValidatorError(Exception):

"""Used for errors raised by custom validators of blueprint variables.
"""

def __init__(self, variable, validator, value, exception=None):
self.variable = variable
self.validator = validator
Expand Down
48 changes: 47 additions & 1 deletion stacker/tests/blueprints/test_base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import unittest
from mock import patch

from mock import MagicMock
from troposphere import (
Base64,
Ref,
s3,
s3
)

from stacker.blueprints.base import (
Expand All @@ -14,6 +15,7 @@
validate_allowed_values,
validate_variable_type,
resolve_variable,
parse_user_data
)
from stacker.blueprints.variables.types import (
CFNNumber,
Expand All @@ -28,6 +30,7 @@
UnresolvedVariables,
ValidatorError,
VariableTypeRequired,
InvalidUserdataPlaceholder
)
from stacker.variables import Variable
from stacker.lookups import register_lookup_handler
Expand All @@ -44,6 +47,7 @@ def mock_lookup_handler(value, provider=None, context=None, fqn=False,


class TestBuildParameter(unittest.TestCase):

def test_base_parameter(self):
p = build_parameter("BasicParam", {"type": "String"})
p.validate()
Expand Down Expand Up @@ -72,6 +76,7 @@ class TestBlueprint(Blueprint):
}

class TestBlueprintSublcass(TestBlueprint):

def defined_variables(self):
variables = super(TestBlueprintSublcass,
self).defined_variables()
Expand Down Expand Up @@ -529,3 +534,44 @@ class TestBlueprint(Blueprint):

with self.assertRaises(AttributeError):
TestBlueprint(name="test", context=MagicMock())

def test_parse_user_data(self):
expected = 'name: tom, last: taubkin and $'
variables = {
'name': 'tom',
'last': 'taubkin'
}

raw_user_data = 'name: ${name}, last: $last and $$'
blueprint_name = 'test'
res = parse_user_data(variables, raw_user_data, blueprint_name)
self.assertEqual(res, expected)

def test_parse_user_data_missing_variable(self):
variables = {
'name': 'tom',
}

raw_user_data = 'name: ${name}, last: $last and $$'
blueprint_name = 'test'
with self.assertRaises(MissingVariable):
parse_user_data(variables, raw_user_data, blueprint_name)

def test_parse_user_data_invaled_placeholder(self):
raw_user_data = '$100'
blueprint_name = 'test'
with self.assertRaises(InvalidUserdataPlaceholder):
parse_user_data({}, raw_user_data, blueprint_name)

@patch('stacker.blueprints.base.read_value_from_path',
return_value='contents')
@patch('stacker.blueprints.base.parse_user_data')
def test_read_user_data(self, parse_mock, file_mock):
class TestBlueprint(Blueprint):
VARIABLES = {}

blueprint = TestBlueprint(name="blueprint_name", context=MagicMock())
blueprint.resolve_variables({})
blueprint.read_user_data('file://test.txt')
file_mock.assert_called_with('file://test.txt')
parse_mock.assert_called_with({}, 'contents', 'blueprint_name')

0 comments on commit c3fbc36

Please sign in to comment.