Merge pull request #4 from pawansingh126/master

Add yaml-editor to utils for editing yaml-files on the go
This commit is contained in:
gpsingh-1991 2018-11-29 18:34:50 +05:30 committed by GitHub
commit 89a0a3c487
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 524 additions and 2 deletions

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
recursive-include spyglass/utils/editor/static *
recursive-include spyglass/utils/editor/templates *

View File

@ -36,6 +36,7 @@ setup(
entry_points={
'console_scripts': [
'spyglass=spyglass.spyglass:main',
'yaml-editor=spyglass.utils.editor.editor:main',
],
'data_extractor_plugins':
['formation=spyglass.data_extractor.plugins.formation:FormationPlugin',

View File

@ -15,9 +15,12 @@
import copy
import json
import logging
import os
import pkg_resources
import pprint
import sys
import tempfile
import jsonschema
import netaddr
import yaml
@ -81,6 +84,17 @@ class ProcessDataSource():
LOG.debug("Genesis Node Details:\n{}".format(
pprint.pformat(self.genesis_node)))
def _get_genesis_node_ip(self):
""" Returns the genesis node ip """
ip = '0.0.0.0'
LOG.info("Getting Genesis Node IP")
if not self.genesis_node:
self._get_genesis_node_details()
ips = self.genesis_node.get('ip', '')
if ips:
ip = ips.get('oam', '0.0.0.0')
return ip
def _validate_intermediary_data(self, data):
""" Validates the intermediary data before generating manifests.
@ -347,11 +361,27 @@ class ProcessDataSource():
f.write(yaml_file)
f.close()
def generate_intermediary_yaml(self):
def generate_intermediary_yaml(self, edit_intermediary=False):
""" Generating intermediary yaml """
LOG.info("Start: Generate Intermediary")
self._apply_design_rules()
self._get_genesis_node_details()
# This will validate the extracted data from different sources.
self._validate_intermediary_data(self.data)
if edit_intermediary:
self.edit_intermediary_yaml()
# This will check if user edited changes are in order.
self._validate_intermediary_data(self.data)
self.intermediary_yaml = self.data
return self.intermediary_yaml
def edit_intermediary_yaml(self):
""" Edit generated data using on browser """
LOG.info(
"edit_intermediary_yaml: Invoking web server for yaml editing")
with tempfile.NamedTemporaryFile(mode='r+') as file_obj:
yaml.safe_dump(self.data, file_obj, default_flow_style=False)
host = self._get_genesis_node_ip()
os.system('yaml-editor -f {0} -h {1}'.format(file_obj.name, host))
file_obj.seek(0)
self.data = yaml.safe_load(file_obj)

View File

@ -57,6 +57,11 @@ LOG = logging.getLogger('spyglass')
'-idir',
type=click.Path(exists=True),
help='The path where intermediary file needs to be generated')
@click.option(
'--edit_intermediary/--no_edit_intermediary',
'-e/-nedit',
default=True,
help='Flag to let user edit intermediary')
@click.option(
'--generate_manifests',
'-m',
@ -96,6 +101,7 @@ def main(*args, **kwargs):
# Extract user provided inputs
generate_intermediary = kwargs['generate_intermediary']
intermediary_dir = kwargs['intermediary_dir']
edit_intermediary = kwargs['edit_intermediary']
generate_manifests = kwargs['generate_manifests']
manifest_dir = kwargs['manifest_dir']
intermediary = kwargs['intermediary']
@ -176,7 +182,8 @@ def main(*args, **kwargs):
data_extractor.site_data)
LOG.info("Generate intermediary yaml")
intermediary_yaml = process_input_ob.generate_intermediary_yaml()
intermediary_yaml = process_input_ob.generate_intermediary_yaml(
edit_intermediary)
else:
LOG.info("Loading intermediary from user provided input")
with open(intermediary, 'r') as intermediary_file:

View File

View File

@ -0,0 +1,157 @@
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import os
import sys
import click
import yaml
from flask import Flask, request, render_template, send_from_directory
from flask_bootstrap import Bootstrap
app_path = os.path.dirname(os.path.abspath(__file__))
app = Flask('Yaml Editor!',
template_folder=os.path.join(app_path, 'templates'),
static_folder=os.path.join(app_path, 'static'))
Bootstrap(app)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
LOG = app.logger
@app.route('/favicon.ico')
def favicon():
return send_from_directory(app.static_folder, 'favicon.ico')
@app.route('/', methods=['GET', 'POST'])
def index():
"""Renders index page to edit provided yaml file."""
LOG.info('Rendering yaml file for editing')
with open(app.config['YAML_FILE']) as file_obj:
data = yaml.safe_load(file_obj)
return render_template('yaml.html',
data=json.dumps(data),
change_str=app.config['STRING_TO_CHANGE'])
@app.route('/save', methods=['POST'])
def save():
"""Save current progress on file."""
LOG.info('Saving edited inputs from user to yaml file')
out = request.json.get('yaml_data')
with open(app.config['YAML_FILE'], 'w') as file_obj:
yaml.safe_dump(out, file_obj, default_flow_style=False)
return "Data saved successfully!"
@app.route('/saveExit', methods=['POST'])
def save_exit():
"""Save current progress on file and shuts down the server."""
LOG.info('Saving edited inputs from user to yaml file and shutting'
' down server')
out = request.json.get('yaml_data')
with open(app.config['YAML_FILE'], 'w') as file_obj:
yaml.safe_dump(out, file_obj, default_flow_style=False)
func = request.environ.get('werkzeug.server.shutdown')
if func:
func()
return "Saved successfully, Shutting down app! You may close the tab!"
@app.errorhandler(404)
def page_not_found(e):
"""Serves 404 error."""
LOG.info('User tried to access unavailable page.')
return '<h1>404: Page not Found!</h1>'
def run(*args, **kwargs):
"""Starts the server."""
LOG.info('Initiating web server for yaml editing')
port = kwargs.get('port', None)
if not port:
port = 8161
app.run(host='0.0.0.0', port=port, debug=False)
@click.command()
@click.option(
'--file',
'-f',
required=True,
type=click.File(),
multiple=False,
help="Path with file name to the intermediary yaml file."
)
@click.option(
'--host',
'-h',
default='0.0.0.0',
type=click.STRING,
multiple=False,
help="Optional port parameter to run Flask on."
)
@click.option(
'--port',
'-p',
default=8161,
type=click.INT,
multiple=False,
help="Optional port parameter to run Flask on."
)
@click.option(
'--string',
'-s',
default='#CHANGE_ME',
type=click.STRING,
multiple=False,
help="Text which is required to be changed on yaml file."
)
def main(*args, **kwargs):
LOG.setLevel(logging.INFO)
LOG.info('Initiating yaml-editor')
try:
yaml.safe_load(kwargs['file'])
except yaml.YAMLError as e:
LOG.error('EXITTING - Please provide a valid yaml file.')
if hasattr(e, 'problem_mark'):
mark = e.problem_mark
LOG.error("Error position: ({0}:{1})".format(
mark.line + 1, mark.column + 1))
sys.exit(2)
except Exception:
LOG.error('EXITTING - Please provide a valid yaml file.')
sys.exit(2)
LOG.info("""
##############################################################################
Please go to http://{0}:{1}/ to edit your yaml file.
##############################################################################
""".format(kwargs['host'], kwargs['port']))
app.config['YAML_FILE'] = kwargs['file'].name
app.config['STRING_TO_CHANGE'] = kwargs['string']
run(*args, **kwargs)
if __name__ == '__main__':
"""Invoked when used as a script."""
main()

View File

@ -0,0 +1,92 @@
// Copyright 2018 AT&T Intellectual Property. All other rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file includes all the frond-end functionality being used for the
// yaml editor application.
/**
* Calls /save URL to save edit progress.
* @param {String} data Stringified JSON data.
*/
function save(data) {
$.ajax({
type: 'POST',
url: '/save',
data: data,
success: function(res) {
setTimeout(function() { alert(res); }, 3);
},
contentType: 'application/json;charset=UTF-8'
});
}
/**
* Calls /saveExit URL to save edit progress and shut down web server.
* @param {String} data Stringified JSON data.
*/
function saveAndExit(data) {
$.ajax({
type: 'POST',
url: '/saveExit',
data: data,
success: function(res) {
setTimeout(function() { alert(res); }, 3);
},
contentType: 'application/json;charset=UTF-8'
});
}
/**
* Collects and validates data from textarea.
* @returns {String} Stringified JSON data.
*/
function getSimpleData() {
var data = $("#yaml_data").val();
try {
var index = data.indexOf(changeStr)
if (index != -1) {
var lineNum = data.substring(0, index).split('\n').length;
alert('Please change value on line '+ lineNum + '!')
return null
}
data = jsyaml.load(data)
}
catch(err) {
alert(err)
return null
}
return JSON.stringify({yaml_data : data})
}
/**
* Function to save edit progress.
*/
function saveSimple() {
var data = getSimpleData()
if (data) {
save(data)
}
}
/**
* Function to save edit progress and shut down web server.
*/
function saveExitSimple() {
var data = getSimpleData()
if (data) {
saveAndExit(data)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,68 @@
/**
* jQuery Lined Textarea Plugin
* http://alan.blog-city.com/jquerylinedtextarea.htm
*
* Copyright (c) 2010 Alan Williamson
*
* Released under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
* Usage:
* Displays a line number count column to the left of the textarea
*
* Class up your textarea with a given class, or target it directly
* with JQuery Selectors
*
* $(".lined").linedtextarea({
* selectedLine: 10,
* selectedClass: 'lineselect'
* });
*
*/
.linedwrap {
border: 1px solid #c0c0c0;
padding: 3px;
}
.linedtextarea {
padding: 0px;
margin: 0px;
}
.linedtextarea textarea, .linedwrap .codelines .lineno {
font-size: 10pt;
font-family: monospace;
line-height: normal !important;
}
.linedtextarea textarea {
padding-right:0.3em;
padding-top:0.3em;
border: 0;
}
.linedwrap .lines {
margin-top: 0px;
width: 50px;
float: left;
overflow: hidden;
border-right: 1px solid #c0c0c0;
margin-right: 10px;
}
.linedwrap .codelines {
padding-top: 5px;
}
.linedwrap .codelines .lineno {
color:#AAAAAA;
padding-right: 0.5em;
padding-top: 0.0em;
text-align: right;
white-space: nowrap;
}
.linedwrap .codelines .lineselect {
color: red;
}

View File

@ -0,0 +1,126 @@
/**
* jQuery Lined Textarea Plugin
* http://alan.blog-city.com/jquerylinedtextarea.htm
*
* Copyright (c) 2010 Alan Williamson
*
* Version:
* $Id: jquery-linedtextarea.js 464 2010-01-08 10:36:33Z alan $
*
* Released under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
* Usage:
* Displays a line number count column to the left of the textarea
*
* Class up your textarea with a given class, or target it directly
* with JQuery Selectors
*
* $(".lined").linedtextarea({
* selectedLine: 10,
* selectedClass: 'lineselect'
* });
*
* History:
* - 2010.01.08: Fixed a Google Chrome layout problem
* - 2010.01.07: Refactored code for speed/readability; Fixed horizontal sizing
* - 2010.01.06: Initial Release
*
*/
(function($) {
$.fn.linedtextarea = function(options) {
// Get the Options
var opts = $.extend({}, $.fn.linedtextarea.defaults, options);
/*
* Helper function to make sure the line numbers are always
* kept up to the current system
*/
var fillOutLines = function(codeLines, h, lineNo){
while ( (codeLines.height() - h ) <= 0 ){
if ( lineNo == opts.selectedLine )
codeLines.append("<div class='lineno lineselect'>" + lineNo + "</div>");
else
codeLines.append("<div class='lineno'>" + lineNo + "</div>");
lineNo++;
}
return lineNo;
};
/*
* Iterate through each of the elements are to be applied to
*/
return this.each(function() {
var lineNo = 1;
var textarea = $(this);
/* Turn off the wrapping of as we don't want to screw up the line numbers */
textarea.attr("wrap", "off");
textarea.css({resize:'none'});
var originalTextAreaWidth = textarea.outerWidth();
/* Wrap the text area in the elements we need */
textarea.wrap("<div class='linedtextarea'></div>");
var linedTextAreaDiv = textarea.parent().wrap("<div class='linedwrap' style='width:" + originalTextAreaWidth + "px'></div>");
var linedWrapDiv = linedTextAreaDiv.parent();
linedWrapDiv.prepend("<div class='lines' style='width:50px'></div>");
var linesDiv = linedWrapDiv.find(".lines");
linesDiv.height( textarea.height() + 6 );
/* Draw the number bar; filling it out where necessary */
linesDiv.append( "<div class='codelines'></div>" );
var codeLinesDiv = linesDiv.find(".codelines");
lineNo = fillOutLines( codeLinesDiv, linesDiv.height(), 1 );
/* Move the textarea to the selected line */
if ( opts.selectedLine != -1 && !isNaN(opts.selectedLine) ){
var fontSize = parseInt( textarea.height() / (lineNo-2) );
var position = parseInt( fontSize * opts.selectedLine ) - (textarea.height()/2);
textarea[0].scrollTop = position;
}
/* Set the width */
var sidebarWidth = linesDiv.outerWidth();
var paddingHorizontal = parseInt( linedWrapDiv.css("border-left-width") ) + parseInt( linedWrapDiv.css("border-right-width") ) + parseInt( linedWrapDiv.css("padding-left") ) + parseInt( linedWrapDiv.css("padding-right") );
var linedWrapDivNewWidth = originalTextAreaWidth - paddingHorizontal;
var textareaNewWidth = originalTextAreaWidth - sidebarWidth - paddingHorizontal - 20;
textarea.width( textareaNewWidth );
linedWrapDiv.width( linedWrapDivNewWidth );
/* React to the scroll event */
textarea.scroll( function(tn){
var domTextArea = $(this)[0];
var scrollTop = domTextArea.scrollTop;
var clientHeight = domTextArea.clientHeight;
codeLinesDiv.css( {'margin-top': (-1*scrollTop) + "px"} );
lineNo = fillOutLines( codeLinesDiv, scrollTop + clientHeight, lineNo );
});
/* Should the textarea get resized outside of our control */
textarea.resize( function(tn){
var domTextArea = $(this)[0];
linesDiv.height( domTextArea.clientHeight + 6 );
});
});
};
// default options
$.fn.linedtextarea.defaults = {
selectedLine: -1,
selectedClass: 'lineselect'
};
})(jQuery);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
{% extends "bootstrap/base.html" %}
{% block title %}YAML Editor{% endblock %}
{% block styles %}
{{super()}}
<link href="{{ url_for('static', filename='jquery-linedtextarea.css') }}" rel="stylesheet">
{% endblock %}
{% block scripts %}
{{super()}}
<script src="{{ url_for('static', filename='js-yaml.min.js') }}"></script>
<script src="{{ url_for('static', filename='jquery-linedtextarea.js') }}"></script>
<script type="text/javascript">
var changeStr = '{{ change_str }}'
$(document).ready(function(){
$("#yaml_data").val(jsyaml.dump(JSON.parse('{{ data|safe }}')))
$("#yaml_data").linedtextarea();
});
</script>
<script src="{{ url_for('static', filename='app.js') }}"></script>
{% endblock %}
{% block content %}
<div class="container" style="margin-top:30px;">
<div class="form-group">
<pre>Edit your YAML (Update corresponding fields with {{ change_str }} text):</pre>
<div>
<textarea class="form-control linedtextarea" id='yaml_data' style="height: 500px;box-shadow: none;"></textarea>
</div>
</div>
<div class="form-group pull-right">
<button type="button" onclick="saveSimple()" class="btn btn-lg btn-success ">Save</button>
<button type="button" onclick="saveExitSimple()" class="btn btn-lg btn-primary ">Save and Exit</button>
</div>
</div>
{% endblock %}