From 9253e0bac6affe39e10022834ce3e07f2a996de5 Mon Sep 17 00:00:00 2001 From: Jordan OMara Date: Thu, 10 Jul 2014 16:52:53 -0400 Subject: [PATCH] Initial commit of sat6 addon, probably too much --- HACKING.rst | 60 + LICENSE | 176 +++ MANIFEST.in | 20 + Makefile | 24 + README.rst | 41 + _10_admin.py.example | 2 + _60_sat.py.example | 12 + bin/less/lessc | 139 ++ bin/lib/less/browser.js | 380 +++++ bin/lib/less/colors.js | 152 ++ bin/lib/less/cssmin.js | 355 +++++ bin/lib/less/functions.js | 228 +++ bin/lib/less/index.js | 148 ++ bin/lib/less/parser.js | 1334 +++++++++++++++++ bin/lib/less/rhino.js | 62 + bin/lib/less/tree.js | 17 + bin/lib/less/tree/alpha.js | 17 + bin/lib/less/tree/anonymous.js | 13 + bin/lib/less/tree/assignment.js | 17 + bin/lib/less/tree/call.js | 48 + bin/lib/less/tree/color.js | 101 ++ bin/lib/less/tree/comment.js | 14 + bin/lib/less/tree/condition.js | 42 + bin/lib/less/tree/dimension.js | 49 + bin/lib/less/tree/directive.js | 35 + bin/lib/less/tree/element.js | 52 + bin/lib/less/tree/expression.js | 23 + bin/lib/less/tree/import.js | 83 + bin/lib/less/tree/javascript.js | 51 + bin/lib/less/tree/keyword.js | 19 + bin/lib/less/tree/media.js | 114 ++ bin/lib/less/tree/mixin.js | 146 ++ bin/lib/less/tree/operation.js | 32 + bin/lib/less/tree/paren.js | 16 + bin/lib/less/tree/quoted.js | 29 + bin/lib/less/tree/rule.js | 42 + bin/lib/less/tree/ruleset.js | 225 +++ bin/lib/less/tree/selector.js | 42 + bin/lib/less/tree/url.js | 25 + bin/lib/less/tree/value.js | 24 + bin/lib/less/tree/variable.js | 26 + doc/Makefile | 153 ++ doc/make.bat | 190 +++ doc/source/HACKING.rst | 1 + doc/source/README.rst | 1 + doc/source/conf.py | 246 +++ doc/source/devstack_baremetal.rst | 17 + doc/source/index.rst | 19 + doc/source/install.rst | 139 ++ local_settings.py.example | 375 +++++ manage.py | 11 + requirements.txt | 24 + run_tests.sh | 469 ++++++ setup.cfg | 41 + setup.py | 21 + test-requirements.txt | 22 + tools/install_venv.py | 154 ++ tools/with_venv.sh | 4 + tox.ini | 71 + tuskar_ui.egg-info/PKG-INFO | 66 + tuskar_ui.egg-info/SOURCES.txt | 197 +++ tuskar_ui.egg-info/dependency_links.txt | 1 + tuskar_ui.egg-info/not-zip-safe | 1 + tuskar_ui.egg-info/requires.txt | 20 + tuskar_ui.egg-info/top_level.txt | 1 + tuskar_ui/__init__.py | 0 tuskar_ui/__init__.pyc | Bin 0 -> 135 bytes tuskar_ui/api/__init__.py | 24 + tuskar_ui/api/__init__.pyc | Bin 0 -> 370 bytes tuskar_ui/api/node.py | 557 +++++++ tuskar_ui/api/node.pyc | Bin 0 -> 18185 bytes tuskar_ui/forms.py | 59 + tuskar_ui/forms.pyc | Bin 0 -> 2934 bytes tuskar_ui/infrastructure/__init__.py | 0 tuskar_ui/infrastructure/__init__.pyc | Bin 0 -> 150 bytes tuskar_ui/infrastructure/nodes/__init__.py | 0 tuskar_ui/infrastructure/nodes/__init__.pyc | Bin 0 -> 156 bytes .../nodes/templates/nodes/details.html | 156 ++ tuskar_ui/infrastructure/nodes/views.py | 177 +++ tuskar_ui/infrastructure/nodes/views.pyc | Bin 0 -> 6466 bytes .../js/angular/horizon.base64.js | 84 ++ .../js/angular/horizon.node_errata.js | 96 ++ .../test/test_data/node_data.py | 219 +++ 83 files changed, 8051 insertions(+) create mode 100644 HACKING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 README.rst create mode 100644 _10_admin.py.example create mode 100644 _60_sat.py.example create mode 100755 bin/less/lessc create mode 100644 bin/lib/less/browser.js create mode 100644 bin/lib/less/colors.js create mode 100644 bin/lib/less/cssmin.js create mode 100644 bin/lib/less/functions.js create mode 100644 bin/lib/less/index.js create mode 100644 bin/lib/less/parser.js create mode 100644 bin/lib/less/rhino.js create mode 100644 bin/lib/less/tree.js create mode 100644 bin/lib/less/tree/alpha.js create mode 100644 bin/lib/less/tree/anonymous.js create mode 100644 bin/lib/less/tree/assignment.js create mode 100644 bin/lib/less/tree/call.js create mode 100644 bin/lib/less/tree/color.js create mode 100644 bin/lib/less/tree/comment.js create mode 100644 bin/lib/less/tree/condition.js create mode 100644 bin/lib/less/tree/dimension.js create mode 100644 bin/lib/less/tree/directive.js create mode 100644 bin/lib/less/tree/element.js create mode 100644 bin/lib/less/tree/expression.js create mode 100644 bin/lib/less/tree/import.js create mode 100644 bin/lib/less/tree/javascript.js create mode 100644 bin/lib/less/tree/keyword.js create mode 100644 bin/lib/less/tree/media.js create mode 100644 bin/lib/less/tree/mixin.js create mode 100644 bin/lib/less/tree/operation.js create mode 100644 bin/lib/less/tree/paren.js create mode 100644 bin/lib/less/tree/quoted.js create mode 100644 bin/lib/less/tree/rule.js create mode 100644 bin/lib/less/tree/ruleset.js create mode 100644 bin/lib/less/tree/selector.js create mode 100644 bin/lib/less/tree/url.js create mode 100644 bin/lib/less/tree/value.js create mode 100644 bin/lib/less/tree/variable.js create mode 100644 doc/Makefile create mode 100644 doc/make.bat create mode 120000 doc/source/HACKING.rst create mode 120000 doc/source/README.rst create mode 100644 doc/source/conf.py create mode 100644 doc/source/devstack_baremetal.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/install.rst create mode 100644 local_settings.py.example create mode 100755 manage.py create mode 100644 requirements.txt create mode 100755 run_tests.sh create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 test-requirements.txt create mode 100644 tools/install_venv.py create mode 100755 tools/with_venv.sh create mode 100644 tox.ini create mode 100644 tuskar_ui.egg-info/PKG-INFO create mode 100644 tuskar_ui.egg-info/SOURCES.txt create mode 100644 tuskar_ui.egg-info/dependency_links.txt create mode 100644 tuskar_ui.egg-info/not-zip-safe create mode 100644 tuskar_ui.egg-info/requires.txt create mode 100644 tuskar_ui.egg-info/top_level.txt create mode 100644 tuskar_ui/__init__.py create mode 100644 tuskar_ui/__init__.pyc create mode 100644 tuskar_ui/api/__init__.py create mode 100644 tuskar_ui/api/__init__.pyc create mode 100644 tuskar_ui/api/node.py create mode 100644 tuskar_ui/api/node.pyc create mode 100644 tuskar_ui/forms.py create mode 100644 tuskar_ui/forms.pyc create mode 100644 tuskar_ui/infrastructure/__init__.py create mode 100644 tuskar_ui/infrastructure/__init__.pyc create mode 100644 tuskar_ui/infrastructure/nodes/__init__.py create mode 100644 tuskar_ui/infrastructure/nodes/__init__.pyc create mode 100644 tuskar_ui/infrastructure/nodes/templates/nodes/details.html create mode 100644 tuskar_ui/infrastructure/nodes/views.py create mode 100644 tuskar_ui/infrastructure/nodes/views.pyc create mode 100644 tuskar_ui/infrastructure/static/infrastructure/js/angular/horizon.base64.js create mode 100644 tuskar_ui/infrastructure/static/infrastructure/js/angular/horizon.node_errata.js create mode 100644 tuskar_ui/infrastructure/test/test_data/node_data.py diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..442a927 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,60 @@ +Contributing +============ + +The code repository is located at `OpenStack `__. +Please go there if you want to check it out: + + git clone https://github.com/openstack/tuskar-ui.git + +The list of bugs and blueprints is on Launchpad: + +``__ + +We use OpenStack's Gerrit for the code contributions: + +``__ + +and we follow the `OpenStack Gerrit Workflow `__. + +If you're interested in the code, here are some key places to start: + +* `tuskar_ui/api.py `_ + - This file contains all the API calls made to the Tuskar API + (through python-tuskarclient). +* `tuskar_ui/infrastructure `_ + - The Tuskar UI code is contained within this directory. + +Running tests +============= + +There are several ways to run tests for tuskar-ui. + +Using ``tox``: + + This is the easiest way to run tests. When run, tox installs dependencies, + prepares the virtual python environment, then runs test commands. The gate + tests in gerrit usually also use tox to run tests. For avaliable tox + environments, see ``tox.ini``. + +By running ``run_tests.sh``: + + Tests can also be run using the ``run_tests.sh`` script, to see available + options, run it with the ``--help`` option. It handles preparing the + virtual environment and executing tests, but in contrast with tox, it does + not install all dependencies, e.g. ``jshint`` must be installed before + running the jshint testcase. + +Manual tests: + + To manually check tuskar-ui, it is possible to run a development server + for tuskar-ui by running ``run_tests.sh --runserver``. + + To run the server with the settings used by the test environment: + ``run_tests.sh --runserver 0.0.0.0:8000 --settings=tuskar_ui.test.settings`` + +OpenStack Style Commandments +============================ + +- Step 1: Read http://www.python.org/dev/peps/pep-0008/ +- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again +- Step 3: Read https://github.com/openstack-dev/hacking/blob/master/HACKING.rst diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2dd815e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,20 @@ +recursive-include bin *.js +recursive-include doc *.py *.rst *.css *.js *.html *.conf *.jpg *.gif *.png *.css_t +recursive-include tools *.py *.sh +recursive-include tuskar_ui *.py *.html *.js *.less *.mo *.po *.example *.eot *.svg *.ttf *.woff *.png *.ico *.wsgi *.gif *.csv *.template + +include AUTHORS +include ChangeLog +include LICENSE +include Makefile +include manage.py +include README.rst +include run_tests.sh +include tox.ini +include bin/less/lessc +include doc/Makefile +include doc/source/_templates/.placeholder +include requirements.txt +include test-requirements.txt + +exclude openstack_dashboard/local/local_settings.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..49e4f72 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +PYTHON=`which python` +DESTDIR=/ +PROJECT=horizon + +all: + @echo "make test - Run tests" + @echo "make source - Create source package" + @echo "make install - Install on local system" + @echo "make buildrpm - Generate a rpm package" + @echo "make clean - Get rid of scratch and byte files" + +source: + $(PYTHON) setup.py sdist $(COMPILE) + +install: + $(PYTHON) setup.py install --root $(DESTDIR) $(COMPILE) + +buildrpm: + $(PYTHON) setup.py bdist_rpm --post-install=rpm/postinstall --pre-uninstall=rpm/preuninstall + +clean: + $(PYTHON) setup.py clean + rm -rf build/ MANIFEST + find . -name '*.pyc' -delete diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..bd1d9d3 --- /dev/null +++ b/README.rst @@ -0,0 +1,41 @@ +========= +Tuskar UI +========= + +**Tuskar UI** is a user interface for +`Tuskar `__, a management API for +OpenStack deployments. It is a plugin for `OpenStack +Horizon `__. + +High-Level Overview +------------------- + +Tuskar UI endeavours to be a stateless UI, relying on Tuskar API calls +as much as possible. We use existing Horizon libraries and components +where possible. If added libraries and components are needed, we will +work with the OpenStack community to push those changes back into Horizon. + +Interested in seeing Tuskar and Tuskar UI in action? +`Watch a demo! `_ + + +Installation Guide +------------------ + +Use the `Installation Guide `_ to install Tuskar UI. + +License +------- + +This project is licensed under the Apache License, version 2. More +information can be found in the LICENSE file. + +Contact Us +---------- + +Join us on IRC (Internet Relay Chat):: + + Network: Freenode (irc.freenode.net/tuskar) + Channel: #tuskar + +Or send an email to openstack-dev@lists.openstack.org. diff --git a/_10_admin.py.example b/_10_admin.py.example new file mode 100644 index 0000000..868d719 --- /dev/null +++ b/_10_admin.py.example @@ -0,0 +1,2 @@ +DASHBOARD = 'admin' +DISABLED = True diff --git a/_60_sat.py.example b/_60_sat.py.example new file mode 100644 index 0000000..7da2969 --- /dev/null +++ b/_60_sat.py.example @@ -0,0 +1,12 @@ +from tuskar_ui import exceptions + +DASHBOARD = 'infrastructure' +ADD_INSTALLED_APPS = [ + 'tuskar_ui.infrastructure', +] +ADD_EXCEPTIONS = { + 'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED, +} +DEFAULT = True diff --git a/bin/less/lessc b/bin/less/lessc new file mode 100755 index 0000000..30ae352 --- /dev/null +++ b/bin/less/lessc @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +var path = require('path'), + fs = require('fs'), + sys = require('util'), + os = require('os'); + +var less = require('../lib/less'); +var args = process.argv.slice(1); +var options = { + compress: false, + yuicompress: false, + optimization: 1, + silent: false, + paths: [], + color: true, + strictImports: false +}; + +args = args.filter(function (arg) { + var match; + + if (match = arg.match(/^-I(.+)$/)) { + options.paths.push(match[1]); + return false; + } + + if (match = arg.match(/^--?([a-z][0-9a-z-]*)(?:=([^\s]+))?$/i)) { arg = match[1] } + else { return arg } + + switch (arg) { + case 'v': + case 'version': + sys.puts("lessc " + less.version.join('.') + " (LESS Compiler) [JavaScript]"); + process.exit(0); + case 'verbose': + options.verbose = true; + break; + case 's': + case 'silent': + options.silent = true; + break; + case 'strict-imports': + options.strictImports = true; + break; + case 'h': + case 'help': + sys.puts("usage: lessc source [destination]"); + process.exit(0); + case 'x': + case 'compress': + options.compress = true; + break; + case 'yui-compress': + options.yuicompress = true; + break; + case 'no-color': + options.color = false; + break; + case 'include-path': + options.paths = match[2].split(os.type().match(/Windows/) ? ';' : ':') + .map(function(p) { + if (p) { + return path.resolve(process.cwd(), p); + } + }); + break; + case 'O0': options.optimization = 0; break; + case 'O1': options.optimization = 1; break; + case 'O2': options.optimization = 2; break; + } +}); + +var input = args[1]; +if (input && input != '-') { + input = path.resolve(process.cwd(), input); +} +var output = args[2]; +if (output) { + output = path.resolve(process.cwd(), output); +} + +var css, fd, tree; + +if (! input) { + sys.puts("lessc: no input files"); + process.exit(1); +} + +var parseLessFile = function (e, data) { + if (e) { + sys.puts("lessc: " + e.message); + process.exit(1); + } + + new(less.Parser)({ + paths: [path.dirname(input)].concat(options.paths), + optimization: options.optimization, + filename: input, + strictImports: options.strictImports + }).parse(data, function (err, tree) { + if (err) { + less.writeError(err, options); + process.exit(1); + } else { + try { + css = tree.toCSS({ + compress: options.compress, + yuicompress: options.yuicompress + }); + if (output) { + fd = fs.openSync(output, "w"); + fs.writeSync(fd, css, 0, "utf8"); + } else { + sys.print(css); + } + } catch (e) { + less.writeError(e, options); + process.exit(2); + } + } + }); +}; + +if (input != '-') { + fs.readFile(input, 'utf-8', parseLessFile); +} else { + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + var buffer = ''; + process.stdin.on('data', function(data) { + buffer += data; + }); + + process.stdin.on('end', function() { + parseLessFile(false, buffer); + }); +} diff --git a/bin/lib/less/browser.js b/bin/lib/less/browser.js new file mode 100644 index 0000000..cab913b --- /dev/null +++ b/bin/lib/less/browser.js @@ -0,0 +1,380 @@ +// +// browser.js - client-side engine +// + +var isFileProtocol = (location.protocol === 'file:' || + location.protocol === 'chrome:' || + location.protocol === 'chrome-extension:' || + location.protocol === 'resource:'); + +less.env = less.env || (location.hostname == '127.0.0.1' || + location.hostname == '0.0.0.0' || + location.hostname == 'localhost' || + location.port.length > 0 || + isFileProtocol ? 'development' + : 'production'); + +// Load styles asynchronously (default: false) +// +// This is set to `false` by default, so that the body +// doesn't start loading before the stylesheets are parsed. +// Setting this to `true` can result in flickering. +// +less.async = false; + +// Interval between watch polls +less.poll = less.poll || (isFileProtocol ? 1000 : 1500); + +// +// Watch mode +// +less.watch = function () { return this.watchMode = true }; +less.unwatch = function () { return this.watchMode = false }; + +if (less.env === 'development') { + less.optimization = 0; + + if (/!watch/.test(location.hash)) { + less.watch(); + } + less.watchTimer = setInterval(function () { + if (less.watchMode) { + loadStyleSheets(function (e, root, _, sheet, env) { + if (root) { + createCSS(root.toCSS(), sheet, env.lastModified); + } + }); + } + }, less.poll); +} else { + less.optimization = 3; +} + +var cache; + +try { + cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; +} catch (_) { + cache = null; +} + +// +// Get all tags with the 'rel' attribute set to "stylesheet/less" +// +var links = document.getElementsByTagName('link'); +var typePattern = /^text\/(x-)?less$/; + +less.sheets = []; + +for (var i = 0; i < links.length; i++) { + if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && + (links[i].type.match(typePattern)))) { + less.sheets.push(links[i]); + } +} + + +less.refresh = function (reload) { + var startTime, endTime; + startTime = endTime = new(Date); + + loadStyleSheets(function (e, root, _, sheet, env) { + if (env.local) { + log("loading " + sheet.href + " from cache."); + } else { + log("parsed " + sheet.href + " successfully."); + createCSS(root.toCSS(), sheet, env.lastModified); + } + log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); + (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); + endTime = new(Date); + }, reload); + + loadStyles(); +}; +less.refreshStyles = loadStyles; + +less.refresh(less.env === 'development'); + +function loadStyles() { + var styles = document.getElementsByTagName('style'); + for (var i = 0; i < styles.length; i++) { + if (styles[i].type.match(typePattern)) { + new(less.Parser)().parse(styles[i].innerHTML || '', function (e, tree) { + var css = tree.toCSS(); + var style = styles[i]; + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + }); + } + } +} + +function loadStyleSheets(callback, reload) { + for (var i = 0; i < less.sheets.length; i++) { + loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1)); + } +} + +function loadStyleSheet(sheet, callback, reload, remaining) { + var url = window.location.href.replace(/[#?].*$/, ''); + var href = sheet.href.replace(/\?.*$/, ''); + var css = cache && cache.getItem(href); + var timestamp = cache && cache.getItem(href + ':timestamp'); + var styles = { css: css, timestamp: timestamp }; + + // Stylesheets in IE don't always return the full path + if (! /^(https?|file):/.test(href)) { + if (href.charAt(0) == "/") { + href = window.location.protocol + "//" + window.location.host + href; + } else { + href = url.slice(0, url.lastIndexOf('/') + 1) + href; + } + } + var filename = href.match(/([^\/]+)$/)[1]; + + xhr(sheet.href, sheet.type, function (data, lastModified) { + if (!reload && styles && lastModified && + (new(Date)(lastModified).valueOf() === + new(Date)(styles.timestamp).valueOf())) { + // Use local copy + createCSS(styles.css, sheet); + callback(null, null, data, sheet, { local: true, remaining: remaining }); + } else { + // Use remote copy (re-parse) + try { + new(less.Parser)({ + optimization: less.optimization, + paths: [href.replace(/[\w\.-]+$/, '')], + mime: sheet.type, + filename: filename + }).parse(data, function (e, root) { + if (e) { return error(e, href) } + try { + callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining }); + removeNode(document.getElementById('less-error-message:' + extractId(href))); + } catch (e) { + error(e, href); + } + }); + } catch (e) { + error(e, href); + } + } + }, function (status, url) { + throw new(Error)("Couldn't load " + url + " (" + status + ")"); + }); +} + +function extractId(href) { + return href.replace(/^[a-z]+:\/\/?[^\/]+/, '' ) // Remove protocol & domain + .replace(/^\//, '' ) // Remove root / + .replace(/\?.*$/, '' ) // Remove query + .replace(/\.[^\.\/]+$/, '' ) // Remove file extension + .replace(/[^\.\w-]+/g, '-') // Replace illegal characters + .replace(/\./g, ':'); // Replace dots with colons(for valid id) +} + +function createCSS(styles, sheet, lastModified) { + var css; + + // Strip the query-string + var href = sheet.href ? sheet.href.replace(/\?.*$/, '') : ''; + + // If there is no title set, use the filename, minus the extension + var id = 'less:' + (sheet.title || extractId(href)); + + // If the stylesheet doesn't exist, create a new node + if ((css = document.getElementById(id)) === null) { + css = document.createElement('style'); + css.type = 'text/css'; + css.media = sheet.media || 'screen'; + css.id = id; + document.getElementsByTagName('head')[0].appendChild(css); + } + + if (css.styleSheet) { // IE + try { + css.styleSheet.cssText = styles; + } catch (e) { + throw new(Error)("Couldn't reassign styleSheet.cssText."); + } + } else { + (function (node) { + if (css.childNodes.length > 0) { + if (css.firstChild.nodeValue !== node.nodeValue) { + css.replaceChild(node, css.firstChild); + } + } else { + css.appendChild(node); + } + })(document.createTextNode(styles)); + } + + // Don't update the local store if the file wasn't modified + if (lastModified && cache) { + log('saving ' + href + ' to cache.'); + cache.setItem(href, styles); + cache.setItem(href + ':timestamp', lastModified); + } +} + +function xhr(url, type, callback, errback) { + var xhr = getXMLHttpRequest(); + var async = isFileProtocol ? false : less.async; + + if (typeof(xhr.overrideMimeType) === 'function') { + xhr.overrideMimeType('text/css'); + } + xhr.open('GET', url, async); + xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); + xhr.send(null); + + if (isFileProtocol) { + if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { + callback(xhr.responseText); + } else { + errback(xhr.status, url); + } + } else if (async) { + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + handleResponse(xhr, callback, errback); + } + }; + } else { + handleResponse(xhr, callback, errback); + } + + function handleResponse(xhr, callback, errback) { + if (xhr.status >= 200 && xhr.status < 300) { + callback(xhr.responseText, + xhr.getResponseHeader("Last-Modified")); + } else if (typeof(errback) === 'function') { + errback(xhr.status, url); + } + } +} + +function getXMLHttpRequest() { + if (window.XMLHttpRequest) { + return new(XMLHttpRequest); + } else { + try { + return new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); + } catch (e) { + log("browser doesn't support AJAX."); + return null; + } + } +} + +function removeNode(node) { + return node && node.parentNode.removeChild(node); +} + +function log(str) { + if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) } +} + +function error(e, href) { + var id = 'less-error-message:' + extractId(href); + var template = '
  • {content}
  • '; + var elem = document.createElement('div'), timer, content, error = []; + var filename = e.filename || href; + + elem.id = id; + elem.className = "less-error-message"; + + content = '

    ' + (e.message || 'There is an error in your .less file') + + '

    ' + '

    in ' + filename + " "; + + var errorline = function (e, i, classname) { + if (e.extract[i]) { + error.push(template.replace(/\{line\}/, parseInt(e.line) + (i - 1)) + .replace(/\{class\}/, classname) + .replace(/\{content\}/, e.extract[i])); + } + }; + + if (e.stack) { + content += '
    ' + e.stack.split('\n').slice(1).join('
    '); + } else if (e.extract) { + errorline(e, 0, ''); + errorline(e, 1, 'line'); + errorline(e, 2, ''); + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':

    ' + + '
      ' + error.join('') + '
    '; + } + elem.innerHTML = content; + + // CSS for error messages + createCSS([ + '.less-error-message ul, .less-error-message li {', + 'list-style-type: none;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'margin: 0;', + '}', + '.less-error-message label {', + 'font-size: 12px;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'color: #cc7777;', + '}', + '.less-error-message pre {', + 'color: #dd6666;', + 'padding: 4px 0;', + 'margin: 0;', + 'display: inline-block;', + '}', + '.less-error-message pre.line {', + 'color: #ff0000;', + '}', + '.less-error-message h3 {', + 'font-size: 20px;', + 'font-weight: bold;', + 'padding: 15px 0 5px 0;', + 'margin: 0;', + '}', + '.less-error-message a {', + 'color: #10a', + '}', + '.less-error-message .error {', + 'color: red;', + 'font-weight: bold;', + 'padding-bottom: 2px;', + 'border-bottom: 1px dashed red;', + '}' + ].join('\n'), { title: 'error-message' }); + + elem.style.cssText = [ + "font-family: Arial, sans-serif", + "border: 1px solid #e00", + "background-color: #eee", + "border-radius: 5px", + "-webkit-border-radius: 5px", + "-moz-border-radius: 5px", + "color: #e00", + "padding: 15px", + "margin-bottom: 15px" + ].join(';'); + + if (less.env == 'development') { + timer = setInterval(function () { + if (document.body) { + if (document.getElementById(id)) { + document.body.replaceChild(elem, document.getElementById(id)); + } else { + document.body.insertBefore(elem, document.body.firstChild); + } + clearInterval(timer); + } + }, 10); + } +} + diff --git a/bin/lib/less/colors.js b/bin/lib/less/colors.js new file mode 100644 index 0000000..ed4c283 --- /dev/null +++ b/bin/lib/less/colors.js @@ -0,0 +1,152 @@ +(function (tree) { + tree.colors = { + 'aliceblue':'#f0f8ff', + 'antiquewhite':'#faebd7', + 'aqua':'#00ffff', + 'aquamarine':'#7fffd4', + 'azure':'#f0ffff', + 'beige':'#f5f5dc', + 'bisque':'#ffe4c4', + 'black':'#000000', + 'blanchedalmond':'#ffebcd', + 'blue':'#0000ff', + 'blueviolet':'#8a2be2', + 'brown':'#a52a2a', + 'burlywood':'#deb887', + 'cadetblue':'#5f9ea0', + 'chartreuse':'#7fff00', + 'chocolate':'#d2691e', + 'coral':'#ff7f50', + 'cornflowerblue':'#6495ed', + 'cornsilk':'#fff8dc', + 'crimson':'#dc143c', + 'cyan':'#00ffff', + 'darkblue':'#00008b', + 'darkcyan':'#008b8b', + 'darkgoldenrod':'#b8860b', + 'darkgray':'#a9a9a9', + 'darkgrey':'#a9a9a9', + 'darkgreen':'#006400', + 'darkkhaki':'#bdb76b', + 'darkmagenta':'#8b008b', + 'darkolivegreen':'#556b2f', + 'darkorange':'#ff8c00', + 'darkorchid':'#9932cc', + 'darkred':'#8b0000', + 'darksalmon':'#e9967a', + 'darkseagreen':'#8fbc8f', + 'darkslateblue':'#483d8b', + 'darkslategray':'#2f4f4f', + 'darkslategrey':'#2f4f4f', + 'darkturquoise':'#00ced1', + 'darkviolet':'#9400d3', + 'deeppink':'#ff1493', + 'deepskyblue':'#00bfff', + 'dimgray':'#696969', + 'dimgrey':'#696969', + 'dodgerblue':'#1e90ff', + 'firebrick':'#b22222', + 'floralwhite':'#fffaf0', + 'forestgreen':'#228b22', + 'fuchsia':'#ff00ff', + 'gainsboro':'#dcdcdc', + 'ghostwhite':'#f8f8ff', + 'gold':'#ffd700', + 'goldenrod':'#daa520', + 'gray':'#808080', + 'grey':'#808080', + 'green':'#008000', + 'greenyellow':'#adff2f', + 'honeydew':'#f0fff0', + 'hotpink':'#ff69b4', + 'indianred':'#cd5c5c', + 'indigo':'#4b0082', + 'ivory':'#fffff0', + 'khaki':'#f0e68c', + 'lavender':'#e6e6fa', + 'lavenderblush':'#fff0f5', + 'lawngreen':'#7cfc00', + 'lemonchiffon':'#fffacd', + 'lightblue':'#add8e6', + 'lightcoral':'#f08080', + 'lightcyan':'#e0ffff', + 'lightgoldenrodyellow':'#fafad2', + 'lightgray':'#d3d3d3', + 'lightgrey':'#d3d3d3', + 'lightgreen':'#90ee90', + 'lightpink':'#ffb6c1', + 'lightsalmon':'#ffa07a', + 'lightseagreen':'#20b2aa', + 'lightskyblue':'#87cefa', + 'lightslategray':'#778899', + 'lightslategrey':'#778899', + 'lightsteelblue':'#b0c4de', + 'lightyellow':'#ffffe0', + 'lime':'#00ff00', + 'limegreen':'#32cd32', + 'linen':'#faf0e6', + 'magenta':'#ff00ff', + 'maroon':'#800000', + 'mediumaquamarine':'#66cdaa', + 'mediumblue':'#0000cd', + 'mediumorchid':'#ba55d3', + 'mediumpurple':'#9370d8', + 'mediumseagreen':'#3cb371', + 'mediumslateblue':'#7b68ee', + 'mediumspringgreen':'#00fa9a', + 'mediumturquoise':'#48d1cc', + 'mediumvioletred':'#c71585', + 'midnightblue':'#191970', + 'mintcream':'#f5fffa', + 'mistyrose':'#ffe4e1', + 'moccasin':'#ffe4b5', + 'navajowhite':'#ffdead', + 'navy':'#000080', + 'oldlace':'#fdf5e6', + 'olive':'#808000', + 'olivedrab':'#6b8e23', + 'orange':'#ffa500', + 'orangered':'#ff4500', + 'orchid':'#da70d6', + 'palegoldenrod':'#eee8aa', + 'palegreen':'#98fb98', + 'paleturquoise':'#afeeee', + 'palevioletred':'#d87093', + 'papayawhip':'#ffefd5', + 'peachpuff':'#ffdab9', + 'peru':'#cd853f', + 'pink':'#ffc0cb', + 'plum':'#dda0dd', + 'powderblue':'#b0e0e6', + 'purple':'#800080', + 'red':'#ff0000', + 'rosybrown':'#bc8f8f', + 'royalblue':'#4169e1', + 'saddlebrown':'#8b4513', + 'salmon':'#fa8072', + 'sandybrown':'#f4a460', + 'seagreen':'#2e8b57', + 'seashell':'#fff5ee', + 'sienna':'#a0522d', + 'silver':'#c0c0c0', + 'skyblue':'#87ceeb', + 'slateblue':'#6a5acd', + 'slategray':'#708090', + 'slategrey':'#708090', + 'snow':'#fffafa', + 'springgreen':'#00ff7f', + 'steelblue':'#4682b4', + 'tan':'#d2b48c', + 'teal':'#008080', + 'thistle':'#d8bfd8', + 'tomato':'#ff6347', + 'transparent':'rgba(0,0,0,0)', + 'turquoise':'#40e0d0', + 'violet':'#ee82ee', + 'wheat':'#f5deb3', + 'white':'#ffffff', + 'whitesmoke':'#f5f5f5', + 'yellow':'#ffff00', + 'yellowgreen':'#9acd32' + }; +})(require('./tree')); diff --git a/bin/lib/less/cssmin.js b/bin/lib/less/cssmin.js new file mode 100644 index 0000000..427de71 --- /dev/null +++ b/bin/lib/less/cssmin.js @@ -0,0 +1,355 @@ +/** + * cssmin.js + * Author: Stoyan Stefanov - http://phpied.com/ + * This is a JavaScript port of the CSS minification tool + * distributed with YUICompressor, itself a port + * of the cssmin utility by Isaac Schlueter - http://foohack.com/ + * Permission is hereby granted to use the JavaScript version under the same + * conditions as the YUICompressor (original YUICompressor note below). + */ + +/* +* YUI Compressor +* http://developer.yahoo.com/yui/compressor/ +* Author: Julien Lecomte - http://www.julienlecomte.net/ +* Copyright (c) 2011 Yahoo! Inc. All rights reserved. +* The copyrights embodied in the content of this file are licensed +* by Yahoo! Inc. under the BSD (revised) open source license. +*/ +var YAHOO = YAHOO || {}; +YAHOO.compressor = YAHOO.compressor || {}; + +/** + * Utility method to replace all data urls with tokens before we start + * compressing, to avoid performance issues running some of the subsequent + * regexes against large strings chunks. + * + * @private + * @method _extractDataUrls + * @param {String} css The input css + * @param {Array} The global array of tokens to preserve + * @returns String The processed css + */ +YAHOO.compressor._extractDataUrls = function (css, preservedTokens) { + + // Leave data urls alone to increase parse performance. + var maxIndex = css.length - 1, + appendIndex = 0, + startIndex, + endIndex, + terminator, + foundTerminator, + sb = [], + m, + preserver, + token, + pattern = /url\(\s*(["']?)data\:/g; + + // Since we need to account for non-base64 data urls, we need to handle + // ' and ) being part of the data string. Hence switching to indexOf, + // to determine whether or not we have matching string terminators and + // handling sb appends directly, instead of using matcher.append* methods. + + while ((m = pattern.exec(css)) !== null) { + + startIndex = m.index + 4; // "url(".length() + terminator = m[1]; // ', " or empty (not quoted) + + if (terminator.length === 0) { + terminator = ")"; + } + + foundTerminator = false; + + endIndex = pattern.lastIndex - 1; + + while(foundTerminator === false && endIndex+1 <= maxIndex) { + endIndex = css.indexOf(terminator, endIndex + 1); + + // endIndex == 0 doesn't really apply here + if ((endIndex > 0) && (css.charAt(endIndex - 1) !== '\\')) { + foundTerminator = true; + if (")" != terminator) { + endIndex = css.indexOf(")", endIndex); + } + } + } + + // Enough searching, start moving stuff over to the buffer + sb.push(css.substring(appendIndex, m.index)); + + if (foundTerminator) { + token = css.substring(startIndex, endIndex); + token = token.replace(/\s+/g, ""); + preservedTokens.push(token); + + preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___)"; + sb.push(preserver); + + appendIndex = endIndex + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + sb.push(css.substring(m.index, pattern.lastIndex)); + appendIndex = pattern.lastIndex; + } + } + + sb.push(css.substring(appendIndex)); + + return sb.join(""); +}; + +/** + * Utility method to compress hex color values of the form #AABBCC to #ABC. + * + * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). + * e.g. #AddressForm { ... } + * + * DOES NOT compress IE filters, which have hex color values (which would break things). + * e.g. filter: chroma(color="#FFFFFF"); + * + * DOES NOT compress invalid hex values. + * e.g. background-color: #aabbccdd + * + * @private + * @method _compressHexColors + * @param {String} css The input css + * @returns String The processed css + */ +YAHOO.compressor._compressHexColors = function(css) { + + // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) + var pattern = /(\=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi, + m, + index = 0, + isFilter, + sb = []; + + while ((m = pattern.exec(css)) !== null) { + + sb.push(css.substring(index, m.index)); + + isFilter = m[1]; + + if (isFilter) { + // Restore, maintain case, otherwise filter will break + sb.push(m[1] + "#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7])); + } else { + if (m[2].toLowerCase() == m[3].toLowerCase() && + m[4].toLowerCase() == m[5].toLowerCase() && + m[6].toLowerCase() == m[7].toLowerCase()) { + + // Compress. + sb.push("#" + (m[3] + m[5] + m[7]).toLowerCase()); + } else { + // Non compressible color, restore but lower case. + sb.push("#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]).toLowerCase()); + } + } + + index = pattern.lastIndex = pattern.lastIndex - m[8].length; + } + + sb.push(css.substring(index)); + + return sb.join(""); +}; + +YAHOO.compressor.cssmin = function (css, linebreakpos) { + + var startIndex = 0, + endIndex = 0, + i = 0, max = 0, + preservedTokens = [], + comments = [], + token = '', + totallen = css.length, + placeholder = ''; + + css = this._extractDataUrls(css, preservedTokens); + + // collect all comment blocks... + while ((startIndex = css.indexOf("/*", startIndex)) >= 0) { + endIndex = css.indexOf("*/", startIndex + 2); + if (endIndex < 0) { + endIndex = totallen; + } + token = css.slice(startIndex + 2, endIndex); + comments.push(token); + css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); + startIndex += 2; + } + + // preserve strings so their content doesn't get accidentally minified + css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) { + var i, max, quote = match.substring(0, 1); + + match = match.slice(1, -1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { + for (i = 0, max = comments.length; i < max; i = i + 1) { + match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]); + } + } + + // minify alpha opacity in filter strings + match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); + + preservedTokens.push(match); + return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote; + }); + + // strings are safe, now wrestle the comments + for (i = 0, max = comments.length; i < max; i = i + 1) { + + token = comments[i]; + placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens keeping the ! + if (token.charAt(0) === "!") { + preservedTokens.push(token); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (token.charAt(token.length - 1) === "\\") { + preservedTokens.push("\\"); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + i = i + 1; // attn: advancing the loop + preservedTokens.push(""); + css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (token.length === 0) { + startIndex = css.indexOf(placeholder); + if (startIndex > 2) { + if (css.charAt(startIndex - 3) === '>') { + preservedTokens.push(""); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); + } + } + } + + // in all other cases kill the comment + css = css.replace("/*" + placeholder + "*/", ""); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + css = css.replace(/\s+/g, " "); + + // Remove the spaces before the things that should not have spaces before them. + // But, be careful not to turn "p :link {...}" into "p:link{...}" + // Swap out any pseudo-class colons with the token, and then swap back. + css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function (m) { + return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); + }); + css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1'); + css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":"); + + // retain space for special IE6 cases + css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2"); + + // no space after the end of a preserved comment + css = css.replace(/\*\/ /g, '*/'); + + + // If there is a @charset, then only allow one, and push to the top of the file. + css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1'); + css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1'); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + css = css.replace(/\band\(/gi, "and ("); + + + // Remove the spaces after the things that should not have spaces after them. + css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1'); + + // remove unnecessary semicolons + css = css.replace(/;+\}/g, "}"); + + // Replace 0(px,em,%) with 0. + css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2"); + + // Replace 0 0 0 0; with 0. + css = css.replace(/:0 0 0 0(;|\})/g, ":0$1"); + css = css.replace(/:0 0 0(;|\})/g, ":0$1"); + css = css.replace(/:0 0(;|\})/g, ":0$1"); + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + css = css.replace(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/gi, function(all, prop, tail) { + return prop.toLowerCase() + ":0 0" + tail; + }); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2"); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function () { + var i, rgbcolors = arguments[1].split(','); + for (i = 0; i < rgbcolors.length; i = i + 1) { + rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16); + if (rgbcolors[i].length === 1) { + rgbcolors[i] = '0' + rgbcolors[i]; + } + } + return '#' + rgbcolors.join(''); + }); + + // Shorten colors from #AABBCC to #ABC. + css = this._compressHexColors(css); + + // border: none -> border:0 + css = css.replace(/(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|\})/gi, function(all, prop, tail) { + return prop.toLowerCase() + ":0" + tail; + }); + + // shorter opacity IE filter + css = css.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); + + // Remove empty rules. + css = css.replace(/[^\};\{\/]+\{\}/g, ""); + + if (linebreakpos >= 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + startIndex = 0; + i = 0; + while (i < css.length) { + i = i + 1; + if (css[i - 1] === '}' && i - startIndex > linebreakpos) { + css = css.slice(0, i) + '\n' + css.slice(i); + startIndex = i; + } + } + } + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + css = css.replace(/;;+/g, ";"); + + // restore preserved comments and strings + for (i = 0, max = preservedTokens.length; i < max; i = i + 1) { + css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]); + } + + // Trim the final string (for any leading or trailing white spaces) + css = css.replace(/^\s+|\s+$/g, ""); + + return css; + +}; + +exports.compressor = YAHOO.compressor; diff --git a/bin/lib/less/functions.js b/bin/lib/less/functions.js new file mode 100644 index 0000000..6eb34ba --- /dev/null +++ b/bin/lib/less/functions.js @@ -0,0 +1,228 @@ +(function (tree) { + +tree.functions = { + rgb: function (r, g, b) { + return this.rgba(r, g, b, 1.0); + }, + rgba: function (r, g, b, a) { + var rgb = [r, g, b].map(function (c) { return number(c) }), + a = number(a); + return new(tree.Color)(rgb, a); + }, + hsl: function (h, s, l) { + return this.hsla(h, s, l, 1.0); + }, + hsla: function (h, s, l, a) { + h = (number(h) % 360) / 360; + s = number(s); l = number(l); a = number(a); + + var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; + var m1 = l * 2 - m2; + + return this.rgba(hue(h + 1/3) * 255, + hue(h) * 255, + hue(h - 1/3) * 255, + a); + + function hue(h) { + h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); + if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; + else if (h * 2 < 1) return m2; + else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6; + else return m1; + } + }, + hue: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().h)); + }, + saturation: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%'); + }, + lightness: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%'); + }, + alpha: function (color) { + return new(tree.Dimension)(color.toHSL().a); + }, + saturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s += amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + desaturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s -= amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + lighten: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l += amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + darken: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l -= amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + fadein: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a += amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fadeout: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a -= amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fade: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a = amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + spin: function (color, amount) { + var hsl = color.toHSL(); + var hue = (hsl.h + amount.value) % 360; + + hsl.h = hue < 0 ? 360 + hue : hue; + + return hsla(hsl); + }, + // + // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein + // http://sass-lang.com + // + mix: function (color1, color2, weight) { + var p = weight.value / 100.0; + var w = p * 2 - 1; + var a = color1.toHSL().a - color2.toHSL().a; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, + color1.rgb[1] * w1 + color2.rgb[1] * w2, + color1.rgb[2] * w1 + color2.rgb[2] * w2]; + + var alpha = color1.alpha * p + color2.alpha * (1 - p); + + return new(tree.Color)(rgb, alpha); + }, + greyscale: function (color) { + return this.desaturate(color, new(tree.Dimension)(100)); + }, + e: function (str) { + return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str); + }, + escape: function (str) { + return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); + }, + '%': function (quoted /* arg, arg, ...*/) { + var args = Array.prototype.slice.call(arguments, 1), + str = quoted.value; + + for (var i = 0; i < args.length; i++) { + str = str.replace(/%[sda]/i, function(token) { + var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); + return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; + }); + } + str = str.replace(/%%/g, '%'); + return new(tree.Quoted)('"' + str + '"', str); + }, + round: function (n) { + return this._math('round', n); + }, + ceil: function (n) { + return this._math('ceil', n); + }, + floor: function (n) { + return this._math('floor', n); + }, + _math: function (fn, n) { + if (n instanceof tree.Dimension) { + return new(tree.Dimension)(Math[fn](number(n)), n.unit); + } else if (typeof(n) === 'number') { + return Math[fn](n); + } else { + throw { type: "Argument", message: "argument must be a number" }; + } + }, + argb: function (color) { + return new(tree.Anonymous)(color.toARGB()); + + }, + percentage: function (n) { + return new(tree.Dimension)(n.value * 100, '%'); + }, + color: function (n) { + if (n instanceof tree.Quoted) { + return new(tree.Color)(n.value.slice(1)); + } else { + throw { type: "Argument", message: "argument must be a string" }; + } + }, + iscolor: function (n) { + return this._isa(n, tree.Color); + }, + isnumber: function (n) { + return this._isa(n, tree.Dimension); + }, + isstring: function (n) { + return this._isa(n, tree.Quoted); + }, + iskeyword: function (n) { + return this._isa(n, tree.Keyword); + }, + isurl: function (n) { + return this._isa(n, tree.URL); + }, + ispixel: function (n) { + return (n instanceof tree.Dimension) && n.unit === 'px' ? tree.True : tree.False; + }, + ispercentage: function (n) { + return (n instanceof tree.Dimension) && n.unit === '%' ? tree.True : tree.False; + }, + isem: function (n) { + return (n instanceof tree.Dimension) && n.unit === 'em' ? tree.True : tree.False; + }, + _isa: function (n, Type) { + return (n instanceof Type) ? tree.True : tree.False; + } +}; + +function hsla(hsla) { + return tree.functions.hsla(hsla.h, hsla.s, hsla.l, hsla.a); +} + +function number(n) { + if (n instanceof tree.Dimension) { + return parseFloat(n.unit == '%' ? n.value / 100 : n.value); + } else if (typeof(n) === 'number') { + return n; + } else { + throw { + error: "RuntimeError", + message: "color functions take numbers as parameters" + }; + } +} + +function clamp(val) { + return Math.min(1, Math.max(0, val)); +} + +})(require('./tree')); diff --git a/bin/lib/less/index.js b/bin/lib/less/index.js new file mode 100644 index 0000000..a11fa99 --- /dev/null +++ b/bin/lib/less/index.js @@ -0,0 +1,148 @@ +var path = require('path'), + sys = require('util'), + fs = require('fs'); + +var less = { + version: [1, 3, 0], + Parser: require('./parser').Parser, + importer: require('./parser').importer, + tree: require('./tree'), + render: function (input, options, callback) { + options = options || {}; + + if (typeof(options) === 'function') { + callback = options, options = {}; + } + + var parser = new(less.Parser)(options), + ee; + + if (callback) { + parser.parse(input, function (e, root) { + callback(e, root && root.toCSS && root.toCSS(options)); + }); + } else { + ee = new(require('events').EventEmitter); + + process.nextTick(function () { + parser.parse(input, function (e, root) { + if (e) { ee.emit('error', e) } + else { ee.emit('success', root.toCSS(options)) } + }); + }); + return ee; + } + }, + writeError: function (ctx, options) { + options = options || {}; + + var message = ""; + var extract = ctx.extract; + var error = []; + var stylize = options.color ? less.stylize : function (str) { return str }; + + if (options.silent) { return } + + if (ctx.stack) { return sys.error(stylize(ctx.stack, 'red')) } + + if (!ctx.hasOwnProperty('index')) { + return sys.error(ctx.stack || ctx.message); + } + + if (typeof(extract[0]) === 'string') { + error.push(stylize((ctx.line - 1) + ' ' + extract[0], 'grey')); + } + + if (extract[1]) { + error.push(ctx.line + ' ' + extract[1].slice(0, ctx.column) + + stylize(stylize(stylize(extract[1][ctx.column], 'bold') + + extract[1].slice(ctx.column + 1), 'red'), 'inverse')); + } + + if (typeof(extract[2]) === 'string') { + error.push(stylize((ctx.line + 1) + ' ' + extract[2], 'grey')); + } + error = error.join('\n') + '\033[0m\n'; + + message += stylize(ctx.type + 'Error: ' + ctx.message, 'red'); + ctx.filename && (message += stylize(' in ', 'red') + ctx.filename + + stylize(':' + ctx.line + ':' + ctx.column, 'grey')); + + sys.error(message, error); + + if (ctx.callLine) { + sys.error(stylize('from ', 'red') + (ctx.filename || '')); + sys.error(stylize(ctx.callLine, 'grey') + ' ' + ctx.callExtract); + } + } +}; + +['color', 'directive', 'operation', 'dimension', + 'keyword', 'variable', 'ruleset', 'element', + 'selector', 'quoted', 'expression', 'rule', + 'call', 'url', 'alpha', 'import', + 'mixin', 'comment', 'anonymous', 'value', + 'javascript', 'assignment', 'condition', 'paren', + 'media' +].forEach(function (n) { + require('./tree/' + n); +}); + +less.Parser.importer = function (file, paths, callback, env) { + var pathname; + + // TODO: Undo this at some point, + // or use different approach. + paths.unshift('.'); + + for (var i = 0; i < paths.length; i++) { + try { + pathname = path.join(paths[i], file); + fs.statSync(pathname); + break; + } catch (e) { + pathname = null; + } + } + + if (pathname) { + fs.readFile(pathname, 'utf-8', function(e, data) { + if (e) return callback(e); + + new(less.Parser)({ + paths: [path.dirname(pathname)].concat(paths), + filename: pathname + }).parse(data, function (e, root) { + callback(e, root, data); + }); + }); + } else { + if (typeof(env.errback) === "function") { + env.errback(file, paths, callback); + } else { + callback({ type: 'File', message: "'" + file + "' wasn't found.\n" }); + } + } +} + +require('./functions'); +require('./colors'); + +for (var k in less) { exports[k] = less[k] } + +// Stylize a string +function stylize(str, style) { + var styles = { + 'bold' : [1, 22], + 'inverse' : [7, 27], + 'underline' : [4, 24], + 'yellow' : [33, 39], + 'green' : [32, 39], + 'red' : [31, 39], + 'grey' : [90, 39] + }; + return '\033[' + styles[style][0] + 'm' + str + + '\033[' + styles[style][1] + 'm'; +} +less.stylize = stylize; + diff --git a/bin/lib/less/parser.js b/bin/lib/less/parser.js new file mode 100644 index 0000000..238d83d --- /dev/null +++ b/bin/lib/less/parser.js @@ -0,0 +1,1334 @@ +var less, tree; + +if (typeof environment === "object" && ({}).toString.call(environment) === "[object Environment]") { + // Rhino + // Details on how to detect Rhino: https://github.com/ringo/ringojs/issues/88 + if (typeof(window) === 'undefined') { less = {} } + else { less = window.less = {} } + tree = less.tree = {}; + less.mode = 'rhino'; +} else if (typeof(window) === 'undefined') { + // Node.js + less = exports, + tree = require('./tree'); + less.mode = 'node'; +} else { + // Browser + if (typeof(window.less) === 'undefined') { window.less = {} } + less = window.less, + tree = window.less.tree = {}; + less.mode = 'browser'; +} +// +// less.js - parser +// +// A relatively straight-forward predictive parser. +// There is no tokenization/lexing stage, the input is parsed +// in one sweep. +// +// To make the parser fast enough to run in the browser, several +// optimization had to be made: +// +// - Matching and slicing on a huge input is often cause of slowdowns. +// The solution is to chunkify the input into smaller strings. +// The chunks are stored in the `chunks` var, +// `j` holds the current chunk index, and `current` holds +// the index of the current chunk in relation to `input`. +// This gives us an almost 4x speed-up. +// +// - In many cases, we don't need to match individual tokens; +// for example, if a value doesn't hold any variables, operations +// or dynamic references, the parser can effectively 'skip' it, +// treating it as a literal. +// An example would be '1px solid #000' - which evaluates to itself, +// we don't need to know what the individual components are. +// The drawback, of course is that you don't get the benefits of +// syntax-checking on the CSS. This gives us a 50% speed-up in the parser, +// and a smaller speed-up in the code-gen. +// +// +// Token matching is done with the `$` function, which either takes +// a terminal string or regexp, or a non-terminal function to call. +// It also takes care of moving all the indices forwards. +// +// +less.Parser = function Parser(env) { + var input, // LeSS input string + i, // current index in `input` + j, // current chunk + temp, // temporarily holds a chunk's state, for backtracking + memo, // temporarily holds `i`, when backtracking + furthest, // furthest index the parser has gone to + chunks, // chunkified input + current, // index of current chunk, in `input` + parser; + + var that = this; + + // This function is called after all files + // have been imported through `@import`. + var finish = function () {}; + + var imports = this.imports = { + paths: env && env.paths || [], // Search paths, when importing + queue: [], // Files which haven't been imported yet + files: {}, // Holds the imported parse trees + contents: {}, // Holds the imported file contents + mime: env && env.mime, // MIME type of .less files + error: null, // Error in parsing/evaluating an import + push: function (path, callback) { + var that = this; + this.queue.push(path); + + // + // Import a file asynchronously + // + less.Parser.importer(path, this.paths, function (e, root, contents) { + that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue + + var imported = path in that.files; + + that.files[path] = root; // Store the root + that.contents[path] = contents; + + if (e && !that.error) { that.error = e } + + callback(e, root, imported); + + if (that.queue.length === 0) { finish() } // Call `finish` if we're done importing + }, env); + } + }; + + function save() { temp = chunks[j], memo = i, current = i } + function restore() { chunks[j] = temp, i = memo, current = i } + + function sync() { + if (i > current) { + chunks[j] = chunks[j].slice(i - current); + current = i; + } + } + // + // Parse from a token, regexp or string, and move forward if match + // + function $(tok) { + var match, args, length, c, index, endIndex, k, mem; + + // + // Non-terminal + // + if (tok instanceof Function) { + return tok.call(parser.parsers); + // + // Terminal + // + // Either match a single character in the input, + // or match a regexp in the current chunk (chunk[j]). + // + } else if (typeof(tok) === 'string') { + match = input.charAt(i) === tok ? tok : null; + length = 1; + sync (); + } else { + sync (); + + if (match = tok.exec(chunks[j])) { + length = match[0].length; + } else { + return null; + } + } + + // The match is confirmed, add the match length to `i`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + // + if (match) { + mem = i += length; + endIndex = i + chunks[j].length - length; + + while (i < endIndex) { + c = input.charCodeAt(i); + if (! (c === 32 || c === 10 || c === 9)) { break } + i++; + } + chunks[j] = chunks[j].slice(length + (i - mem)); + current = i; + + if (chunks[j].length === 0 && j < chunks.length - 1) { j++ } + + if(typeof(match) === 'string') { + return match; + } else { + return match.length === 1 ? match[0] : match; + } + } + } + + function expect(arg, msg) { + var result = $(arg); + if (! result) { + error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'" + : "unexpected token")); + } else { + return result; + } + } + + function error(msg, type) { + throw { index: i, type: type || 'Syntax', message: msg }; + } + + // Same as $(), but don't change the state of the parser, + // just return the match. + function peek(tok) { + if (typeof(tok) === 'string') { + return input.charAt(i) === tok; + } else { + if (tok.test(chunks[j])) { + return true; + } else { + return false; + } + } + } + + function basename(pathname) { + if (less.mode === 'node') { + return require('path').basename(pathname); + } else { + return pathname.match(/[^\/]+$/)[0]; + } + } + + function getInput(e, env) { + if (e.filename && env.filename && (e.filename !== env.filename)) { + return parser.imports.contents[basename(e.filename)]; + } else { + return input; + } + } + + function getLocation(index, input) { + for (var n = index, column = -1; + n >= 0 && input.charAt(n) !== '\n'; + n--) { column++ } + + return { line: typeof(index) === 'number' ? (input.slice(0, index).match(/\n/g) || "").length : null, + column: column }; + } + + function LessError(e, env) { + var input = getInput(e, env), + loc = getLocation(e.index, input), + line = loc.line, + col = loc.column, + lines = input.split('\n'); + + this.type = e.type || 'Syntax'; + this.message = e.message; + this.filename = e.filename || env.filename; + this.index = e.index; + this.line = typeof(line) === 'number' ? line + 1 : null; + this.callLine = e.call && (getLocation(e.call, input).line + 1); + this.callExtract = lines[getLocation(e.call, input).line]; + this.stack = e.stack; + this.column = col; + this.extract = [ + lines[line - 1], + lines[line], + lines[line + 1] + ]; + } + + this.env = env = env || {}; + + // The optimization level dictates the thoroughness of the parser, + // the lower the number, the less nodes it will create in the tree. + // This could matter for debugging, or if you want to access + // the individual nodes in the tree. + this.optimization = ('optimization' in this.env) ? this.env.optimization : 1; + + this.env.filename = this.env.filename || null; + + // + // The Parser + // + return parser = { + + imports: imports, + // + // Parse an input string into an abstract syntax tree, + // call `callback` when done. + // + parse: function (str, callback) { + var root, start, end, zone, line, lines, buff = [], c, error = null; + + i = j = current = furthest = 0; + input = str.replace(/\r\n/g, '\n'); + + // Split the input into chunks. + chunks = (function (chunks) { + var j = 0, + skip = /[^"'`\{\}\/\(\)\\]+/g, + comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g, + string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`\\\r\n]|\\.)*)`/g, + level = 0, + match, + chunk = chunks[0], + inParam; + + for (var i = 0, c, cc; i < input.length; i++) { + skip.lastIndex = i; + if (match = skip.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + } + } + c = input.charAt(i); + comment.lastIndex = string.lastIndex = i; + + if (match = string.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + c = input.charAt(i); + } + } + + if (!inParam && c === '/') { + cc = input.charAt(i + 1); + if (cc === '/' || cc === '*') { + if (match = comment.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + c = input.charAt(i); + } + } + } + } + + switch (c) { + case '{': if (! inParam) { level ++; chunk.push(c); break } + case '}': if (! inParam) { level --; chunk.push(c); chunks[++j] = chunk = []; break } + case '(': if (! inParam) { inParam = true; chunk.push(c); break } + case ')': if ( inParam) { inParam = false; chunk.push(c); break } + default: chunk.push(c); + } + } + if (level > 0) { + error = new(LessError)({ + index: i, + type: 'Parse', + message: "missing closing `}`", + filename: env.filename + }, env); + } + + return chunks.map(function (c) { return c.join('') });; + })([[]]); + + if (error) { + return callback(error); + } + + // Start with the primary rule. + // The whole syntax tree is held under a Ruleset node, + // with the `root` property set to true, so no `{}` are + // output. The callback is called when the input is parsed. + try { + root = new(tree.Ruleset)([], $(this.parsers.primary)); + root.root = true; + } catch (e) { + return callback(new(LessError)(e, env)); + } + + root.toCSS = (function (evaluate) { + var line, lines, column; + + return function (options, variables) { + var frames = [], importError; + + options = options || {}; + // + // Allows setting variables with a hash, so: + // + // `{ color: new(tree.Color)('#f01') }` will become: + // + // new(tree.Rule)('@color', + // new(tree.Value)([ + // new(tree.Expression)([ + // new(tree.Color)('#f01') + // ]) + // ]) + // ) + // + if (typeof(variables) === 'object' && !Array.isArray(variables)) { + variables = Object.keys(variables).map(function (k) { + var value = variables[k]; + + if (! (value instanceof tree.Value)) { + if (! (value instanceof tree.Expression)) { + value = new(tree.Expression)([value]); + } + value = new(tree.Value)([value]); + } + return new(tree.Rule)('@' + k, value, false, 0); + }); + frames = [new(tree.Ruleset)(null, variables)]; + } + + try { + var css = evaluate.call(this, { frames: frames }) + .toCSS([], { compress: options.compress || false }); + } catch (e) { + throw new(LessError)(e, env); + } + + if ((importError = parser.imports.error)) { // Check if there was an error during importing + if (importError instanceof LessError) throw importError; + else throw new(LessError)(importError, env); + } + + if (options.yuicompress && less.mode === 'node') { + return require('./cssmin').compressor.cssmin(css); + } else if (options.compress) { + return css.replace(/(\s)+/g, "$1"); + } else { + return css; + } + }; + })(root.eval); + + // If `i` is smaller than the `input.length - 1`, + // it means the parser wasn't able to parse the whole + // string, so we've got a parsing error. + // + // We try to extract a \n delimited string, + // showing the line where the parse error occurred. + // We split it up into two parts (the part which parsed, + // and the part which didn't), so we can color them differently. + if (i < input.length - 1) { + i = furthest; + lines = input.split('\n'); + line = (input.slice(0, i).match(/\n/g) || "").length + 1; + + for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ } + + error = { + type: "Parse", + message: "Syntax Error on line " + line, + index: i, + filename: env.filename, + line: line, + column: column, + extract: [ + lines[line - 2], + lines[line - 1], + lines[line] + ] + }; + } + + if (this.imports.queue.length > 0) { + finish = function () { callback(error, root) }; + } else { + callback(error, root); + } + }, + + // + // Here in, the parsing rules/functions + // + // The basic structure of the syntax tree generated is as follows: + // + // Ruleset -> Rule -> Value -> Expression -> Entity + // + // Here's some LESS code: + // + // .class { + // color: #fff; + // border: 1px solid #000; + // width: @w + 4px; + // > .child {...} + // } + // + // And here's what the parse tree might look like: + // + // Ruleset (Selector '.class', [ + // Rule ("color", Value ([Expression [Color #fff]])) + // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) + // Ruleset (Selector [Element '>', '.child'], [...]) + // ]) + // + // In general, most rules will try to parse a token with the `$()` function, and if the return + // value is truly, will return a new node, of the relevant type. Sometimes, we need to check + // first, before parsing, that's when we use `peek()`. + // + parsers: { + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + primary: function () { + var node, root = []; + + while ((node = $(this.mixin.definition) || $(this.rule) || $(this.ruleset) || + $(this.mixin.call) || $(this.comment) || $(this.directive)) + || $(/^[\s\n]+/)) { + node && root.push(node); + } + return root; + }, + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them. + comment: function () { + var comment; + + if (input.charAt(i) !== '/') return; + + if (input.charAt(i + 1) === '/') { + return new(tree.Comment)($(/^\/\/.*/), true); + } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) { + return new(tree.Comment)(comment); + } + }, + + // + // Entities are tokens which can be found inside an Expression + // + entities: { + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + quoted: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '"' && input.charAt(j) !== "'") return; + + e && $('~'); + + if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) { + return new(tree.Quoted)(str[0], str[1] || str[2], e); + } + }, + + // + // A catch-all word, such as: + // + // black border-collapse + // + keyword: function () { + var k; + + if (k = $(/^[_A-Za-z-][_A-Za-z0-9-]*/)) { + if (tree.colors.hasOwnProperty(k)) { + // detect named color + return new(tree.Color)(tree.colors[k].slice(1)); + } else { + return new(tree.Keyword)(k); + } + } + }, + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + call: function () { + var name, args, index = i; + + if (! (name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(chunks[j]))) return; + + name = name[1].toLowerCase(); + + if (name === 'url') { return null } + else { i += name.length } + + if (name === 'alpha') { return $(this.alpha) } + + $('('); // Parse the '(' and consume whitespace. + + args = $(this.entities.arguments); + + if (! $(')')) return; + + if (name) { return new(tree.Call)(name, args, index, env.filename) } + }, + arguments: function () { + var args = [], arg; + + while (arg = $(this.entities.assignment) || $(this.expression)) { + args.push(arg); + if (! $(',')) { break } + } + return args; + }, + literal: function () { + return $(this.entities.dimension) || + $(this.entities.color) || + $(this.entities.quoted); + }, + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + + assignment: function () { + var key, value; + if ((key = $(/^\w+(?=\s?=)/i)) && $('=') && (value = $(this.entity))) { + return new(tree.Assignment)(key, value); + } + }, + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + url: function () { + var value; + + if (input.charAt(i) !== 'u' || !$(/^url\(/)) return; + value = $(this.entities.quoted) || $(this.entities.variable) || + $(this.entities.dataURI) || $(/^[-\w%@$\/.&=:;#+?~]+/) || ""; + + expect(')'); + + return new(tree.URL)((value.value || value.data || value instanceof tree.Variable) + ? value : new(tree.Anonymous)(value), imports.paths); + }, + + dataURI: function () { + var obj; + + if ($(/^data:/)) { + obj = {}; + obj.mime = $(/^[^\/]+\/[^,;)]+/) || ''; + obj.charset = $(/^;\s*charset=[^,;)]+/) || ''; + obj.base64 = $(/^;\s*base64/) || ''; + obj.data = $(/^,\s*[^)]+/); + + if (obj.data) { return obj } + } + }, + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + variable: function () { + var name, index = i; + + if (input.charAt(i) === '@' && (name = $(/^@@?[\w-]+/))) { + return new(tree.Variable)(name, index, env.filename); + } + }, + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + color: function () { + var rgb; + + if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) { + return new(tree.Color)(rgb[1]); + } + }, + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + dimension: function () { + var value, c = input.charCodeAt(i); + if ((c > 57 || c < 45) || c === 47) return; + + if (value = $(/^(-?\d*\.?\d+)(px|%|em|rem|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn|dpi)?/)) { + return new(tree.Dimension)(value[1], value[2]); + } + }, + + // + // JavaScript code to be evaluated + // + // `window.location.href` + // + javascript: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++, e = true } // Escaped strings + if (input.charAt(j) !== '`') { return } + + e && $('~'); + + if (str = $(/^`([^`]*)`/)) { + return new(tree.JavaScript)(str[1], i, e); + } + } + }, + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + variable: function () { + var name; + + if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] } + }, + + // + // A font size/line-height shorthand + // + // small/12px + // + // We need to peek first, or we'll match on keywords and dimensions + // + shorthand: function () { + var a, b; + + if (! peek(/^[@\w.%-]+\/[@\w.-]+/)) return; + + if ((a = $(this.entity)) && $('/') && (b = $(this.entity))) { + return new(tree.Shorthand)(a, b); + } + }, + + // + // Mixins + // + mixin: { + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + call: function () { + var elements = [], e, c, args = [], arg, index = i, s = input.charAt(i), name, value, important = false; + + if (s !== '.' && s !== '#') { return } + + while (e = $(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)) { + elements.push(new(tree.Element)(c, e, i)); + c = $('>'); + } + if ($('(')) { + while (arg = $(this.expression)) { + value = arg; + name = null; + + // Variable + if (arg.value.length == 1) { + var val = arg.value[0]; + if (val instanceof tree.Variable) { + if ($(':')) { + if (value = $(this.expression)) { + name = val.name; + } else { + throw new(Error)("Expected value"); + } + } + } + } + + args.push({ name: name, value: value }); + + if (! $(',')) { break } + } + if (! $(')')) throw new(Error)("Expected )"); + } + + if ($(this.important)) { + important = true; + } + + if (elements.length > 0 && ($(';') || peek('}'))) { + return new(tree.mixin.Call)(elements, args, index, env.filename, important); + } + }, + + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + definition: function () { + var name, params = [], match, ruleset, param, value, cond, variadic = false; + if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') || + peek(/^[^{]*(;|})/)) return; + + save(); + + if (match = $(/^([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)\s*\(/)) { + name = match[1]; + + do { + if (input.charAt(i) === '.' && $(/^\.{3}/)) { + variadic = true; + break; + } else if (param = $(this.entities.variable) || $(this.entities.literal) + || $(this.entities.keyword)) { + // Variable + if (param instanceof tree.Variable) { + if ($(':')) { + value = expect(this.expression, 'expected expression'); + params.push({ name: param.name, value: value }); + } else if ($(/^\.{3}/)) { + params.push({ name: param.name, variadic: true }); + variadic = true; + break; + } else { + params.push({ name: param.name }); + } + } else { + params.push({ value: param }); + } + } else { + break; + } + } while ($(',')) + + expect(')'); + + if ($(/^when/)) { // Guard + cond = expect(this.conditions, 'expected condition'); + } + + ruleset = $(this.block); + + if (ruleset) { + return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic); + } else { + restore(); + } + } + } + }, + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + entity: function () { + return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) || + $(this.entities.call) || $(this.entities.keyword) || $(this.entities.javascript) || + $(this.comment); + }, + + // + // A Rule terminator. Note that we use `peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was omitted. + // + end: function () { + return $(';') || peek('}'); + }, + + // + // IE's alpha function + // + // alpha(opacity=88) + // + alpha: function () { + var value; + + if (! $(/^\(opacity=/i)) return; + if (value = $(/^\d+/) || $(this.entities.variable)) { + expect(')'); + return new(tree.Alpha)(value); + } + }, + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + element: function () { + var e, t, c, v; + + c = $(this.combinator); + e = $(/^(?:\d+\.\d+|\d+)%/) || $(/^(?:[.#]?|:*)(?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/) || + $('*') || $(this.attribute) || $(/^\([^)@]+\)/); + + if (! e) { + $('(') && (v = $(this.entities.variable)) && $(')') && (e = new(tree.Paren)(v)); + } + + if (e) { return new(tree.Element)(c, e, i) } + + if (c.value && c.value.charAt(0) === '&') { + return new(tree.Element)(c, null, i); + } + }, + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. More info on how + // we deal with this in *combinator.js*. + // + combinator: function () { + var match, c = input.charAt(i); + + if (c === '>' || c === '+' || c === '~') { + i++; + while (input.charAt(i) === ' ') { i++ } + return new(tree.Combinator)(c); + } else if (c === '&') { + match = '&'; + i++; + if(input.charAt(i) === ' ') { + match = '& '; + } + while (input.charAt(i) === ' ') { i++ } + return new(tree.Combinator)(match); + } else if (input.charAt(i - 1) === ' ') { + return new(tree.Combinator)(" "); + } else { + return new(tree.Combinator)(null); + } + }, + + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + selector: function () { + var sel, e, elements = [], c, match; + + if ($('(')) { + sel = $(this.entity); + expect(')'); + return new(tree.Selector)([new(tree.Element)('', sel, i)]); + } + + while (e = $(this.element)) { + c = input.charAt(i); + elements.push(e) + if (c === '{' || c === '}' || c === ';' || c === ',') { break } + } + + if (elements.length > 0) { return new(tree.Selector)(elements) } + }, + tag: function () { + return $(/^[a-zA-Z][a-zA-Z-]*[0-9]?/) || $('*'); + }, + attribute: function () { + var attr = '', key, val, op; + + if (! $('[')) return; + + if (key = $(/^[a-zA-Z-]+/) || $(this.entities.quoted)) { + if ((op = $(/^[|~*$^]?=/)) && + (val = $(this.entities.quoted) || $(/^[\w-]+/))) { + attr = [key, op, val.toCSS ? val.toCSS() : val].join(''); + } else { attr = key } + } + + if (! $(']')) return; + + if (attr) { return "[" + attr + "]" } + }, + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + block: function () { + var content; + + if ($('{') && (content = $(this.primary)) && $('}')) { + return content; + } + }, + + // + // div, .class, body > p {...} + // + ruleset: function () { + var selectors = [], s, rules, match; + save(); + + while (s = $(this.selector)) { + selectors.push(s); + $(this.comment); + if (! $(',')) { break } + $(this.comment); + } + + if (selectors.length > 0 && (rules = $(this.block))) { + return new(tree.Ruleset)(selectors, rules, env.strictImports); + } else { + // Backtrack + furthest = i; + restore(); + } + }, + rule: function () { + var name, value, c = input.charAt(i), important, match; + save(); + + if (c === '.' || c === '#' || c === '&') { return } + + if (name = $(this.variable) || $(this.property)) { + if ((name.charAt(0) != '@') && (match = /^([^@+\/'"*`(;{}-]*);/.exec(chunks[j]))) { + i += match[0].length - 1; + value = new(tree.Anonymous)(match[1]); + } else if (name === "font") { + value = $(this.font); + } else { + value = $(this.value); + } + important = $(this.important); + + if (value && $(this.end)) { + return new(tree.Rule)(name, value, important, memo); + } else { + furthest = i; + restore(); + } + } + }, + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environemnt, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + "import": function () { + var path, features, index = i; + var dir = $(/^@import(?:-(once))?\s+/); + + if (dir && (path = $(this.entities.quoted) || $(this.entities.url))) { + features = $(this.mediaFeatures); + if ($(';')) { + return new(tree.Import)(path, imports, features, (dir[1] === 'once'), index); + } + } + }, + + mediaFeature: function () { + var e, p, nodes = []; + + do { + if (e = $(this.entities.keyword)) { + nodes.push(e); + } else if ($('(')) { + p = $(this.property); + e = $(this.entity); + if ($(')')) { + if (p && e) { + nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, i, true))); + } else if (e) { + nodes.push(new(tree.Paren)(e)); + } else { + return null; + } + } else { return null } + } + } while (e); + + if (nodes.length > 0) { + return new(tree.Expression)(nodes); + } + }, + + mediaFeatures: function () { + var e, features = []; + + do { + if (e = $(this.mediaFeature)) { + features.push(e); + if (! $(',')) { break } + } else if (e = $(this.entities.variable)) { + features.push(e); + if (! $(',')) { break } + } + } while (e); + + return features.length > 0 ? features : null; + }, + + media: function () { + var features, rules; + + if ($(/^@media/)) { + features = $(this.mediaFeatures); + + if (rules = $(this.block)) { + return new(tree.Media)(rules, features); + } + } + }, + + // + // A CSS Directive + // + // @charset "utf-8"; + // + directive: function () { + var name, value, rules, types, e, nodes; + + if (input.charAt(i) !== '@') return; + + if (value = $(this['import']) || $(this.media)) { + return value; + } else if (name = $(/^@page|@keyframes/) || $(/^@(?:-webkit-|-moz-|-o-|-ms-)[a-z0-9-]+/)) { + types = ($(/^[^{]+/) || '').trim(); + if (rules = $(this.block)) { + return new(tree.Directive)(name + " " + types, rules); + } + } else if (name = $(/^@[-a-z]+/)) { + if (name === '@font-face') { + if (rules = $(this.block)) { + return new(tree.Directive)(name, rules); + } + } else if ((value = $(this.entity)) && $(';')) { + return new(tree.Directive)(name, value); + } + } + }, + font: function () { + var value = [], expression = [], weight, shorthand, font, e; + + while (e = $(this.shorthand) || $(this.entity)) { + expression.push(e); + } + value.push(new(tree.Expression)(expression)); + + if ($(',')) { + while (e = $(this.expression)) { + value.push(e); + if (! $(',')) { break } + } + } + return new(tree.Value)(value); + }, + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + value: function () { + var e, expressions = [], important; + + while (e = $(this.expression)) { + expressions.push(e); + if (! $(',')) { break } + } + + if (expressions.length > 0) { + return new(tree.Value)(expressions); + } + }, + important: function () { + if (input.charAt(i) === '!') { + return $(/^! *important/); + } + }, + sub: function () { + var e; + + if ($('(') && (e = $(this.expression)) && $(')')) { + return e; + } + }, + multiplication: function () { + var m, a, op, operation; + if (m = $(this.operand)) { + while (!peek(/^\/\*/) && (op = ($('/') || $('*'))) && (a = $(this.operand))) { + operation = new(tree.Operation)(op, [operation || m, a]); + } + return operation || m; + } + }, + addition: function () { + var m, a, op, operation; + if (m = $(this.multiplication)) { + while ((op = $(/^[-+]\s+/) || (input.charAt(i - 1) != ' ' && ($('+') || $('-')))) && + (a = $(this.multiplication))) { + operation = new(tree.Operation)(op, [operation || m, a]); + } + return operation || m; + } + }, + conditions: function () { + var a, b, index = i, condition; + + if (a = $(this.condition)) { + while ($(',') && (b = $(this.condition))) { + condition = new(tree.Condition)('or', condition || a, b, index); + } + return condition || a; + } + }, + condition: function () { + var a, b, c, op, index = i, negate = false; + + if ($(/^not/)) { negate = true } + expect('('); + if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + if (op = $(/^(?:>=|=<|[<=>])/)) { + if (b = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + c = new(tree.Condition)(op, a, b, index, negate); + } else { + error('expected expression'); + } + } else { + c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate); + } + expect(')'); + return $(/^and/) ? new(tree.Condition)('and', c, $(this.condition)) : c; + } + }, + + // + // An operand is anything that can be part of an operation, + // such as a Color, or a Variable + // + operand: function () { + var negate, p = input.charAt(i + 1); + + if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') } + var o = $(this.sub) || $(this.entities.dimension) || + $(this.entities.color) || $(this.entities.variable) || + $(this.entities.call); + return negate ? new(tree.Operation)('*', [new(tree.Dimension)(-1), o]) + : o; + }, + + // + // Expressions either represent mathematical operations, + // or white-space delimited Entities. + // + // 1px solid black + // @var * 2 + // + expression: function () { + var e, delim, entities = [], d; + + while (e = $(this.addition) || $(this.entity)) { + entities.push(e); + } + if (entities.length > 0) { + return new(tree.Expression)(entities); + } + }, + property: function () { + var name; + + if (name = $(/^(\*?-?[-a-z_0-9]+)\s*:/)) { + return name[1]; + } + } + } + }; +}; + +if (less.mode === 'browser' || less.mode === 'rhino') { + // + // Used by `@import` directives + // + less.Parser.importer = function (path, paths, callback, env) { + if (!/^([a-z]+:)?\//.test(path) && paths.length > 0) { + path = paths[0] + path; + } + // We pass `true` as 3rd argument, to force the reload of the import. + // This is so we can get the syntax tree as opposed to just the CSS output, + // as we need this to evaluate the current stylesheet. + loadStyleSheet({ href: path, title: path, type: env.mime }, function (e) { + if (e && typeof(env.errback) === "function") { + env.errback.call(null, path, paths, callback, env); + } else { + callback.apply(null, arguments); + } + }, true); + }; +} + diff --git a/bin/lib/less/rhino.js b/bin/lib/less/rhino.js new file mode 100644 index 0000000..a2c5662 --- /dev/null +++ b/bin/lib/less/rhino.js @@ -0,0 +1,62 @@ +var name; + +function loadStyleSheet(sheet, callback, reload, remaining) { + var sheetName = name.slice(0, name.lastIndexOf('/') + 1) + sheet.href; + var input = readFile(sheetName); + var parser = new less.Parser({ + paths: [sheet.href.replace(/[\w\.-]+$/, '')] + }); + parser.parse(input, function (e, root) { + if (e) { + print("Error: " + e); + quit(1); + } + callback(root, sheet, { local: false, lastModified: 0, remaining: remaining }); + }); + + // callback({}, sheet, { local: true, remaining: remaining }); +} + +function writeFile(filename, content) { + var fstream = new java.io.FileWriter(filename); + var out = new java.io.BufferedWriter(fstream); + out.write(content); + out.close(); +} + +// Command line integration via Rhino +(function (args) { + name = args[0]; + var output = args[1]; + + if (!name) { + print('No files present in the fileset; Check your pattern match in build.xml'); + quit(1); + } + path = name.split("/");path.pop();path=path.join("/") + + var input = readFile(name); + + if (!input) { + print('lesscss: couldn\'t open file ' + name); + quit(1); + } + + var result; + var parser = new less.Parser(); + parser.parse(input, function (e, root) { + if (e) { + quit(1); + } else { + result = root.toCSS(); + if (output) { + writeFile(output, result); + print("Written to " + output); + } else { + print(result); + } + quit(0); + } + }); + print("done"); +}(arguments)); diff --git a/bin/lib/less/tree.js b/bin/lib/less/tree.js new file mode 100644 index 0000000..24ecd71 --- /dev/null +++ b/bin/lib/less/tree.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.find = function (obj, fun) { + for (var i = 0, r; i < obj.length; i++) { + if (r = fun.call(obj, obj[i])) { return r } + } + return null; +}; +tree.jsify = function (obj) { + if (Array.isArray(obj.value) && (obj.value.length > 1)) { + return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']'; + } else { + return obj.toCSS(false); + } +}; + +})(require('./tree')); diff --git a/bin/lib/less/tree/alpha.js b/bin/lib/less/tree/alpha.js new file mode 100644 index 0000000..139ae92 --- /dev/null +++ b/bin/lib/less/tree/alpha.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.Alpha = function (val) { + this.value = val; +}; +tree.Alpha.prototype = { + toCSS: function () { + return "alpha(opacity=" + + (this.value.toCSS ? this.value.toCSS() : this.value) + ")"; + }, + eval: function (env) { + if (this.value.eval) { this.value = this.value.eval(env) } + return this; + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/anonymous.js b/bin/lib/less/tree/anonymous.js new file mode 100644 index 0000000..460c9ec --- /dev/null +++ b/bin/lib/less/tree/anonymous.js @@ -0,0 +1,13 @@ +(function (tree) { + +tree.Anonymous = function (string) { + this.value = string.value || string; +}; +tree.Anonymous.prototype = { + toCSS: function () { + return this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/assignment.js b/bin/lib/less/tree/assignment.js new file mode 100644 index 0000000..70ce6e2 --- /dev/null +++ b/bin/lib/less/tree/assignment.js @@ -0,0 +1,17 @@ +(function (tree) { + +tree.Assignment = function (key, val) { + this.key = key; + this.value = val; +}; +tree.Assignment.prototype = { + toCSS: function () { + return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value); + }, + eval: function (env) { + if (this.value.eval) { this.value = this.value.eval(env) } + return this; + } +}; + +})(require('../tree')); \ No newline at end of file diff --git a/bin/lib/less/tree/call.js b/bin/lib/less/tree/call.js new file mode 100644 index 0000000..c1465dd --- /dev/null +++ b/bin/lib/less/tree/call.js @@ -0,0 +1,48 @@ +(function (tree) { + +// +// A function call node. +// +tree.Call = function (name, args, index, filename) { + this.name = name; + this.args = args; + this.index = index; + this.filename = filename; +}; +tree.Call.prototype = { + // + // When evaluating a function call, + // we either find the function in `tree.functions` [1], + // in which case we call it, passing the evaluated arguments, + // or we simply print it out as it appeared originally [2]. + // + // The *functions.js* file contains the built-in functions. + // + // The reason why we evaluate the arguments, is in the case where + // we try to pass a variable to a function, like: `saturate(@color)`. + // The function should receive the value, not the variable. + // + eval: function (env) { + var args = this.args.map(function (a) { return a.eval(env) }); + + if (this.name in tree.functions) { // 1. + try { + return tree.functions[this.name].apply(tree.functions, args); + } catch (e) { + throw { type: e.type || "Runtime", + message: "error evaluating function `" + this.name + "`" + + (e.message ? ': ' + e.message : ''), + index: this.index, filename: this.filename }; + } + } else { // 2. + return new(tree.Anonymous)(this.name + + "(" + args.map(function (a) { return a.toCSS() }).join(', ') + ")"); + } + }, + + toCSS: function (env) { + return this.eval(env).toCSS(); + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/color.js b/bin/lib/less/tree/color.js new file mode 100644 index 0000000..37ce178 --- /dev/null +++ b/bin/lib/less/tree/color.js @@ -0,0 +1,101 @@ +(function (tree) { +// +// RGB Colors - #ff0014, #eee +// +tree.Color = function (rgb, a) { + // + // The end goal here, is to parse the arguments + // into an integer triplet, such as `128, 255, 0` + // + // This facilitates operations and conversions. + // + if (Array.isArray(rgb)) { + this.rgb = rgb; + } else if (rgb.length == 6) { + this.rgb = rgb.match(/.{2}/g).map(function (c) { + return parseInt(c, 16); + }); + } else { + this.rgb = rgb.split('').map(function (c) { + return parseInt(c + c, 16); + }); + } + this.alpha = typeof(a) === 'number' ? a : 1; +}; +tree.Color.prototype = { + eval: function () { return this }, + + // + // If we have some transparency, the only way to represent it + // is via `rgba`. Otherwise, we use the hex representation, + // which has better compatibility with older browsers. + // Values are capped between `0` and `255`, rounded and zero-padded. + // + toCSS: function () { + if (this.alpha < 1.0) { + return "rgba(" + this.rgb.map(function (c) { + return Math.round(c); + }).concat(this.alpha).join(', ') + ")"; + } else { + return '#' + this.rgb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + } + }, + + // + // Operations have to be done per-channel, if not, + // channels will spill onto each other. Once we have + // our result, in the form of an integer triplet, + // we create a new Color node to hold the result. + // + operate: function (op, other) { + var result = []; + + if (! (other instanceof tree.Color)) { + other = other.toColor(); + } + + for (var c = 0; c < 3; c++) { + result[c] = tree.operate(op, this.rgb[c], other.rgb[c]); + } + return new(tree.Color)(result, this.alpha + other.alpha); + }, + + toHSL: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2, d = max - min; + + if (max === min) { + h = s = 0; + } else { + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, l: l, a: a }; + }, + toARGB: function () { + var argb = [Math.round(this.alpha * 255)].concat(this.rgb); + return '#' + argb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + } +}; + + +})(require('../tree')); diff --git a/bin/lib/less/tree/comment.js b/bin/lib/less/tree/comment.js new file mode 100644 index 0000000..f4a3384 --- /dev/null +++ b/bin/lib/less/tree/comment.js @@ -0,0 +1,14 @@ +(function (tree) { + +tree.Comment = function (value, silent) { + this.value = value; + this.silent = !!silent; +}; +tree.Comment.prototype = { + toCSS: function (env) { + return env.compress ? '' : this.value; + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/condition.js b/bin/lib/less/tree/condition.js new file mode 100644 index 0000000..6b79dc9 --- /dev/null +++ b/bin/lib/less/tree/condition.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Condition = function (op, l, r, i, negate) { + this.op = op.trim(); + this.lvalue = l; + this.rvalue = r; + this.index = i; + this.negate = negate; +}; +tree.Condition.prototype.eval = function (env) { + var a = this.lvalue.eval(env), + b = this.rvalue.eval(env); + + var i = this.index, result; + + var result = (function (op) { + switch (op) { + case 'and': + return a && b; + case 'or': + return a || b; + default: + if (a.compare) { + result = a.compare(b); + } else if (b.compare) { + result = b.compare(a); + } else { + throw { type: "Type", + message: "Unable to perform comparison", + index: i }; + } + switch (result) { + case -1: return op === '<' || op === '=<'; + case 0: return op === '=' || op === '>=' || op === '=<'; + case 1: return op === '>' || op === '>='; + } + } + })(this.op); + return this.negate ? !result : result; +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/dimension.js b/bin/lib/less/tree/dimension.js new file mode 100644 index 0000000..9a6fce3 --- /dev/null +++ b/bin/lib/less/tree/dimension.js @@ -0,0 +1,49 @@ +(function (tree) { + +// +// A number with a unit +// +tree.Dimension = function (value, unit) { + this.value = parseFloat(value); + this.unit = unit || null; +}; + +tree.Dimension.prototype = { + eval: function () { return this }, + toColor: function () { + return new(tree.Color)([this.value, this.value, this.value]); + }, + toCSS: function () { + var css = this.value + this.unit; + return css; + }, + + // In an operation between two Dimensions, + // we default to the first Dimension's unit, + // so `1px + 2em` will yield `3px`. + // In the future, we could implement some unit + // conversions such that `100cm + 10mm` would yield + // `101cm`. + operate: function (op, other) { + return new(tree.Dimension) + (tree.operate(op, this.value, other.value), + this.unit || other.unit); + }, + + // TODO: Perform unit conversion before comparing + compare: function (other) { + if (other instanceof tree.Dimension) { + if (other.value > this.value) { + return -1; + } else if (other.value < this.value) { + return 1; + } else { + return 0; + } + } else { + return -1; + } + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/directive.js b/bin/lib/less/tree/directive.js new file mode 100644 index 0000000..2753833 --- /dev/null +++ b/bin/lib/less/tree/directive.js @@ -0,0 +1,35 @@ +(function (tree) { + +tree.Directive = function (name, value, features) { + this.name = name; + + if (Array.isArray(value)) { + this.ruleset = new(tree.Ruleset)([], value); + this.ruleset.allowImports = true; + } else { + this.value = value; + } +}; +tree.Directive.prototype = { + toCSS: function (ctx, env) { + if (this.ruleset) { + this.ruleset.root = true; + return this.name + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + } else { + return this.name + ' ' + this.value.toCSS() + ';\n'; + } + }, + eval: function (env) { + env.frames.unshift(this); + this.ruleset = this.ruleset && this.ruleset.eval(env); + env.frames.shift(); + return this; + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/element.js b/bin/lib/less/tree/element.js new file mode 100644 index 0000000..14b08d2 --- /dev/null +++ b/bin/lib/less/tree/element.js @@ -0,0 +1,52 @@ +(function (tree) { + +tree.Element = function (combinator, value, index) { + this.combinator = combinator instanceof tree.Combinator ? + combinator : new(tree.Combinator)(combinator); + + if (typeof(value) === 'string') { + this.value = value.trim(); + } else if (value) { + this.value = value; + } else { + this.value = ""; + } + this.index = index; +}; +tree.Element.prototype.eval = function (env) { + return new(tree.Element)(this.combinator, + this.value.eval ? this.value.eval(env) : this.value, + this.index); +}; +tree.Element.prototype.toCSS = function (env) { + var value = (this.value.toCSS ? this.value.toCSS(env) : this.value); + if (value == '' && this.combinator.value.charAt(0) == '&') { + return ''; + } else { + return this.combinator.toCSS(env || {}) + value; + } +}; + +tree.Combinator = function (value) { + if (value === ' ') { + this.value = ' '; + } else if (value === '& ') { + this.value = '& '; + } else { + this.value = value ? value.trim() : ""; + } +}; +tree.Combinator.prototype.toCSS = function (env) { + return { + '' : '', + ' ' : ' ', + '&' : '', + '& ' : ' ', + ':' : ' :', + '+' : env.compress ? '+' : ' + ', + '~' : env.compress ? '~' : ' ~ ', + '>' : env.compress ? '>' : ' > ' + }[this.value]; +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/expression.js b/bin/lib/less/tree/expression.js new file mode 100644 index 0000000..fbfa9c5 --- /dev/null +++ b/bin/lib/less/tree/expression.js @@ -0,0 +1,23 @@ +(function (tree) { + +tree.Expression = function (value) { this.value = value }; +tree.Expression.prototype = { + eval: function (env) { + if (this.value.length > 1) { + return new(tree.Expression)(this.value.map(function (e) { + return e.eval(env); + })); + } else if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return this; + } + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS ? e.toCSS(env) : ''; + }).join(' '); + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/import.js b/bin/lib/less/tree/import.js new file mode 100644 index 0000000..7a977de --- /dev/null +++ b/bin/lib/less/tree/import.js @@ -0,0 +1,83 @@ +(function (tree) { +// +// CSS @import node +// +// The general strategy here is that we don't want to wait +// for the parsing to be completed, before we start importing +// the file. That's because in the context of a browser, +// most of the time will be spent waiting for the server to respond. +// +// On creation, we push the import path to our import queue, though +// `import,push`, we also pass it a callback, which it'll call once +// the file has been fetched, and parsed. +// +tree.Import = function (path, imports, features, once, index) { + var that = this; + + this.once = once; + this.index = index; + this._path = path; + this.features = features && new(tree.Value)(features); + + // The '.less' extension is optional + if (path instanceof tree.Quoted) { + this.path = /\.(le?|c)ss(\?.*)?$/.test(path.value) ? path.value : path.value + '.less'; + } else { + this.path = path.value.value || path.value; + } + + this.css = /css(\?.*)?$/.test(this.path); + + // Only pre-compile .less files + if (! this.css) { + imports.push(this.path, function (e, root, imported) { + if (e) { e.index = index } + if (imported && that.once) that.skip = imported; + that.root = root || new(tree.Ruleset)([], []); + }); + } +}; + +// +// The actual import node doesn't return anything, when converted to CSS. +// The reason is that it's used at the evaluation stage, so that the rules +// it imports can be treated like any other rules. +// +// In `eval`, we make sure all Import nodes get evaluated, recursively, so +// we end up with a flat structure, which can easily be imported in the parent +// ruleset. +// +tree.Import.prototype = { + toCSS: function (env) { + var features = this.features ? ' ' + this.features.toCSS(env) : ''; + + if (this.css) { + return "@import " + this._path.toCSS() + features + ';\n'; + } else { + return ""; + } + }, + eval: function (env) { + var ruleset, features = this.features && this.features.eval(env); + + if (this.skip) return []; + + if (this.css) { + return this; + } else { + ruleset = new(tree.Ruleset)([], this.root.rules.slice(0)); + + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.Import) { + Array.prototype + .splice + .apply(ruleset.rules, + [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules; + } + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/javascript.js b/bin/lib/less/tree/javascript.js new file mode 100644 index 0000000..772a31d --- /dev/null +++ b/bin/lib/less/tree/javascript.js @@ -0,0 +1,51 @@ +(function (tree) { + +tree.JavaScript = function (string, index, escaped) { + this.escaped = escaped; + this.expression = string; + this.index = index; +}; +tree.JavaScript.prototype = { + eval: function (env) { + var result, + that = this, + context = {}; + + var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) { + return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env)); + }); + + try { + expression = new(Function)('return (' + expression + ')'); + } catch (e) { + throw { message: "JavaScript evaluation error: `" + expression + "`" , + index: this.index }; + } + + for (var k in env.frames[0].variables()) { + context[k.slice(1)] = { + value: env.frames[0].variables()[k].value, + toJS: function () { + return this.value.eval(env).toCSS(); + } + }; + } + + try { + result = expression.call(context); + } catch (e) { + throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" , + index: this.index }; + } + if (typeof(result) === 'string') { + return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index); + } else if (Array.isArray(result)) { + return new(tree.Anonymous)(result.join(', ')); + } else { + return new(tree.Anonymous)(result); + } + } +}; + +})(require('../tree')); + diff --git a/bin/lib/less/tree/keyword.js b/bin/lib/less/tree/keyword.js new file mode 100644 index 0000000..701b79e --- /dev/null +++ b/bin/lib/less/tree/keyword.js @@ -0,0 +1,19 @@ +(function (tree) { + +tree.Keyword = function (value) { this.value = value }; +tree.Keyword.prototype = { + eval: function () { return this }, + toCSS: function () { return this.value }, + compare: function (other) { + if (other instanceof tree.Keyword) { + return other.value === this.value ? 0 : 1; + } else { + return -1; + } + } +}; + +tree.True = new(tree.Keyword)('true'); +tree.False = new(tree.Keyword)('false'); + +})(require('../tree')); diff --git a/bin/lib/less/tree/media.js b/bin/lib/less/tree/media.js new file mode 100644 index 0000000..2b7b26e --- /dev/null +++ b/bin/lib/less/tree/media.js @@ -0,0 +1,114 @@ +(function (tree) { + +tree.Media = function (value, features) { + var el = new(tree.Element)('&', null, 0), + selectors = [new(tree.Selector)([el])]; + + this.features = new(tree.Value)(features); + this.ruleset = new(tree.Ruleset)(selectors, value); + this.ruleset.allowImports = true; +}; +tree.Media.prototype = { + toCSS: function (ctx, env) { + var features = this.features.toCSS(env); + + this.ruleset.root = (ctx.length === 0 || ctx[0].multiMedia); + return '@media ' + features + (env.compress ? '{' : ' {\n ') + + this.ruleset.toCSS(ctx, env).trim().replace(/\n/g, '\n ') + + (env.compress ? '}': '\n}\n'); + }, + eval: function (env) { + if (!env.mediaBlocks) { + env.mediaBlocks = []; + env.mediaPath = []; + } + + var blockIndex = env.mediaBlocks.length; + env.mediaPath.push(this); + env.mediaBlocks.push(this); + + var media = new(tree.Media)([], []); + media.features = this.features.eval(env); + + env.frames.unshift(this.ruleset); + media.ruleset = this.ruleset.eval(env); + env.frames.shift(); + + env.mediaBlocks[blockIndex] = media; + env.mediaPath.pop(); + + return env.mediaPath.length === 0 ? media.evalTop(env) : + media.evalNested(env) + }, + variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, + find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, + rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }, + + evalTop: function (env) { + var result = this; + + // Render all dependent Media blocks. + if (env.mediaBlocks.length > 1) { + var el = new(tree.Element)('&', null, 0); + var selectors = [new(tree.Selector)([el])]; + result = new(tree.Ruleset)(selectors, env.mediaBlocks); + result.multiMedia = true; + } + + delete env.mediaBlocks; + delete env.mediaPath; + + return result; + }, + evalNested: function (env) { + var i, value, + path = env.mediaPath.concat([this]); + + // Extract the media-query conditions separated with `,` (OR). + for (i = 0; i < path.length; i++) { + value = path[i].features instanceof tree.Value ? + path[i].features.value : path[i].features; + path[i] = Array.isArray(value) ? value : [value]; + } + + // Trace all permutations to generate the resulting media-query. + // + // (a, b and c) with nested (d, e) -> + // a and d + // a and e + // b and c and d + // b and c and e + this.features = new(tree.Value)(this.permute(path).map(function (path) { + path = path.map(function (fragment) { + return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment); + }); + + for(i = path.length - 1; i > 0; i--) { + path.splice(i, 0, new(tree.Anonymous)("and")); + } + + return new(tree.Expression)(path); + })); + + // Fake a tree-node that doesn't output anything. + return new(tree.Ruleset)([], []); + }, + permute: function (arr) { + if (arr.length === 0) { + return []; + } else if (arr.length === 1) { + return arr[0]; + } else { + var result = []; + var rest = this.permute(arr.slice(1)); + for (var i = 0; i < rest.length; i++) { + for (var j = 0; j < arr[0].length; j++) { + result.push([arr[0][j]].concat(rest[i])); + } + } + return result; + } + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/mixin.js b/bin/lib/less/tree/mixin.js new file mode 100644 index 0000000..b441bf3 --- /dev/null +++ b/bin/lib/less/tree/mixin.js @@ -0,0 +1,146 @@ +(function (tree) { + +tree.mixin = {}; +tree.mixin.Call = function (elements, args, index, filename, important) { + this.selector = new(tree.Selector)(elements); + this.arguments = args; + this.index = index; + this.filename = filename; + this.important = important; +}; +tree.mixin.Call.prototype = { + eval: function (env) { + var mixins, args, rules = [], match = false; + + for (var i = 0; i < env.frames.length; i++) { + if ((mixins = env.frames[i].find(this.selector)).length > 0) { + args = this.arguments && this.arguments.map(function (a) { + return { name: a.name, value: a.value.eval(env) }; + }); + for (var m = 0; m < mixins.length; m++) { + if (mixins[m].match(args, env)) { + try { + Array.prototype.push.apply( + rules, mixins[m].eval(env, this.arguments, this.important).rules); + match = true; + } catch (e) { + throw { message: e.message, index: this.index, filename: this.filename, stack: e.stack }; + } + } + } + if (match) { + return rules; + } else { + throw { type: 'Runtime', + message: 'No matching definition was found for `' + + this.selector.toCSS().trim() + '(' + + this.arguments.map(function (a) { + return a.toCSS(); + }).join(', ') + ")`", + index: this.index, filename: this.filename }; + } + } + } + throw { type: 'Name', + message: this.selector.toCSS().trim() + " is undefined", + index: this.index, filename: this.filename }; + } +}; + +tree.mixin.Definition = function (name, params, rules, condition, variadic) { + this.name = name; + this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])]; + this.params = params; + this.condition = condition; + this.variadic = variadic; + this.arity = params.length; + this.rules = rules; + this._lookups = {}; + this.required = params.reduce(function (count, p) { + if (!p.name || (p.name && !p.value)) { return count + 1 } + else { return count } + }, 0); + this.parent = tree.Ruleset.prototype; + this.frames = []; +}; +tree.mixin.Definition.prototype = { + toCSS: function () { return "" }, + variable: function (name) { return this.parent.variable.call(this, name) }, + variables: function () { return this.parent.variables.call(this) }, + find: function () { return this.parent.find.apply(this, arguments) }, + rulesets: function () { return this.parent.rulesets.apply(this) }, + + evalParams: function (env, args) { + var frame = new(tree.Ruleset)(null, []), varargs, arg; + + for (var i = 0, val, name; i < this.params.length; i++) { + arg = args && args[i] + + if (arg && arg.name) { + frame.rules.unshift(new(tree.Rule)(arg.name, arg.value.eval(env))); + args.splice(i, 1); + i--; + continue; + } + + if (name = this.params[i].name) { + if (this.params[i].variadic && args) { + varargs = []; + for (var j = i; j < args.length; j++) { + varargs.push(args[j].value.eval(env)); + } + frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env))); + } else if (val = (arg && arg.value) || this.params[i].value) { + frame.rules.unshift(new(tree.Rule)(name, val.eval(env))); + } else { + throw { type: 'Runtime', message: "wrong number of arguments for " + this.name + + ' (' + args.length + ' for ' + this.arity + ')' }; + } + } + } + return frame; + }, + eval: function (env, args, important) { + var frame = this.evalParams(env, args), context, _arguments = [], rules, start; + + for (var i = 0; i < Math.max(this.params.length, args && args.length); i++) { + _arguments.push((args[i] && args[i].value) || this.params[i].value); + } + frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env))); + + rules = important ? + this.rules.map(function (r) { + return new(tree.Rule)(r.name, r.value, '!important', r.index); + }) : this.rules.slice(0); + + return new(tree.Ruleset)(null, rules).eval({ + frames: [this, frame].concat(this.frames, env.frames) + }); + }, + match: function (args, env) { + var argsLength = (args && args.length) || 0, len, frame; + + if (! this.variadic) { + if (argsLength < this.required) { return false } + if (argsLength > this.params.length) { return false } + if ((this.required > 0) && (argsLength > this.params.length)) { return false } + } + + if (this.condition && !this.condition.eval({ + frames: [this.evalParams(env, args)].concat(env.frames) + })) { return false } + + len = Math.min(argsLength, this.arity); + + for (var i = 0; i < len; i++) { + if (!this.params[i].name) { + if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) { + return false; + } + } + } + return true; + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/operation.js b/bin/lib/less/tree/operation.js new file mode 100644 index 0000000..1776c52 --- /dev/null +++ b/bin/lib/less/tree/operation.js @@ -0,0 +1,32 @@ +(function (tree) { + +tree.Operation = function (op, operands) { + this.op = op.trim(); + this.operands = operands; +}; +tree.Operation.prototype.eval = function (env) { + var a = this.operands[0].eval(env), + b = this.operands[1].eval(env), + temp; + + if (a instanceof tree.Dimension && b instanceof tree.Color) { + if (this.op === '*' || this.op === '+') { + temp = b, b = a, a = temp; + } else { + throw { name: "OperationError", + message: "Can't subtract or divide a color from a number" }; + } + } + return a.operate(this.op, b); +}; + +tree.operate = function (op, a, b) { + switch (op) { + case '+': return a + b; + case '-': return a - b; + case '*': return a * b; + case '/': return a / b; + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/paren.js b/bin/lib/less/tree/paren.js new file mode 100644 index 0000000..384a43c --- /dev/null +++ b/bin/lib/less/tree/paren.js @@ -0,0 +1,16 @@ + +(function (tree) { + +tree.Paren = function (node) { + this.value = node; +}; +tree.Paren.prototype = { + toCSS: function (env) { + return '(' + this.value.toCSS(env) + ')'; + }, + eval: function (env) { + return new(tree.Paren)(this.value.eval(env)); + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/quoted.js b/bin/lib/less/tree/quoted.js new file mode 100644 index 0000000..794bf4c --- /dev/null +++ b/bin/lib/less/tree/quoted.js @@ -0,0 +1,29 @@ +(function (tree) { + +tree.Quoted = function (str, content, escaped, i) { + this.escaped = escaped; + this.value = content || ''; + this.quote = str.charAt(0); + this.index = i; +}; +tree.Quoted.prototype = { + toCSS: function () { + if (this.escaped) { + return this.value; + } else { + return this.quote + this.value + this.quote; + } + }, + eval: function (env) { + var that = this; + var value = this.value.replace(/`([^`]+)`/g, function (_, exp) { + return new(tree.JavaScript)(exp, that.index, true).eval(env).value; + }).replace(/@\{([\w-]+)\}/g, function (_, name) { + var v = new(tree.Variable)('@' + name, that.index).eval(env); + return ('value' in v) ? v.value : v.toCSS(); + }); + return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index); + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/rule.js b/bin/lib/less/tree/rule.js new file mode 100644 index 0000000..9e4e54a --- /dev/null +++ b/bin/lib/less/tree/rule.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Rule = function (name, value, important, index, inline) { + this.name = name; + this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]); + this.important = important ? ' ' + important.trim() : ''; + this.index = index; + this.inline = inline || false; + + if (name.charAt(0) === '@') { + this.variable = true; + } else { this.variable = false } +}; +tree.Rule.prototype.toCSS = function (env) { + if (this.variable) { return "" } + else { + return this.name + (env.compress ? ':' : ': ') + + this.value.toCSS(env) + + this.important + (this.inline ? "" : ";"); + } +}; + +tree.Rule.prototype.eval = function (context) { + return new(tree.Rule)(this.name, + this.value.eval(context), + this.important, + this.index, this.inline); +}; + +tree.Shorthand = function (a, b) { + this.a = a; + this.b = b; +}; + +tree.Shorthand.prototype = { + toCSS: function (env) { + return this.a.toCSS(env) + "/" + this.b.toCSS(env); + }, + eval: function () { return this } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/ruleset.js b/bin/lib/less/tree/ruleset.js new file mode 100644 index 0000000..3100cc3 --- /dev/null +++ b/bin/lib/less/tree/ruleset.js @@ -0,0 +1,225 @@ +(function (tree) { + +tree.Ruleset = function (selectors, rules, strictImports) { + this.selectors = selectors; + this.rules = rules; + this._lookups = {}; + this.strictImports = strictImports; +}; +tree.Ruleset.prototype = { + eval: function (env) { + var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) }); + var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports); + + ruleset.root = this.root; + ruleset.allowImports = this.allowImports; + + // push the current ruleset to the frames stack + env.frames.unshift(ruleset); + + // Evaluate imports + if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) { + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.Import) { + Array.prototype.splice + .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + } + + // Store the frames around mixin definitions, + // so they can be evaluated like closures when the time comes. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Definition) { + ruleset.rules[i].frames = env.frames.slice(0); + } + } + + // Evaluate mixin calls. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Call) { + Array.prototype.splice + .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); + } + } + + // Evaluate everything else + for (var i = 0, rule; i < ruleset.rules.length; i++) { + rule = ruleset.rules[i]; + + if (! (rule instanceof tree.mixin.Definition)) { + ruleset.rules[i] = rule.eval ? rule.eval(env) : rule; + } + } + + // Pop the stack + env.frames.shift(); + + return ruleset; + }, + match: function (args) { + return !args || args.length === 0; + }, + variables: function () { + if (this._variables) { return this._variables } + else { + return this._variables = this.rules.reduce(function (hash, r) { + if (r instanceof tree.Rule && r.variable === true) { + hash[r.name] = r; + } + return hash; + }, {}); + } + }, + variable: function (name) { + return this.variables()[name]; + }, + rulesets: function () { + if (this._rulesets) { return this._rulesets } + else { + return this._rulesets = this.rules.filter(function (r) { + return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition); + }); + } + }, + find: function (selector, self) { + self = self || this; + var rules = [], rule, match, + key = selector.toCSS(); + + if (key in this._lookups) { return this._lookups[key] } + + this.rulesets().forEach(function (rule) { + if (rule !== self) { + for (var j = 0; j < rule.selectors.length; j++) { + if (match = selector.match(rule.selectors[j])) { + if (selector.elements.length > rule.selectors[j].elements.length) { + Array.prototype.push.apply(rules, rule.find( + new(tree.Selector)(selector.elements.slice(1)), self)); + } else { + rules.push(rule); + } + break; + } + } + } + }); + return this._lookups[key] = rules; + }, + // + // Entry point for code generation + // + // `context` holds an array of arrays. + // + toCSS: function (context, env) { + var css = [], // The CSS output + rules = [], // node.Rule instances + _rules = [], // + rulesets = [], // node.Ruleset instances + paths = [], // Current selectors + selector, // The fully rendered selector + rule; + + if (! this.root) { + if (context.length === 0) { + paths = this.selectors.map(function (s) { return [s] }); + } else { + this.joinSelectors(paths, context, this.selectors); + } + } + + // Compile rules and rulesets + for (var i = 0; i < this.rules.length; i++) { + rule = this.rules[i]; + + if (rule.rules || (rule instanceof tree.Directive) || (rule instanceof tree.Media)) { + rulesets.push(rule.toCSS(paths, env)); + } else if (rule instanceof tree.Comment) { + if (!rule.silent) { + if (this.root) { + rulesets.push(rule.toCSS(env)); + } else { + rules.push(rule.toCSS(env)); + } + } + } else { + if (rule.toCSS && !rule.variable) { + rules.push(rule.toCSS(env)); + } else if (rule.value && !rule.variable) { + rules.push(rule.value.toString()); + } + } + } + + rulesets = rulesets.join(''); + + // If this is the root node, we don't render + // a selector, or {}. + // Otherwise, only output if this ruleset has rules. + if (this.root) { + css.push(rules.join(env.compress ? '' : '\n')); + } else { + if (rules.length > 0) { + selector = paths.map(function (p) { + return p.map(function (s) { + return s.toCSS(env); + }).join('').trim(); + }).join(env.compress ? ',' : ',\n'); + + // Remove duplicates + for (var i = rules.length - 1; i >= 0; i--) { + if (_rules.indexOf(rules[i]) === -1) { + _rules.unshift(rules[i]); + } + } + rules = _rules; + + css.push(selector, + (env.compress ? '{' : ' {\n ') + + rules.join(env.compress ? '' : '\n ') + + (env.compress ? '}' : '\n}\n')); + } + } + css.push(rulesets); + + return css.join('') + (env.compress ? '\n' : ''); + }, + + joinSelectors: function (paths, context, selectors) { + for (var s = 0; s < selectors.length; s++) { + this.joinSelector(paths, context, selectors[s]); + } + }, + + joinSelector: function (paths, context, selector) { + var before = [], after = [], beforeElements = [], + afterElements = [], hasParentSelector = false, el; + + for (var i = 0; i < selector.elements.length; i++) { + el = selector.elements[i]; + if (el.combinator.value.charAt(0) === '&') { + hasParentSelector = true; + } + if (hasParentSelector) afterElements.push(el); + else beforeElements.push(el); + } + + if (! hasParentSelector) { + afterElements = beforeElements; + beforeElements = []; + } + + if (beforeElements.length > 0) { + before.push(new(tree.Selector)(beforeElements)); + } + + if (afterElements.length > 0) { + after.push(new(tree.Selector)(afterElements)); + } + + for (var c = 0; c < context.length; c++) { + paths.push(before.concat(context[c]).concat(after)); + } + } +}; +})(require('../tree')); diff --git a/bin/lib/less/tree/selector.js b/bin/lib/less/tree/selector.js new file mode 100644 index 0000000..65abbb6 --- /dev/null +++ b/bin/lib/less/tree/selector.js @@ -0,0 +1,42 @@ +(function (tree) { + +tree.Selector = function (elements) { + this.elements = elements; + if (this.elements[0].combinator.value === "") { + this.elements[0].combinator.value = ' '; + } +}; +tree.Selector.prototype.match = function (other) { + var len = this.elements.length, + olen = other.elements.length, + max = Math.min(len, olen); + + if (len < olen) { + return false; + } else { + for (var i = 0; i < max; i++) { + if (this.elements[i].value !== other.elements[i].value) { + return false; + } + } + } + return true; +}; +tree.Selector.prototype.eval = function (env) { + return new(tree.Selector)(this.elements.map(function (e) { + return e.eval(env); + })); +}; +tree.Selector.prototype.toCSS = function (env) { + if (this._css) { return this._css } + + return this._css = this.elements.map(function (e) { + if (typeof(e) === 'string') { + return ' ' + e.trim(); + } else { + return e.toCSS(env); + } + }).join(''); +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/url.js b/bin/lib/less/tree/url.js new file mode 100644 index 0000000..0caec34 --- /dev/null +++ b/bin/lib/less/tree/url.js @@ -0,0 +1,25 @@ +(function (tree) { + +tree.URL = function (val, paths) { + if (val.data) { + this.attrs = val; + } else { + // Add the base path if the URL is relative and we are in the browser + if (typeof(window) !== 'undefined' && !/^(?:https?:\/\/|file:\/\/|data:|\/)/.test(val.value) && paths.length > 0) { + val.value = paths[0] + (val.value.charAt(0) === '/' ? val.value.slice(1) : val.value); + } + this.value = val; + this.paths = paths; + } +}; +tree.URL.prototype = { + toCSS: function () { + return "url(" + (this.attrs ? 'data:' + this.attrs.mime + this.attrs.charset + this.attrs.base64 + this.attrs.data + : this.value.toCSS()) + ")"; + }, + eval: function (ctx) { + return this.attrs ? this : new(tree.URL)(this.value.eval(ctx), this.paths); + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/value.js b/bin/lib/less/tree/value.js new file mode 100644 index 0000000..3c1eb29 --- /dev/null +++ b/bin/lib/less/tree/value.js @@ -0,0 +1,24 @@ +(function (tree) { + +tree.Value = function (value) { + this.value = value; + this.is = 'value'; +}; +tree.Value.prototype = { + eval: function (env) { + if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return new(tree.Value)(this.value.map(function (v) { + return v.eval(env); + })); + } + }, + toCSS: function (env) { + return this.value.map(function (e) { + return e.toCSS(env); + }).join(env.compress ? ',' : ', '); + } +}; + +})(require('../tree')); diff --git a/bin/lib/less/tree/variable.js b/bin/lib/less/tree/variable.js new file mode 100644 index 0000000..ee557e1 --- /dev/null +++ b/bin/lib/less/tree/variable.js @@ -0,0 +1,26 @@ +(function (tree) { + +tree.Variable = function (name, index, file) { this.name = name, this.index = index, this.file = file }; +tree.Variable.prototype = { + eval: function (env) { + var variable, v, name = this.name; + + if (name.indexOf('@@') == 0) { + name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value; + } + + if (variable = tree.find(env.frames, function (frame) { + if (v = frame.variable(name)) { + return v.value.eval(env); + } + })) { return variable } + else { + throw { type: 'Name', + message: "variable " + name + " is undefined", + filename: this.file, + index: this.index }; + } + } +}; + +})(require('../tree')); diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..7454334 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TuskarUI.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TuskarUI.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/TuskarUI" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TuskarUI" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..5004e35 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\TuskarUI.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\TuskarUI.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/doc/source/HACKING.rst b/doc/source/HACKING.rst new file mode 120000 index 0000000..a2f06b7 --- /dev/null +++ b/doc/source/HACKING.rst @@ -0,0 +1 @@ +../../HACKING.rst \ No newline at end of file diff --git a/doc/source/README.rst b/doc/source/README.rst new file mode 120000 index 0000000..c768ff7 --- /dev/null +++ b/doc/source/README.rst @@ -0,0 +1 @@ +../../README.rst \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..f617d40 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# +# Tuskar UI documentation build configuration file, created by +# sphinx-quickstart on Thu Apr 24 09:19:32 2014. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'oslosphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Tuskar UI' +copyright = u'2014, Tuskar Team' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = 'Juno' +# The full version, including alpha/beta/rc tags. +release = 'Juno' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'TuskarUIdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'TuskarUI.tex', u'Tuskar UI Documentation', + u'Tuskar Team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'tuskarui', u'Tuskar UI Documentation', + [u'Tuskar Team'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'TuskarUI', u'Tuskar UI Documentation', + u'Tuskar Team', 'TuskarUI', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/devstack_baremetal.rst b/doc/source/devstack_baremetal.rst new file mode 100644 index 0000000..b9ce5df --- /dev/null +++ b/doc/source/devstack_baremetal.rst @@ -0,0 +1,17 @@ +Bare Metal configuration in DevStack +------------------------------------ + +To enable Bare Metal driver in DevStack you need to: + +1. Add following settings to ``localrc``:: + + VIRT_DRIVER=baremetal + enable_service baremetal + +2. Update ``./lib/baremetal``:: + + - BM_DNSMASQ_FROM_NOVA_NETWORK=`trueorfalse False $BM_DNSMASQ_FROM_NOVA_NETWORK` + + BM_DNSMASQ_FROM_NOVA_NETWORK=`trueorfalse True $BM_DNSMASQ_FROM_NOVA_NETWORK` + +See `Bare Metal DevStack documentation `_ +or `baremetal file itself `_ diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..809c3df --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,19 @@ +Tuskar UI +========= + +Contents: + +.. toctree:: + :maxdepth: 2 + + README + install + HACKING + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/source/install.rst b/doc/source/install.rst new file mode 100644 index 0000000..112c35f --- /dev/null +++ b/doc/source/install.rst @@ -0,0 +1,139 @@ +Installation instructions +========================= + +Note +---- + +If you want to install and configure the entire TripleO + Tuskar + Tuskar UI +stack, you can use +`the devtest installation guide `_. + +Otherwise, you can use the installation instructions for Tuskar UI below. + +Prerequisites +------------- + +Installation prerequisites are: + +1. A functional OpenStack installation. Horizon and Tuskar UI will + connect to the Keystone service here. Keystone does *not* need to be + on the same machine as your Tuskar UI interface, but its HTTP API + must be accessible. +2. A functional Tuskar installation. Tuskar UI talks to Tuskar via an + HTTP interface. It may, but does not have to, reside on the same + machine as Tuskar UI, but it must be network accessible. + +You may find +`the Tuskar install guide `_ +helpful. + +For baremetal provisioning, you will want a Nova Baremetal driver +installed and registered in the Keystone services catalog. (You can +`read more about setting up Nova Baremetal here `_.) + +If you are using Devstack to run OpenStack, you can use +:doc:`Devstack Baremetal configuration `. + +Installing the packages +----------------------- + +Tuskar UI is a Django app written in Python and has a few installation +dependencies: + +On a RHEL 6 system, you should install the following: + +:: + + yum install git python-devel swig openssl-devel mysql-devel libxml2-devel libxslt-devel gcc gcc-c++ + +The above should work well for similar RPM-based distributions. For +other distros or platforms, you will obviously need to convert as +appropriate. + +Then, you'll want to use the ``easy_install`` utility to set up a few +other tools: + +:: + + easy_install pip + easy_install nose + +Install the management UI +------------------------- + +Begin by cloning the Horizon and Tuskar UI repositories: + +:: + + git clone git://github.com/openstack/horizon.git + git clone git://github.com/openstack/tuskar-ui.git + +Go into ``horizon`` and install a virtual environment for your setup:: + + cd horizon + python tools/install_venv.py + + +Next, run ``run_tests.sh`` to have pip install Horizon dependencies: + +:: + + ./run_tests.sh + +Set up your ``local_settings.py`` file: + +:: + + cp ../tuskar-ui/local_settings.py.example openstack_dashboard/local/local_settings.py + +Open up the copied ``local_settings.py`` file in your preferred text +editor. You will want to customize several settings: + +- ``OPENSTACK_HOST`` should be configured with the hostname of your + OpenStack server. Verify that the ``OPENSTACK_KEYSTONE_URL`` and + ``OPENSTACK_KEYSTONE_DEFAULT_ROLE`` settings are correct for your + environment. (They should be correct unless you modified your + OpenStack server to change them.) +- ``TUSKAR_ENDPOINT_URL`` should point to the Tuskar server you + configured. It normally runs on port 8585. + +Install Tuskar UI with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -r ../tuskar-ui/requirements.txt + tools/with_venv.sh pip install -e ../tuskar-ui/ + +And enable it in Horizon:: + + cp ../tuskar-ui/_50_tuskar.py.example openstack_dashboard/local/enabled/_50_tuskar.py + +Then disable the other dashboards:: + + cp ../tuskar-ui/_10_admin.py.example openstack_dashboard/local/enabled/_10_admin.py + cp ../tuskar-ui/_20_project.py.example openstack_dashboard/local/enabled/_20_project.py + + +Starting the app +---------------- + +If everything has gone according to plan, you should be able to run: + +:: + + tools/with_venv.sh ./manage.py runserver + +and have the application start on port 8080. The Tuskar UI dashboard will +be located at http://localhost:8080/infrastructure + +If you wish to access it remotely (i.e., not just from localhost), you +need to open port 8080 in iptables: + +:: + + iptables -I INPUT -p tcp --dport 8080 -j ACCEPT + +and launch the server with ``0.0.0.0:8080`` on the end: + +:: + + tools/with_venv.sh ./manage.py runserver 0.0.0.0:8080 + diff --git a/local_settings.py.example b/local_settings.py.example new file mode 100644 index 0000000..37986ac --- /dev/null +++ b/local_settings.py.example @@ -0,0 +1,375 @@ +import os + +from django.utils.translation import ugettext_lazy as _ + +from tuskar_ui import exceptions + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +# Required for Django 1.5. +# If horizon is running in production (DEBUG is False), set this +# with the list of host/domain names that the application can serve. +# For more information see: +# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +#ALLOWED_HOSTS = ['horizon.example.com', ] + +# Set SSL proxy settings: +# For Django 1.4+ pass this header from the proxy after terminating the SSL, +# and don't forget to strip it from the client's request. +# For more information see: +# https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') + +# If Horizon is being served through SSL, then uncomment the following two +# settings to better secure the cookies from security exploits +#CSRF_COOKIE_SECURE = True +#SESSION_COOKIE_SECURE = True + +# Overrides for OpenStack API versions. Use this setting to force the +# OpenStack dashboard to use a specific API version for a given service API. +# NOTE: The version should be formatted as it appears in the URL for the +# service API. For example, The identity service APIs have inconsistent +# use of the decimal point, so valid options would be "2.0" or "3". +# OPENSTACK_API_VERSIONS = { +# "identity": 3 +# } + +# Set this to True if running on multi-domain model. When this is enabled, it +# will require user to enter the Domain name in addition to username for login. +# OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = False + +# Overrides the default domain used when running on single-domain model +# with Keystone V3. All entities will be created in the default domain. +# OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = 'Default' + +# Set Console type: +# valid options would be "AUTO", "VNC" or "SPICE" +# CONSOLE_TYPE = "AUTO" + +# Default OpenStack Dashboard configuration. +HORIZON_CONFIG = { + 'dashboards': ('project', 'admin', 'settings',), + 'default_dashboard': 'project', + 'user_home': 'openstack_dashboard.views.get_user_home', + 'ajax_queue_limit': 10, + 'auto_fade_alerts': { + 'delay': 3000, + 'fade_duration': 1500, + 'types': ['alert-success', 'alert-info'] + }, + 'help_url': "http://docs.openstack.org", + 'exceptions': {'recoverable': exceptions.RECOVERABLE, + 'not_found': exceptions.NOT_FOUND, + 'unauthorized': exceptions.UNAUTHORIZED}, +} + +# Specify a regular expression to validate user passwords. +# HORIZON_CONFIG["password_validator"] = { +# "regex": '.*', +# "help_text": _("Your password does not meet the requirements.") +# } + +# Disable simplified floating IP address management for deployments with +# multiple floating IP pools or complex network requirements. +# HORIZON_CONFIG["simple_ip_management"] = False + +# Turn off browser autocompletion for the login form if so desired. +# HORIZON_CONFIG["password_autocomplete"] = "off" + +LOCAL_PATH = os.path.dirname(os.path.abspath(__file__)) + +# Set custom secret key: +# You can either set it to a specific value or you can let horizion generate a +# default secret key that is unique on this machine, e.i. regardless of the +# amount of Python WSGI workers (if used behind Apache+mod_wsgi): However, there +# may be situations where you would want to set this explicitly, e.g. when +# multiple dashboard instances are distributed on different machines (usually +# behind a load-balancer). Either you have to make sure that a session gets all +# requests routed to the same dashboard instance or you set the same SECRET_KEY +# for all of them. +from horizon.utils import secret_key +SECRET_KEY = secret_key.generate_or_read_from_file(os.path.join(LOCAL_PATH, '.secret_key_store')) + +# We recommend you use memcached for development; otherwise after every reload +# of the django development server, you will have to login again. To use +# memcached set CACHES to something like +# CACHES = { +# 'default': { +# 'BACKEND' : 'django.core.cache.backends.memcached.MemcachedCache', +# 'LOCATION' : '127.0.0.1:11211', +# } +#} + +CACHES = { + 'default': { + 'BACKEND' : 'django.core.cache.backends.locmem.LocMemCache' + } +} + +# Send email to the console by default +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +# Or send them to /dev/null +#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + +# Configure these for your outgoing email host +# EMAIL_HOST = 'smtp.my-company.com' +# EMAIL_PORT = 25 +# EMAIL_HOST_USER = 'djangomail' +# EMAIL_HOST_PASSWORD = 'top-secret!' + +# For multiple regions uncomment this configuration, and add (endpoint, title). +# AVAILABLE_REGIONS = [ +# ('http://cluster1.example.com:5000/v2.0', 'cluster1'), +# ('http://cluster2.example.com:5000/v2.0', 'cluster2'), +# ] + +OPENSTACK_HOST = "stormsend.usersys.redhat.com" +OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_HOST +OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" + +# Disable SSL certificate checks (useful for self-signed certificates): +# OPENSTACK_SSL_NO_VERIFY = True + +# The OPENSTACK_KEYSTONE_BACKEND settings can be used to identify the +# capabilities of the auth backend for Keystone. +# If Keystone has been configured to use LDAP as the auth backend then set +# can_edit_user to False and name to 'ldap'. +# +# TODO(tres): Remove these once Keystone has an API to identify auth backend. +OPENSTACK_KEYSTONE_BACKEND = { + 'name': 'native', + 'can_edit_user': True, + 'can_edit_group': True, + 'can_edit_project': True, + 'can_edit_domain': True, + 'can_edit_role': True +} + +OPENSTACK_HYPERVISOR_FEATURES = { + 'can_set_mount_point': True, + + # NOTE: as of Grizzly this is not yet supported in Nova so enabling this + # setting will not do anything useful + 'can_encrypt_volumes': False +} + +# The OPENSTACK_NEUTRON_NETWORK settings can be used to enable optional +# services provided by neutron. Currently only the load balancer service +# is available. +OPENSTACK_NEUTRON_NETWORK = { + 'enable_security_group': True, + 'enable_lb': False, +} + +# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints +# in the Keystone service catalog. Use this setting when Horizon is running +# external to the OpenStack environment. The default is 'publicURL'. +#OPENSTACK_ENDPOINT_TYPE = "publicURL" + +# SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the +# case that OPENSTACK_ENDPOINT_TYPE is not present in the endpoints +# in the Keystone service catalog. Use this setting when Horizon is running +# external to the OpenStack environment. The default is None. This +# value should differ from OPENSTACK_ENDPOINT_TYPE if used. +#SECONDARY_ENDPOINT_TYPE = "publicURL" + +# The number of objects (Swift containers/objects or images) to display +# on a single page before providing a paging element (a "more" link) +# to paginate results. +API_RESULT_LIMIT = 1000 +API_RESULT_PAGE_SIZE = 20 + +# The timezone of the server. This should correspond with the timezone +# of your entire OpenStack installation, and hopefully be in UTC. +TIME_ZONE = "UTC" + +# When launching an instance, the menu of available flavors is +# sorted by RAM usage, ascending. Provide a callback method here +# (and/or a flag for reverse sort) for the sorted() method if you'd +# like a different behaviour. For more info, see +# http://docs.python.org/2/library/functions.html#sorted +# CREATE_INSTANCE_FLAVOR_SORT = { +# 'key': my_awesome_callback_method, +# 'reverse': False, +# } + +LOGGING = { + 'version': 1, + # When set to True this will disable all logging except + # for loggers specified in this configuration dictionary. Note that + # if nothing is specified here and disable_existing_loggers is True, + # django.db.backends will still log unless it is disabled explicitly. + 'disable_existing_loggers': False, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'django.utils.log.NullHandler', + }, + 'console': { + # Set the level to "DEBUG" for verbose output logging. + 'level': 'INFO', + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + # Logging from django.db.backends is VERY verbose, send to null + # by default. + 'django.db.backends': { + 'handlers': ['null'], + 'propagate': False, + }, + 'requests': { + 'handlers': ['null'], + 'propagate': False, + }, + 'horizon': { + 'handlers': ['console'], + 'propagate': False, + }, + 'openstack_dashboard': { + 'handlers': ['console'], + 'propagate': False, + }, + 'novaclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'cinderclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'keystoneclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'glanceclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'heatclient': { + 'handlers': ['console'], + 'propagate': False, + }, + 'nose.plugins.manager': { + 'handlers': ['console'], + 'propagate': False, + } + } +} + +SECURITY_GROUP_RULES = { + 'all_tcp': { + 'name': 'ALL TCP', + 'ip_protocol': 'tcp', + 'from_port': '1', + 'to_port': '65535', + }, + 'all_udp': { + 'name': 'ALL UDP', + 'ip_protocol': 'udp', + 'from_port': '1', + 'to_port': '65535', + }, + 'all_icmp': { + 'name': 'ALL ICMP', + 'ip_protocol': 'icmp', + 'from_port': '-1', + 'to_port': '-1', + }, + 'ssh': { + 'name': 'SSH', + 'ip_protocol': 'tcp', + 'from_port': '22', + 'to_port': '22', + }, + 'smtp': { + 'name': 'SMTP', + 'ip_protocol': 'tcp', + 'from_port': '25', + 'to_port': '25', + }, + 'dns': { + 'name': 'DNS', + 'ip_protocol': 'tcp', + 'from_port': '53', + 'to_port': '53', + }, + 'http': { + 'name': 'HTTP', + 'ip_protocol': 'tcp', + 'from_port': '80', + 'to_port': '80', + }, + 'pop3': { + 'name': 'POP3', + 'ip_protocol': 'tcp', + 'from_port': '110', + 'to_port': '110', + }, + 'imap': { + 'name': 'IMAP', + 'ip_protocol': 'tcp', + 'from_port': '143', + 'to_port': '143', + }, + 'ldap': { + 'name': 'LDAP', + 'ip_protocol': 'tcp', + 'from_port': '389', + 'to_port': '389', + }, + 'https': { + 'name': 'HTTPS', + 'ip_protocol': 'tcp', + 'from_port': '443', + 'to_port': '443', + }, + 'smtps': { + 'name': 'SMTPS', + 'ip_protocol': 'tcp', + 'from_port': '465', + 'to_port': '465', + }, + 'imaps': { + 'name': 'IMAPS', + 'ip_protocol': 'tcp', + 'from_port': '993', + 'to_port': '993', + }, + 'pop3s': { + 'name': 'POP3S', + 'ip_protocol': 'tcp', + 'from_port': '995', + 'to_port': '995', + }, + 'ms_sql': { + 'name': 'MS SQL', + 'ip_protocol': 'tcp', + 'from_port': '1443', + 'to_port': '1443', + }, + 'mysql': { + 'name': 'MYSQL', + 'ip_protocol': 'tcp', + 'from_port': '3306', + 'to_port': '3306', + }, + 'rdp': { + 'name': 'RDP', + 'ip_protocol': 'tcp', + 'from_port': '3389', + 'to_port': '3389', + }, +} + +TUSKAR_ENDPOINT_URL = "http://127.0.0.1:8585" + +HORIZON_CONFIG['user_home'] = '' + +OVERCLOUD_CREDS = { + 'user': 'admin', + 'password': 'password', + 'tenant': 'admin', + 'auth_url': 'http://localhost:5000/v2.0/', +} diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..256981c --- /dev/null +++ b/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +import os +import sys + +from django.core.management import execute_from_command_line + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", + "openstack_dashboard.settings") + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f69d9de --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +pbr>=0.6,!=0.7,<1.0 +# Horizon Core Requirements +django>=1.4,<1.6 +django_compressor>=1.3 +django_openstack_auth>=1.1.3 +eventlet>=0.13.0 +kombu>=2.4.8 +iso8601>=0.1.8 +netaddr>=0.7.6 +python-cinderclient>=1.0.6 +python-glanceclient>=0.9.0 +python-heatclient>=0.2.3 +python-keystoneclient>=0.4.1 +python-novaclient>=2.15.0 +python-neutronclient>=2.3.0,<3 +python-swiftclient>=1.5 +python-ceilometerclient>=1.0.6 +pytz>=2010h +# Horizon Utility Requirements +# for SECURE_KEY generation +lockfile>=0.8 + +python-ironicclient +http://tarballs.openstack.org/python-tuskarclient/python-tuskarclient-master.tar.gz#egg=python-tuskarclient diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..664f34d --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,469 @@ +#!/bin/bash + +set -o errexit + +# ---------------UPDATE ME-------------------------------# +# Increment me any time the environment should be rebuilt. +# This includes dependency changes, directory renames, etc. +# Simple integer sequence: 1, 2, 3... +environment_version=42 +#--------------------------------------------------------# + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Horizon's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically" + echo " if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local" + echo " environment" + echo " -c, --coverage Generate reports using Coverage" + echo " -f, --force Force a clean re-build of the virtual" + echo " environment. Useful when dependencies have" + echo " been added." + echo " -m, --manage Run a Django management command." + echo " --makemessages Update all translation files." + echo " --compilemessages Compile all translation files." + echo " -p, --pep8 Just run pep8" + echo " -t, --tabs Check for tab characters in files." + echo " -y, --pylint Just run pylint" + echo " -j, --jshint Just run jshint" + echo " -q, --quiet Run non-interactively. (Relatively) quiet." + echo " Implies -V if -N is not set." + echo " --only-selenium Run only the Selenium unit tests" + echo " --with-selenium Run unit tests including Selenium tests" + echo " --selenium-headless Run Selenium tests headless" + echo " --runserver Run the Django development server for" + echo " openstack_dashboard in the virtual" + echo " environment." + echo " --docs Just build the documentation" + echo " --backup-environment Make a backup of the environment on exit" + echo " --restore-environment Restore the environment before running" + echo " --destroy-environment Destroy the environment and exit" + echo " -h, --help Print this usage message" + echo "" + echo "Note: with no options specified, the script will try to run the tests in" + echo " a virtual environment, If no virtualenv is found, the script will ask" + echo " if you would like to create one. If you prefer to run tests NOT in a" + echo " virtual environment, simply pass the -N option." + exit +} + +# DEFAULTS FOR RUN_TESTS.SH +# +root=`pwd` +venv=$root/.venv +with_venv=tools/with_venv.sh +included_dirs="tuskar_ui" + +always_venv=0 +backup_env=0 +command_wrapper="" +destroy=0 +force=0 +just_pep8=0 +just_pylint=0 +just_docs=0 +just_tabs=0 +just_jshint=0 +never_venv=0 +quiet=0 +restore_env=0 +runserver=0 +only_selenium=0 +with_selenium=0 +selenium_headless=0 +testopts="" +testargs="" +with_coverage=0 +makemessages=0 +compilemessages=0 +manage=0 + +COVERAGE_CMD="python -m coverage.__main__" + +# Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default" +[ "$JOB_NAME" ] || JOB_NAME="default" + +function process_option { + # If running manage command, treat the rest of options as arguments. + if [ $manage -eq 1 ]; then + testargs="$testargs $1" + return 0 + fi + + case "$1" in + -h|--help) usage;; + -V|--virtual-env) always_venv=1; never_venv=0;; + -N|--no-virtual-env) always_venv=0; never_venv=1;; + -p|--pep8) just_pep8=1;; + -y|--pylint) just_pylint=1;; + -j|--jshint) just_jshint=1;; + -f|--force) force=1;; + -t|--tabs) just_tabs=1;; + -q|--quiet) quiet=1;; + -c|--coverage) with_coverage=1;; + -m|--manage) manage=1;; + --makemessages) makemessages=1;; + --compilemessages) compilemessages=1;; + --only-selenium) only_selenium=1;; + --with-selenium) with_selenium=1;; + --selenium-headless) selenium_headless=1;; + --docs) just_docs=1;; + --runserver) runserver=1;; + --backup-environment) backup_env=1;; + --restore-environment) restore_env=1;; + --destroy-environment) destroy=1;; + -*) testopts="$testopts $1";; + *) testargs="$testargs $1" + esac +} + +function run_management_command { + ${command_wrapper} python $root/manage.py $testopts $testargs +} + +function run_server { + echo "Starting Django development server..." + ${command_wrapper} python $root/manage.py runserver $testopts $testargs + echo "Server stopped." +} + +function run_pylint { + echo "Running pylint ..." + PYTHONPATH=$root ${command_wrapper} pylint --rcfile=.pylintrc -f parseable $included_dirs > pylint.txt || true + CODE=$? + grep Global -A2 pylint.txt + if [ $CODE -lt 32 ]; then + echo "Completed successfully." + exit 0 + else + echo "Completed with problems." + exit $CODE + fi +} + +function run_jshint { + echo "Running jshint ..." + jshint tuskar_ui/infrastructure/static/infrastructure +} + +function run_pep8 { + echo "Running flake8 ..." + DJANGO_SETTINGS_MODULE=tuskar_ui.test.settings ${command_wrapper} flake8 $included_dirs +} + +function run_sphinx { + echo "Building sphinx..." + export DJANGO_SETTINGS_MODULE=openstack_dashboard.settings + ${command_wrapper} sphinx-build -b html doc/source doc/build/html + echo "Build complete." +} + +function tab_check { + TAB_VIOLATIONS=`find $included_dirs -type f -regex ".*\.\(css\|js\|py\|html\)" -print0 | xargs -0 awk '/\t/' | wc -l` + if [ $TAB_VIOLATIONS -gt 0 ]; then + echo "TABS! $TAB_VIOLATIONS of them! Oh no!" + HORIZON_FILES=`find $included_dirs -type f -regex ".*\.\(css\|js\|py|\html\)"` + for TABBED_FILE in $HORIZON_FILES + do + TAB_COUNT=`awk '/\t/' $TABBED_FILE | wc -l` + if [ $TAB_COUNT -gt 0 ]; then + echo "$TABBED_FILE: $TAB_COUNT" + fi + done + fi + return $TAB_VIOLATIONS; +} + +function destroy_venv { + echo "Cleaning environment..." + echo "Removing virtualenv..." + rm -rf $venv + echo "Virtualenv removed." + rm -f .environment_version + echo "Environment cleaned." +} + +function environment_check { + echo "Checking environment." + if [ -f .environment_version ]; then + ENV_VERS=`cat .environment_version` + if [ $ENV_VERS -eq $environment_version ]; then + if [ -e ${venv} ]; then + # If the environment exists and is up-to-date then set our variables + command_wrapper="${root}/${with_venv}" + echo "Environment is up to date." + return 0 + fi + fi + fi + + if [ $always_venv -eq 1 ]; then + install_venv + else + if [ ! -e ${venv} ]; then + echo -e "Environment not found. Install? (Y/n) \c" + else + echo -e "Your environment appears to be out of date. Update? (Y/n) \c" + fi + read update_env + if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then + install_venv + else + # Set our command wrapper anyway. + command_wrapper="${root}/${with_venv}" + fi + fi +} + +function sanity_check { + # Anything that should be determined prior to running the tests, server, etc. + # Don't sanity-check anything environment-related in -N flag is set + if [ $never_venv -eq 0 ]; then + if [ ! -e ${venv} ]; then + echo "Virtualenv not found at $venv. Did install_venv.py succeed?" + exit 1 + fi + fi + # Remove .pyc files. This is sanity checking because they can linger + # after old files are deleted. + find . -name "*.pyc" -exec rm -rf {} \; +} + +function backup_environment { + if [ $backup_env -eq 1 ]; then + echo "Backing up environment \"$JOB_NAME\"..." + if [ ! -e ${venv} ]; then + echo "Environment not installed. Cannot back up." + return 0 + fi + if [ -d /tmp/.horizon_environment/$JOB_NAME ]; then + mv /tmp/.horizon_environment/$JOB_NAME /tmp/.horizon_environment/$JOB_NAME.old + rm -rf /tmp/.horizon_environment/$JOB_NAME + fi + mkdir -p /tmp/.horizon_environment/$JOB_NAME + cp -r $venv /tmp/.horizon_environment/$JOB_NAME/ + cp .environment_version /tmp/.horizon_environment/$JOB_NAME/ + # Remove the backup now that we've completed successfully + rm -rf /tmp/.horizon_environment/$JOB_NAME.old + echo "Backup completed" + fi +} + +function restore_environment { + if [ $restore_env -eq 1 ]; then + echo "Restoring environment from backup..." + if [ ! -d /tmp/.horizon_environment/$JOB_NAME ]; then + echo "No backup to restore from." + return 0 + fi + + cp -r /tmp/.horizon_environment/$JOB_NAME/.venv ./ || true + cp -r /tmp/.horizon_environment/$JOB_NAME/.environment_version ./ || true + + echo "Environment restored successfully." + fi +} + +function install_venv { + # Install with install_venv.py + export PIP_DOWNLOAD_CACHE=${PIP_DOWNLOAD_CACHE-/tmp/.pip_download_cache} + export PIP_USE_MIRRORS=true + if [ $quiet -eq 1 ]; then + export PIP_NO_INPUT=true + fi + echo "Fetching new src packages..." + rm -rf $venv/src + python tools/install_venv.py + command_wrapper="$root/${with_venv}" + # Make sure it worked and record the environment version + sanity_check + chmod -R 754 $venv + echo $environment_version > .environment_version +} + +function run_tests { + sanity_check + + if [ $with_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + elif [ $only_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + export SKIP_UNITTESTS=1 + fi + + if [ $selenium_headless -eq 1 ]; then + export SELENIUM_HEADLESS=1 + fi + + if [ -z "$testargs" ]; then + run_tests_all + else + run_tests_subset + fi +} + +function run_tests_subset { + project=`echo $testargs | awk -F. '{print $1}'` + ${command_wrapper} python $root/manage.py test --settings=$project.test.settings $testopts $testargs +} + +function run_tests_all { + echo "Running Tuskar-UI application tests" + export NOSE_XUNIT_FILE=tuskar_ui/nosetests.xml + if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then + export NOSE_HTML_OUT_FILE='tuskar_ui_nose_results.html' + fi + ${command_wrapper} ${COVERAGE_CMD} erase + ${command_wrapper} ${COVERAGE_CMD} run -p $root/manage.py test tuskar_ui --settings=tuskar_ui.test.settings $testopts + # get results of the Horizon tests + TUSKAR_UI_RESULT=$? + + if [ $with_coverage -eq 1 ]; then + echo "Generating coverage reports" + ${command_wrapper} ${COVERAGE_CMD} combine + ${command_wrapper} ${COVERAGE_CMD} xml -i --omit='/usr*,setup.py,*egg*,.venv/*' + ${command_wrapper} ${COVERAGE_CMD} html -i --omit='/usr*,setup.py,*egg*,.venv/*' -d reports + fi + # Remove the leftover coverage files from the -p flag earlier. + rm -f .coverage.* + + PEP8_RESULT=0 + if [ $only_selenium -eq 0 ]; then + run_pep8 + PEP8_RESULT=$? + fi + + TEST_RESULT=$(($TUSKAR_UI_RESULT || $PEP8_RESULT)) + if [ $TEST_RESULT -eq 0 ]; then + echo "Tests completed successfully." + else + echo "Tests failed." + fi + exit $(($TUSKAR_UI_RESULT)) +} + +function run_makemessages { + cd horizon + ${command_wrapper} $root/manage.py makemessages --all --no-obsolete + HORIZON_PY_RESULT=$? + ${command_wrapper} $root/manage.py makemessages -d djangojs --all --no-obsolete + HORIZON_JS_RESULT=$? + cd ../openstack_dashboard + ${command_wrapper} $root/manage.py makemessages --all --no-obsolete + DASHBOARD_RESULT=$? + cd .. + exit $(($HORIZON_PY_RESULT || $HORIZON_JS_RESULT || $DASHBOARD_RESULT)) +} + +function run_compilemessages { + cd horizon + ${command_wrapper} $root/manage.py compilemessages + HORIZON_PY_RESULT=$? + cd ../openstack_dashboard + ${command_wrapper} $root/manage.py compilemessages + DASHBOARD_RESULT=$? + cd .. + exit $(($HORIZON_PY_RESULT || $DASHBOARD_RESULT)) +} + + +# ---------PREPARE THE ENVIRONMENT------------ # + +# PROCESS ARGUMENTS, OVERRIDE DEFAULTS +for arg in "$@"; do + process_option $arg +done + +if [ $quiet -eq 1 ] && [ $never_venv -eq 0 ] && [ $always_venv -eq 0 ] +then + always_venv=1 +fi + +# If destroy is set, just blow it away and exit. +if [ $destroy -eq 1 ]; then + destroy_venv + exit 0 +fi + +# Ignore all of this if the -N flag was set +if [ $never_venv -eq 0 ]; then + + # Restore previous environment if desired + if [ $restore_env -eq 1 ]; then + restore_environment + fi + + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + destroy_venv + fi + + # Then check if it's up-to-date + environment_check + + # Create a backup of the up-to-date environment if desired + if [ $backup_env -eq 1 ]; then + backup_environment + fi +fi + +# ---------EXERCISE THE CODE------------ # + +# Run management commands +if [ $manage -eq 1 ]; then + run_management_command + exit $? +fi + +# Build the docs +if [ $just_docs -eq 1 ]; then + run_sphinx + exit $? +fi + +# Update translation files +if [ $makemessages -eq 1 ]; then + run_makemessages + exit $? +fi + +# Compile translation files +if [ $compilemessages -eq 1 ]; then + run_compilemessages + exit $? +fi + +# PEP8 +if [ $just_pep8 -eq 1 ]; then + run_pep8 + exit $? +fi + +# Pylint +if [ $just_pylint -eq 1 ]; then + run_pylint + exit $? +fi + +# Tab checker +if [ $just_tabs -eq 1 ]; then + tab_check + exit $? +fi + +# Jshint +if [ $just_jshint -eq 1 ]; then + run_jshint + exit $? +fi + +# Django development server +if [ $runserver -eq 1 ]; then + run_server + exit $? +fi + +# Full test suite +run_tests || exit diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8e4c5c6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,41 @@ +[metadata] +name = tuskar-ui +version = 2013.2 +summary = Tuskar Management Dashboard +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Development Status :: 5 - Production/Stable + Environment :: OpenStack + Framework :: Django + Intended Audience :: Developers + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 + Topic :: Internet :: WWW/HTTP + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[files] +packages = + tuskar_ui + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source + +[nosetests] +verbosity=2 +detailed-errors=1 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..c0a24ea --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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 setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..c497df2 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,22 @@ +# Hacking already pins down pep8, pyflakes and flake8 +hacking>=0.8.0,<0.9 +# Testing Requirements +coverage>=3.6 +django-nose +mock>=1.0 +mox>=0.5.3 +nodeenv +nose +nose-exclude +nosexcover +openstack.nose_plugin>=0.7 +nosehtmloutput>=0.0.3 +selenium +xvfbwrapper +# Docs Requirements +sphinx>=1.1.2,<1.2 +# for bug 1091333, remove after sphinx >1.1.3 is released. +docutils==0.9.1 +oslosphinx + +http://tarballs.openstack.org/horizon/horizon-master.tar.gz#egg=horizon diff --git a/tools/install_venv.py b/tools/install_venv.py new file mode 100644 index 0000000..8550e2c --- /dev/null +++ b/tools/install_venv.py @@ -0,0 +1,154 @@ +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 OpenStack, LLC +# +# Copyright 2012 Nebula, Inc. +# +# 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. + +""" +Installation script for the OpenStack Dashboard development virtualenv. +""" + +import os +import subprocess +import sys + + +ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +VENV = os.path.join(ROOT, '.venv') +WITH_VENV = os.path.join(ROOT, 'tools', 'with_venv.sh') +PIP_REQUIRES = os.path.join(ROOT, 'requirements.txt') +TEST_REQUIRES = os.path.join(ROOT, 'test-requirements.txt') + + +def die(message, *args): + print >> sys.stderr, message % args + sys.exit(1) + + +def run_command(cmd, redirect_output=True, check_exit_code=True, cwd=ROOT, + die_message=None): + """ + Runs a command in an out-of-process shell, returning the + output of that command. Working directory is ROOT. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=cwd, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + if die_message is None: + die('Command "%s" failed.\n%s', ' '.join(cmd), output) + else: + die(die_message) + return output + + +HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'], + check_exit_code=False).strip()) +HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'], + check_exit_code=False).strip()) + + +def check_dependencies(): + """Make sure virtualenv is in the path.""" + + print 'Checking dependencies...' + if not HAS_VIRTUALENV: + print 'Virtual environment not found.' + # Try installing it via easy_install... + if HAS_EASY_INSTALL: + print 'Installing virtualenv via easy_install...', + run_command(['easy_install', 'virtualenv'], + die_message='easy_install failed to install virtualenv' + '\ndevelopment requires virtualenv, please' + ' install it using your favorite tool') + if not run_command(['which', 'virtualenv']): + die('ERROR: virtualenv not found in path.\n\ndevelopment ' + ' requires virtualenv, please install it using your' + ' favorite package management tool and ensure' + ' virtualenv is in your path') + print 'virtualenv installation done.' + else: + die('easy_install not found.\n\nInstall easy_install' + ' (python-setuptools in ubuntu) or virtualenv by hand,' + ' then rerun.') + print 'dependency check done.' + + +def create_virtualenv(venv=VENV): + """Creates the virtual environment and installs PIP only into the + virtual environment + """ + print 'Creating venv...', + run_command(['virtualenv', '-q', '--no-site-packages', VENV]) + print 'done.' + print 'Installing pip in virtualenv...', + if not run_command([WITH_VENV, 'easy_install', 'pip']).strip(): + die("Failed to install pip.") + print 'done.' + print 'Installing distribute in virtualenv...' + pip_install('distribute>=0.6.24') + print 'done.' + + +def pip_install(*args): + args = [WITH_VENV, 'pip', 'install', '--upgrade'] + list(args) + run_command(args, redirect_output=False) + + +def install_dependencies(venv=VENV): + print "Installing dependencies..." + print "(This may take several minutes, don't panic)" + pip_install('-r', TEST_REQUIRES) + pip_install('-r', PIP_REQUIRES) + + # Tell the virtual env how to "import dashboard" + py = 'python%d.%d' % (sys.version_info[0], sys.version_info[1]) + pthfile = os.path.join(venv, "lib", py, "site-packages", "dashboard.pth") + f = open(pthfile, 'w') + f.write("%s\n" % ROOT) + + +def install_horizon(): + print 'Installing horizon module in development mode...' + run_command([WITH_VENV, 'python', 'setup.py', 'develop'], cwd=ROOT) + + +def print_summary(): + summary = """ +Horizon development environment setup is complete. + +To activate the virtualenv for the extent of your current shell session you +can run: + +$ source .venv/bin/activate +""" + print summary + + +def main(): + check_dependencies() + create_virtualenv() + install_dependencies() + install_horizon() + print_summary() + +if __name__ == '__main__': + main() diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 0000000..c8d2940 --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,4 @@ +#!/bin/bash +TOOLS=`dirname $0` +VENV=$TOOLS/../.venv +source $VENV/bin/activate && $@ diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1101345 --- /dev/null +++ b/tox.ini @@ -0,0 +1,71 @@ +[tox] +envlist = py26,py27,py27dj14,py27dj15,py27dj16,pep8,selenium,jshint + +[testenv] +setenv = VIRTUAL_ENV={envdir} + NOSE_WITH_OPENSTACK=1 + NOSE_OPENSTACK_COLOR=1 + NOSE_OPENSTACK_RED=0.05 + NOSE_OPENSTACK_YELLOW=0.025 + NOSE_OPENSTACK_SHOW_ELAPSED=1 +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = /bin/bash run_tests.sh -N + +[testenv:pep8] +commands = /bin/bash run_tests.sh -N --pep8 + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = /bin/bash run_tests.sh -N --coverage + +[testenv:py27dj14] +basepython = python2.7 +commands = pip install django>=1.4,<1.5 + /bin/bash run_tests.sh -N + +[testenv:py27dj15] +basepython = python2.7 +commands = pip install django>=1.5,<1.6 + /bin/bash run_tests.sh -N + +[testenv:py27dj16] +basepython = python2.7 +commands = pip install django>=1.6,<1.7 + /bin/bash run_tests.sh -N + +[testenv:selenium] +commands = /bin/bash run_tests.sh -N --only-selenium + +[testenv:jshint] +commands = nodeenv -p + npm install jshint -g + /bin/bash run_tests.sh -N --jshint + +[tox:jenkins] +downloadcache = ~/cache/pip + +[flake8] +builtins = _ +exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg, + build,panel_template,dash_template,local_settings.py + +[hacking] +import_exceptions = collections.defaultdict, + django.conf.settings, + django.core.urlresolvers.reverse, + django.core.urlresolvers.reverse_lazy, + django.template.loader.render_to_string, + django.utils.datastructures.SortedDict, + django.utils.encoding.force_unicode, + django.utils.html.conditional_escape, + django.utils.html.escape, + django.utils.http.urlencode, + django.utils.safestring.mark_safe, + django.utils.translation.pgettext_lazy, + django.utils.translation.ugettext_lazy, + django.utils.translation.ungettext_lazy, + operator.attrgetter, + StringIO.StringIO diff --git a/tuskar_ui.egg-info/PKG-INFO b/tuskar_ui.egg-info/PKG-INFO new file mode 100644 index 0000000..33cd66a --- /dev/null +++ b/tuskar_ui.egg-info/PKG-INFO @@ -0,0 +1,66 @@ +Metadata-Version: 1.1 +Name: tuskar-ui +Version: 2013.2.dev66.ga4cfd9c +Summary: Tuskar Management Dashboard +Home-page: http://www.openstack.org/ +Author: OpenStack +Author-email: openstack-dev@lists.openstack.org +License: UNKNOWN +Description: ========= + Tuskar UI + ========= + + **Tuskar UI** is a user interface for + `Tuskar `__, a management API for + OpenStack deployments. It is a plugin for `OpenStack + Horizon `__. + + High-Level Overview + ------------------- + + Tuskar UI endeavours to be a stateless UI, relying on Tuskar API calls + as much as possible. We use existing Horizon libraries and components + where possible. If added libraries and components are needed, we will + work with the OpenStack community to push those changes back into Horizon. + + Interested in seeing Tuskar and Tuskar UI in action? + `Watch a demo! `_ + + + Installation Guide + ------------------ + + Use the `Installation Guide `_ to install Tuskar UI. + + License + ------- + + This project is licensed under the Apache License, version 2. More + information can be found in the LICENSE file. + + Contact Us + ---------- + + Join us on IRC (Internet Relay Chat):: + + Network: Freenode (irc.freenode.net/tuskar) + Channel: #tuskar + + Or send an email to openstack-dev@lists.openstack.org. + + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: OpenStack +Classifier: Framework :: Django +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Topic :: Internet :: WWW/HTTP diff --git a/tuskar_ui.egg-info/SOURCES.txt b/tuskar_ui.egg-info/SOURCES.txt new file mode 100644 index 0000000..9cbfbf6 --- /dev/null +++ b/tuskar_ui.egg-info/SOURCES.txt @@ -0,0 +1,197 @@ +.mailmap +.pylintrc +HACKING.rst +LICENSE +MANIFEST.in +Makefile +README.rst +_10_admin.py.example +_20_project.py.example +_50_tuskar.py.example +local_settings.py.example +manage.py +requirements.txt +run_tests.sh +setup.cfg +setup.py +test-requirements.txt +tox.ini +.tx/config +bin/less/lessc +bin/lib/less/browser.js +bin/lib/less/colors.js +bin/lib/less/cssmin.js +bin/lib/less/functions.js +bin/lib/less/index.js +bin/lib/less/parser.js +bin/lib/less/rhino.js +bin/lib/less/tree.js +bin/lib/less/tree/alpha.js +bin/lib/less/tree/anonymous.js +bin/lib/less/tree/assignment.js +bin/lib/less/tree/call.js +bin/lib/less/tree/color.js +bin/lib/less/tree/comment.js +bin/lib/less/tree/condition.js +bin/lib/less/tree/dimension.js +bin/lib/less/tree/directive.js +bin/lib/less/tree/element.js +bin/lib/less/tree/expression.js +bin/lib/less/tree/import.js +bin/lib/less/tree/javascript.js +bin/lib/less/tree/keyword.js +bin/lib/less/tree/media.js +bin/lib/less/tree/mixin.js +bin/lib/less/tree/operation.js +bin/lib/less/tree/paren.js +bin/lib/less/tree/quoted.js +bin/lib/less/tree/rule.js +bin/lib/less/tree/ruleset.js +bin/lib/less/tree/selector.js +bin/lib/less/tree/url.js +bin/lib/less/tree/value.js +bin/lib/less/tree/variable.js +doc/Makefile +doc/make.bat +doc/source/HACKING.rst +doc/source/README.rst +doc/source/conf.py +doc/source/devstack_baremetal.rst +doc/source/index.rst +doc/source/install.rst +tools/install_venv.py +tools/with_venv.sh +tuskar_ui/__init__.py +tuskar_ui/cached_property.py +tuskar_ui/exceptions.py +tuskar_ui/forms.py +tuskar_ui/handle_errors.py +tuskar_ui/tables.py +tuskar_ui/utils.py +tuskar_ui/workflows.py +tuskar_ui.egg-info/PKG-INFO +tuskar_ui.egg-info/SOURCES.txt +tuskar_ui.egg-info/dependency_links.txt +tuskar_ui.egg-info/not-zip-safe +tuskar_ui.egg-info/requires.txt +tuskar_ui.egg-info/top_level.txt +tuskar_ui/api/__init__.py +tuskar_ui/api/flavor.py +tuskar_ui/api/heat.py +tuskar_ui/api/node.py +tuskar_ui/api/tuskar.py +tuskar_ui/infrastructure/__init__.py +tuskar_ui/infrastructure/dashboard.py +tuskar_ui/infrastructure/flavors/__init__.py +tuskar_ui/infrastructure/flavors/panel.py +tuskar_ui/infrastructure/flavors/tables.py +tuskar_ui/infrastructure/flavors/tabs.py +tuskar_ui/infrastructure/flavors/tests.py +tuskar_ui/infrastructure/flavors/urls.py +tuskar_ui/infrastructure/flavors/views.py +tuskar_ui/infrastructure/flavors/workflows.py +tuskar_ui/infrastructure/flavors/templates/flavors/create.html +tuskar_ui/infrastructure/flavors/templates/flavors/details.html +tuskar_ui/infrastructure/flavors/templates/flavors/index.html +tuskar_ui/infrastructure/nodes/__init__.py +tuskar_ui/infrastructure/nodes/forms.py +tuskar_ui/infrastructure/nodes/panel.py +tuskar_ui/infrastructure/nodes/tables.py +tuskar_ui/infrastructure/nodes/tabs.py +tuskar_ui/infrastructure/nodes/tests.py +tuskar_ui/infrastructure/nodes/urls.py +tuskar_ui/infrastructure/nodes/views.py +tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_field.html +tuskar_ui/infrastructure/nodes/templates/nodes/_nodes_formset_form.html +tuskar_ui/infrastructure/nodes/templates/nodes/_overview.html +tuskar_ui/infrastructure/nodes/templates/nodes/_register.html +tuskar_ui/infrastructure/nodes/templates/nodes/details.html +tuskar_ui/infrastructure/nodes/templates/nodes/index.html +tuskar_ui/infrastructure/nodes/templates/nodes/register.html +tuskar_ui/infrastructure/overcloud/__init__.py +tuskar_ui/infrastructure/overcloud/forms.py +tuskar_ui/infrastructure/overcloud/panel.py +tuskar_ui/infrastructure/overcloud/tables.py +tuskar_ui/infrastructure/overcloud/tabs.py +tuskar_ui/infrastructure/overcloud/tests.py +tuskar_ui/infrastructure/overcloud/urls.py +tuskar_ui/infrastructure/overcloud/views.py +tuskar_ui/infrastructure/overcloud/templates/overcloud/_detail_overview.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/_role_edit.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_confirmation.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/_undeploy_in_progress.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/detail.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/node_counts.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/overcloud_role.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/role_edit.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/scale_node_counts.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/undeploy_confirmation.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_configuration.html +tuskar_ui/infrastructure/overcloud/templates/overcloud/undeployed_overview.html +tuskar_ui/infrastructure/overcloud/workflows/__init__.py +tuskar_ui/infrastructure/overcloud/workflows/scale.py +tuskar_ui/infrastructure/overcloud/workflows/scale_node_counts.py +tuskar_ui/infrastructure/overcloud/workflows/undeployed.py +tuskar_ui/infrastructure/overcloud/workflows/undeployed_configuration.py +tuskar_ui/infrastructure/overcloud/workflows/undeployed_overview.py +tuskar_ui/infrastructure/static/bootstrap/less/variables.less +tuskar_ui/infrastructure/static/infrastructure/angular_templates/numberpicker.html +tuskar_ui/infrastructure/static/infrastructure/js/horizon.capacity.js +tuskar_ui/infrastructure/static/infrastructure/js/horizon.d3circleschart.js +tuskar_ui/infrastructure/static/infrastructure/js/horizon.d3singlebarchart.js +tuskar_ui/infrastructure/static/infrastructure/js/tuskar.formset_table.js +tuskar_ui/infrastructure/static/infrastructure/js/tuskar.js +tuskar_ui/infrastructure/static/infrastructure/js/tuskar.menu_formset.js +tuskar_ui/infrastructure/static/infrastructure/js/tuskar.templates.js +tuskar_ui/infrastructure/static/infrastructure/js/angular/horizon.number_picker.js +tuskar_ui/infrastructure/static/infrastructure/less/bootstrap.less +tuskar_ui/infrastructure/static/infrastructure/less/breadcrumbs.less +tuskar_ui/infrastructure/static/infrastructure/less/buttons.less +tuskar_ui/infrastructure/static/infrastructure/less/capacities.less +tuskar_ui/infrastructure/static/infrastructure/less/flavor_usages.less +tuskar_ui/infrastructure/static/infrastructure/less/formsets.less +tuskar_ui/infrastructure/static/infrastructure/less/horizon_upgrades.less +tuskar_ui/infrastructure/static/infrastructure/less/individual_pages.less +tuskar_ui/infrastructure/static/infrastructure/less/infrastructure.less +tuskar_ui/infrastructure/static/infrastructure/less/numberpicker.less +tuskar_ui/infrastructure/static/infrastructure/less/tables.less +tuskar_ui/infrastructure/static/infrastructure/tests/formset_table.js +tuskar_ui/infrastructure/templates/client_side/_modal_chart.html +tuskar_ui/infrastructure/templates/client_side/templates.html +tuskar_ui/infrastructure/templates/formset_table/_row.html +tuskar_ui/infrastructure/templates/formset_table/_table.html +tuskar_ui/infrastructure/templates/formset_table/menu_formset.html +tuskar_ui/infrastructure/templates/horizon/common/_form_errors.html +tuskar_ui/infrastructure/templates/horizon/common/_form_field.html +tuskar_ui/infrastructure/templates/horizon/common/_form_fields.html +tuskar_ui/infrastructure/templates/horizon/common/_horizontal_field.html +tuskar_ui/infrastructure/templates/horizon/common/_horizontal_fields.html +tuskar_ui/infrastructure/templates/horizon/common/_items_count_domain_page_header.html +tuskar_ui/infrastructure/templates/horizon/common/_items_count_tab_group.html +tuskar_ui/infrastructure/templates/infrastructure/_fullscreen_workflow.html +tuskar_ui/infrastructure/templates/infrastructure/_fullscreen_workflow_base.html +tuskar_ui/infrastructure/templates/infrastructure/_performance_chart.html +tuskar_ui/infrastructure/templates/infrastructure/_scripts.html +tuskar_ui/infrastructure/templates/infrastructure/_workflow_base.html +tuskar_ui/infrastructure/templates/infrastructure/base.html +tuskar_ui/infrastructure/templates/infrastructure/base_detail.html +tuskar_ui/infrastructure/templates/infrastructure/qunit.html +tuskar_ui/infrastructure/templatetags/__init__.py +tuskar_ui/infrastructure/templatetags/chart_helpers.py +tuskar_ui/test/__init__.py +tuskar_ui/test/formset_table_tests.py +tuskar_ui/test/helpers.py +tuskar_ui/test/selenium.py +tuskar_ui/test/settings.py +tuskar_ui/test/urls.py +tuskar_ui/test/api_tests/__init__.py +tuskar_ui/test/api_tests/heat_tests.py +tuskar_ui/test/api_tests/node_tests.py +tuskar_ui/test/api_tests/tuskar_tests.py +tuskar_ui/test/test_data/__init__.py +tuskar_ui/test/test_data/exceptions.py +tuskar_ui/test/test_data/flavor_data.py +tuskar_ui/test/test_data/heat_data.py +tuskar_ui/test/test_data/node_data.py +tuskar_ui/test/test_data/tuskar_data.py +tuskar_ui/test/test_data/utils.py \ No newline at end of file diff --git a/tuskar_ui.egg-info/dependency_links.txt b/tuskar_ui.egg-info/dependency_links.txt new file mode 100644 index 0000000..89b2c2f --- /dev/null +++ b/tuskar_ui.egg-info/dependency_links.txt @@ -0,0 +1 @@ +http://tarballs.openstack.org/python-tuskarclient/python-tuskarclient-master.tar.gz#egg=python-tuskarclient diff --git a/tuskar_ui.egg-info/not-zip-safe b/tuskar_ui.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tuskar_ui.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/tuskar_ui.egg-info/requires.txt b/tuskar_ui.egg-info/requires.txt new file mode 100644 index 0000000..e1d200e --- /dev/null +++ b/tuskar_ui.egg-info/requires.txt @@ -0,0 +1,20 @@ +pbr>=0.6,!=0.7,<1.0 +django>=1.4,<1.6 +django_compressor>=1.3 +django_openstack_auth>=1.1.3 +eventlet>=0.13.0 +kombu>=2.4.8 +iso8601>=0.1.8 +netaddr>=0.7.6 +python-cinderclient>=1.0.6 +python-glanceclient>=0.9.0 +python-heatclient>=0.2.3 +python-keystoneclient>=0.4.1 +python-novaclient>=2.15.0 +python-neutronclient>=2.3.0,<3 +python-swiftclient>=1.5 +python-ceilometerclient>=1.0.6 +pytz>=2010h +lockfile>=0.8 +python-ironicclient +python>=tuskarclient diff --git a/tuskar_ui.egg-info/top_level.txt b/tuskar_ui.egg-info/top_level.txt new file mode 100644 index 0000000..d119287 --- /dev/null +++ b/tuskar_ui.egg-info/top_level.txt @@ -0,0 +1 @@ +tuskar_ui diff --git a/tuskar_ui/__init__.py b/tuskar_ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tuskar_ui/__init__.pyc b/tuskar_ui/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73d258cf892bd554d980d7156e82db8d7891ef28 GIT binary patch literal 135 zcmZSn%*&N2xFF4FGnX7v literal 0 HcmV?d00001 diff --git a/tuskar_ui/api/__init__.py b/tuskar_ui/api/__init__.py new file mode 100644 index 0000000..a63c736 --- /dev/null +++ b/tuskar_ui/api/__init__.py @@ -0,0 +1,24 @@ +# 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. + +from tuskar_ui.api import flavor +from tuskar_ui.api import heat +from tuskar_ui.api import node +from tuskar_ui.api import tuskar + + +__all__ = [ + "flavor", + "heat", + "node", + "tuskar", +] diff --git a/tuskar_ui/api/__init__.pyc b/tuskar_ui/api/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ba8e4acb014363c7e0809a136277a2b1d0d5d96 GIT binary patch literal 370 zcmZvWK~BRk5JktaX-lOdxJ5S98F0$$UuUL)H7qH2LDEg<_~&at03f8pRvn&}v=o4siqK26`iMNJN9sw@)nQs=C+ z$x=)_`mH(n|Ga4l7qbb0xyDGd3=cqkh@I0Pu`|8V)}8f~ct85fglvs$&FqgpF>g;_ YX@!T6U9@9xPx%+a4dhmoR#N2t1AlKs00000 literal 0 HcmV?d00001 diff --git a/tuskar_ui/api/node.py b/tuskar_ui/api/node.py new file mode 100644 index 0000000..3c74fe3 --- /dev/null +++ b/tuskar_ui/api/node.py @@ -0,0 +1,557 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ +from horizon.utils import memoized +#from ironicclient.v1 import client as ironicclient +from novaclient.v1_1.contrib import baremetal +from openstack_dashboard.api import base +from openstack_dashboard.api import glance +from openstack_dashboard.api import nova +from openstack_dashboard.test.test_data import utils as test_utils + +from tuskar_ui.cached_property import cached_property # noqa +from tuskar_ui.handle_errors import handle_errors # noqa +from tuskar_ui.test.test_data import heat_data +from tuskar_ui.test.test_data import node_data +from tuskar_ui import utils + + +TEST_DATA = test_utils.TestDataContainer() +node_data.data(TEST_DATA) +heat_data.data(TEST_DATA) + +LOG = logging.getLogger(__name__) + + +def baremetalclient(request): + nc = nova.novaclient(request) + return baremetal.BareMetalNodeManager(nc) + + +# FIXME(lsmola) This should be done in Horizon, they don't have caching +@memoized.memoized +def image_get(request, image_id): + """Returns an Image object with metadata + + Returns an Image object populated with metadata for image + with supplied identifier. + + :param image_id: list of objects to be put into a dict + :type object_list: list + + :return: object + :rtype: glanceclient.v1.images.Image + """ + image = glance.image_get(request, image_id) + return image + + +class IronicNode(base.APIResourceWrapper): + _attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info', + 'properties', 'power_state') + + @classmethod + def create(cls, request, ipmi_address, cpu, ram, local_disk, + mac_addresses, ipmi_username=None, ipmi_password=None): + """Create a Node in Ironic + + :param request: request object + :type request: django.http.HttpRequest + + :param ipmi_address: IPMI address + :type ipmi_address: str + + :param cpu: number of cores + :type cpu: int + + :param ram: RAM in GB + :type ram: int + + :param local_disk: local disk in TB + :type local_disk: int + + :param mac_addresses: list of mac addresses + :type mac_addresses: list of str + + :param ipmi_username: IPMI username + :type ipmi_username: str + + :param ipmi_password: IPMI password + :type ipmi_password: str + + :return: the created Node object + :rtype: tuskar_ui.api.node.IronicNode + """ + #node = ironicclient(request).node.create( + # driver='pxe_ipmitool', + # driver_info={'ipmi_address': ipmi_address, + # 'ipmi_username': ipmi_username, + # 'password': ipmi_password}, + # properties={'cpu': cpu, + # 'ram': ram, + # 'local_disk': local_disk}) + #for mac_address in mac_addresses: + # ironicclient(request).port.create( + # node_uuid=node.uuid, + # address=mac_address + # ) + node = TEST_DATA.ironicclient_nodes.first() + return cls(node) + + @classmethod + def get(cls, request, uuid): + """Return the IronicNode that matches the ID + + :param request: request object + :type request: django.http.HttpRequest + + :param uuid: ID of IronicNode to be retrieved + :type uuid: str + + :return: matching IronicNode, or None if no IronicNode matches the ID + :rtype: tuskar_ui.api.node.IronicNode + """ + #node = ironicclient(request).nodes.get(uuid) + #return cls(node) + for node in IronicNode.list(request): + if node.uuid == uuid: + return node + + @classmethod + def get_by_instance_uuid(cls, request, instance_uuid): + """Return the IronicNode associated with the instance ID + + :param request: request object + :type request: django.http.HttpRequest + + :param instance_uuid: ID of Instance that is deployed on the IronicNode + to be retrieved + :type instance_uuid: str + + :return: matching IronicNode + :rtype: tuskar_ui.api.node.IronicNode + + :raises: ironicclient.exc.HTTPNotFound if there is no IronicNode with + the matching instance UUID + """ + #node = ironicclient(request).nodes.get_by_instance_uuid(instance_uuid) + #return cls(node) + for node in IronicNode.list(request): + if node.instance_uuid == instance_uuid: + return node + + @classmethod + @handle_errors(_("Unable to retrieve nodes"), []) + def list(cls, request, associated=None): + """Return a list of IronicNodes + + :param request: request object + :type request: django.http.HttpRequest + + :param associated: should we also retrieve all IronicNodes, only those + associated with an Instance, or only those not + associated with an Instance? + :type associated: bool + + :return: list of IronicNodes, or an empty list if there are none + :rtype: list of tuskar_ui.api.node.IronicNode + """ + #nodes = ironicclient(request).nodes.list( + # associated=associated) + nodes = TEST_DATA.ironicclient_nodes.list() + if associated is not None: + if associated: + nodes = [node for node in nodes + if node.instance_uuid is not None] + else: + nodes = [node for node in nodes + if node.instance_uuid is None] + + return [cls(node) for node in nodes] + + @classmethod + def delete(cls, request, uuid): + """Remove the IronicNode matching the ID if it + exists; otherwise, does nothing. + + :param request: request object + :type request: django.http.HttpRequest + + :param uuid: ID of IronicNode to be removed + :type uuid: str + """ + #ironicclient(request).nodes.delete(uuid) + return + + @cached_property + def addresses(self): + """Return a list of port addresses associated with this IronicNode + + :return: list of port addresses associated with this IronicNode, or + an empty list if no addresses are associated with + this IronicNode + :rtype: list of str + """ + ports = self.list_ports() + return [port.address for port in ports] + + +class BareMetalNode(base.APIResourceWrapper): + _attrs = ('id', 'uuid', 'instance_uuid', 'memory_mb', 'cpus', 'local_gb', + 'task_state', 'pm_user', 'pm_address', 'interfaces') + + @classmethod + def create(cls, request, ipmi_address, cpu, ram, local_disk, + mac_addresses, ipmi_username=None, ipmi_password=None): + """Create a Nova BareMetalNode + + :param request: request object + :type request: django.http.HttpRequest + + :param ipmi_address: IPMI address + :type ipmi_address: str + + :param cpu: number of cores + :type cpu: int + + :param ram: RAM in GB + :type ram: int + + :param local_disk: local disk in TB + :type local_disk: int + + :param mac_addresses: list of mac addresses + :type mac_addresses: list of str + + :param ipmi_username: IPMI username + :type ipmi_username: str + + :param ipmi_password: IPMI password + :type ipmi_password: str + + :return: the created BareMetalNode object + :rtype: tuskar_ui.api.node.BareMetalNode + """ + #node = baremetalclient(request).create( + # 'undercloud', + # cpu, + # ram, + # local_disk, + # mac_addresses, + # pm_address=ipmi_address, + # pm_user=ipmi_username, + # pm_password=ipmi_password) + node = TEST_DATA.baremetalclient_nodes.first() + return cls(node) + + @classmethod + def get(cls, request, uuid): + """Return the BareMetalNode that matches the ID + + :param request: request object + :type request: django.http.HttpRequest + + :param uuid: ID of BareMetalNode to be retrieved + :type uuid: str + + :return: matching BareMetalNode, or None if no BareMetalNode matches + the ID + :rtype: tuskar_ui.api.node.BareMetalNode + """ + #node = baremetalclient(request).get(uuid) + #return cls(node) + for node in BareMetalNode.list(request): + if node.uuid == uuid: + return node + + @classmethod + def get_by_instance_uuid(cls, request, instance_uuid): + """Return the BareMetalNode associated with the instance ID + + :param request: request object + :type request: django.http.HttpRequest + + :param instance_uuid: ID of Instance that is deployed on the + BareMetalNode to be retrieved + :type instance_uuid: str + + :return: matching BareMetalNode + :rtype: tuskar_ui.api.node.BareMetalNode + + :raises: ironicclient.exc.HTTPNotFound if there is no BareMetalNode + with the matching instance UUID + """ + #nodes = baremetalclient(request).list() + #node = next((n for n in nodes if instance_uuid == n.instance_uuid), + # None) + #return cls(node) + for node in BareMetalNode.list(request): + if node.instance_uuid == instance_uuid: + return node + + @classmethod + def list(cls, request, associated=None): + """Return a list of BareMetalNodes + + :param request: request object + :type request: django.http.HttpRequest + + :param associated: should we also retrieve all BareMetalNodes, only + those associated with an Instance, or only those not + associated with an Instance? + :type associated: bool + + :return: list of BareMetalNodes, or an empty list if there are none + :rtype: list of tuskar_ui.api.node.BareMetalNode + """ + #nodes = baremetalclient(request).list() + nodes = TEST_DATA.baremetalclient_nodes.list() + if associated is not None: + if associated: + nodes = [node for node in nodes + if node.instance_uuid is not None] + else: + nodes = [node for node in nodes + if node.instance_uuid is None] + return [cls(node) for node in nodes] + + @classmethod + def delete(cls, request, uuid): + """Remove the BareMetalNode if it exists; otherwise, do nothing. + + :param request: request object + :type request: django.http.HttpRequest + + :param uuid: ID of BareMetalNode to be removed + :type uuid: str + """ + #baremetalclient(request).delete(uuid) + return + + @cached_property + def power_state(self): + """Return a power state of this BareMetalNode + + :return: power state of this node + :rtype: str + """ + task_state_dict = { + 'initializing': 'initializing', + 'active': 'on', + 'reboot': 'rebooting', + 'building': 'building', + 'deploying': 'deploying', + 'prepared': 'prepared', + 'deleting': 'deleting', + 'deploy failed': 'deploy failed', + 'deploy complete': 'deploy complete', + 'deleted': 'deleted', + 'error': 'error', + } + return task_state_dict.get(self.task_state, 'off') + + @cached_property + def properties(self): + """Return properties of this BareMetalNode + + :return: return memory, cpus and local_disk properties + of this BareMetalNode, ram and local_disk properties + are in bytes + :rtype: dict of str + """ + return { + 'ram': self.memory_mb * 1024.0 * 1024.0, + 'cpu': self.cpus, + 'local_disk': self.local_gb * 1024.0 * 1024.0 * 1024.0 + } + + @cached_property + def driver_info(self): + """Return driver_info for this BareMetalNode + + :return: return pm_address property of this BareMetalNode + :rtype: dict of str + """ + try: + ip_address = (self.instance._apiresource.addresses['ctlplane'][0] + ['addr']) + except Exception: + LOG.error("Couldn't obtain IP address") + ip_address = None + + return { + 'ipmi_username': self.pm_user, + 'ipmi_address': self.pm_address, + 'ip_address': ip_address + } + + @cached_property + def addresses(self): + """Return a list of port addresses associated with this BareMetalNode + + :return: list of port addresses associated with this BareMetalNode, or + an empty list if no addresses are associated with + this BareMetalNode + :rtype: list of str + """ + return [interface["address"] for interface in + self.interfaces] + + +class NodeClient(object): + def __init__(self, request): + ironic_enabled = base.is_service_enabled(request, 'baremetal') + + if ironic_enabled: + self.node_class = IronicNode + else: + self.node_class = BareMetalNode + + +class Node(base.APIResourceWrapper): + _attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info', + 'properties', 'power_state', 'addresses') + + def __init__(self, apiresource, request=None, **kwargs): + """Initialize a Node + + :param apiresource: apiresource we want to wrap + :type apiresource: novaclient.v1_1.contrib.baremetal.BareMetalNode + + :param request: request + :type request: django.core.handlers.wsgi.WSGIRequest + + :param instance: instance relation we want to cache + :type instance: openstack_dashboard.api.nova.Server + + :return: Node object + :rtype: tusar_ui.api.node.Node + """ + super(Node, self).__init__(apiresource) + self._request = request + if 'instance' in kwargs: + self._instance = kwargs['instance'] + + @classmethod + def create(cls, request, ipmi_address, cpu, ram, local_disk, + mac_addresses, ipmi_username=None, ipmi_password=None): + return cls(NodeClient(request).node_class.create( + request, ipmi_address, cpu, ram, local_disk, + mac_addresses, ipmi_username=ipmi_username, + ipmi_password=ipmi_password)) + + @classmethod + @handle_errors(_("Unable to retrieve node")) + def get(cls, request, uuid): + node = NodeClient(request).node_class.get(request, uuid) + if node.instance_uuid is not None: + #server = nova.server_get(request, node.instance_uuid) + server = TEST_DATA.novaclient_servers.first() + return cls(node, instance=server, request=request) + + return cls(node) + + @classmethod + @handle_errors(_("Unable to retrieve node")) + def get_by_instance_uuid(cls, request, instance_uuid): + node = NodeClient(request).node_class.get_by_instance_uuid( + request, instance_uuid) + #server = nova.server_get(request, instance_uuid) + server = TEST_DATA.novaclient_servers.first() + return cls(node, instance=server, request=request) + + @classmethod + @handle_errors(_("Unable to retrieve nodes"), []) + def list(cls, request, associated=None): + nodes = NodeClient(request).node_class.list( + request, associated=associated) + + if associated is None or associated: + #servers = nova.server_list(request)[0] + servers = TEST_DATA.novaclient_servers.list() + servers_dict = utils.list_to_dict(servers) + nodes_with_instance = [] + for n in nodes: + server = servers_dict.get(n.instance_uuid, None) + nodes_with_instance.append(cls(n, instance=server, + request=request)) + return nodes_with_instance + else: + return [cls(node, request=request) for node in nodes] + + @classmethod + def delete(cls, request, uuid): + NodeClient(request).node_class.delete(request, uuid) + + @cached_property + def instance(self): + """Return the Nova Instance associated with this Node + + :return: Nova Instance associated with this Node; or + None if there is no Instance associated with this + Node, or no matching Instance is found + :rtype: Instance + """ + if hasattr(self, '_instance'): + return self._instance + + if self.instance_uuid: + #server = nova.server_get(self._request, self.instance_uuid) + server = TEST_DATA.novaclient_servers.first() + return server + + return None + + @cached_property + def image_name(self): + """Return image name of associated instance + + Returns image name of instance associated with node + + :return: Image name of instance + :rtype: string + """ + if self.instance is None: + return + return image_get(self._request, self.instance.image['id']).name + + @cached_property + def instance_status(self): + return getattr(getattr(self, 'instance', None), + 'status', None) + + +def filter_nodes(nodes, healthy=None): + """Filters the list of Nodes and returns the filtered list. + + :param nodes: list of tuskar_ui.api.node.Node objects to filter + :type nodes: list + :param healthy: retrieve all Nodes (healthy=None), + only the healthly ones (healthy=True), + or only those in an error state (healthy=False) + :type healthy: None or bool + :return: list of filtered tuskar_ui.api.node.Node objects + :rtype: list + """ + error_states = ('deploy failed', 'error',) + + if healthy is not None: + if healthy: + nodes = [node for node in nodes + if node.power_state not in error_states] + else: + nodes = [node for node in nodes + if node.power_state in error_states] + return nodes diff --git a/tuskar_ui/api/node.pyc b/tuskar_ui/api/node.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53e2a23e7a9c3963ed078a7e6b3b3d3b3dd2fc1f GIT binary patch literal 18185 zcmeHP%WoV-gNuK;(J}c?9p7)_lm5stCcvL#y2fm3x@NkL>DA0; z-8d|(o9=|^O`6R~%}?w%}+^w&TP(Uep>SLW^-QiGp4&> zdW&ZBl;&qmw_$oqW^+mNbEbRR^p?%$vgYSa_l)VSn9UW686H( z@9M1yxs`^Rn?i1<>-Rf(o~8Zxj<2PY$PKfon`*X!Y{&2HhCzFf#Dg%&@)xr|yM90D zhV3v(;v}!nx9^62)(-qEZ;`k9aS+O_4hK`qKUeXcK8s6+g>cM$ne7LT$!cb=Ztgpr z{0-?xYmL41*tu*D7xuT)jlzEBv9W9gmn#_b`y6x&FMHqb`#WK>CZ*FTO~Tv5FwM|3 zp6;ySYUGl>hSbH~xEEgBi+g_JUkt)K7qek{%THb%MtQ1@ls||ravaUUe#U9BldK(i z3Aa-A(l`yL!`G0z_>p0rL&6S|L#vqwHFLj~4={ZW6XS*1FzKhR-*>O~(0w=F+6z0G zdpF82Dv_380g@Vo>#mZd&Smq?>rLrx+LVcqin5j-XE-BCNqHL_vBo*}NRJu0LGMRFMq-i7 zF&EogLPHnu(bZjCc7z14CviXOkk&dVlONLA6@1d)!zF|$0aKqcn>7>GDM9!*Z9c1s zOqrCMGa>@O^;43aEwag| zTnn-!DPAaj6$z#EJq*>D2eaht`?Kt^a&!7GsC+enm%J>J`jRw7_?FT^;rs=wS{e$$u!|rO<^U$F6<6Y92u0*9{~FRN}3gNtT%1xV>aYKi#~o;L+wZ+9DDm?Q=@(BV-i`Vz8|GjF;!XE4DWTC-@9?+gI1iq z9S{4kNVhr61WS^R5j!nc+nOCMSI;s{F(~)L4Eu0HijbVwK7PhLm@xMz%-%#%V~yG0u9@3wl(x6;9#wV5BpXH54OZ=AFnO!X z(ke0&H0RjZ@4~1P>HMFiboQ+YQ3}NLXC{zK-iE^8jfY)0LohVEX=R4}Znq*sQ;64o zcOOI-r|KaUWHa_%Hm&{@J+*v&MM9UYU=7tH8f&4*KBT4ZTB6N$MOk;<-HPLGWhOx~ za2ZRJtuo)zA#^b84YGY*5X)C^mC?a|IabTah3)rvrcP^(1nND*3mti3Ezuv;Rmx*h z!|D&QTFL>51tjEt5_~7G|6}wcrQ2hc8_t3=43oBdX{x!;{s9fFf9&}~;h8t66-GzL+;07`55KzD_w0Q1RbK$>Wm*x?c2uJo1 zJEWk@mMV5mlxLSsO#$3G4r(G!X4nn0@GsC%N~vKMI$RCDjGwFcq%P(duOTv>W#3{(3FwS+ zAuPDu*#w=C8OQo}u!->w7i}M4Tq^ORP~k7S7G&!-hmvMQDe{-Ar$R1B!|t~5qSoOp zrjBdl5gf1s{ae0=Et}Z|XN}zu5{Lr1N#acp02AI*tTqNoq#RKRNl0-8l4z}+Lb-rJ z?KWM`c3Y%QyWNX}Appy^cq?r`%iwvj?M@f-4}fhq4!mDN-g}vs^Sr#m%M30V>9no3 zLz6c-0Pk0DDJMx%PlPCXG`zo!Pf8nP3P|--tzKJhHqJDjsyoZh@-vN7;=PTaoE_Aa zA5O>>eA1udk|R%N3gl_U1o)d9MVp z9|z`ytU}_zoE9W|x+qCBny}$Ot$?F?5HO}sQwMYse(}5@!gS$ugD9abRo+)oA~03ODtI3EJeQZR@$v#MU+3jTUcSK#wRtHapG-JX9RD*usy)u1FiLQ* zv>#~SjZ6_N!7m50G}fyV0GCEu6lbA=FO3uy5R;{s!oE;DxpcCvF>;9aFw&xJDR;}) zNhzd~FBEbTjZQ0HUGSw7$D7VUYgF*2ks*(pcrtedIPj&1oa}#-<yWviA(tPVMp>Mpnp z-})S_VD%X|vbCR8<2(vClB>h1kL!M1 zKainCJ+3jy*31$Zn{%v8ApMC<2$9%-K3`ERp|0I3gu^OUa~14;v;U%*0GZyJGs#=V zaU5Vy4EdB@EeB!(b^4C^D5nbu5Qq~}0b~dy3Zw%i%ElU*DgAw1astp0Dv6ttkh{l{ z2ssIq09LW$`$tewvHDh09JWHj&7G_}K&WRJp^Lh&PmD}h=_JXVICf)j=v1VzFj0w>Ut}TwH@q zh9xdN*Vi$t++~<6wl|+gv-v8Q2I=E$4m?UB6gzI8yR9#FuVJ*?2S~2ybyX;MuVF5g z+b=UkMJ;?U^k5O$<1R8W_aAeSxrUSif=WGlFCitjT9jFyrnnVhQlQMTwNPrNlpueO zGV88`MVTdQ$CS3jJn()M^*w47@7H+wCNGEWC~ywMxY2C{>MW1a%_9Y$+^z!s&40v4 zINt6mywX@{tk#{zYGbi+uF+_$2%ooLgj|)F>JFZeA55}tE1X6GQk4O%hS)x0T7l%V z_+X^7D0oqw%0@QQIeC$hs9>}!XHH4rg!Q8r~Oqrokb7>oqgLfiCs)iCN z5y)PNbeVQjTQ+Yzj#QNo->(sL+y>G>lj6~1lAegZHpF|Bwc9@>3C`dZ##ydSISVy? zeMtd7L{a%dNj;fV`v?vFgl{85O;J}F!_~6u#9my%C;cZ}@^zB!A8zCz#u!^K6Fk+x zEdCH?tU<`w`#~HswtS%0B+U3zEO4Kc)Q&+!Y&akZM$i7O0ss-2xl!&YB!CRj@UGktwH4%;T+*1%7&2;vftpeP~Bws z(=!G*T>vtazB!1Q$%znUum`40@&*$W|16kb@Cyuw4O!guudoW&3o9J{u7rzLjC|b; zs;I+tw~}EP%3Z&oG1lj9;t#AjSHyXgY82tb_J{+b;oGk_J5Z!awAI{#u_oYrb5uf? zjUB!tRhAzfFwLOlrk?0X(&pWCCu-i@c<1_&;nR883qrYyXtgB7$ph&7>M%PzJ2L+2 z;t8As7=(Pfa|?$<(%r4tPl7zO_Kx4&0NaPvU~4s$)*l(8>n9^2x?~mG1#bhipx}Zy zEI0)bCf>)m>5*TR!}Co%QCl{EOM}Hky3M0zWnER;(BZmu*H3oRlIfp}-`SBrv(g1& zk+X+RCH0!c$>Vu%zJ#<=i2Kz?zs8ZzqaG9FC#*3!AThba72up4UyyA%bffGi9;uZ zT1Z2T!Q5CVM9h8PfCBcW%-*yFnut15*2mOwx+o%Q8<=9;t;`xJNS1t!a@Q+o?L4zY zr&Q(3ei{S6OI8+D7AC1~rcJWyXo!u#SK_-<>EQqqsvO54W|#hsQ_2VdQ-8*3)Edq? z4Ta+C?Zmx_%tIy)Har3e;UayCkes7tN~owM9H#Tb)+_V#3bULQufvOMD2~m6%-%~Z z#UfR-=aVrKU-%CtL4lkw`bP&{a;`}HLQ#K%#KDOnPf>?DBr51TP}Gb;Cy`EmLemCQ z1udhqh4loDX~^V*Nvg;PQ|8_klGU}3*UbZzq4o^9bPwp1XwV19;;}h$xaD4WuS%4l zM(6f#nVYDxf##&4r^}NzMLmZe%%7MIG`S(-tEE6`Nx_t4l@%hnmBg#d#6=W0PhyC& zSkM{`j^S!40yTgj55{+gEhsB6krH$&P3386Dm#N#Xvw8*?xt9ID*-osp@S0xb^n#b zKoi^48(@WHkV8Wv1kXBWom0+486#Ln9a(=;>B05q;@9NdX>mZ24^(E7z@NdpUZu% z7R(SE<9cAPLhudIuU5gXmsM;{a6mV@F|_3JD3t;V6Mn90zxDgCL$?Otb{m z4bx?ggp_1z%Z`{gj{&Q2@)h_@sZJ^ws6r-sZX9PvrMMEPDS(U(4{fcT#?#{nimc}k zS@2C>Xd+5DruXYiiNz&mg=j7cM+H=b7JAGMa4ChZq@K)}ppWpUWI-1R#Znel5TiL& zcb=QWXGY>QPd6qIv$@!zOy)#dwoH(LUgn{1IN1|of%chBuIBiooY_ztRji9!(3cvN;qUyV>3bz%Eo^Mgm26{W{A|g^x#}(imCAV%jsLj)2DAK_F*cGNWsA^IziY)Y#c00DUD=A zE!9oJSp-egvE}BusyAH(vD2i!2Hd76VT4JeI(j_;`!od(#kEOW|sK{vAnDyWi9U(@O{wQm6le4zOFwJ`Qqg}Q!3syf z5uVs~)Swi3rsG9dM;V2Uz^EcTX z6@|w|5uwuG6h7m1oH&<~mQqogNXwQs740cf+^+~w#zg>`vjf%=OwH-9;|MB&2U=N2#Lu2JYvr?CV(qd)O$C}?&DqL=1FQ+9c_ z*w@zTW9!Gk&!cvim(|c_(NZ*X3~^GSzBmj2&MI{3(21h#13DqYkwwYA+lzD@8uw*jbm(Hz9_T1{lVGS#)3Q5Ru6xmO zyTNW*-Ze`zTg9`TCR$dMTgcczkBptiq_IJ6%~50zJoHG$Hq#;rAM<$)-dJ8RI_`-Q zXsr3rByG)X zWasckX{@$yLN{zFIXBc>YDwKub^*P1F?^}>og^!4muLCMncYM)*B~^&2=qZ#$m^77 zV0RLEQQ|HK`99n0`{KIq57V%SFS-$L@7qy!#h)pi^4`GNt85%ctfuUBJW3~aqwe5U zS9p=Qaudqf74haAOmx^GUeRU2x#qToW*$LA6=p5awayQR)=w*p=KTH`<jQ4d*D?U+czcFp$LpiXsaGs|63H0GK^C ze;np0iLCES;JrEzGim~Bb7PsMq3#7mY<(G#AWs|Vx3SxMXksI&o2sUkRa=boIO^UF z7VMzMODAVddmuacF4{$E=b%5`*b)l84o)`V5}fB(@p+j}=CGvm7br*~^3YdO8Y>s& z4>=vRID0%N)e80J469L;s$7F5nHA&{g$U!F7fFk%ytqbomU<{$^OTQBDTVy>>r8!= zYOKfqS4D}U?*#~KOIs*647z?81+nh_1LfYi>J=yltASI15cdSo>R{S1{5v3~&l}vI z&lPO_5t`w8Q3HHHS3des=LR-M51`AyI2r{zznxeV@1XR}Mf%+!40CNvv%&RB)}s*f zpk?@TG<6X@{7j0qvAI0AzuPI+9+G9T_(VC)Ab9%b54wCy&%pwG-+f_%^)rabqDycqUTocoi<3#)S zXL@(A%RRIUvhk^HZ2^VBbcEv?%8qRDrcWV8t4t~&5H~++Wm!wllmaopqI+NwrX1nV zH^QGPa}4zl+>p;q?$M@|d%u_^|^Ykg&xPCfn74`G~NoEVjtn*QvgqmtFe&r$w`$4jw zmZ0V$UG|%WjbeQ7 +
    +

    {% trans "Info" %}

    +
    +
    {% trans "MAC Addresses" %}
    +
    {{ node.addresses|join:", "|default:"—" }}
    +
    {% trans "UUID" %}
    +
    {{ node.uuid|default:"—" }}
    +
    {% trans "Instance UUID" %}
    +
    {{ node.instance_uuid|default:"—" }}
    +
    {% trans "Driver" %}
    +
    {{ node.driver|default:"—" }}
    +
    {% trans "Power state" %}
    +
    {{ node.power_state|default:"—" }}
    +
    +
    + +
    +
    +

    {% trans "Driver Info" %}

    +
    +
    {% trans "IPMI address" %}
    +
    {{ node.driver_info.ipmi_address|default:"—" }}
    +
    {% trans "IPMI username" %}
    +
    {{ node.driver_info.ipmi_username|default:"—" }}
    +
    +
    +
    +

    {% trans "Properties" %}

    +
    +
    {% trans "Local disk" %}
    +
    {{ node.properties.local_disk|filesizeformat|default:"—" }}
    +
    {% trans "RAM" %}
    +
    {{ node.properties.ram|filesizeformat|default:"—" }}
    +
    {% trans "CPU" %}
    +
    {{ node.properties.cpu|default:"—" }}
    +
    +
    + +
    + +{% if node.uuid %} +
    +

    {% trans "Content" %}

    +
    +
    +
    +
    +{% endif %} + + +

    {% trans "Performance and Capacity" %}

    + +{% if meters %} +
    +{% url 'horizon:infrastructure:nodes:performance' node.id as node_perf_url %} + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + + + +
    +
    + + {% for meter_name, meter_label in meters %} + {% if forloop.counter0|divisibleby:"4" %} + + {% endif %} + + {% if forloop.counter|divisibleby:"4" %} + + {% endif %} + {% endfor %} +
    + {% include "infrastructure/_performance_chart.html" with label=meter_label url=node_perf_url|add:"?meter="|add:meter_name only %} +
    +
    +{% else %} + Metering service is not enabled. +{% endif %} +{% endblock %} + diff --git a/tuskar_ui/infrastructure/nodes/views.py b/tuskar_ui/infrastructure/nodes/views.py new file mode 100644 index 0000000..54788fa --- /dev/null +++ b/tuskar_ui/infrastructure/nodes/views.py @@ -0,0 +1,177 @@ +# -*- coding: utf8 -*- +# +# 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 + +from django.core.urlresolvers import reverse_lazy +from django import http +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import base + +from horizon import forms as horizon_forms +from horizon import tabs as horizon_tabs +from horizon import views as horizon_views + +from openstack_dashboard.api import base as api_base +from openstack_dashboard.dashboards.admin.metering import views as metering + +from tuskar_ui import api +from tuskar_ui.infrastructure.nodes import forms +from tuskar_ui.infrastructure.nodes import tabs + + +class IndexView(horizon_tabs.TabbedTableView): + tab_group_class = tabs.NodeTabs + template_name = 'infrastructure/nodes/index.html' + + def get_free_nodes_count(self): + free_nodes_count = len(api.node.Node.list( + self.request, associated=False)) + return free_nodes_count + + def get_deployed_nodes_count(self): + deployed_nodes_count = len(api.node.Node.list(self.request, + associated=True)) + return deployed_nodes_count + + def get_context_data(self, **kwargs): + context = super(IndexView, self).get_context_data(**kwargs) + + context['free_nodes_count'] = self.get_free_nodes_count() + context['deployed_nodes_count'] = self.get_deployed_nodes_count() + context['nodes_count'] = (context['free_nodes_count'] + + context['deployed_nodes_count']) + + return context + + +class RegisterView(horizon_forms.ModalFormView): + form_class = forms.NodeFormset + form_prefix = 'register_nodes' + template_name = 'infrastructure/nodes/register.html' + success_url = reverse_lazy( + 'horizon:infrastructure:nodes:index') + + def get_data(self): + return [] + + def get_form(self, form_class): + return form_class(self.request.POST or None, + initial=self.get_data(), + prefix=self.form_prefix) + + +class DetailView(horizon_views.APIView): + template_name = 'infrastructure/nodes/details.html' + + def get_data(self, request, context, *args, **kwargs): + node_uuid = kwargs.get('node_uuid') + redirect = reverse_lazy('horizon:infrastructure:nodes:index') + node = api.node.Node.get(request, node_uuid, _error_redirect=redirect) + context['node'] = node + if api_base.is_service_enabled(request, 'metering'): + context['meters'] = ( + ('cpu', _('CPU')), + ('disk', _('Disk')), + ('network', _('Network Bandwidth (In)')), + ('energy', _('Energy')), + ('memory', _('Memory')), + ('swap', _('Swap')), + ('network-out', _('Network Bandwidth (Out)')), + ('power', _('Power')), + ) + + return context + + +class PerformanceView(base.TemplateView): + def get(self, request, *args, **kwargs): + meter = request.GET.get('meter') + date_options = request.GET.get('date_options') + date_from = request.GET.get('date_from') + date_to = request.GET.get('date_to') + stats_attr = request.GET.get('stats_attr', 'avg') + group_by = request.GET.get('group_by') + + meter_name = meter.replace(".", "_") + resource_name = 'id' if group_by == "project" else 'resource_id' + node_uuid = kwargs.get('node_uuid') + + additional_query = [{'field': 'resource_id', + 'op': 'eq', + 'value': node_uuid}] + + resources, unit = metering.query_data( + request=request, + date_from=date_from, + date_to=date_to, + date_options=date_options, + group_by=group_by, + meter=meter, + additional_query=additional_query) + series = metering.SamplesView._series_for_meter(resources, + resource_name, + meter_name, + stats_attr, + unit) + + average = used = 0 + tooltip_average = '' + + if series: + values = [point['y'] for point in series[0]['data']] + average = sum(values) / len(values) + used = values[-1] + first_date = series[0]['data'][0]['x'] + last_date = series[0]['data'][-1]['x'] + tooltip_average = _( + 'Average %(average)s %(unit)s
    From: %(first_date)s, to: ' + '%(last_date)s' + ) % (dict(average=average, unit=unit, first_date=first_date, + last_date=last_date) + ) + + ret = { + 'series': series, + 'settings': { + 'renderer': 'StaticAxes', + 'yMin': 0, + 'yMax': 100, + 'higlight_last_point': True, + 'auto_size': False, + 'auto_resize': False, + 'axes_x': False, + 'axes_y': False, + 'bar_chart_settings': { + 'orientation': 'vertical', + 'used_label_placement': 'left', + 'width': 30, + 'color_scale_domain': [0, 80, 80, 100], + 'color_scale_range': [ + '#0000FF', + '#0000FF', + '#FF0000', + '#FF0000' + ], + 'average_color_scale_domain': [0, 100], + 'average_color_scale_range': ['#0000FF', '#0000FF'] + } + }, + 'stats': { + 'average': average, + 'used': used, + 'tooltip_average': tooltip_average, + } + } + + return http.HttpResponse(json.dumps(ret), mimetype='application/json') diff --git a/tuskar_ui/infrastructure/nodes/views.pyc b/tuskar_ui/infrastructure/nodes/views.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac974aa4da1411a98c90a30a51939838e9886ad2 GIT binary patch literal 6466 zcmcIo+j1Mn5$y$d7w-~pmMqhD6oa-!I*y|xvf@OtL|bu*RxOoOnX$9A#0<%W7Fc*^ zAc-y@l`8o)Px+R- z|5fq+4IcMVBx3N3Bo{ddbVc$=niV;yNW`|PEJ?Uw>SE%}NxSLI+e@R)y1 z=f4nm%nu&(&w-y2J1>oOIf}$wkncsFi+pvkA!b(WtTbNJhPh$GWij(&7o~AU8y1EQ zuZvj}J1vc?+ORZic!SE?uPsA+*?;i2SAz=0&!Q;p6I(i)(p>_V=*^f^%PTgs-*Y=DVkzTTZ`vWs0tpHouH7IdQdTqZRf?QH9xFE z*0CHF@~YL|MO$Af>(Ojb3b&lk7i^1B1T%v5JclJ!Xfb~g>4AVfRq5Q_3eETy`LO|C z0k72?d{ty)jqj?~2D{#imoRzE{qj`Wi&jAsQaf^;wk_i+vQ!(tqh7Ssdrj)om>3YR zpr}#gpjvM7dR*}}w#RUHTb@BVx;eeqeT>5X50nGMS%6yw&_i(RqujKZF@?Js z!kwyx9#i(037+BJLe(q1e$Y=rP`jNJW%gasx_P$yO*Jl50x;F{iI;pn#p4KP#{wK= z&0f$dNj0aDlFv!MY8dK}FyPxp2zVR)3cpC&%l$-Q=y_0Q-|M2v zzPiAk+I-f^e3s?_dXaW%kFsZ~+YuT){Br+MAI4iYrhe528TFF>0E!YEigutaYL-8o zLb%3Vtkp-P6(CYg;q)>^xx#{5rIONdb{V6sk10|74hx1#LRRSGI`z7BF6orHeqy|Z zmXY{Mozteqo0$409!K+4r->$_YBXJ$uB!9jwLZ=AGj2l@yTc)CD1zG6UAAB?q>jq#hBHF9DoQog@@bqB_8)L6x}fz58xSY*BfU(5L~b~A%2h;paabe zxj|LB$PFq{Z&JGWL6kAM1t4xru!k9E2q^z427}?tlbrMjou-tX9t2Jm`e)MuH0owQ}1 zX2zdv*6z2q)D>;ZmdB?`-W!ok+bqgZ`yi0Ca`!ZC>mvIgoCe4+@%5tP)ns0=(3c&* zrE#-eJhi1#50xSi=r_3_@ zWyYXx{x@I*yc1rG7}581Wc$M!4Y~0BQ4O*-pn>{ye-!dI)WGG3qksm40_nl$hL!5^ zy=g@i^?L)SN1qw#B9hO@W5B~-dFpFbdEDz=kZx6_Qt$80N~ZNV@ZB&AJl@^t&Wc%* z8^>6Ej%JAr(kyeufJML`01?9+0T1ECL4dO$hY^R-UtwKT|9F)b$-;f&_#%+pu6KaEWZ{hW}m>O879(t zn-p!I6)mU0t`$dR(Nv^W&KDX}olm_>QtwN}v-HWaLQP1a4^I`Ow&UAaM2EJ^q5+Uc z*n?_~W)Y=zMW+O=WhOusGti^V=0o7ZY-ptc<74pO!)ck8hHWOElEdlOYhS?O?Y zgF?34`q2{}&>ZJyvHRJToZ%dVn_Zi+_O>>*i( zs(O(lo;yrx6N77ntmPP=-~Ir9ySstj-6iJ}=GJcKP!{#G z({M$rM)}q5KfCuRrYEUAl(0(8aEFxE z-6OTj;xiU>jQBo_FHorDDvq9e^&~v>RUWC;TjNLU%ER`c&BeGtmG~PLU$H1zI20;U zFjpF+`m&4vz~-;n%&#u(`QJzB&KzQL_h%FmE%*Odkee(=>$okL!JWZe1*hy6{+Yj2 z^?w@e6P1bRTC|FMW)=Tc@axfBwTf|k&i)T-E>^<$naW(WqJ1Z$`LS?IGRl5vHk$5? ziu20I?qcc`kDhKPqS_|t_)jQ2?@det-d1eai_&i6c*gz9d2<`1O%x|Wk6>sojOcit zcRc*f;aUXF5EIS=$nz?xKk*aZ&~6_y;m+zYH&?x*e>)J4k-T2%SJ3>ME?mp-HfK=H zYu}jmq82!))yX}N9!1+CN~QJ3a4>grSfr)dM(P~@nnRfTzN*8ptJ_XZGizZN{&BP?$95X_;=XM^)9v z?T7cZf;p->^P~P{lh$$rM`oeaIoXDS+pTaJ_6g6+ZWG0<-k;$PExcI\n' + + '\n' + + '\n' + + '{{e.title}}\n' + + '\n' + + '{{ e.type }}\n' + + '{{ e.errata_id }}\n' + + '{{ e.issued }}\n' + + '\n'+ + '\n', + link: function (scope, element, attrs, modelCtrl, transclude) { + scope.modelCtrl = modelCtrl; + scope.$transcludeFn = transclude; + } + }; + }] +}); + +angular.module('hz').controller({ + ErrataController: ['$scope', + function ($scope ) { + + }]}); diff --git a/tuskar_ui/infrastructure/test/test_data/node_data.py b/tuskar_ui/infrastructure/test/test_data/node_data.py new file mode 100644 index 0000000..4222995 --- /dev/null +++ b/tuskar_ui/infrastructure/test/test_data/node_data.py @@ -0,0 +1,219 @@ +# 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. + +from openstack_dashboard.test.test_data import utils as test_data_utils + +from ironicclient.v1 import node +from ironicclient.v1 import port +from novaclient.v1_1.contrib import baremetal + + +def data(TEST): + + # BareMetalNode + TEST.baremetalclient_nodes = test_data_utils.TestDataContainer() + bm_node_1 = baremetal.BareMetalNode( + baremetal.BareMetalNodeManager(None), + {'id': '1', + 'uuid': 'd0ace338-a702-426a-b344-394ce861e070', + 'instance_uuid': 'aa', + "service_host": "undercloud", + "cpus": 1, + "memory_mb": 4096, + "local_gb": 20, + 'task_state': 'active', + "pm_address": None, + "pm_user": None, + "interfaces": [{"address": "52:54:00:90:38:01"}, + {"address": "52:54:00:90:38:01"}], + }) + bm_node_2 = baremetal.BareMetalNode( + baremetal.BareMetalNodeManager(None), + {'id': '2', + 'uuid': 'bb-22', + 'instance_uuid': 'bb', + "service_host": "undercloud", + "cpus": 1, + "memory_mb": 4096, + "local_gb": 20, + 'task_state': 'active', + "pm_address": None, + "pm_user": None, + "interfaces": [{"address": "52:54:00:90:38:01"}], + }) + bm_node_3 = baremetal.BareMetalNode( + baremetal.BareMetalNodeManager(None), + {'id': '3', + 'uuid': 'cc-33', + 'instance_uuid': 'cc', + "service_host": "undercloud", + "cpus": 1, + "memory_mb": 4096, + "local_gb": 20, + 'task_state': 'reboot', + "pm_address": None, + "pm_user": None, + "interfaces": [{"address": "52:54:00:90:38:01"}], + }) + bm_node_4 = baremetal.BareMetalNode( + baremetal.BareMetalNodeManager(None), + {'id': '4', + 'uuid': 'cc-44', + 'instance_uuid': 'cc', + "service_host": "undercloud", + "cpus": 1, + "memory_mb": 4096, + "local_gb": 20, + 'task_state': 'active', + "pm_address": None, + "pm_user": None, + "interfaces": [{"address": "52:54:00:90:38:01"}], + }) + bm_node_5 = baremetal.BareMetalNode( + baremetal.BareMetalNodeManager(None), + {'id': '5', + 'uuid': 'dd-55', + 'instance_uuid': 'dd', + "service_host": "undercloud", + "cpus": 1, + "memory_mb": 4096, + "local_gb": 20, + 'task_state': 'error', + "pm_address": None, + "pm_user": None, + "interfaces": [{"address": "52:54:00:90:38:01"}], + }) + TEST.baremetalclient_nodes.add( + bm_node_1, bm_node_2, bm_node_3, bm_node_4, bm_node_5) + + # IronicNode + TEST.ironicclient_nodes = test_data_utils.TestDataContainer() + node_1 = node.Node( + node.NodeManager(None), + {'id': '1', + 'uuid': 'aa-11', + 'instance_uuid': 'aa', + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '1.1.1.1', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + 'ip_address': '1.2.2.2' + }, + 'properties': { + 'cpu': '8', + 'ram': '16', + 'local_disk': '10', + }, + 'power_state': 'on', + }) + node_2 = node.Node( + node.NodeManager(None), + {'id': '2', + 'uuid': 'bb-22', + 'instance_uuid': 'bb', + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '2.2.2.2', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + 'ip_address': '1.2.2.3' + }, + 'properties': { + 'cpu': '16', + 'ram': '32', + 'local_disk': '100', + }, + 'power_state': 'on', + }) + node_3 = node.Node( + node.NodeManager(None), + {'id': '3', + 'uuid': 'cc-33', + 'instance_uuid': 'cc', + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '3.3.3.3', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + 'ip_address': '1.2.2.4' + }, + 'properties': { + 'cpu': '32', + 'ram': '64', + 'local_disk': '1', + }, + 'power_state': 'rebooting', + }) + node_4 = node.Node( + node.NodeManager(None), + {'id': '4', + 'uuid': 'cc-44', + 'instance_uuid': 'cc', + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '4.4.4.4', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + 'ip_address': '1.2.2.5' + }, + 'properties': { + 'cpu': '8', + 'ram': '16', + 'local_disk': '10', + }, + 'power_state': 'on', + }) + node_5 = node.Node( + node.NodeManager(None), + {'id': '5', + 'uuid': 'dd-55', + 'instance_uuid': 'dd', + 'driver': 'pxe_ipmitool', + 'driver_info': { + 'ipmi_address': '5.5.5.5', + 'ipmi_username': 'admin', + 'ipmi_password': 'password', + 'ip_address': '1.2.2.6' + }, + 'properties': { + 'cpu': '8', + 'ram': '16', + 'local_disk': '10', + }, + 'power_state': 'error', + }) + TEST.ironicclient_nodes.add(node_1, node_2, node_3, node_4, node_5) + + # Ports + TEST.ironicclient_ports = test_data_utils.TestDataContainer() + port_1 = port.Port( + port.PortManager(None), + {'id': '1-port-id', + 'type': 'port', + 'address': 'aa:aa:aa:aa:aa:aa'}) + port_2 = port.Port( + port.PortManager(None), + {'id': '2-port-id', + 'type': 'port', + 'address': 'bb:bb:bb:bb:bb:bb'}) + port_3 = port.Port( + port.PortManager(None), + {'id': '3-port-id', + 'type': 'port', + 'address': 'cc:cc:cc:cc:cc:cc'}) + port_4 = port.Port( + port.PortManager(None), + {'id': '4-port-id', + 'type': 'port', + 'address': 'dd:dd:dd:dd:dd:dd'}) + TEST.ironicclient_ports.add(port_1, port_2, port_3, port_4)