├── .gitignore ├── Containerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── bower.json ├── gcp └── Dockerfile ├── gulpfile.js ├── my_ansible_inventory ├── group_vars │ ├── all │ │ └── vars.yml │ ├── routers │ │ └── vars.yml │ └── servers │ │ └── vars.yml ├── host_vars │ ├── router00 │ │ └── vars.yml │ ├── server00 │ │ └── vars.yml │ └── test00 │ │ └── vars.yml └── inventory.yml ├── my_filter_plugins └── mask_convert.py ├── package.json ├── requirements.txt ├── screenshot.png ├── setup.py ├── td4a-server └── td4a ├── __init__.py ├── controllers ├── __init__.py ├── config.py ├── hosts.py ├── inventory.py ├── link.py ├── render.py ├── retrieve.py ├── schema.py └── validate.py ├── models ├── __init__.py ├── exception_handler.py ├── filters.py ├── inventory.py ├── sort_commented_map.py └── td4ayaml.py └── static ├── css ├── angular-material.css ├── app.css ├── codemirror.css ├── dialog.css └── material.css ├── data.yml ├── favicon.ico ├── index.html ├── js ├── app.js └── main.min.js └── template.j2 /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.pyc 4 | venv 5 | dist 6 | *.egg-info 7 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.5-alpine3.12 2 | 3 | # Update the packages 4 | RUN apk update 5 | 6 | # Install the ansible dependancies 7 | RUN apk add gcc libffi-dev musl-dev openssl-dev sshpass make 8 | # RUN apk add py-crypto python-dev 9 | 10 | # Install td4a 11 | RUN pip install td4a==2.0.3 12 | 13 | # Clear out extras 14 | RUN rm -rf /var/cache/apk/* 15 | 16 | # Start td4a 17 | CMD [ "td4a-server" ] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bradley A. Thornton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include td4a/static * 2 | recursive-include td4a/models * 3 | recursive-include td4a/controllers * 4 | global-exclude *.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![screenshot](screenshot.png) 2 | ## Template Designer for Automation 3 | 4 | ### Try it now 5 | 6 | https://td4a.codethenetwork.com 7 | 8 | ### Overview 9 | 10 | TD4A is a visual design aid for building and testing jinja2 templates. It will combine data in yaml format with a jinja2 template and render the output. 11 | 12 | All jinja2 filters are supported along with the filter plugins from Ansible version 2.9.12 13 | 14 | ### Installation: 15 | 16 | ##### using podman (or docker): 17 | ``` 18 | podman pull cidrblock/td4a 19 | ``` 20 | 21 | The container registry page can be found here: 22 | 23 | https://hub.docker.com/r/cidrblock/td4a/ 24 | 25 | #### using the cli: 26 | ``` 27 | $ virtualenv venv 28 | $ source venv/bin/activate 29 | $ pip install td4a 30 | ``` 31 | 32 | The pip package can be found here: 33 | 34 | https://pypi.python.org/pypi/td4a 35 | 36 | ### Starting the TD4A server 37 | 38 | 39 | #### Simple 40 | 41 | ##### using podman (or docker): 42 | ``` 43 | podman run -p 5000:5000 cidrblock/td4a 44 | ``` 45 | 46 | ##### using the cli: 47 | ``` 48 | td4a-server 49 | ``` 50 | 51 | ##### open your browser to: 52 | 53 | http://127.0.0.1:5000 54 | 55 | ### Modes 56 | 57 | TD4A support two different modes of operation. 58 | - Templating: Build and render jinja2 templates 59 | - Schema: Build and validate a json schema for your data 60 | 61 | #### Enabling a mode 62 | 63 | ##### using podman (or docker): 64 | ``` 65 | podman run -p 5000:5000 \ 66 | -it \ 67 | cidrblock/td4a \ 68 | td4a-server -m mode 69 | ``` 70 | 71 | ##### using the cli: 72 | ``` 73 | td4a-server -m mode 74 | ``` 75 | 76 | where `mode` is either jinja2 (default) or schema 77 | 78 | ### Additional configuration options 79 | 80 | #### Loading custom filter plugins (jinja2 mode only) 81 | 82 | 83 | TD4A supports custom filter plugins within the container. Pass your custom filter_plugins directory as a volume and use the -f option to specify to custom filter plugin directory. 84 | 85 | ##### using podman (or docker): 86 | ``` 87 | podman run -p 5000:5000 \ 88 | -it \ 89 | -v `pwd`/my_filter_plugins:/filter_plugins \ 90 | cidrblock/td4a \ 91 | td4a-server -f /filter_plugins 92 | ``` 93 | 94 | ##### using the cli: 95 | 96 | TD4A can load custom filters from a directory specified from the command line: 97 | ``` 98 | td4a-server -f ./my_filter_plugins 99 | ``` 100 | 101 | #### Loading an ansible inventory (jinja2 and schema mode) 102 | 103 | Mount the inventory as `/inventory` in the container, and run TD4A with the `-i` option. 104 | 105 | 106 | ##### using podman (or docker): 107 | 108 | ``` 109 | podman run -p 5000:5000 \ 110 | -it \ 111 | -v '/Users/me/github/ansible_network_inventory:/inventory' \ 112 | cidrblock/td4a \ 113 | td4a-server -i /inventory -m mode -v 'my_vault_password' 114 | ``` 115 | 116 | If environment variables are needed for a dynamic inventory, they can be passed to the docker container. 117 | ``` 118 | podman run -p 5000:5000 \ 119 | -it \ 120 | -v `pwd`/my_filter_plugins:/filter_plugins \ 121 | -v '/Users/me/github/ansible_network_inventory:/inventory' \ 122 | -e "COUCH_USERNAME=admin" \ 123 | -e "COUCH_PASSWORD=password" \ 124 | -e "COUCH_URL=http://192.168.1.5:5984/td4a" \ 125 | -e "DYNAMIC_INVENTORY_USERNAME=api" \ 126 | -e "DYNAMIC_INVENTORY_PASSWORD=password" \ 127 | cidrblock/td4a \ 128 | td4a-server -f /filter_plugins -m mode -i /inventory -v 'my_vault_password' 129 | ``` 130 | 131 | ##### using the cli: 132 | 133 | TD4A can load multiple ansible inventories, specifc each with `-i` on the command line: 134 | ``` 135 | td4a-server -i ./my_ansible_inventory -v 'my_vault_password' 136 | ``` 137 | 138 | #### Enabling storage and links using a couch database (jinja2 and schema mode) 139 | 140 | TD4A has the ability to store data and templates in a CouchDB. This is disabled by default. 141 | 142 | The CouchDB needs to previously created. 143 | 144 | To enable link support, and add the link button to the UI, set the following environ variables: 145 | 146 | ##### using podman (or docker): 147 | 148 | ``` 149 | podman run -p 5000:5000 \ 150 | -v `pwd`/my_filter_plugins:/filter_plugins \ 151 | -e "COUCH_USERNAME=admin" \ 152 | -e "COUCH_PASSWORD=password" \ 153 | -e "COUCH_URL=http://192.168.1.5:5984/td4a" \ 154 | cidrblock/td4a \ 155 | td4a-server -m mode 156 | ``` 157 | 158 | ##### using the cli: 159 | ``` 160 | export COUCH_USERNAME=admin 161 | export COUCH_PASSWORD=password 162 | export COUCH_URL=http://localhost:5984/td4a 163 | td4a-server -m mode 164 | ``` 165 | 166 | ### User Interface 167 | 168 | The interface is browser based and has been tested using Chrome. If your browser did not automatically open when TD4A was started, you can visit http://127.0.0.1:5000 to see the interface. 169 | 170 | The UI is broken into three sections: 171 | 172 | 1) DATA, this is where the data in yaml format is provided. 173 | 2) TEMPLATE, the jinja2 template to be rendered. 174 | 3) RESULT, after clicking the render button, the result pane will be populated with the rendered template. 175 | 176 | #### Keyboard shortcuts 177 | 178 | `cmd+r`: Render the template 179 | 180 | `cmd+s`: Save the data in browser local storage 181 | 182 | `cmd+b`: Begin new, clear the screen 183 | 184 | ### Python version 185 | 186 | To date, this has only been tested with python 3.8.5. 187 | 188 | ### Development 189 | 190 | NPM and related packages are required to build the UI. 191 | 192 | ``` 193 | npm install 194 | ``` 195 | 196 | The UI components can be installed for development purposes using bower. 197 | 198 | ``` 199 | bower install 200 | ``` 201 | 202 | The dependancies can be concatenated and minified using gulp. 203 | 204 | ``` 205 | gulp 206 | ``` 207 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jinja_api", 3 | "authors": [ 4 | "bthornto " 5 | ], 6 | "description": "", 7 | "main": "", 8 | "license": "MIT", 9 | "homepage": "", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "codemirror": "^5.32.0", 19 | "angular": "^1.6.7", 20 | "angular-animate": "^1.6.7", 21 | "angular-aria": "^1.6.7", 22 | "angular-messages": "^1.6.7", 23 | "angular-route": "^1.6.7", 24 | "Split.js": "^1.3.5", 25 | "angular-ui-codemirror": "^0.3.0", 26 | "ng-split": "^0.2.0", 27 | "angular-material": "^1.1.5", 28 | "angular-cookies": "^1.6.7", 29 | "angular-local-storage": "^0.7.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /gcp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cidrblock/td4a:2.0.3 2 | EXPOSE 5000 3 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | uglify = require('gulp-uglify'), 3 | jshint = require('gulp-jshint'), 4 | concat = require('gulp-concat'), 5 | notify = require('gulp-notify'), 6 | rename = require('gulp-rename'), 7 | clean = require('gulp-clean'); 8 | 9 | var jsDest = 'td4a/static/jsTemp'; 10 | var cssDest = 'td4a/static/css'; 11 | 12 | gulp.task('default', function() { 13 | }); 14 | 15 | gulp.task('codemirror', function(done) { 16 | gulp.src("bower_components/codemirror/lib/codemirror.js") 17 | .pipe(gulp.dest(jsDest)); 18 | gulp.src("bower_components/codemirror/mode/jinja2/jinja2.js") 19 | .pipe(gulp.dest(jsDest)); 20 | gulp.src("bower_components/codemirror/mode/yaml/yaml.js") 21 | .pipe(gulp.dest(jsDest)); 22 | gulp.src("bower_components/codemirror/addon/dialog/dialog.js") 23 | .pipe(gulp.dest(jsDest)); 24 | gulp.src("bower_components/codemirror/addon/search/searchcursor.js") 25 | .pipe(gulp.dest(jsDest)); 26 | gulp.src("bower_components/codemirror/addon/search/search.js") 27 | .pipe(gulp.dest(jsDest)); 28 | gulp.src("bower_components/codemirror/addon/search/matchesonscrollbar.js") 29 | .pipe(gulp.dest(jsDest)); 30 | gulp.src("bower_components/codemirror/addon/scroll/annotatescrollbar.js") 31 | .pipe(gulp.dest(jsDest)); 32 | gulp.src("bower_components/codemirror/addon/search/jump-to-line.js") 33 | .pipe(gulp.dest(jsDest)); 34 | gulp.src("bower_components/codemirror/lib/codemirror.css") 35 | .pipe(gulp.dest(cssDest)); 36 | gulp.src("bower_components/codemirror/addon/dialog/dialog.css") 37 | .pipe(gulp.dest(cssDest)); 38 | gulp.src("bower_components/codemirror/theme/material.css") 39 | .pipe(gulp.dest(cssDest)); 40 | done(); 41 | }); 42 | 43 | gulp.task('angular', function(done) { 44 | gulp.src("bower_components/angular/angular.js") 45 | .pipe(gulp.dest(jsDest)); 46 | gulp.src("bower_components/angular-animate/angular-animate.js") 47 | .pipe(gulp.dest(jsDest)); 48 | gulp.src("bower_components/angular-aria/angular-aria.js") 49 | .pipe(gulp.dest(jsDest)); 50 | gulp.src("bower_components/angular-messages/angular-messages.js") 51 | .pipe(gulp.dest(jsDest)); 52 | gulp.src("bower_components/angular-route/angular-route.js") 53 | .pipe(gulp.dest(jsDest)); 54 | gulp.src("bower_components/angular-cookies/angular-cookies.js") 55 | .pipe(gulp.dest(jsDest)); 56 | gulp.src("bower_components/angular-local-storage/dist/angular-local-storage.js") 57 | .pipe(gulp.dest(jsDest)); 58 | done(); 59 | }); 60 | 61 | gulp.task('angular-material', function(done) { 62 | gulp.src("bower_components/angular-material/angular-material.js") 63 | .pipe(gulp.dest(jsDest)); 64 | gulp.src("bower_components/angular-material/angular-material.css") 65 | .pipe(gulp.dest(cssDest)); 66 | done(); 67 | }); 68 | 69 | gulp.task('split', function(done) { 70 | gulp.src("bower_components/Split.js/split.js") 71 | .pipe(gulp.dest(jsDest)); 72 | done(); 73 | }); 74 | 75 | gulp.task('ui-codemirror', function(done) { 76 | gulp.src("bower_components/angular-ui-codemirror/ui-codemirror.js") 77 | .pipe(gulp.dest(jsDest)); 78 | done(); 79 | }); 80 | 81 | gulp.task('ng-split', function(done) { 82 | gulp.src("bower_components/ng-split/dist/ng-split.js") 83 | .pipe(gulp.dest(jsDest)); 84 | done(); 85 | }); 86 | 87 | gulp.task('scripts', function(done) { 88 | gulp.src([ 89 | 'td4a/static/jsTemp/angular.js', 90 | 'td4a/static/jsTemp/codemirror.js', 91 | 'td4a/static/jsTemp/*.js' 92 | ]) 93 | .pipe(concat('main.js')) 94 | .pipe(gulp.dest('dist')) 95 | .pipe(rename('main.min.js')) 96 | .pipe(uglify()) 97 | .pipe(gulp.dest('td4a/static/js')) 98 | .pipe(notify({ message: 'Scripts task complete' })); 99 | done(); 100 | }); 101 | 102 | gulp.task('clean', function (done) { 103 | gulp.src(['td4a/static/jsTemp', 104 | 'dist' 105 | ], {read: false}) 106 | .pipe(clean({force: true})); 107 | done(); 108 | }); 109 | 110 | gulp.task('default', gulp.series('codemirror', 'angular', 'angular-material', 'split', 'ng-split', 'ui-codemirror', 'scripts')) 111 | -------------------------------------------------------------------------------- /my_ansible_inventory/group_vars/all/vars.yml: -------------------------------------------------------------------------------- 1 | application_name: td4a 2 | -------------------------------------------------------------------------------- /my_ansible_inventory/group_vars/routers/vars.yml: -------------------------------------------------------------------------------- 1 | manufacturer: quagga 2 | -------------------------------------------------------------------------------- /my_ansible_inventory/group_vars/servers/vars.yml: -------------------------------------------------------------------------------- 1 | manufacturer: whitebox 2 | -------------------------------------------------------------------------------- /my_ansible_inventory/host_vars/router00/vars.yml: -------------------------------------------------------------------------------- 1 | ansible_ssh_pass: "{{ lookup('env', 'ANSIBLE_PASSWORD') }}" 2 | 3 | interfaces: 4 | Ethernet4/10: 5 | description: siteassw100-g0/1-siteasrt001-eth4/10 6 | name: Ethernet4/10 7 | shutdown: 8 | negate: true 9 | switchport: 10 | mode: 11 | - trunk 12 | switchport: true 13 | Ethernet4/11: 14 | channel_group: 15 | id: 31 16 | mode: active 17 | description: unity1interface1 18 | mtu: 9216 19 | name: Ethernet4/11 20 | shutdown: 21 | negate: true 22 | switchport: 23 | mode: 24 | - trunk 25 | switchport: true 26 | trunk: 27 | allowed_vlans: 28 | vlans: 3605,3607 29 | native_vlan: 5 30 | -------------------------------------------------------------------------------- /my_ansible_inventory/host_vars/server00/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for ansible-apache2 3 | 4 | # Defines if Apache2 should be configured 5 | apache2_config: false 6 | 7 | # Defines if php.ini should be configured for Apache2 8 | apache2_config_php: false 9 | 10 | # Defines if Apache2 virtual hosts should be configured 11 | apache2_config_virtual_hosts: true 12 | 13 | # Defines Apache2 default listen port 14 | apache2_default_port: 80 15 | 16 | # Defines if php-sqlite should be installed 17 | apache2_install_php_sqlite: false 18 | 19 | # Defines if php should be installed 20 | apache2_install_php: false 21 | 22 | apache2_php_max_execution_time: 30 23 | 24 | apache2_php_max_input_time: 60 25 | 26 | # Defines max memory for Apache php 27 | # default is 128M 28 | apache2_php_max_memory: 128M 29 | 30 | apache2_php_post_max_size: 8M 31 | 32 | apache2_php_timezone: "UTC" 33 | 34 | apache2_php_upload_max_filesize: 2M 35 | 36 | apache2_server_admin: webmaster@localhost 37 | 38 | # Define Apache2 virtual hosts 39 | apache2_virtual_hosts: 40 | - documentroot: '/var/www/example.com' 41 | default_site: false 42 | port: 80 43 | serveradmin: '{{ apache2_server_admin }}' 44 | servername: 'www.example.com' 45 | - documentroot: '/var/www/example.org' 46 | default_site: false 47 | port: 80 48 | serveradmin: '{{ apache2_server_admin }}' 49 | servername: 'www.example.org' 50 | - documentroot: '/var/www/html' 51 | default_site: true 52 | port: 80 53 | serveradmin: '{{ apache2_server_admin }}' 54 | servername: '' 55 | 56 | apache2_web_root: /var/www/html 57 | apache2_log_dir: /var/log/apache 58 | 59 | testVar: testing 60 | 61 | anotherTestVar: "{{ testVar }}" 62 | -------------------------------------------------------------------------------- /my_ansible_inventory/host_vars/test00/vars.yml: -------------------------------------------------------------------------------- 1 | a: "{{ b }}" 2 | b: 3 | c: this 4 | d: that 5 | 6 | foo: "{{ bar }}" 7 | bar: 8 | - 1 9 | - 2 10 | - 3 11 | 12 | ansible_ssh_pass: "{{ lookup('env', 'FOO') }}" 13 | cisco_xr_cli: 14 | username: "{{ ansible_ssh_user }}" 15 | 16 | 17 | y: "{{ z }}" 18 | z: 19 | aa: 20 | bb: 21 | cc: 22 | dd: dd 23 | 24 | f: "{{ bar + bar }}" 25 | -------------------------------------------------------------------------------- /my_ansible_inventory/inventory.yml: -------------------------------------------------------------------------------- 1 | router[00:03] 2 | server[00:03] 3 | test[00:03] 4 | 5 | [routers] 6 | router[00:03] 7 | 8 | [servers] 9 | server[00:03] 10 | -------------------------------------------------------------------------------- /my_filter_plugins/mask_convert.py: -------------------------------------------------------------------------------- 1 | from netaddr import IPNetwork 2 | 3 | def convert(network, netmask): 4 | entry = IPNetwork('%s/%s' % (network, netmask)) 5 | answer = {} 6 | answer['slashbits'] = '/%s' % getattr(entry, 'prefixlen') 7 | answer['bits'] = '%s' % getattr(entry, 'prefixlen') 8 | answer['hostmask'] = str(entry.hostmask) 9 | answer['netmask'] = str(entry.netmask) 10 | answer['network'] = network 11 | answer['net_netmask'] = '%s/%s' % (network, answer['netmask']) 12 | answer['net_bits'] = '%s/%s' % (network, answer['bits']) 13 | answer['net_hostmask'] = '%s/%s' % (network, answer['hostmask']) 14 | return answer 15 | 16 | class FilterModule(object): 17 | def filters(self): 18 | return { 19 | 'convert': convert 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jinja_api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "gulpfile.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "del": "^3.0.0", 9 | "gulp": "github:gulpjs/gulp#4.0", 10 | "gulp-clean": "^0.3.2", 11 | "gulp-concat": "^2.6.1", 12 | "gulp-jshint": "^2.0.4", 13 | "gulp-notify": "^3.0.0", 14 | "gulp-rename": "^1.2.2", 15 | "gulp-sass": "^3.1.0", 16 | "gulp-uglify": "^3.0.0", 17 | "jshint": "^2.9.5" 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "author": "", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ansible==2.9.12 2 | Flask==1.1.2 3 | netaddr==0.8.0 4 | Twisted==20.3.0 5 | requests==2.24.0 6 | ruamel.yaml==0.16.10 7 | genson==1.2.1 8 | jsonschema==3.2.0 -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cidrblock/td4a/54f99283772220fab9e7191b32f89c589a6db127/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='td4a', 4 | version='2.0.3', 5 | description='A browser based jinja template renderer', 6 | url='http://github.com/cidrblock/td4a', 7 | author='Bradley A. Thornton', 8 | author_email='brad@thethorntons.net', 9 | license='MIT', 10 | include_package_data=True, 11 | packages=[ 12 | 'td4a' 13 | ], 14 | scripts=['td4a-server'], 15 | install_requires=[ 16 | 'ansible==2.9.12', 17 | 'Flask==1.1.2', 18 | 'netaddr==0.8.0', 19 | 'Twisted==20.3.0', 20 | 'requests==2.24.0', 21 | 'ruamel.yaml==0.16.10', 22 | 'genson==1.2.1', 23 | 'jsonschema==3.2.0' 24 | ], 25 | zip_safe=False) 26 | -------------------------------------------------------------------------------- /td4a-server: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from td4a import app 3 | from td4a.models.filters import filters_load 4 | from td4a.models.inventory import inventory_load 5 | from argparse import ArgumentParser, RawTextHelpFormatter 6 | import os 7 | from twisted.internet import reactor 8 | from twisted.web.server import Site 9 | from twisted.web.wsgi import WSGIResource 10 | 11 | def parse_args(): 12 | """ parse the cli args and add environ 13 | """ 14 | parser = ArgumentParser(description='', 15 | formatter_class=RawTextHelpFormatter) 16 | parser.add_argument('-f', action="store", dest="custom_filters", 17 | required=False, 18 | help="A folder containing custom filters.") 19 | parser.add_argument('-i', action="append", dest="inventory_source", 20 | required=False, 21 | help="A folder containing the inventory.") 22 | parser.add_argument('-v', action="store", dest="vault_secret", 23 | required=False, 24 | help="A vault secret.") 25 | parser.add_argument('-m', action="store", dest="ui_mode", 26 | choices=['jinja', 'schema'], 27 | required=False, 28 | default='jinja', 29 | help="The mode for the UI. (default: jinja)") 30 | args = parser.parse_args() 31 | args.username = os.environ.get('COUCH_USERNAME', False) 32 | args.password = os.environ.get('COUCH_PASSWORD', False) 33 | args.url = os.environ.get('COUCH_URL', False) 34 | return args 35 | 36 | def main(): 37 | print("Loading...") 38 | app.args = parse_args() 39 | app.filters = filters_load(app.args.custom_filters) 40 | if app.args.inventory_source: 41 | app.inventory = inventory_load(inventory_sources=app.args.inventory_source, 42 | vault_secret=app.args.vault_secret) 43 | reactor_args = {} 44 | app.debug = False 45 | def wsgi(): 46 | resource = WSGIResource(reactor, reactor.getThreadPool(), app) 47 | site = Site(resource) 48 | reactor.listenTCP(5000, site) 49 | reactor.run(**reactor_args) 50 | if app.debug: 51 | reactor_args['installSignalHandlers'] = 0 52 | import werkzeug.serving 53 | wsgi = werkzeug.serving.run_with_reloader(wsgi) 54 | print("Ready.") 55 | wsgi() 56 | 57 | if __name__ == '__main__': 58 | main() 59 | -------------------------------------------------------------------------------- /td4a/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from td4a.controllers.config import api_config 3 | from td4a.controllers.hosts import api_hosts 4 | from td4a.controllers.inventory import api_inventory 5 | from td4a.controllers.link import api_link 6 | from td4a.controllers.render import api_render 7 | from td4a.controllers.retrieve import api_retrieve 8 | from td4a.controllers.schema import api_schema 9 | from td4a.controllers.validate import api_validate 10 | 11 | 12 | 13 | 14 | app = Flask(__name__, static_url_path='') # pylint: disable=invalid-name 15 | app.register_blueprint(api_config) 16 | app.register_blueprint(api_hosts) 17 | app.register_blueprint(api_inventory) 18 | app.register_blueprint(api_link) 19 | app.register_blueprint(api_render) 20 | app.register_blueprint(api_retrieve) 21 | app.register_blueprint(api_schema) 22 | app.register_blueprint(api_validate) 23 | 24 | @app.route('/') 25 | def root(): 26 | """ root path 27 | """ 28 | return app.send_static_file('index.html') 29 | -------------------------------------------------------------------------------- /td4a/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cidrblock/td4a/54f99283772220fab9e7191b32f89c589a6db127/td4a/controllers/__init__.py -------------------------------------------------------------------------------- /td4a/controllers/config.py: -------------------------------------------------------------------------------- 1 | """ /config 2 | """ 3 | from flask import jsonify, Blueprint 4 | from flask import current_app as app 5 | 6 | api_config = Blueprint('api_config', __name__) # pylint: disable=invalid-name 7 | 8 | @api_config.route('/config', methods=['GET']) 9 | def config(): 10 | """ provide some config options to the UI 11 | """ 12 | if app.args.ui_mode == "jinja": 13 | ui_config = { 14 | "p1": { 15 | "options": { 16 | "lineNumbers": True, 17 | "theme":"material", 18 | "lineWrapping" : True, 19 | "mode": "yaml", 20 | "indentUnit": 2, 21 | "tabSize": 2 22 | }, 23 | "title": "DATA", 24 | "inventory": bool(app.args.inventory_source), 25 | "b1": { 26 | "icon": None, 27 | "show": False, 28 | "text": None, 29 | "url": None 30 | } 31 | }, 32 | "p2": { 33 | "options": { 34 | "lineNumbers": True, 35 | "theme": "material", 36 | "lineWrapping" : True, 37 | "mode": "jinja2" 38 | }, 39 | "title": "RENDER", 40 | "b1": { 41 | "icon": "create", 42 | "show": True, 43 | "text": "Render", 44 | "url": "/render" 45 | } 46 | }, 47 | "p3": { 48 | "options": { 49 | "lineNumbers": True, 50 | "theme": "material", 51 | "lineWrapping" : True, 52 | "mode": 'text' 53 | }, 54 | "title": "RESULT", 55 | "b1": { 56 | "icon": "link", 57 | "show": bool(app.args.url), 58 | "text": "link" 59 | } 60 | } 61 | } 62 | elif app.args.ui_mode == "schema": 63 | ui_config = { 64 | "p1": { 65 | "options": { 66 | "lineNumbers": True, 67 | "theme":"material", 68 | "lineWrapping" : True, 69 | "mode": "yaml", 70 | "indentUnit": 2, 71 | "tabSize": 2 72 | }, 73 | "title": "DATA", 74 | "inventory": bool(app.args.inventory_source), 75 | "b1": { 76 | "icon": "create", 77 | "show": True, 78 | "text": "schema", 79 | "url": "/schema" 80 | } 81 | }, 82 | "p2": { 83 | "options": { 84 | "lineNumbers": True, 85 | "theme": "material", 86 | "lineWrapping" : True, 87 | "mode": "yaml" 88 | }, 89 | "title": "SCHEMA", 90 | "b1": { 91 | "icon": "check", 92 | "show": True, 93 | "text": "Validate", 94 | "url": "/validate" 95 | } 96 | }, 97 | "p3": { 98 | "options": { 99 | "lineNumbers": True, 100 | "theme": "material", 101 | "lineWrapping" : True, 102 | "mode": "yaml" 103 | }, 104 | "title": "VALIDATION SUCCESS/ERRORS", 105 | "b1": { 106 | "icon": "link", 107 | "show": bool(app.args.url), 108 | "text": "link" 109 | } 110 | } 111 | } 112 | return jsonify(ui_config) 113 | -------------------------------------------------------------------------------- /td4a/controllers/hosts.py: -------------------------------------------------------------------------------- 1 | """ /hosts 2 | """ 3 | from flask import jsonify, Blueprint 4 | from flask import current_app as app 5 | 6 | api_hosts = Blueprint('api_hosts', __name__) # pylint: disable=invalid-name 7 | 8 | @api_hosts.route('/hosts', methods=['GET']) 9 | def hosts(): 10 | """ check to see if the link button should be enabled 11 | """ 12 | devices = app.inventory.keys() 13 | return jsonify({"hosts": sorted(devices)}) 14 | -------------------------------------------------------------------------------- /td4a/controllers/inventory.py: -------------------------------------------------------------------------------- 1 | """ /inventory 2 | """ 3 | from flask import request, jsonify, Blueprint 4 | from flask import current_app as app 5 | from td4a.models.exception_handler import ExceptionHandler, HandledException 6 | from td4a.models.td4ayaml import Td4aYaml 7 | import json 8 | import collections 9 | 10 | api_inventory = Blueprint('api_inventory', __name__) # pylint: disable=invalid-name 11 | 12 | @api_inventory.route('/inventory', methods=['GET']) 13 | def rest_inventory(): 14 | """ return inventory for host 15 | """ 16 | yaml = Td4aYaml() 17 | inventory = app.inventory.get(request.args.get('host'), "") 18 | data = json.loads(json.dumps(inventory)) 19 | response_text = '' 20 | for section in sorted(data.keys()): 21 | response_text += yaml.dump({section: data[section]}) 22 | response = {"p1": response_text} 23 | return jsonify(response) 24 | -------------------------------------------------------------------------------- /td4a/controllers/link.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify, Blueprint 2 | from flask import current_app as app 3 | from td4a.models.exception_handler import ExceptionHandler, HandledException 4 | import requests 5 | 6 | api_link = Blueprint('api_link', __name__) 7 | 8 | @ExceptionHandler 9 | def link(payload, args, typ): 10 | """ store a doc in the db 11 | """ 12 | _ = typ 13 | auth = (args.username, args.password) 14 | url = args.url 15 | response = requests.post("%s" % url, json=payload, auth=auth) 16 | return {"id": response.json()['id']} 17 | 18 | 19 | @api_link.route('/link', methods=['POST']) 20 | def rest_link(): 21 | """ Save the documents in a couchdb and returns an id 22 | """ 23 | try: 24 | response = link(payload=request.json, args=app.args, typ="link") 25 | return jsonify(response) 26 | except HandledException as error: 27 | return jsonify(error.json()) 28 | -------------------------------------------------------------------------------- /td4a/controllers/render.py: -------------------------------------------------------------------------------- 1 | """ /render 2 | """ 3 | from flask import request, jsonify, Blueprint 4 | from flask import current_app as app 5 | from jinja2 import meta, Environment, StrictUndefined, Undefined 6 | from td4a.models.exception_handler import ExceptionHandler, HandledException 7 | from td4a.models.td4ayaml import Td4aYaml 8 | from ruamel.yaml import YAML 9 | import re 10 | 11 | api_render = Blueprint('api_render', __name__) # pylint: disable=invalid-name 12 | 13 | @ExceptionHandler 14 | def jinja_unresolved(template, typ): 15 | """ Check a jinja template for any unresolved vars 16 | """ 17 | _ = typ 18 | env = Environment() 19 | env.trim_blocks = True 20 | unresolved = meta.find_undeclared_variables(env.parse(template)) 21 | return unresolved 22 | 23 | def lookup(*args, **kwargs): 24 | return "unsupported" 25 | 26 | @ExceptionHandler 27 | def jinja_render(data, template, filters, typ): 28 | """ Render a jinja template 29 | """ 30 | _ = typ 31 | if typ == 'p1': 32 | env = Environment(undefined=Undefined) 33 | else: 34 | env = Environment(undefined=StrictUndefined) 35 | env.trim_blocks = True 36 | for entry in filters: 37 | env.filters[entry[0]] = entry[1] 38 | env.globals.update(lookup=lookup) 39 | result = env.from_string(template).render(data) 40 | return result 41 | 42 | @ExceptionHandler 43 | def yaml_parse(string, typ): 44 | """ load yaml from string 45 | """ 46 | _ = typ 47 | yaml = YAML() 48 | yaml.load(string) 49 | 50 | @ExceptionHandler 51 | def render(payload, filters, typ): 52 | """ Given the payload, render the result 53 | """ 54 | _ = typ 55 | try: 56 | loader = YAML(typ='unsafe') 57 | result = None 58 | if payload['p1'] and payload['p2']: 59 | # check for error in data 60 | yaml_parse(string=payload['p1'], typ="p1") 61 | # swap '{{ }}' for "{{ }}" 62 | dq_jinja = re.compile(r"'\{\{([^\{\}]+)\}\}'") 63 | payload['p1'] = dq_jinja.sub('"{{\\1}}"', payload['p1']) 64 | # remove the quotes around dicts put into jinja 65 | expose_dicts = re.compile(r'"\{([^\{].*)\}"') 66 | # remove the quotes aournd lists put into jinja 67 | expose_lists1 = re.compile(r'"\[(.*)\]"') 68 | # change '{{ }}' to "{{ }}" 69 | dq_jinja = re.compile(r"'\{\{([^\{\}]+)\}\}'") 70 | 71 | raw_data = None 72 | after_jinja = payload['p1'] 73 | tvars = loader.load(payload['p1']) 74 | 75 | if jinja_unresolved(template=after_jinja, typ="p1"): 76 | while after_jinja != raw_data: 77 | raw_data = after_jinja 78 | after_jinja = jinja_render(data=tvars, 79 | template=raw_data, 80 | filters=filters, 81 | typ="p1") 82 | yaml_ready = expose_dicts.sub("{\\1}", after_jinja) 83 | yaml_ready = expose_lists1.sub("[\\1]", yaml_ready) 84 | yaml_ready = dq_jinja.sub('"{{\\1}}"', yaml_ready) 85 | tvars = loader.load(yaml_ready) 86 | result = jinja_render(data=tvars, 87 | template=payload['p2'], 88 | filters=filters, 89 | typ="p2") 90 | return {"p3": result} 91 | except HandledException as error: 92 | return error.json() 93 | 94 | @api_render.route('/render', methods=['POST']) 95 | def rest_render(): 96 | """ render path 97 | """ 98 | try: 99 | print("Checking and parsing data...") 100 | response = render(payload=request.json, filters=app.filters, typ="page") 101 | print("Done.") 102 | return jsonify(response) 103 | except HandledException as error: 104 | return jsonify(error.json()) 105 | -------------------------------------------------------------------------------- /td4a/controllers/retrieve.py: -------------------------------------------------------------------------------- 1 | """ /retrieve 2 | """ 3 | from flask import request, jsonify, Blueprint 4 | from flask import current_app as app 5 | from td4a.models.exception_handler import ExceptionHandler, HandledException 6 | import requests 7 | 8 | api_retrieve = Blueprint('api_retrieve', __name__) # pylint: disable=invalid-name 9 | 10 | @ExceptionHandler 11 | def retrieve(doc_id, typ): 12 | """ get a doc from the db 13 | """ 14 | _ = typ 15 | auth = (app.args.username, app.args.password) 16 | url = app.args.url 17 | cdoc = requests.get("%s/%s?include_docs=true" % (url, doc_id), auth=auth) 18 | doc = cdoc.json() 19 | if cdoc.status_code == 200: 20 | response = {"panels": doc['panels'], "config": doc['config']} 21 | else: 22 | response = {"handled_error": { 23 | "in": "document retrieval", 24 | "title": "Message: Issue loading saved document.", 25 | "line_number": None, 26 | "details": "Details: %s" % doc['error'], 27 | "raw_error": "%s" % cdoc.text 28 | } 29 | } 30 | return response 31 | 32 | @api_retrieve.route('/retrieve', methods=['GET']) 33 | def rest_retrieve(): 34 | """ return a doc from the couchdb 35 | """ 36 | try: 37 | response = retrieve(doc_id=request.args.get('id'), typ="link") 38 | return jsonify(response) 39 | except HandledException as error: 40 | return jsonify(error.json()) 41 | -------------------------------------------------------------------------------- /td4a/controllers/schema.py: -------------------------------------------------------------------------------- 1 | """ /retrieve 2 | """ 3 | import json 4 | from flask import current_app as app 5 | from flask import request, jsonify, Blueprint 6 | from td4a.models.exception_handler import ExceptionHandler, HandledException 7 | from td4a.models.sort_commented_map import sort_commented_map 8 | from td4a.models.td4ayaml import Td4aYaml 9 | import genson 10 | 11 | api_schema = Blueprint('api_schema', __name__) # pylint: disable=invalid-name 12 | 13 | @ExceptionHandler 14 | def schema(data, typ): 15 | """ Build schema from data 16 | """ 17 | _ = typ 18 | yaml = Td4aYaml() 19 | obj_data = yaml.load(data['p1']) 20 | json_schema = genson.Schema() 21 | json_schema.add_object(obj_data) 22 | schema_dict = json_schema.to_dict() 23 | schema_yaml = yaml.load(yaml.dump(schema_dict)) 24 | sorted_schema_yaml = sort_commented_map(commented_map=schema_yaml) 25 | sorted_schema_string = yaml.dump(sorted_schema_yaml) 26 | return sorted_schema_string 27 | 28 | @api_schema.route('/schema', methods=['POST']) 29 | def rest_schema(): 30 | """ Build a schema for data 31 | """ 32 | try: 33 | payload = request.json 34 | response = schema(data=payload, typ="data") 35 | return jsonify({"p2": response}) 36 | except HandledException as error: 37 | return jsonify(error.json()) 38 | -------------------------------------------------------------------------------- /td4a/controllers/validate.py: -------------------------------------------------------------------------------- 1 | """ /retrieve 2 | """ 3 | import json 4 | from flask import current_app as app 5 | from flask import request, jsonify, Blueprint 6 | from td4a.models.exception_handler import ExceptionHandler, HandledException 7 | from td4a.models.td4ayaml import Td4aYaml 8 | from jsonschema import validate 9 | from jsonschema import Draft4Validator, FormatChecker 10 | from jsonschema.exceptions import UnknownType 11 | 12 | 13 | api_validate = Blueprint('api_validate', __name__) # pylint: disable=invalid-name 14 | 15 | @ExceptionHandler 16 | def parse_yaml(yamul, typ): 17 | _ = typ 18 | yaml = Td4aYaml() 19 | obj = yaml.load(yamul) 20 | return obj 21 | 22 | def validation(payload): 23 | """ Validate schema from data 24 | """ 25 | try: 26 | yaml_safe = Td4aYaml(typ='safe') 27 | yaml = Td4aYaml() 28 | data = yaml_safe.load(payload['p1']) 29 | schema = yaml_safe.load(payload['p2']) 30 | errors = [] 31 | v = Draft4Validator(schema, format_checker=FormatChecker()) 32 | for error in sorted(v.iter_errors(data)): 33 | errors.append(error.message) 34 | if errors: 35 | return {"p3": yaml.dump({"messages":errors})} 36 | return {"p3": yaml.dump({"messages":["validation passed"]})} 37 | except UnknownType as error: 38 | error_message = str(error) 39 | lines = error_message.splitlines() 40 | message = [x for x in lines if x.startswith('Unknown type')] 41 | return {"p3": yaml.dump({"messages":message})} 42 | 43 | @api_validate.route('/validate', methods=['POST']) 44 | def rest_validate(): 45 | """ Build a schema for data 46 | """ 47 | try: 48 | payload = request.json 49 | data = parse_yaml(yamul=payload['p1'], typ='p1') 50 | schema = parse_yaml(yamul=payload['p2'], typ='p2') 51 | response = validation(payload=payload) 52 | return jsonify(response) 53 | except HandledException as error: 54 | return jsonify(error.json()) 55 | -------------------------------------------------------------------------------- /td4a/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cidrblock/td4a/54f99283772220fab9e7191b32f89c589a6db127/td4a/models/__init__.py -------------------------------------------------------------------------------- /td4a/models/exception_handler.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import traceback 4 | 5 | class HandledException(Exception): 6 | def __init__(self, value): 7 | self.value = value 8 | Exception.__init__(self) 9 | 10 | def __str__(self): 11 | return repr(self.value) 12 | 13 | def json(self): 14 | return self.value['json'] 15 | 16 | class ExceptionHandler(object): 17 | """ Handle the expected errors.""" 18 | def __init__(self, function): 19 | self.function = function 20 | self.error = self.typ = self.tback = self.exc_type = self.exc_value = self.exc_traceback = None 21 | self.error_map = { 22 | "requests.exceptions": self.requests_error, 23 | "ruamel.yaml.parser.ParserError": self.parser_error, 24 | "ruamel.yaml.constructor.ConstructorError": self.constructor_error, 25 | "ruamel.yaml.constructor.DuplicateKeyError": self.duplicate_key_error, 26 | "ruamel.yaml.scanner.ScannerError": self.scanner_error, 27 | "jinja2.exceptions": self.jinja_error, 28 | "TypeError": self.type_error 29 | } 30 | 31 | def __call__(self, *args, **kwargs): 32 | try: 33 | return self.function(*args, **kwargs) 34 | except Exception as error: 35 | self.error = error 36 | self.exc_type, self.exc_value, self.exc_traceback = sys.exc_info() 37 | self.tback = traceback.extract_tb(self.exc_traceback) 38 | 39 | error_module = getattr(error, '__module__', None) 40 | if error_module: 41 | full_error = "%s.%s" % (error.__module__, self.exc_type.__name__) 42 | else: 43 | full_error = self.exc_type.__name__ 44 | handler = self.error_map.get(full_error, 45 | self.error_map.get(error_module, 46 | self.unhandled)) 47 | self.typ = kwargs.get('typ') 48 | message = handler() 49 | raise HandledException({"json": message}) 50 | 51 | def error_response(self, message, line_number): 52 | error_payload = {"handled_error": { 53 | "in": self.typ, 54 | "title": "Message: Issue found loading %s." % self.typ, 55 | "line_number": line_number, 56 | "details": "Details: %s" % message, 57 | "raw_error": "%s\n%s" % (self.exc_type, self.exc_value) 58 | } 59 | } 60 | return error_payload 61 | 62 | def constructor_error(self): 63 | line_number = self.error.problem_mark.line+1 64 | message = next(x for x in str(self.error).splitlines() 65 | if x.startswith('found')) 66 | return self.error_response(message=message, 67 | line_number=line_number) 68 | 69 | def duplicate_key_error(self): 70 | line_number = self.error.problem_mark.line+1 71 | message = next(x for x in str(self.error).splitlines() 72 | if x.startswith('found')).split('with')[0] 73 | return self.error_response(message=message, 74 | line_number=line_number) 75 | 76 | def jinja_error(self): 77 | message = str(self.error).replace("'ruamel.yaml.comments.CommentedMap object'", 'Object') 78 | line_numbers = [x for x in self.tback if re.search('^<.*>$', x[0])] 79 | if line_numbers: 80 | line_number = line_numbers[0][1] 81 | else: 82 | line_number = 'unknown' 83 | return self.error_response(message=message, 84 | line_number=line_number) 85 | 86 | def parser_error(self): 87 | line_number = self.error.problem_mark.line + 1 88 | messages = [x for x in str(self.error).splitlines() if x.startswith('expected')] 89 | if messages: 90 | message = messages[0] 91 | else: 92 | message = str(self.error) 93 | return self.error_response(message=message, 94 | line_number=line_number) 95 | 96 | def scanner_error(self): 97 | line_number = self.error.problem_mark.line + 1 98 | message = str(self.error).splitlines()[0] 99 | return self.error_response(message=message, 100 | line_number=line_number) 101 | 102 | def requests_error(self): 103 | message = "DB connection problems, see the browser developer tools for the full error." 104 | return self.error_response(message=message, 105 | line_number=None) 106 | 107 | def type_error(self): 108 | message = str(self.error) 109 | line_numbers = [x for x in self.tback if re.search('^<.*>$', x[0])] 110 | if line_numbers: 111 | line_number = line_numbers[0][1] 112 | else: 113 | line_number = 'unknown' 114 | return self.error_response(message=message, 115 | line_number=line_number) 116 | 117 | def unhandled(self): 118 | print(self.exc_type, self.exc_value, self.exc_traceback, self.tback, self.error) 119 | line_numbers = [x for x in self.tback if re.search('^<.*>$', x[0])] 120 | if line_numbers: 121 | line_number = line_numbers[0][1] 122 | else: 123 | line_number = None 124 | message = "Please see the console for details. %s" % str(self.error) 125 | return self.error_response(message=message, 126 | line_number=line_number) 127 | -------------------------------------------------------------------------------- /td4a/models/filters.py: -------------------------------------------------------------------------------- 1 | """ filter loader helper 2 | """ 3 | import os 4 | import sys 5 | import importlib 6 | import ansible.plugins.filter as apf 7 | 8 | def load_dir(directory): 9 | """ Load jinja filters in ansible format 10 | """ 11 | filter_list = [] 12 | sys.path.append(directory) 13 | for entry in os.listdir(directory): 14 | if entry != '__init__.py' and entry.split('.')[-1] == 'py': 15 | filters = importlib.import_module(entry[:-3]).FilterModule().filters() 16 | for key, value in filters.items(): 17 | filter_list.append((key, value)) 18 | return filter_list 19 | 20 | def filters_load(custom_filters): 21 | """ load the filters 22 | """ 23 | filters = [] 24 | filters.extend(load_dir(os.path.dirname(apf.__file__))) 25 | if custom_filters: 26 | filters.extend(load_dir(custom_filters)) 27 | return filters 28 | -------------------------------------------------------------------------------- /td4a/models/inventory.py: -------------------------------------------------------------------------------- 1 | from ansible.parsing.dataloader import DataLoader 2 | from ansible.vars.manager import VariableManager 3 | from ansible.inventory.manager import InventoryManager 4 | from ansible.module_utils._text import to_bytes 5 | from ansible.parsing.vault import VaultSecret 6 | 7 | class TextVaultSecret(VaultSecret): 8 | '''A secret piece of text. ie, a password. Tracks text encoding. 9 | The text encoding of the text may not be the default text encoding so 10 | we keep track of the encoding so we encode it to the same bytes.''' 11 | 12 | def __init__(self, text, encoding=None, errors=None, _bytes=None): 13 | super(TextVaultSecret, self).__init__() 14 | self.text = text 15 | self.encoding = encoding or 'utf-8' 16 | self._bytes = _bytes 17 | self.errors = errors or 'strict' 18 | 19 | @property 20 | def bytes(self): 21 | '''The text encoded with encoding, unless we specifically set _bytes.''' 22 | return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors) 23 | 24 | def inventory_load(inventory_sources, vault_secret): 25 | """ Load the inventory 26 | """ 27 | loader = DataLoader() 28 | vault_secrets = [('default', TextVaultSecret(vault_secret))] 29 | loader.set_vault_secrets(vault_secrets) 30 | inventory = InventoryManager(loader=loader, sources=inventory_sources) 31 | result = {} 32 | for hostname in inventory.hosts: 33 | host = inventory.get_host(hostname) 34 | variable_manager = VariableManager(loader=loader, inventory=inventory) 35 | magic_vars = ['ansible_playbook_python', 'groups', 'group_names', 'inventory_dir', 36 | 'inventory_file', 'inventory_hostname', 'inventory_hostname_short', 37 | 'omit', 'playbook_dir'] 38 | all_vars = variable_manager.get_vars(host=host, include_hostvars=True) 39 | cleaned = ({k: v for (k, v) in all_vars.items() if k not in magic_vars}) 40 | result[hostname] = cleaned 41 | return result 42 | -------------------------------------------------------------------------------- /td4a/models/sort_commented_map.py: -------------------------------------------------------------------------------- 1 | from ruamel.yaml.comments import CommentedMap 2 | 3 | def sort_commented_map(commented_map): 4 | """ Sort a ruamel commented map 5 | 6 | Args: 7 | commented_map (CommentedMap): The cm to order 8 | 9 | Returns: 10 | CommentedMap: The sorted commented map 11 | 12 | """ 13 | cmap = CommentedMap() 14 | for key, value in sorted(commented_map.iteritems()): 15 | if isinstance(value, CommentedMap): 16 | cmap[key] = sort_commented_map(value) 17 | elif isinstance(value, list): 18 | for i in enumerate(value): 19 | if isinstance(value[i[0]], CommentedMap): 20 | value[i[0]] = sort_commented_map(value[i[0]]) 21 | cmap[key] = value 22 | else: 23 | cmap[key] = value 24 | return cmap 25 | -------------------------------------------------------------------------------- /td4a/models/td4ayaml.py: -------------------------------------------------------------------------------- 1 | from ruamel.yaml import YAML 2 | from ruamel.yaml.compat import StringIO 3 | 4 | class Td4aYaml(YAML): 5 | """ Build a string dumper for ruamel 6 | """ 7 | def dump(self, data, stream=None, **kw): 8 | """ dump as string 9 | """ 10 | inefficient = False 11 | if stream is None: 12 | inefficient = True 13 | stream = StringIO() 14 | YAML.dump(self, data, stream, **kw) 15 | if inefficient: 16 | return stream.getvalue() 17 | -------------------------------------------------------------------------------- /td4a/static/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: white; 4 | } 5 | .split { 6 | -webkit-box-sizing: border-box; 7 | -moz-box-sizing: border-box; 8 | box-sizing: border-box; 9 | 10 | overflow-y: auto; 11 | overflow-x: hidden; 12 | } 13 | 14 | .split p { 15 | padding: 20px; 16 | } 17 | 18 | .gutter { 19 | background-color: black; 20 | background-repeat: no-repeat; 21 | background-position: 50%; 22 | } 23 | 24 | .gutter.gutter-horizontal { 25 | width: 3px; 26 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 27 | cursor: ew-resize; 28 | } 29 | 30 | .gutter.gutter-vertical { 31 | height: 3px !important; 32 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFCAMAAABl/6zIAAAABlBMVEUAAADMzMzIT8AyAAAAAXRSTlMAQObYZgAAABRJREFUeAFjYGRkwIMJSeMHlBkOABP7AEGzSuPKAAAAAElFTkSuQmCC'); 33 | cursor: ns-resize; 34 | } 35 | 36 | .split.split-horizontal, .gutter.gutter-horizontal { 37 | height: 100%; 38 | float: left; 39 | } 40 | 41 | menubar { 42 | display: flex; 43 | justify-content: flex-end; 44 | flex-direction: row; 45 | align-items: center; 46 | /* width: 100%; */ 47 | height: 40px; 48 | padding-left: 5px; 49 | padding-right: 0px; 50 | } 51 | 52 | menubar menuitem:first-child { 53 | margin-right: auto; 54 | } 55 | 56 | .md-button.md-small { 57 | min-width: 1%; 58 | line-height: 20px; 59 | min-height: 1%; 60 | vertical-align: top; 61 | font-size: 10px; 62 | margin: 2px 2px; 63 | } 64 | .md-button.md-ok { 65 | background-color: darkgreen; 66 | color: white; 67 | } 68 | .md-button.md-ok:not([disabled]):hover { 69 | background-color: darkgreen; 70 | } 71 | 72 | md-autocomplete { 73 | min-width: 250px; 74 | } 75 | md-autocomplete input:not(.md-input) { 76 | background: black; 77 | color: white !important; 78 | } 79 | 80 | md-autocomplete md-autocomplete-wrap, .md-autocomplete-suggestions, .md-autocomplete-suggestions li { 81 | background: black !important; 82 | color: white !important; 83 | } 84 | 85 | .md-autocomplete-suggestions-container.md-default-theme li.selected, 86 | .md-autocomplete-suggestions-container li.selected, 87 | .md-autocomplete-suggestions-container.md-default-theme li:hover, 88 | .md-autocomplete-suggestions-container li:hover { 89 | background: darkgrey !important; 90 | color: black !important; 91 | } 92 | .md-autocomplete-suggestions-container.md-default-theme li .highlight, 93 | .md-autocomplete-suggestions-container li .highlight { 94 | color: rgb(63,81,181); 95 | } 96 | 97 | .column { 98 | height: 100%; 99 | display: flex; 100 | flex-direction: column; 101 | align-items: stretch; 102 | } 103 | .editor { 104 | height: 100%; 105 | display: flex; 106 | flex-direction: column; 107 | align-items: stretch; 108 | padding: 0em; 109 | background: #eee; 110 | } 111 | .CodeMirror { 112 | width: 100%; 113 | font-size: 10pt; 114 | } 115 | .editor .CodeMirror { 116 | flex: 1; 117 | } 118 | .error { background-color: rgba(255, 0, 0, 0.3) !important; } 119 | -------------------------------------------------------------------------------- /td4a/static/css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 4px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre { 17 | padding: 0 4px; /* Horizontal padding of content */ 18 | } 19 | 20 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 21 | background-color: white; /* The little square between H and V scrollbars */ 22 | } 23 | 24 | /* GUTTER */ 25 | 26 | .CodeMirror-gutters { 27 | border-right: 1px solid #ddd; 28 | background-color: #f7f7f7; 29 | white-space: nowrap; 30 | } 31 | .CodeMirror-linenumbers {} 32 | .CodeMirror-linenumber { 33 | padding: 0 3px 0 5px; 34 | min-width: 20px; 35 | text-align: right; 36 | color: #999; 37 | white-space: nowrap; 38 | } 39 | 40 | .CodeMirror-guttermarker { color: black; } 41 | .CodeMirror-guttermarker-subtle { color: #999; } 42 | 43 | /* CURSOR */ 44 | 45 | .CodeMirror-cursor { 46 | border-left: 1px solid black; 47 | border-right: none; 48 | width: 0; 49 | } 50 | /* Shown when moving in bi-directional text */ 51 | .CodeMirror div.CodeMirror-secondarycursor { 52 | border-left: 1px solid silver; 53 | } 54 | .cm-fat-cursor .CodeMirror-cursor { 55 | width: auto; 56 | border: 0 !important; 57 | background: #7e7; 58 | } 59 | .cm-fat-cursor div.CodeMirror-cursors { 60 | z-index: 1; 61 | } 62 | .cm-fat-cursor-mark { 63 | background-color: rgba(20, 255, 20, 0.5); 64 | -webkit-animation: blink 1.06s steps(1) infinite; 65 | -moz-animation: blink 1.06s steps(1) infinite; 66 | animation: blink 1.06s steps(1) infinite; 67 | } 68 | .cm-animate-fat-cursor { 69 | width: auto; 70 | border: 0; 71 | -webkit-animation: blink 1.06s steps(1) infinite; 72 | -moz-animation: blink 1.06s steps(1) infinite; 73 | animation: blink 1.06s steps(1) infinite; 74 | background-color: #7e7; 75 | } 76 | @-moz-keyframes blink { 77 | 0% {} 78 | 50% { background-color: transparent; } 79 | 100% {} 80 | } 81 | @-webkit-keyframes blink { 82 | 0% {} 83 | 50% { background-color: transparent; } 84 | 100% {} 85 | } 86 | @keyframes blink { 87 | 0% {} 88 | 50% { background-color: transparent; } 89 | 100% {} 90 | } 91 | 92 | /* Can style cursor different in overwrite (non-insert) mode */ 93 | .CodeMirror-overwrite .CodeMirror-cursor {} 94 | 95 | .cm-tab { display: inline-block; text-decoration: inherit; } 96 | 97 | .CodeMirror-rulers { 98 | position: absolute; 99 | left: 0; right: 0; top: -50px; bottom: -20px; 100 | overflow: hidden; 101 | } 102 | .CodeMirror-ruler { 103 | border-left: 1px solid #ccc; 104 | top: 0; bottom: 0; 105 | position: absolute; 106 | } 107 | 108 | /* DEFAULT THEME */ 109 | 110 | .cm-s-default .cm-header {color: blue;} 111 | .cm-s-default .cm-quote {color: #090;} 112 | .cm-negative {color: #d44;} 113 | .cm-positive {color: #292;} 114 | .cm-header, .cm-strong {font-weight: bold;} 115 | .cm-em {font-style: italic;} 116 | .cm-link {text-decoration: underline;} 117 | .cm-strikethrough {text-decoration: line-through;} 118 | 119 | .cm-s-default .cm-keyword {color: #708;} 120 | .cm-s-default .cm-atom {color: #219;} 121 | .cm-s-default .cm-number {color: #164;} 122 | .cm-s-default .cm-def {color: #00f;} 123 | .cm-s-default .cm-variable, 124 | .cm-s-default .cm-punctuation, 125 | .cm-s-default .cm-property, 126 | .cm-s-default .cm-operator {} 127 | .cm-s-default .cm-variable-2 {color: #05a;} 128 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 129 | .cm-s-default .cm-comment {color: #a50;} 130 | .cm-s-default .cm-string {color: #a11;} 131 | .cm-s-default .cm-string-2 {color: #f50;} 132 | .cm-s-default .cm-meta {color: #555;} 133 | .cm-s-default .cm-qualifier {color: #555;} 134 | .cm-s-default .cm-builtin {color: #30a;} 135 | .cm-s-default .cm-bracket {color: #997;} 136 | .cm-s-default .cm-tag {color: #170;} 137 | .cm-s-default .cm-attribute {color: #00c;} 138 | .cm-s-default .cm-hr {color: #999;} 139 | .cm-s-default .cm-link {color: #00c;} 140 | 141 | .cm-s-default .cm-error {color: #f00;} 142 | .cm-invalidchar {color: #f00;} 143 | 144 | .CodeMirror-composing { border-bottom: 2px solid; } 145 | 146 | /* Default styles for common addons */ 147 | 148 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 149 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 150 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 151 | .CodeMirror-activeline-background {background: #e8f2ff;} 152 | 153 | /* STOP */ 154 | 155 | /* The rest of this file contains styles related to the mechanics of 156 | the editor. You probably shouldn't touch them. */ 157 | 158 | .CodeMirror { 159 | position: relative; 160 | overflow: hidden; 161 | background: white; 162 | } 163 | 164 | .CodeMirror-scroll { 165 | overflow: scroll !important; /* Things will break if this is overridden */ 166 | /* 30px is the magic margin used to hide the element's real scrollbars */ 167 | /* See overflow: hidden in .CodeMirror */ 168 | margin-bottom: -30px; margin-right: -30px; 169 | padding-bottom: 30px; 170 | height: 100%; 171 | outline: none; /* Prevent dragging from highlighting the element */ 172 | position: relative; 173 | } 174 | .CodeMirror-sizer { 175 | position: relative; 176 | border-right: 30px solid transparent; 177 | } 178 | 179 | /* The fake, visible scrollbars. Used to force redraw during scrolling 180 | before actual scrolling happens, thus preventing shaking and 181 | flickering artifacts. */ 182 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 183 | position: absolute; 184 | z-index: 6; 185 | display: none; 186 | } 187 | .CodeMirror-vscrollbar { 188 | right: 0; top: 0; 189 | overflow-x: hidden; 190 | overflow-y: scroll; 191 | } 192 | .CodeMirror-hscrollbar { 193 | bottom: 0; left: 0; 194 | overflow-y: hidden; 195 | overflow-x: scroll; 196 | } 197 | .CodeMirror-scrollbar-filler { 198 | right: 0; bottom: 0; 199 | } 200 | .CodeMirror-gutter-filler { 201 | left: 0; bottom: 0; 202 | } 203 | 204 | .CodeMirror-gutters { 205 | position: absolute; left: 0; top: 0; 206 | min-height: 100%; 207 | z-index: 3; 208 | } 209 | .CodeMirror-gutter { 210 | white-space: normal; 211 | height: 100%; 212 | display: inline-block; 213 | vertical-align: top; 214 | margin-bottom: -30px; 215 | } 216 | .CodeMirror-gutter-wrapper { 217 | position: absolute; 218 | z-index: 4; 219 | background: none !important; 220 | border: none !important; 221 | } 222 | .CodeMirror-gutter-background { 223 | position: absolute; 224 | top: 0; bottom: 0; 225 | z-index: 4; 226 | } 227 | .CodeMirror-gutter-elt { 228 | position: absolute; 229 | cursor: default; 230 | z-index: 4; 231 | } 232 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 233 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 234 | 235 | .CodeMirror-lines { 236 | cursor: text; 237 | min-height: 1px; /* prevents collapsing before first draw */ 238 | } 239 | .CodeMirror pre { 240 | /* Reset some styles that the rest of the page might have set */ 241 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 242 | border-width: 0; 243 | background: transparent; 244 | font-family: inherit; 245 | font-size: inherit; 246 | margin: 0; 247 | white-space: pre; 248 | word-wrap: normal; 249 | line-height: inherit; 250 | color: inherit; 251 | z-index: 2; 252 | position: relative; 253 | overflow: visible; 254 | -webkit-tap-highlight-color: transparent; 255 | -webkit-font-variant-ligatures: contextual; 256 | font-variant-ligatures: contextual; 257 | } 258 | .CodeMirror-wrap pre { 259 | word-wrap: break-word; 260 | white-space: pre-wrap; 261 | word-break: normal; 262 | } 263 | 264 | .CodeMirror-linebackground { 265 | position: absolute; 266 | left: 0; right: 0; top: 0; bottom: 0; 267 | z-index: 0; 268 | } 269 | 270 | .CodeMirror-linewidget { 271 | position: relative; 272 | z-index: 2; 273 | overflow: auto; 274 | } 275 | 276 | .CodeMirror-widget {} 277 | 278 | .CodeMirror-rtl pre { direction: rtl; } 279 | 280 | .CodeMirror-code { 281 | outline: none; 282 | } 283 | 284 | /* Force content-box sizing for the elements where we expect it */ 285 | .CodeMirror-scroll, 286 | .CodeMirror-sizer, 287 | .CodeMirror-gutter, 288 | .CodeMirror-gutters, 289 | .CodeMirror-linenumber { 290 | -moz-box-sizing: content-box; 291 | box-sizing: content-box; 292 | } 293 | 294 | .CodeMirror-measure { 295 | position: absolute; 296 | width: 100%; 297 | height: 0; 298 | overflow: hidden; 299 | visibility: hidden; 300 | } 301 | 302 | .CodeMirror-cursor { 303 | position: absolute; 304 | pointer-events: none; 305 | } 306 | .CodeMirror-measure pre { position: static; } 307 | 308 | div.CodeMirror-cursors { 309 | visibility: hidden; 310 | position: relative; 311 | z-index: 3; 312 | } 313 | div.CodeMirror-dragcursors { 314 | visibility: visible; 315 | } 316 | 317 | .CodeMirror-focused div.CodeMirror-cursors { 318 | visibility: visible; 319 | } 320 | 321 | .CodeMirror-selected { background: #d9d9d9; } 322 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 323 | .CodeMirror-crosshair { cursor: crosshair; } 324 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 325 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 326 | 327 | .cm-searching { 328 | background-color: #ffa; 329 | background-color: rgba(255, 255, 0, .4); 330 | } 331 | 332 | /* Used to force a border model for a node */ 333 | .cm-force-border { padding-right: .1px; } 334 | 335 | @media print { 336 | /* Hide the cursor when printing */ 337 | .CodeMirror div.CodeMirror-cursors { 338 | visibility: hidden; 339 | } 340 | } 341 | 342 | /* See issue #2901 */ 343 | .cm-tab-wrap-hack:after { content: ''; } 344 | 345 | /* Help users use markselection to safely style text background */ 346 | span.CodeMirror-selectedtext { background: none; } 347 | -------------------------------------------------------------------------------- /td4a/static/css/dialog.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-dialog { 2 | position: absolute; 3 | left: 0; right: 0; 4 | background: inherit; 5 | z-index: 15; 6 | padding: .1em .8em; 7 | overflow: hidden; 8 | color: inherit; 9 | } 10 | 11 | .CodeMirror-dialog-top { 12 | border-bottom: 1px solid #eee; 13 | top: 0; 14 | } 15 | 16 | .CodeMirror-dialog-bottom { 17 | border-top: 1px solid #eee; 18 | bottom: 0; 19 | } 20 | 21 | .CodeMirror-dialog input { 22 | border: none; 23 | outline: none; 24 | background: transparent; 25 | width: 20em; 26 | color: inherit; 27 | font-family: monospace; 28 | } 29 | 30 | .CodeMirror-dialog button { 31 | font-size: 70%; 32 | } 33 | -------------------------------------------------------------------------------- /td4a/static/css/material.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: material 4 | Author: Michael Kaminsky (http://github.com/mkaminsky11) 5 | 6 | Original material color scheme by Mattia Astorino (https://github.com/equinusocio/material-theme) 7 | 8 | */ 9 | 10 | .cm-s-material.CodeMirror { 11 | background-color: #263238; 12 | color: rgba(233, 237, 237, 1); 13 | } 14 | .cm-s-material .CodeMirror-gutters { 15 | background: #263238; 16 | color: rgb(83,127,126); 17 | border: none; 18 | } 19 | .cm-s-material .CodeMirror-guttermarker, .cm-s-material .CodeMirror-guttermarker-subtle, .cm-s-material .CodeMirror-linenumber { color: rgb(83,127,126); } 20 | .cm-s-material .CodeMirror-cursor { border-left: 1px solid #f8f8f0; } 21 | .cm-s-material div.CodeMirror-selected { background: rgba(255, 255, 255, 0.15); } 22 | .cm-s-material.CodeMirror-focused div.CodeMirror-selected { background: rgba(255, 255, 255, 0.10); } 23 | .cm-s-material .CodeMirror-line::selection, .cm-s-material .CodeMirror-line > span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); } 24 | .cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); } 25 | 26 | .cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); } 27 | .cm-s-material .cm-keyword { color: rgba(199, 146, 234, 1); } 28 | .cm-s-material .cm-operator { color: rgba(233, 237, 237, 1); } 29 | .cm-s-material .cm-variable-2 { color: #80CBC4; } 30 | .cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #82B1FF; } 31 | .cm-s-material .cm-builtin { color: #DECB6B; } 32 | .cm-s-material .cm-atom { color: #F77669; } 33 | .cm-s-material .cm-number { color: #F77669; } 34 | .cm-s-material .cm-def { color: rgba(233, 237, 237, 1); } 35 | .cm-s-material .cm-string { color: #C3E88D; } 36 | .cm-s-material .cm-string-2 { color: #80CBC4; } 37 | .cm-s-material .cm-comment { color: #546E7A; } 38 | .cm-s-material .cm-variable { color: #82B1FF; } 39 | .cm-s-material .cm-tag { color: #80CBC4; } 40 | .cm-s-material .cm-meta { color: #80CBC4; } 41 | .cm-s-material .cm-attribute { color: #FFCB6B; } 42 | .cm-s-material .cm-property { color: #80CBAE; } 43 | .cm-s-material .cm-qualifier { color: #DECB6B; } 44 | .cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #DECB6B; } 45 | .cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); } 46 | .cm-s-material .cm-error { 47 | color: rgba(255, 255, 255, 1.0); 48 | background-color: #EC5F67; 49 | } 50 | .cm-s-material .CodeMirror-matchingbracket { 51 | text-decoration: underline; 52 | color: white !important; 53 | } 54 | -------------------------------------------------------------------------------- /td4a/static/data.yml: -------------------------------------------------------------------------------- 1 | interfaces: 2 | Ethernet4/10: 3 | description: siteassw100-g0/1-siteasrt001-eth4/10 4 | name: Ethernet4/10 5 | shutdown: 6 | negate: true 7 | switchport: 8 | mode: 9 | - trunk 10 | switchport: true 11 | Ethernet4/11: 12 | channel_group: 13 | id: 31 14 | mode: active 15 | description: unity1interface1 16 | mtu: 9216 17 | name: Ethernet4/11 18 | shutdown: 19 | negate: true 20 | switchport: 21 | mode: 22 | - trunk 23 | switchport: true 24 | trunk: 25 | allowed_vlans: 26 | vlans: 3605,3607 27 | native_vlan: 5 28 | -------------------------------------------------------------------------------- /td4a/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cidrblock/td4a/54f99283772220fab9e7191b32f89c589a6db127/td4a/static/favicon.ico -------------------------------------------------------------------------------- /td4a/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Template Designer for Automation 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | {{ config.p1.title }} 26 | 27 | 36 | 37 | {{ host }} 38 | 39 | 40 | 41 | 42 | 43 | {{ config.p1.b1.icon }} 44 | {{ config.p1.b1.text }} 45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 | 54 | {{ config.p2.title }} 55 | 56 | 57 | {{ config.p2.b1.icon }} 58 | {{ config.p2.b1.text }} 59 | 60 | 61 | 62 |
63 |
64 |
65 | 66 |
67 | 68 | {{ config.p3.title }} 69 | 70 | 71 | {{ config.p3.b1.icon }} 72 | {{ config.p3.b1.text }} 73 | 74 | 75 | 76 | 77 | code 78 | github 79 | 80 | 81 | 82 |
83 |
84 |
85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /td4a/static/js/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('mainController', ['ngRoute', 'ngMaterial', 'ngMessages', 'ui.codemirror', 'ng-split', 'ngCookies', 'LocalStorageModule']) 2 | 3 | .config(function($routeProvider,$locationProvider) { 4 | $locationProvider.html5Mode(true); 5 | }) 6 | .config(function (localStorageServiceProvider) { 7 | localStorageServiceProvider 8 | .setPrefix('td4a') 9 | }); 10 | 11 | app.controller('main', function($scope, $http, $window, $mdToast, $timeout, $routeParams, $location, $cookies, localStorageService) { 12 | $scope.error = {} 13 | $scope.panels = {} 14 | $scope.config = {} 15 | $scope.demoShown = $cookies.get('demoShown') || false; 16 | 17 | $scope.extraKeys = { 18 | Tab: function(cm) { 19 | var spaces = Array(cm.getOption("indentUnit") + 1).join(" "); 20 | cm.replaceSelection(spaces); 21 | }, 22 | "Cmd-S": function(cm) { 23 | localStorageService.set('panels', $scope.panels) 24 | localStorageService.set('config', $scope.config) 25 | var toast = $mdToast.simple() 26 | .textContent("Saved") 27 | .action('close') 28 | .highlightAction(true) 29 | .highlightClass('md-primary') 30 | .position('top right') 31 | .hideDelay('2000'); 32 | $mdToast.show(toast) 33 | }, 34 | "Cmd-R": function(cm) { 35 | $scope.p2_b1_click() 36 | }, 37 | "Cmd-B": function(cm) { 38 | $scope.panels = { p1: '', p2: '', p3: '' }; 39 | $timeout(function() {cm.refresh();}); 40 | }, 41 | 42 | } 43 | 44 | $scope.getter = function(rroute) { 45 | return $http 46 | .get(rroute).then(function(response) { 47 | if ((typeof(response.data) == 'object') && ("handled_error" in response.data)) { 48 | $scope.handledError(response.data.handled_error) 49 | return response.data; 50 | } else { 51 | return response.data; 52 | } 53 | }) 54 | .catch(function(error) { 55 | console.log(error) 56 | return error 57 | }) //catch 58 | }; 59 | 60 | $scope.init = function() { 61 | if (Object.keys($scope.config).length == 0) { 62 | $scope.getter('/config') 63 | .then(function(data) { 64 | $scope.config = data; 65 | $scope.config.p1.options.extraKeys = $scope.config.p2.options.extraKeys = $scope.extraKeys 66 | $scope.inventory() 67 | }) 68 | } else { 69 | $scope.config.p1.options.extraKeys = $scope.config.p2.options.extraKeys = $scope.extraKeys 70 | $scope.inventory() 71 | 72 | } 73 | } 74 | 75 | $scope.inventory = function() { 76 | if ($scope.config.p1.inventory) { 77 | $scope.getter('/hosts') 78 | .then(function(data){ 79 | $scope.hosts = data['hosts']; 80 | }) 81 | } 82 | } 83 | 84 | $scope.handledError = function(error) { 85 | console.log(error.raw_error) 86 | if (error.line_number) { 87 | var errorMessage = `${error.title} ${error.details} Line number: ${error.line_number}\n`; 88 | var actualLineNumber = error.line_number -1 ; 89 | if (error.in == "p2") { 90 | var myEditor = angular.element(document.getElementById('p2_editor')) 91 | } else if (error.in == "p1") { 92 | var myEditor = angular.element(document.getElementById('p1_editor')) 93 | } 94 | var codeMirrorEditor = myEditor[0].childNodes[0].CodeMirror 95 | $scope.error.codeMirrorEditor = codeMirrorEditor 96 | $scope.error.line_number = actualLineNumber 97 | $scope.error.codeMirrorEditor.addLineClass($scope.error.line_number, 'wrap', 'error'); 98 | codeMirrorEditor.scrollIntoView({line: actualLineNumber}); 99 | } else { 100 | var errorMessage = `${error.title} ${error.details}\n`; 101 | } 102 | var toast = $mdToast.simple() 103 | .textContent(errorMessage) 104 | .action('close') 105 | .highlightAction(true) 106 | .highlightClass('md-warn') 107 | .position('top right') 108 | .hideDelay('60000'); 109 | $mdToast.show(toast) 110 | }; 111 | 112 | $scope.link = function() { 113 | $http({ 114 | method : 'POST', 115 | url : '/link', 116 | data : {"panels": {"p1": $scope.panels.p1, "p2": $scope.panels.p2}, "config": $scope.config}, 117 | headers : { 'Content-Type': 'application/json' } 118 | }) 119 | .then(function(response) { 120 | if (response.status == 200) { 121 | if ("handled_error" in response.data) { 122 | $scope.handledError(response.data.handled_error) 123 | } else { 124 | $location.search(`id=${response.data.id}`) 125 | } 126 | } 127 | }) //then 128 | .catch(function(error) { 129 | console.log(error.data) 130 | }) //catch 131 | } 132 | 133 | $scope.p1_b1_click = function() { 134 | $scope.config.p1.b1.show = false; 135 | $http({ 136 | method : 'POST', 137 | url : $scope.config.p1.b1.url, 138 | data : { "p1": $scope.panels.p1 }, 139 | headers : { 'Content-Type': 'application/json' } 140 | }) 141 | .then(function(response) { 142 | if (response.status == 200) { 143 | if ("handled_error" in response.data) { 144 | $scope.handledError(response.data.handled_error) 145 | } else { 146 | Object.assign($scope.panels, response.data); 147 | } 148 | $scope.config.p1.b1.show = true; 149 | } 150 | }) 151 | .catch(function(error) { 152 | console.log(error.data) 153 | $scope.config.p1.b1.show = true; 154 | }) //catch 155 | } //render 156 | 157 | $scope.p2_b1_click = function() { 158 | $scope.config.p2.b1.show = false; 159 | if ('line_number' in $scope.error) { 160 | $scope.error.codeMirrorEditor.removeLineClass($scope.error.line_number, 'wrap', 'error'); 161 | } 162 | $http({ 163 | method : 'POST', 164 | url : $scope.config.p2.b1.url, 165 | data : { "p1": $scope.panels.p1, "p2": $scope.panels.p2 }, 166 | headers : { 'Content-Type': 'application/json' } 167 | }) 168 | .then(function(response) { 169 | if (response.status == 200) { 170 | if ("handled_error" in response.data) { 171 | $scope.handledError(response.data.handled_error) 172 | } else { 173 | Object.assign($scope.panels, response.data); 174 | } 175 | $scope.config.p2.b1.show = true; 176 | } 177 | }) 178 | .catch(function(error) { 179 | console.log(error.data) 180 | $scope.config.p2.b1.show = true; 181 | }) //catch 182 | } //render 183 | 184 | $scope.SelectedItemChange = function(host) { 185 | if (host != null) { 186 | $scope.getter(`inventory?host=${host}`) 187 | .then(function(data) { 188 | Object.assign($scope.panels, data) 189 | }); 190 | } 191 | } 192 | 193 | $scope.showDemo = function() { 194 | $scope.getter('data.yml') 195 | .then(function(data) { 196 | $scope.panels.p1 = data 197 | }) 198 | $scope.getter('template.j2') 199 | .then(function(data) { 200 | $scope.panels.p2 = data 201 | }) 202 | } 203 | 204 | if ('id' in $location.search()) { 205 | $scope.getter(`/retrieve?id=${$location.search().id}`) 206 | .then(function(data) { 207 | if ((typeof(data) == 'object') && ("handled_error" in data)) { 208 | $scope.showDemo() 209 | $scope.init() 210 | } else { 211 | Object.assign($scope.config, data['config']) 212 | Object.assign($scope.panels, data['panels']) 213 | $scope.init() 214 | } 215 | }) 216 | } else if (!($scope.demoShown)) { 217 | $scope.showDemo() 218 | $cookies.put('demoShown',true); 219 | $scope.init() 220 | } else if ( localStorageService.get('panels') && localStorageService.get('config')) { 221 | $scope.panels = localStorageService.get('panels') 222 | $scope.config = localStorageService.get('config') 223 | $scope.init() 224 | } else { 225 | $scope.panels = { p1: '', p2: '' } 226 | $scope.init() 227 | }; 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | }); //controller 236 | -------------------------------------------------------------------------------- /td4a/static/template.j2: -------------------------------------------------------------------------------- 1 | {% for key, value in interfaces.items() %} 2 | {% if value['name'].startswith('Ethernet') %} 3 | - parents: 4 | - interface {{ value['name'] }} 5 | lines: 6 | {% if 'description' in value %} 7 | - description {{ value['description'] }} 8 | {% endif %}{# description #} 9 | {% if 'lacp' in value and 'rate' in value['lacp'] %} 10 | - lacp rate {{ value['lacp']['rate'] }} 11 | {% endif %}{# lacp rate #} 12 | {% if 'switchport' in value %} 13 | {% if 'switchport' in value['switchport'] and value['switchport']['switchport'] %} 14 | - switchport 15 | {% endif %} 16 | {% if 'mode' in value['switchport'] %} 17 | - switchport mode {{ value['switchport']['mode']|join(' ') }} 18 | {% endif %}{# switchport mode #} 19 | {% if 'trunk' in value['switchport'] %} 20 | {% if 'native_vlan' in value['switchport']['trunk'] %} 21 | - switchport trunk native vlan {{ value['switchport']['trunk']['native_vlan'] }} 22 | {% endif %}{# switchport trunk native_vlan #} 23 | {% if 'allowed_vlans' in value['switchport']['trunk'] %} 24 | {% if 'vlans' in value['switchport']['trunk']['allowed_vlans'] %} 25 | - switchport trunk allowed vlan {{ value['switchport']['trunk']['allowed_vlans']['vlans'] }} 26 | {% endif %}{# trunk vlans #} 27 | {% if 'add' in value['switchport']['trunk']['allowed_vlans'] %} 28 | {% for add in value['switchport']['trunk']['allowed_vlans']['add'] %} 29 | - switchport trunk allowed vlan add {{ add }} 30 | {% endfor %}{# add entry #} 31 | {% endif %}{# trunk add vlans #} 32 | {% endif %}{# trunk allowed-vlans #} 33 | {% endif %}{# switchport trunk #} 34 | {% endif %}{# switchport #} 35 | {% if 'speed' in value %} 36 | - speed {{ value['speed'] }} 37 | {% endif %}{# speed #} 38 | {% if 'duplex' in value %} 39 | - duplex {{ value['duplex'] }} 40 | {% endif %}{# duplex #} 41 | {% if 'vrf' in value %} 42 | - vrf member {{ value['vrf'] }}" 43 | {% endif %}{# vrf #} 44 | {% if 'ip' in value %} 45 | {% if 'flow' in value['ip'] and 'monitor' in value['ip']['flow'] %} 46 | - ip flow monitor {{ value['ip']['flow']['monitor']['name'] }} {{ value['ip']['flow']['monitor']['direction'] }} sampler {{ value['ip']['flow']['monitor']['sampler'] }} 47 | {% endif %}{# flow/monitor #} 48 | {% if 'redirects' in value['ip'] and 'negate' in value['ip']['redirects'] and value['ip']['redirects']['negate'] %} 49 | - no ip redirects 50 | {% endif %}{# ip redirects #} 51 | {% if 'address' in value['ip'] %} 52 | {% if 'ipv4_address' in value['ip']['address'] and 'ipv4_netmask_bits' in value['ip']['address'] %} 53 | - ip address {{ value['ip']['address']['ipv4_address'] }}/{{ value['ip']['address']['ipv4_netmask_bits'] }} 54 | {% endif %}{# ipv4_address #} 55 | {% endif %}{# address #} 56 | {% if 'ospf' in value['ip'] and 'passive_interface' in value['ip']['ospf'] and value['ip']['ospf']['passive_interface'] %} 57 | - ip ospf passive-interface 58 | {% endif %}{# ip ospf passive interface #} 59 | {% if 'router' in value['ip'] and 'ospf' in value['ip']['router'] %} 60 | - ip router ospf {{ value['ip']['router']['ospf']['process_id'] }} area {{ value['ip']['router']['ospf']['area'] }} 61 | {% endif %}{# router/ospf #} 62 | {% if 'dhcp' in value['ip'] and 'relay_addresses' in value['ip']['dhcp'] %} 63 | {% for relay_address in value['ip']['dhcp']['relay_addresses'] %} 64 | - ip dhcp relay address {{ relay_address }} 65 | {% endfor %}{# relay_address #} 66 | {% endif %}{# dhcp relay addresses #} 67 | {% endif %}{# ip #} 68 | {% if 'spanning_tree' in value %} 69 | {% if 'port' in value['spanning_tree'] %} 70 | {% if 'types' in value['spanning_tree']['port'] %} 71 | - spanning-tree port type {{ value['spanning_tree']['port']['types']|join(' ') }} 72 | {% endif %}{# types #} 73 | {% endif %}{# port #} 74 | {% if 'bpduguard' in value['spanning_tree'] %} 75 | {% if 'enabled' in value['spanning_tree']['bpduguard'] and value['spanning_tree']['bpduguard'] %} 76 | - spanning-tree bpduguard enable 77 | {% endif %}{# bpduguard enabled #} 78 | {% endif %}{# bpduguard #} 79 | {% endif %}{# spanning_tree #} 80 | {% if 'mtu' in value %} 81 | - mtu {{ value['mtu'] }} 82 | {% endif %}{# mtu #} 83 | {% if 'channel_group' in value and 'id' in value['channel_group'] %} 84 | {% if 'mode' in value['channel_group'] %} 85 | - channel-group {{ value['channel_group']['id'] }} mode {{ value['channel_group']['mode'] }} 86 | {% else %} 87 | - channel-group {{ value['channel_group']['id'] }} 88 | {% endif %}{# mode #} 89 | {% endif %}{# channel-group #} 90 | {% if 'shutdown' in value and 'negate' in value['shutdown'] and value['shutdown']['negate'] %} 91 | - no shutdown 92 | {% endif %}{# no shut #} 93 | {% endif %}{# ethernet #} 94 | {% endfor %} 95 | --------------------------------------------------------------------------------