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
Merged

Self parse user data #306

merged 14 commits into from
Jan 30, 2017

Conversation

ttaub
Copy link
Contributor

@ttaub ttaub commented Jan 25, 2017

This PR adds the ability to parse variables referenced in a userdata file.

UserData.yaml

#!/bin/sh
echo Hey, {{name}}

stack.yaml

- name: stack
   variables: 
     name: tom
     UserData: ${file plain:file:/path/to/file}

stack.py - somewhere where userdata needs to be passed in

...
UserData = self.parse_user_data( variables['Userdata'] )
...

this will convert all variables enclosed in {{ ... }} to their respective values and base64 encode the entire string.

the data file to base64 after it is processed.

Args:
raw_userdata (str of userdata):
Copy link
Member

Choose a reason for hiding this comment

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

Just str instead of str of userdata. Also, make sure you put the return statement in the docstring as well - see: http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

variables = self.get_variables()
parameters = self.get_cfn_parameters()

for match in pattern.finditer(raw_user_data):
Copy link
Member

Choose a reason for hiding this comment

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

Lets go with the standard python string.Template method of templating: https://docs.python.org/3/library/string.html#template-strings

Also, please double check that the parameters aren't availabe in get_variables - seems strange, but worth double checking (@mhahn might know)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. I decided to stick to just using a regex because the standard delimiter in the Template strings is $ which is very commonly used in UserData for other purposes. We can subclass the Template class and define our own custom rules for the delimiters, but I would argue that what we have right now is cleaner.

  2. Parameters are in get_variables, however, they are stored as CFNParameter objects. In my new commit I addressed this is a cleaner way.

start_index = match.end()

user_data.append(raw_user_data[start_index:])
res = Join("", user_data)
Copy link
Member

Choose a reason for hiding this comment

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

Since this is just a template, and you're not doing anything w/ Ref, etc, you probably won't need any of this join logic - it'll just be a template.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

refs replaced with their resolved values.

"""
pattern = re.compile(r'{{([::|\w]+)}}')
Copy link
Member

Choose a reason for hiding this comment

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

One issue w/ building your own templating engine is that there's no way to escape this with the current implementation, right? Python actually has a second string formatting/templating system that you can use perhaps? https://docs.python.org/3/library/string.html#format-string-syntax


key = match.group(1)

if key in variables:
Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't usually check for the existence of a key - instead you should use try/except:

try:
    v = variables[key]
except KeyError:
    raise MissingVariable(self.name, key)

Then work with v for the rest of the logic, outside of the block.


res += raw_user_data[start_index:]

return base64.b64encode(res)
Copy link
Member

Choose a reason for hiding this comment

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

It might be better to not encode this at all, letting the user choose whether to encode it either using base64 or the Base64 troposphere method. what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's fair. They could extend the script if they wanted to.

@ttaub
Copy link
Contributor Author

ttaub commented Jan 27, 2017

The way userdata templating works is fixed, it uses the default string template provided by python. The main problem with my previous attempt was the way I was passing in the userdata was as a plain string using the file lookup handler which was passing its output to the default output handler and causing all sorts of problems. Now, the function only requires the path and it reads the file itself.

Example config file: test.yaml

...
UserDataPath: file://tmp.txt
#file:// is required for it to work 
...

Example template file: test.py

...
variables = self.get_variables()
...
userdata=self.read_user_data(variables['UserDataPath'])
...

@ttaub
Copy link
Contributor Author

ttaub commented Jan 27, 2017

I can move this to stacker_blueprints, I think it may be a better fit.

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

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

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.

the data file to base64 after it is processed.

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.

Copy link
Contributor

@mwildehahn mwildehahn left a comment

Choose a reason for hiding this comment

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

looks good overall

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
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

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


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

@@ -219,23 +220,28 @@ def resolve_variable(var_name, var_def, provided_variable, blueprint_name):


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

Choose a reason for hiding this comment

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

Maybe Parse the given user data and render it as a template


res = ""

try:
res = template.substitute(variable_values)
except Exception as e:
raise MissingVariable(blueprint_name, e)
except ValueError:
Copy link
Member

Choose a reason for hiding this comment

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

how does this get raised? Not sure I understand from the exception, seems similar to the MissingVariable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This gets raised when the placeholder name for the variable in the template is actually invalid. E.g ${100} raises a ValueError because the placeholder name (100) can not only be a number.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is also explained in the Raises section of the comments.

@@ -44,6 +54,14 @@ def __init__(self, blueprint_name, variable_name, *args, **kwargs):
super(MissingVariable, self).__init__(message, *args, **kwargs)


class InvalidUserdataParameter(Exception):
Copy link
Member

Choose a reason for hiding this comment

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

Where is this used?

Copy link
Member

@phobologic phobologic left a comment

Choose a reason for hiding this comment

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

Minor changes requested, but this looks good to me.

is not valid. E.g ${100} would raise this.


"""
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.

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

InvalidUserdataPlaceholder: Raised when a placeholder name in user_data
Copy link
Member

Choose a reason for hiding this comment

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

user_data -> raw_user_data - this actually happens in multiple places in the doc string.

class InvalidUserdataPlaceholder(Exception):

def __init__(self, blueprint_name, *args, **kwargs):
message = "Could not parse userdata in blueprint \"%s\". " % (
Copy link
Member

Choose a reason for hiding this comment

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

I'd include the text from the ValueError that raises this - would make it easier to track down the issues:

>>> t.substitute({"100": "blue", "a": "apple"})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python/2.7.12_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/string.py", line 176, in substitute
    return self.pattern.sub(convert, self.template)
  File "/usr/local/Cellar/python/2.7.12_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/string.py", line 173, in convert
    self._invalid(mo)
  File "/usr/local/Cellar/python/2.7.12_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/string.py", line 146, in _invalid
    (lineno, colno))
ValueError: Invalid placeholder in string: line 2, col 1

The line # & col # seem useful. You can get that from .args of the original error.

@ttaub ttaub merged commit c3fbc36 into release-1.0 Jan 30, 2017
@ttaub ttaub deleted the self_parse_user_data branch January 30, 2017 22:24
phrohdoh pushed a commit to phrohdoh/stacker that referenced this pull request Dec 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants