Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Self parse user data #306

Merged
merged 14 commits into from
Jan 30, 2017
62 changes: 61 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
from string import Template as StringTemplate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just use import string here, and then use Template as string.Template later

from stacker.util import read_value_from_path

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

from ..exceptions import (
Expand Down Expand Up @@ -220,7 +222,48 @@ def resolve_variable(var_name, var_def, provided_variable, blueprint_name):
return value


def parse_user_data(variables, raw_user_data, blueprint_name):
"""Translate a userdata file to into the file contents.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sentence doesn't make sense


It supports referencing template variables to create userdata
that's supplemented with information from the data, as commonly
required when creating EC2 userdata files. Automatically, encodes
the data file to base64 after it is processed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true any longer, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this indentation looks off


Args:
raw_user_data (str): the user data with the cloud-init info
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing some Args here.


Returns:
str: The parsed user data, with all the variables values and
refs replaced with their resolved values.

Raises:
MissingVariable: Raised when a variable is in the user_data that
is not given in the blueprint

"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be over indented - I think the whole docstring block is actually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indentation is still off, should be flush with the start of the doc string.

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 = StringTemplate(raw_user_data)

res = ""

try:
res = template.substitute(variable_values)
except Exception as e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be catching the exact exception that gets raised

raise MissingVariable(blueprint_name, e)

return res


class Blueprint(object):

"""Base implementation for rendering a troposphere template.

Args:
Expand Down Expand Up @@ -402,6 +445,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
41 changes: 40 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 Down Expand Up @@ -44,6 +46,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 +75,7 @@ class TestBlueprint(Blueprint):
}

class TestBlueprintSublcass(TestBlueprint):

def defined_variables(self):
variables = super(TestBlueprintSublcass,
self).defined_variables()
Expand Down Expand Up @@ -528,3 +532,38 @@ 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_fails(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)

@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')