File: //proc/self/root/usr/lib/python2.7/site-packages/salt/utils/extend.py
# -*- coding: utf-8 -*-
'''
SaltStack Extend
~~~~~~~~~~~~~~~~
A templating tool for extending SaltStack.
Takes a template directory and merges it into a SaltStack source code
directory. This tool uses Jinja2 for templating.
This tool is accessed using `salt-extend`
:codeauthor: Anthony Shaw <anthonyshaw@apache.org>
'''
# Import Python libs
from __future__ import absolute_import, unicode_literals, print_function
from datetime import date
import logging
import tempfile
import os
import sys
import shutil
from jinja2 import Template
# Import Salt libs
from salt.serializers.yaml import deserialize
from salt.ext.six.moves import zip
from salt.utils.odict import OrderedDict
import salt.utils.files
import salt.version
log = logging.getLogger(__name__)
try:
import click
HAS_CLICK = True
except ImportError as ie:
HAS_CLICK = False
TEMPLATE_FILE_NAME = 'template.yml'
def _get_template(path, option_key):
'''
Get the contents of a template file and provide it as a module type
:param path: path to the template.yml file
:type path: ``str``
:param option_key: The unique key of this template
:type option_key: ``str``
:returns: Details about the template
:rtype: ``tuple``
'''
with salt.utils.files.fopen(path, 'r') as template_f:
template = deserialize(template_f)
info = (option_key, template.get('description', ''), template)
return info
def _fetch_templates(src):
'''
Fetch all of the templates in the src directory
:param src: The source path
:type src: ``str``
:rtype: ``list`` of ``tuple``
:returns: ``list`` of ('key', 'description')
'''
templates = []
log.debug('Listing contents of %s', src)
for item in os.listdir(src):
s = os.path.join(src, item)
if os.path.isdir(s):
template_path = os.path.join(s, TEMPLATE_FILE_NAME)
if os.path.isfile(template_path):
templates.append(_get_template(template_path, item))
else:
log.debug("Directory does not contain %s %s", template_path,
TEMPLATE_FILE_NAME)
return templates
def _mergetree(src, dst):
'''
Akin to shutils.copytree but over existing directories, does a recursive merge copy.
:param src: The source path
:type src: ``str``
:param dst: The destination path
:type dst: ``str``
'''
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
log.info("Copying folder %s to %s", s, d)
if os.path.exists(d):
_mergetree(s, d)
else:
shutil.copytree(s, d)
else:
log.info("Copying file %s to %s", s, d)
shutil.copy2(s, d)
def _mergetreejinja(src, dst, context):
'''
Merge directory A to directory B, apply Jinja2 templating to both
the file/folder names AND to the contents of the files
:param src: The source path
:type src: ``str``
:param dst: The destination path
:type dst: ``str``
:param context: The dictionary to inject into the Jinja template as context
:type context: ``dict``
'''
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
log.info("Copying folder %s to %s", s, d)
if os.path.exists(d):
_mergetreejinja(s, d, context)
else:
os.mkdir(d)
_mergetreejinja(s, d, context)
else:
if item != TEMPLATE_FILE_NAME:
d = Template(d).render(context)
log.info("Copying file %s to %s", s, d)
with salt.utils.files.fopen(s, 'r') as source_file:
src_contents = salt.utils.stringutils.to_unicode(source_file.read())
dest_contents = Template(src_contents).render(context)
with salt.utils.files.fopen(d, 'w') as dest_file:
dest_file.write(salt.utils.stringutils.to_str(dest_contents))
def _prompt_user_variable(var_name, default_value):
'''
Prompt the user to enter the value of a variable
:param var_name: The question to ask the user
:type var_name: ``str``
:param default_value: The default value
:type default_value: ``str``
:rtype: ``str``
:returns: the value from the user
'''
return click.prompt(var_name, default=default_value)
def _prompt_choice(var_name, options):
'''
Prompt the user to choose between a list of options, index each one by adding an enumerator
based on https://github.com/audreyr/cookiecutter/blob/master/cookiecutter/prompt.py#L51
:param var_name: The question to ask the user
:type var_name: ``str``
:param options: A list of options
:type options: ``list`` of ``tupple``
:rtype: ``tuple``
:returns: The selected user
'''
choice_map = OrderedDict(
('{0}'.format(i), value) for i, value in enumerate(options, 1) if value[0] != 'test'
)
choices = choice_map.keys()
default = '1'
choice_lines = ['{0} - {1} - {2}'.format(c[0], c[1][0], c[1][1]) for c in choice_map.items()]
prompt = '\n'.join((
'Select {0}:'.format(var_name),
'\n'.join(choice_lines),
'Choose from {0}'.format(', '.join(choices))
))
user_choice = click.prompt(
prompt, type=click.Choice(choices), default=default
)
return choice_map[user_choice]
def apply_template(template_dir, output_dir, context):
'''
Apply the template from the template directory to the output
using the supplied context dict.
:param src: The source path
:type src: ``str``
:param dst: The destination path
:type dst: ``str``
:param context: The dictionary to inject into the Jinja template as context
:type context: ``dict``
'''
_mergetreejinja(template_dir, output_dir, context)
def run(extension=None, name=None, description=None, salt_dir=None, merge=False, temp_dir=None):
'''
A template factory for extending the salt ecosystem
:param extension: The extension type, e.g. 'module', 'state', if omitted, user will be prompted
:type extension: ``str``
:param name: Python-friendly name for the module, if omitted, user will be prompted
:type name: ``str``
:param description: A description of the extension, if omitted, user will be prompted
:type description: ``str``
:param salt_dir: The targeted Salt source directory
:type salt_dir: ``str``
:param merge: Merge with salt directory, `False` to keep separate, `True` to merge trees.
:type merge: ``bool``
:param temp_dir: The directory for generated code, if omitted, system temp will be used
:type temp_dir: ``str``
'''
if not HAS_CLICK:
print("click is not installed, please install using pip")
sys.exit(1)
if salt_dir is None:
salt_dir = '.'
MODULE_OPTIONS = _fetch_templates(os.path.join(salt_dir, 'templates'))
if extension is None:
print('Choose which type of extension you are developing for SaltStack')
extension_type = 'Extension type'
chosen_extension = _prompt_choice(extension_type, MODULE_OPTIONS)
else:
if extension not in list(zip(*MODULE_OPTIONS))[0]:
print("Module extension option not valid")
sys.exit(1)
chosen_extension = [m for m in MODULE_OPTIONS if m[0] == extension][0]
extension_type = chosen_extension[0]
extension_context = chosen_extension[2]
if name is None:
print('Enter the short name for the module (e.g. mymodule)')
name = _prompt_user_variable('Module name', '')
if description is None:
description = _prompt_user_variable('Short description of the module', '')
template_dir = 'templates/{0}'.format(extension_type)
module_name = name
param_dict = {
"version": salt.version.SaltStackVersion.next_release().name,
"module_name": module_name,
"short_description": description,
"release_date": date.today().strftime('%Y-%m-%d'),
"year": date.today().strftime('%Y'),
}
# get additional questions from template
additional_context = {}
for key, val in extension_context.get('questions', {}).items():
# allow templates to be used in default values.
default = Template(val.get('default', '')).render(param_dict)
prompt_var = _prompt_user_variable(val['question'], default)
additional_context[key] = prompt_var
context = param_dict.copy()
context.update(extension_context)
context.update(additional_context)
if temp_dir is None:
temp_dir = tempfile.mkdtemp()
apply_template(
template_dir,
temp_dir,
context)
if not merge:
path = temp_dir
else:
_mergetree(temp_dir, salt_dir)
path = salt_dir
log.info('New module stored in %s', path)
return path
if __name__ == '__main__':
run()