From 3e6678f9c692553e8902da4d6fb1fe6c087db1f4 Mon Sep 17 00:00:00 2001 From: Marcel Enguehard Date: Wed, 19 Jul 2017 11:26:26 +0200 Subject: * GUI resource * MemIf interface for VPP * Better netmodel integration * Draft documentation * New tutorials * Improved monitoring and error handling * Refactored IP addresses and prefixes representation * Improved image mgmt for LXD * Various bugfixes and code refactoring Change-Id: I90da6cf7b5716bc7deb6bf4e24d3f9f01b5a9b0f Signed-off-by: Marcel Enguehard --- .gitignore | 31 +- README.md | 24 + bootstrap.sh | 3 - doc/Makefile | 223 + doc/build/.gitignore | 4 + doc/source/.gitignore | 1 + doc/source/conf.py | 292 + doc/source/index.rst | 23 + emu-radio/lte-emulator/CMakeLists.txt | 73 +- emu-radio/scripts/build-package.sh | 36 +- emu-radio/wifi-emulator/CMakeLists.txt | 71 +- examples/tutorial/tutorial01.json | 23 +- examples/tutorial/tutorial02-dumbell.json | 60 +- examples/tutorial/tutorial03-hetnet.json | 47 +- examples/tutorial/tutorial04-caching.json | 44 +- examples/tutorial/tutorial05-icn2020-emulator.json | 98 + examples/tutorial/tutorial05-icn2020.json | 96 + netmodel/interfaces/process/__init__.py | 49 +- netmodel/interfaces/vicn.py | 39 +- netmodel/interfaces/vpp/__init__.py | 225 + netmodel/interfaces/websocket/__init__.py | 38 +- netmodel/model/attribute.py | 120 +- netmodel/model/collection.py | 5 +- netmodel/model/key.py | 19 + netmodel/model/mapper.py | 4 +- netmodel/model/object.py | 126 +- netmodel/model/query.py | 13 +- netmodel/model/sa_collections.py | 265 + netmodel/model/sa_compat.py | 270 + netmodel/model/type.py | 185 +- netmodel/model/uuid.py | 51 + netmodel/network/fib.py | 10 +- netmodel/network/flow_table.py | 145 +- netmodel/network/interface.py | 20 +- netmodel/network/packet.py | 34 +- netmodel/network/prefix.py | 13 +- netmodel/network/router.py | 35 +- netmodel/util/daemon.py | 2 +- netmon/bin/netmon.py | 4 +- requirements.txt | 3 +- scripts/vppctl_wrapper | 4 + setup.py | 51 +- vicn/core/api.py | 11 +- vicn/core/attribute.py | 136 +- vicn/core/collection.py | 23 - vicn/core/commands.py | 2 +- vicn/core/exception.py | 2 - vicn/core/resource.py | 341 +- vicn/core/resource_mgr.py | 357 +- vicn/core/sa_collections.py | 264 - vicn/core/sa_compat.py | 270 - vicn/core/state.py | 37 +- vicn/core/task.py | 96 +- vicn/resource/application.py | 9 +- vicn/resource/central.py | 535 +- vicn/resource/group.py | 9 + vicn/resource/gui.py | 96 + vicn/resource/icn/ccnx_keystore.py | 9 +- vicn/resource/icn/ccnx_metis.py | 83 +- vicn/resource/icn/ccnx_simpleTrafficGenerator.py | 9 +- vicn/resource/icn/central.py | 236 + vicn/resource/icn/cicn.py | 139 + vicn/resource/icn/face.py | 87 +- vicn/resource/icn/forwarder.py | 2 +- vicn/resource/icn/icn_application.py | 1 + vicn/resource/icn/iping.py | 26 +- vicn/resource/icn/ndnpingserver.py | 2 +- vicn/resource/icn/nfd.py | 8 +- vicn/resource/icn/producer.py | 6 +- vicn/resource/icn/route.py | 6 +- vicn/resource/interface.py | 8 +- vicn/resource/ip/central.py | 288 + vicn/resource/ip/prefix_tree.py | 156 +- vicn/resource/ip/route.py | 10 +- vicn/resource/ip/routing_table.py | 51 +- vicn/resource/ip_assignment.py | 106 +- vicn/resource/linux/bridge.py | 28 +- vicn/resource/linux/certificate.py | 30 +- vicn/resource/linux/certificate_store.py | 54 + vicn/resource/linux/dnsmasq.py | 14 +- vicn/resource/linux/file.py | 12 +- vicn/resource/linux/folder.py | 88 + vicn/resource/linux/gre_tunnel.py | 63 + vicn/resource/linux/iperf.py | 4 +- vicn/resource/linux/keypair.py | 8 +- vicn/resource/linux/link.py | 26 +- vicn/resource/linux/macvlan.py | 8 +- vicn/resource/linux/macvtap.py | 8 +- vicn/resource/linux/net_device.py | 106 +- vicn/resource/linux/netmon.py | 2 +- vicn/resource/linux/ovs.py | 6 +- vicn/resource/linux/package_manager.py | 17 +- vicn/resource/linux/phy_interface.py | 20 +- vicn/resource/linux/phy_link.py | 51 +- vicn/resource/linux/physical.py | 6 +- vicn/resource/linux/qtplayer.py | 5 + vicn/resource/linux/repository.py | 6 +- vicn/resource/linux/service.py | 29 +- vicn/resource/linux/sym_veth_pair.py | 52 +- vicn/resource/linux/tap_device.py | 25 +- vicn/resource/linux/veth_pair.py | 94 +- vicn/resource/linux/veth_pair_lxc.py | 74 + vicn/resource/lxd/lxc_container.py | 69 +- vicn/resource/lxd/lxc_image.py | 7 +- vicn/resource/lxd/lxd_certificate_store.py | 48 + vicn/resource/lxd/lxd_hypervisor.py | 30 +- vicn/resource/lxd/lxd_profile.py | 12 +- vicn/resource/lxd/lxd_remote.py | 99 + vicn/resource/node.py | 20 +- vicn/resource/ns3/emulated_channel.py | 65 +- vicn/resource/ns3/emulated_lte_channel.py | 57 +- vicn/resource/ns3/emulated_wifi_channel.py | 23 +- vicn/resource/symmetric_channel.py | 16 + vicn/resource/vpp/cicn.py | 138 - vicn/resource/vpp/dpdk_device.py | 2 +- vicn/resource/vpp/interface.py | 103 +- vicn/resource/vpp/memif_device.py | 77 + vicn/resource/vpp/memif_link.py | 144 + vicn/resource/vpp/scripts.py | 2 +- vicn/resource/vpp/vpp.py | 96 +- vicn/resource/vpp/vpp_bridge.py | 7 +- vicn/resource/vpp/vpp_commands.py | 50 +- vicn/resource/vpp/vpp_host.py | 10 +- www/css/contrib/bootstrap-dropmenu.min.css | 5 + www/css/contrib/bootstrap-table.min.css | 1 + www/css/contrib/bootstrap.min.css | 5 + www/css/contrib/jasny-bootstrap.min.css | 7 + www/css/contrib/led.css | 37 + www/css/contrib/vis-timeline-graph2d.min.css | 1 + www/css/contrib/vis.css | 1295 + www/css/contrib/vis.min.css | 1 + www/css/main.css | 204 + www/favicon.ico | 0 www/fonts/glyphicons-halflings-regular.eot | 0 www/fonts/glyphicons-halflings-regular.svg | 288 + www/fonts/glyphicons-halflings-regular.ttf | 0 www/fonts/glyphicons-halflings-regular.woff | 0 www/fonts/glyphicons-halflings-regular.woff2 | 0 www/img/icn-router-vpp.png | 0 www/img/icn-router.png | 0 www/img/ip-router-vpp.png | 0 www/img/ip-router.png | 0 www/img/lte.png | 0 www/img/pc-video-client.png | 0 www/img/server.png | 0 www/img/smartphone.png | 0 www/img/tablet.png | 0 www/img/undefined.png | 0 www/img/user.png | 0 www/img/vicn-logo.png | 0 www/img/video-server.png | 0 www/img/wifi.png | 0 www/index.html | 94 + www/js/class.js | 64 + www/js/contrib/bootstrap-table.min.js | 8 + www/js/contrib/bootstrap.min.js | 6 + www/js/contrib/d3-array.v1.min.js | 2 + www/js/contrib/d3-collection.v1.min.js | 2 + www/js/contrib/d3-color.v1.min.js | 2 + www/js/contrib/d3-format.v1.min.js | 2 + www/js/contrib/d3-interpolate.v1.min.js | 2 + www/js/contrib/d3-scale.js | 903 + www/js/contrib/d3-scale.min.js | 2 + www/js/contrib/d3-scale.v1.min.js | 2 + www/js/contrib/d3-scale.zip | 0 www/js/contrib/d3-time-format.v2.min.js | 2 + www/js/contrib/d3-time.v1.min.js | 2 + www/js/contrib/jasny-bootstrap.min.js | 6 + www/js/contrib/jquery-1.11.3.min.js | 5 + www/js/contrib/jquery-3.1.1.min.js | 4 + www/js/contrib/jquery.flot.js | 3168 ++ www/js/contrib/jquery.flot.threshold.js | 142 + www/js/contrib/jquery.flot.time.js | 432 + www/js/contrib/vis.js | 51961 +++++++++++++++++++ www/js/contrib/vis.min.js | 46 + www/js/main.js | 39 + www/js/network.js | 356 + www/js/query.js | 55 + www/js/server-event-dispatcher.js | 66 + 179 files changed, 64614 insertions(+), 2814 deletions(-) delete mode 100755 bootstrap.sh create mode 100644 doc/Makefile create mode 100644 doc/build/.gitignore create mode 100644 doc/source/.gitignore create mode 100644 doc/source/conf.py create mode 100644 doc/source/index.rst create mode 100644 examples/tutorial/tutorial05-icn2020-emulator.json create mode 100644 examples/tutorial/tutorial05-icn2020.json create mode 100644 netmodel/interfaces/vpp/__init__.py create mode 100644 netmodel/model/key.py create mode 100644 netmodel/model/sa_collections.py create mode 100644 netmodel/model/sa_compat.py create mode 100644 netmodel/model/uuid.py create mode 100644 scripts/vppctl_wrapper delete mode 100644 vicn/core/collection.py delete mode 100644 vicn/core/sa_collections.py delete mode 100644 vicn/core/sa_compat.py create mode 100644 vicn/resource/gui.py create mode 100644 vicn/resource/icn/central.py create mode 100644 vicn/resource/icn/cicn.py create mode 100644 vicn/resource/ip/central.py create mode 100644 vicn/resource/linux/certificate_store.py create mode 100644 vicn/resource/linux/folder.py create mode 100644 vicn/resource/linux/gre_tunnel.py create mode 100644 vicn/resource/linux/qtplayer.py create mode 100644 vicn/resource/linux/veth_pair_lxc.py create mode 100644 vicn/resource/lxd/lxd_certificate_store.py create mode 100644 vicn/resource/lxd/lxd_remote.py create mode 100644 vicn/resource/symmetric_channel.py delete mode 100644 vicn/resource/vpp/cicn.py create mode 100644 vicn/resource/vpp/memif_device.py create mode 100644 vicn/resource/vpp/memif_link.py create mode 100644 www/css/contrib/bootstrap-dropmenu.min.css create mode 100644 www/css/contrib/bootstrap-table.min.css create mode 100644 www/css/contrib/bootstrap.min.css create mode 100644 www/css/contrib/jasny-bootstrap.min.css create mode 100644 www/css/contrib/led.css create mode 100644 www/css/contrib/vis-timeline-graph2d.min.css create mode 100644 www/css/contrib/vis.css create mode 100644 www/css/contrib/vis.min.css create mode 100644 www/css/main.css create mode 100644 www/favicon.ico create mode 100644 www/fonts/glyphicons-halflings-regular.eot create mode 100644 www/fonts/glyphicons-halflings-regular.svg create mode 100644 www/fonts/glyphicons-halflings-regular.ttf create mode 100644 www/fonts/glyphicons-halflings-regular.woff create mode 100644 www/fonts/glyphicons-halflings-regular.woff2 create mode 100644 www/img/icn-router-vpp.png create mode 100644 www/img/icn-router.png create mode 100644 www/img/ip-router-vpp.png create mode 100644 www/img/ip-router.png create mode 100644 www/img/lte.png create mode 100644 www/img/pc-video-client.png create mode 100644 www/img/server.png create mode 100644 www/img/smartphone.png create mode 100644 www/img/tablet.png create mode 100644 www/img/undefined.png create mode 100644 www/img/user.png create mode 100644 www/img/vicn-logo.png create mode 100644 www/img/video-server.png create mode 100644 www/img/wifi.png create mode 100644 www/index.html create mode 100644 www/js/class.js create mode 100644 www/js/contrib/bootstrap-table.min.js create mode 100644 www/js/contrib/bootstrap.min.js create mode 100644 www/js/contrib/d3-array.v1.min.js create mode 100644 www/js/contrib/d3-collection.v1.min.js create mode 100644 www/js/contrib/d3-color.v1.min.js create mode 100644 www/js/contrib/d3-format.v1.min.js create mode 100644 www/js/contrib/d3-interpolate.v1.min.js create mode 100644 www/js/contrib/d3-scale.js create mode 100644 www/js/contrib/d3-scale.min.js create mode 100644 www/js/contrib/d3-scale.v1.min.js create mode 100644 www/js/contrib/d3-scale.zip create mode 100644 www/js/contrib/d3-time-format.v2.min.js create mode 100644 www/js/contrib/d3-time.v1.min.js create mode 100644 www/js/contrib/jasny-bootstrap.min.js create mode 100644 www/js/contrib/jquery-1.11.3.min.js create mode 100644 www/js/contrib/jquery-3.1.1.min.js create mode 100644 www/js/contrib/jquery.flot.js create mode 100644 www/js/contrib/jquery.flot.threshold.js create mode 100644 www/js/contrib/jquery.flot.time.js create mode 100644 www/js/contrib/vis.js create mode 100644 www/js/contrib/vis.min.js create mode 100644 www/js/main.js create mode 100644 www/js/network.js create mode 100644 www/js/query.js create mode 100644 www/js/server-event-dispatcher.js diff --git a/.gitignore b/.gitignore index b354d71a..327a1542 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,2 @@ -build/ -======= -liblongbow*tar.gz -lib -bin -include -*.o -*.lo -*.a -*.la -.libs -a.out -.DS_Store -.gcda -.gcno -.dSYM -autom4te.cache -config.h -config.log -config.status -stamp-h1 -*.trs -Makefile -.deps *.pyc -.dirstamp -*.swp -libtool -*~ -*.pyc -.idea +.pybuild diff --git a/README.md b/README.md index 217ae9ad..08a1365d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,30 @@ python3-pylxd (>=2.2.2, use pip3 if necessary, depends on libssl-dev) python3-requests-unixsocket +== Installation + +Install vICN from either a debian package or by cloning the git repositoy. + +Add the image server certificate to your trusted certificates: + + sudo apt-get install ca-certificates + wget https://46.105.122.213/cicn.crt + sudo cp cicn.crt /usr/share/ca-certificates + sudo dpkg-reconfigure ca-certificates + + # This should be done by the LXD remote add + cp /usr/share/ca-certificates/cicn.crt ~/.config/lxc/servercerts/ + + lxc remote add cicn https://46.105.122.213 --protocol=simplestreams + +You can now list image with: + + lxc image list cicn: + +or run containers: + + lxc launch cicn:cicn/1.0 test + == Getting started You can have a look at the tutorials available in the fd.io wiki: diff --git a/bootstrap.sh b/bootstrap.sh deleted file mode 100755 index 3ba8abcb..00000000 --- a/bootstrap.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -mkdir -p ~/.vicn/ssh_client_cert/ && ssh-keygen -t rsa -N "" -f ~/.vicn/ssh_client_cert/ssh_client_key diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 00000000..ae9b2b0f --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,223 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# 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: all +all: apidoc html + +.PHONY: apidoc +apidoc: + sphinx-apidoc -o source/apidoc ../vicn/ + +.PHONY: 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 " applehelp to make an Apple Help Book" + @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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @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 " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +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." + +.PHONY: qthelp +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/vicn.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/vicn.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/vicn" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/vicn" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +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)." + +.PHONY: latexpdf +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." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +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)." + +.PHONY: info +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." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +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." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/doc/build/.gitignore b/doc/build/.gitignore new file mode 100644 index 00000000..40e4c1e3 --- /dev/null +++ b/doc/build/.gitignore @@ -0,0 +1,4 @@ +!.gitignore +doctrees +html + diff --git a/doc/source/.gitignore b/doc/source/.gitignore new file mode 100644 index 00000000..73f85073 --- /dev/null +++ b/doc/source/.gitignore @@ -0,0 +1 @@ +apidoc diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 00000000..0b26e6ee --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# +# vicn documentation build configuration file, created by +# sphinx-quickstart on Wed Jul 12 11:40:27 2017. +# +# 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 +import 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.ifconfig', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +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'vicn' +copyright = u'2017, Cisco Systems' +author = u'Cisco Systems' + +# 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 = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +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 = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- 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 = 'alabaster' + +# 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 (relative to this directory) to use as a 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'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# 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 + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'vicndoc' + +# -- 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': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'vicn.tex', u'vicn Documentation', + u'Cisco Systems', '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 = [ + (master_doc, 'vicn', u'vicn Documentation', + [author], 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 = [ + (master_doc, 'vicn', u'vicn Documentation', + author, 'vicn', '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' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 00000000..f205f88b --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,23 @@ +.. vicn documentation master file, created by + sphinx-quickstart on Wed Jul 12 11:40:27 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to vicn's documentation! +================================ + +Contents: + +.. toctree:: + :maxdepth: 2 + + + apidoc/modules.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/emu-radio/lte-emulator/CMakeLists.txt b/emu-radio/lte-emulator/CMakeLists.txt index 6b8113b0..a265fff5 100644 --- a/emu-radio/lte-emulator/CMakeLists.txt +++ b/emu-radio/lte-emulator/CMakeLists.txt @@ -39,60 +39,51 @@ add_executable(lte_emulator ${SOURCE_FILES} ${COMMON_FILES}) target_link_libraries(lte_emulator ${Boost_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${Lte_Ns3_LIBRARIES}) install(TARGETS lte_emulator DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) -install(FILES ${PROJECT_SOURCE_DIR}/fading-traces/fading_trace_EPA_3kmph.fad DESTINATION share/lte-emulator) -install(FILES ${PROJECT_SOURCE_DIR}/fading-traces/fading_trace_ETU_3kmph.fad DESTINATION share/lte-emulator) -install(FILES ${PROJECT_SOURCE_DIR}/fading-traces/fading_trace_EVA_60kmph.fad DESTINATION share/lte-emulator) -install(FILES ${PROJECT_SOURCE_DIR}/fading-traces/fading_trace_generator.m DESTINATION share/lte-emulator) # Generate DEB / RPM packages option(DEB_PACKAGE "Create deb package" OFF) option(RPM_PACKAGE "Create deb package" OFF) -set(VENDOR "Cisco Systems" CACHE STRING "Vendor") -set(CONTACT "msardara@cisco.com" CACHE STRING "Contact") -set(DISTRIBUTION "xenial" CACHE STRING "Distribution") -set(ARCHITECTURE "amd64" CACHE STRING "Architecture") -set(PACKAGE_MAINTAINER "Mauro Sardara (msardara@cisco.com)" CACHE STRING "Maintainer") -set(BUILD_NUMBER "1" CACHE STRING "Build Number") -set(PACKAGE_NAME lte-emulator) -set(CPACK_PACKAGING_INSTALL_PREFIX "/usr") -set(CPACK_PACKAGE_VENDOR ${VENDOR}) -set(CPACK_PACKAGE_CONTACT ${CONTACT}) +SET(VENDOR "Cisco Systems" CACHE STRING "Vendor") +SET(CONTACT "msardara@cisco.com" CACHE STRING "Contact") +SET(DISTRIBUTION "xenial" CACHE STRING "Distribution") +SET(ARCHITECTURE "amd64" CACHE STRING "Architecture") +SET(PACKAGE_MAINTAINER "Mauro Sardara (msardara@cisco.com)" CACHE STRING "Maintainer") +SET(BUILD_NUMBER "1" CACHE STRING "Build Number") +SET(PACKAGE_NAME lte-emulator) +SET(CPACK_PACKAGING_INSTALL_PREFIX "/usr") +SET(CPACK_PACKAGE_VENDOR ${VENDOR}) +SET(CPACK_PACKAGE_CONTACT ${CONTACT}) # Get the version execute_process(COMMAND bash ${CMAKE_SOURCE_DIR}/scripts/version OUTPUT_VARIABLE PACKAGE_VERSION) - -if (PACKAGE_VERSION) - string(STRIP ${PACKAGE_VERSION} PACKAGE_VERSION) -else() - set(PACKAGE_VERSION 1.0) -endif() +string(STRIP ${PACKAGE_VERSION} PACKAGE_VERSION) if(DEB_PACKAGE) - set(TYPE "DEBIAN") - set(GENERATOR "DEB") - set(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCHITECTURE}") - set(CPACK_${TYPE}_PACKAGE_DEPENDS "libns3sx-3v5, ns3sx, libboost-system1.58.0") + SET(TYPE "DEBIAN") + SET(GENERATOR "DEB") + SET(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCHITECTURE}") + SET(CPACK_${TYPE}_PACKAGE_DEPENDS "libns3sx-3v5, ns3sx, libboost-system1.58.0") elseif(RPM_PACKAGE) - set(TYPE "RPM") - set(GENERATOR "RPM") - set(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}-${PACKAGE_VERSION}.${ARCHITECTURE}") - set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "/usr/etc" "/usr/lib/python2.7" "/usr/lib/python2.7/site-packages") - set(CPACK_${TYPE}_PACKAGE_REQUIRES "") + SET(TYPE "RPM") + SET(GENERATOR "RPM") + SET(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}-${PACKAGE_VERSION}.${ARCHITECTURE}") + SET(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "/usr/etc" "/usr/lib/python2.7" "/usr/lib/python2.7/site-packages") + SET(CPACK_${TYPE}_PACKAGE_REQUIRES "") else() - return() + RETURN() endif() -set(CPACK_GENERATOR ${GENERATOR}) -set(CPACK_${TYPE}_PACKAGE_MAINTAINER ${PACKAGE_MAINTAINER}) -set(CPACK_${TYPE}_PACKAGE_NAME ${PACKAGE_NAME}) -set(CPACK_${TYPE}_PACKAGE_VERSION ${PACKAGE_VERSION}) -set(CPACK_${TYPE}_PACKAGE_ARCHITECTURE ${ARCHITECTURE}) -set(CPACK_${TYPE}_PACKAGE_RELEASE 1) -set(CPACK_${TYPE}_PACKAGE_VENDOR ${VENDOR}) -set(CPACK_${TYPE}_PACKAGE_DESCRIPTION "LTE channel emulator.") -set(CPACK_${TYPE}_PACKAGE_HOMEPAGE "https://wiki.fd.io/view/Vicn") - -include(CPack) +SET(CPACK_GENERATOR ${GENERATOR}) +SET(CPACK_${TYPE}_PACKAGE_MAINTAINER ${PACKAGE_MAINTAINER}) +SET(CPACK_${TYPE}_PACKAGE_NAME ${PACKAGE_NAME}) +SET(CPACK_${TYPE}_PACKAGE_VERSION ${PACKAGE_VERSION}) +SET(CPACK_${TYPE}_PACKAGE_ARCHITECTURE ${ARCHITECTURE}) +SET(CPACK_${TYPE}_PACKAGE_RELEASE 1) +SET(CPACK_${TYPE}_PACKAGE_VENDOR ${VENDOR}) +SET(CPACK_${TYPE}_PACKAGE_DESCRIPTION "LTE channel emulator.") +SET(CPACK_${TYPE}_PACKAGE_HOMEPAGE "https://wiki.fd.io/view/Vicn") + +INCLUDE(CPack) diff --git a/emu-radio/scripts/build-package.sh b/emu-radio/scripts/build-package.sh index 1aa828d0..487f538a 100644 --- a/emu-radio/scripts/build-package.sh +++ b/emu-radio/scripts/build-package.sh @@ -3,7 +3,6 @@ set -euxo pipefail IFS=$'\n\t' -SCRIPT_PATH=$( cd "$(dirname "${BASH_SOURCE}")" ; pwd -P ) APT_PATH=`which apt-get` || true apt_get=${APT_PATH:-"/usr/local/bin/apt-get"} @@ -154,22 +153,6 @@ build() { make } -change_version() { - OLD_PACKAGE=$1 - NEW_PACKAGE=$2 - B_NUMBER=$3 - - mkdir tmp - pushd tmp - ar p ../${OLD_PACKAGE} control.tar.gz | tar -xz - sed -i s/3.24.1-8/3.24.1-${B_NUMBER}/g control - mv ../${OLD_PACKAGE} ../${NEW_PACKAGE} - tar czf control.tar.gz *[!z] - ar r ../${NEW_PACKAGE} control.tar.gz - popd - rm -rf tmp -} - ARCHITECTURE=`uname -m` # Figure out what system we are running on @@ -217,30 +200,31 @@ fi BLD_NUMBER=${BUILD_NUMBER:-"1"} +mkdir -p ../ns3-packages + # Install libns3 -pushd ${SCRIPT_PATH}/../ns3-packages +pushd ../ns3-packages sudo dpkg -i *.deb || true sudo apt-get -f install -y --allow-unauthenticated || true popd # Build wifi-emualtor -pushd ${SCRIPT_PATH}/.. +pushd .. build "-DWIFI=ON -DLTE=OFF" make package find . -not -name '*.deb' -not -name '*.rpm' -print0 | xargs -0 rm -rf -- || true popd # Build lte-emualtor -pushd ${SCRIPT_PATH}/.. +pushd .. build "-DLTE=ON -DWIFI=OFF" make package find . -not -name '*.deb' -not -name '*.rpm' -print0 | xargs -0 rm -rf -- || true popd # Change build number to ns3 packages -pushd ${SCRIPT_PATH}/../ns3-packages - -change_version libns3sx-3v5_3.24.1-8~xenial_amd64.deb libns3sx-3v5_3.24.1-$BLD_NUMBER~xenial_amd64.deb ${BLD_NUMBER} || true -change_version libns3sx-dev_3.24.1-8~xenial_amd64.deb libns3sx-dev_3.24.1-$BLD_NUMBER~xenial_amd64.deb ${BLD_NUMBER} || true -change_version ns3sx_3.24.1-8~xenial_amd64.deb ns3sx_3.24.1-$BLD_NUMBER~xenial_amd64.deb ${BLD_NUMBER} || true -popd +pushd ../ns3-packages +mv libns3sx-3v5_3.24.1-6~xenial_amd64.deb libns3sx-3v5_3.24.1-$BLD_NUMBER~xenial_amd64.deb || true +mv libns3sx-dev_3.24.1-6~xenial_amd64.deb libns3sx-dev_3.24.1-$BLD_NUMBER~xenial_amd64.deb || true +mv ns3sx_3.24.1-6~xenial_amd64.deb ns3sx_3.24.1-$BLD_NUMBER~xenial_amd64.deb || true +popd \ No newline at end of file diff --git a/emu-radio/wifi-emulator/CMakeLists.txt b/emu-radio/wifi-emulator/CMakeLists.txt index eb551fe2..558a751f 100644 --- a/emu-radio/wifi-emulator/CMakeLists.txt +++ b/emu-radio/wifi-emulator/CMakeLists.txt @@ -38,51 +38,46 @@ install(TARGETS wifi_emulator DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) option(DEB_PACKAGE "Create deb package" OFF) option(RPM_PACKAGE "Create deb package" OFF) -set(VENDOR "Cisco Systems" CACHE STRING "Vendor") -set(CONTACT "msardara@cisco.com" CACHE STRING "Contact") -set(DISTRIBUTION "xenial" CACHE STRING "Distribution") -set(ARCHITECTURE "amd64" CACHE STRING "Architecture") -set(PACKAGE_MAINTAINER "Mauro Sardara (msardara@cisco.com)" CACHE STRING "Maintainer") -set(BUILD_NUMBER "1" CACHE STRING "Build Number") -set(PACKAGE_NAME wifi-emulator) - -set(CPACK_PACKAGING_INSTALL_PREFIX "/usr") -set(CPACK_PACKAGE_VENDOR ${VENDOR}) -set(CPACK_PACKAGE_CONTACT ${CONTACT}) +SET(VENDOR "Cisco Systems" CACHE STRING "Vendor") +SET(CONTACT "msardara@cisco.com" CACHE STRING "Contact") +SET(DISTRIBUTION "xenial" CACHE STRING "Distribution") +SET(ARCHITECTURE "amd64" CACHE STRING "Architecture") +SET(PACKAGE_MAINTAINER "Mauro Sardara (msardara@cisco.com)" CACHE STRING "Maintainer") +SET(BUILD_NUMBER "1" CACHE STRING "Build Number") +SET(PACKAGE_NAME wifi-emulator) + +SET(CPACK_PACKAGING_INSTALL_PREFIX "/usr") +SET(CPACK_PACKAGE_VENDOR ${VENDOR}) +SET(CPACK_PACKAGE_CONTACT ${CONTACT}) # Get the version execute_process(COMMAND bash ${CMAKE_SOURCE_DIR}/scripts/version OUTPUT_VARIABLE PACKAGE_VERSION) - -if (PACKAGE_VERSION) - string(STRIP ${PACKAGE_VERSION} PACKAGE_VERSION) -else() - set(PACKAGE_VERSION 1.0) -endif() +string(STRIP ${PACKAGE_VERSION} PACKAGE_VERSION) if(DEB_PACKAGE) - set(TYPE "DEBIAN") - set(GENERATOR "DEB") - set(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCHITECTURE}") - set(CPACK_${TYPE}_PACKAGE_DEPENDS "libns3sx-3v5, ns3sx, libboost-system1.58.0") + SET(TYPE "DEBIAN") + SET(GENERATOR "DEB") + SET(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}_${PACKAGE_VERSION}_${ARCHITECTURE}") + SET(CPACK_${TYPE}_PACKAGE_DEPENDS "libns3sx-3v5, ns3sx, libboost-system1.58.0") elseif(RPM_PACKAGE) - set(TYPE "RPM") - set(GENERATOR "RPM") - set(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}-${PACKAGE_VERSION}.${ARCHITECTURE}") - set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "/usr/etc" "/usr/lib/python2.7" "/usr/lib/python2.7/site-packages") - set(CPACK_${TYPE}_PACKAGE_REQUIRES "") + SET(TYPE "RPM") + SET(GENERATOR "RPM") + SET(CPACK_PACKAGE_FILE_NAME "${PACKAGE_NAME}-${PACKAGE_VERSION}.${ARCHITECTURE}") + SET(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION "/usr/etc" "/usr/lib/python2.7" "/usr/lib/python2.7/site-packages") + SET(CPACK_${TYPE}_PACKAGE_REQUIRES "") else() - return() + RETURN() endif() -set(CPACK_GENERATOR ${GENERATOR}) -set(CPACK_${TYPE}_PACKAGE_MAINTAINER ${PACKAGE_MAINTAINER}) -set(CPACK_${TYPE}_PACKAGE_NAME ${PACKAGE_NAME}) -set(CPACK_${TYPE}_PACKAGE_VERSION ${PACKAGE_VERSION}) -set(CPACK_${TYPE}_PACKAGE_ARCHITECTURE ${ARCHITECTURE}) -set(CPACK_${TYPE}_PACKAGE_RELEASE 1) -set(CPACK_${TYPE}_PACKAGE_VENDOR ${VENDOR}) -set(CPACK_${TYPE}_PACKAGE_DESCRIPTION "Wifi N channel emulator.") -set(CPACK_${TYPE}_PACKAGE_HOMEPAGE "https://wiki.fd.io/view/Vicn") - -include(CPack) +SET(CPACK_GENERATOR ${GENERATOR}) +SET(CPACK_${TYPE}_PACKAGE_MAINTAINER ${PACKAGE_MAINTAINER}) +SET(CPACK_${TYPE}_PACKAGE_NAME ${PACKAGE_NAME}) +SET(CPACK_${TYPE}_PACKAGE_VERSION ${PACKAGE_VERSION}) +SET(CPACK_${TYPE}_PACKAGE_ARCHITECTURE ${ARCHITECTURE}) +SET(CPACK_${TYPE}_PACKAGE_RELEASE 1) +SET(CPACK_${TYPE}_PACKAGE_VENDOR ${VENDOR}) +SET(CPACK_${TYPE}_PACKAGE_DESCRIPTION "Wifi N channel emulator.") +SET(CPACK_${TYPE}_PACKAGE_HOMEPAGE "https://wiki.fd.io/view/Vicn") + +INCLUDE(CPack) diff --git a/examples/tutorial/tutorial01.json b/examples/tutorial/tutorial01.json index fd203cff..e1e97800 100644 --- a/examples/tutorial/tutorial01.json +++ b/examples/tutorial/tutorial01.json @@ -9,49 +9,44 @@ "name": "server", "hostname": "localhost" }, - { - "type": "NetDevice", - "device_name": "eth0", - "node": "server", - "managed": false - }, - { + { "type": "LxcImage", - "name": "ubuntu1604-cicnsuite-rc3", + "name": "cicn-image", + "image": "ubuntu1604-cicnsuite-rc3", "groups": ["topology"], "node": "server" }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "groups": ["topology"], "name": "prod1", "node": "server" }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "groups": ["topology"], "name": "prod2", "node": "server" }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "groups": ["topology"], "name": "core2", "node": "server" }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "groups": ["topology"], "name": "core1", "node": "server" }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "groups": ["topology"], "name": "cons1", "node": "server" @@ -61,7 +56,7 @@ "name": "cons2", "groups": ["topology"], "node": "server", - "image": "ubuntu1604-cicnsuite-rc3" + "image": "cicn-image" }, { "type": "MetisForwarder", diff --git a/examples/tutorial/tutorial02-dumbell.json b/examples/tutorial/tutorial02-dumbell.json index 29ba3042..28873c2f 100644 --- a/examples/tutorial/tutorial02-dumbell.json +++ b/examples/tutorial/tutorial02-dumbell.json @@ -1,5 +1,9 @@ { "resources": [ + { + "type": "Group", + "name": "topology" + }, { "type": "Physical", "name": "server", @@ -15,7 +19,8 @@ }, { "type": "LxcImage", - "name": "ubuntu1604-cicnsuite-rc3", + "name": "lxcimage", + "image": "ubuntu1604-cicnsuite-rc3", "node": "server", "managed": false }, @@ -23,7 +28,8 @@ "type": "LxcContainer", "node": "server", "name": "bridge1", - "image": "ubuntu1604-cicnsuite-rc3" + "groups": ["topology"], + "image": "lxcimage" }, { "type": "VPP", @@ -34,7 +40,7 @@ "type": "DpdkDevice", "node": "bridge1", "device_name": "GigabitEthernet0/8/0", - "pci_address" : "0000:00:08.0", + "pci_address": "0000:00:08.0", "ip_address" : "172.17.1.20", "mac_address": "08:00:27:b8:f3:a3", "name": "bridge1-dpdk1" @@ -43,7 +49,8 @@ "type": "LxcContainer", "node": "server", "name": "core1", - "image": "ubuntu1604-cicnsuite-rc3" + "groups": ["topology"], + "image": "lxcimage" }, { "type": "VPP", @@ -93,7 +100,8 @@ "type": "LxcContainer", "node": "server", "name": "core2", - "image": "ubuntu1604-cicnsuite-rc3" + "groups": ["topology"], + "image": "lxcimage" }, { "type": "VPP", @@ -143,7 +151,8 @@ "type": "LxcContainer", "node": "server", "name": "bridge2", - "image": "ubuntu1604-cicnsuite-rc3" + "groups": ["topology"], + "image": "lxcimage" }, { "type": "VPP", @@ -162,7 +171,7 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", "name": "cons1" }, { @@ -174,7 +183,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "cons2" }, { @@ -186,7 +196,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "cons3" }, { @@ -198,7 +209,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "cons4" }, { @@ -210,7 +222,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "cons5" }, { @@ -222,7 +235,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "prod1" }, { @@ -234,7 +248,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "prod2" }, { @@ -246,7 +261,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "prod3" }, { @@ -258,7 +274,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "prod4" }, { @@ -270,7 +287,8 @@ { "type": "LxcContainer", "node": "server", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "lxcimage", + "groups": ["topology"], "name": "prod5" }, { @@ -304,15 +322,19 @@ }, { "type": "CentralIP", - "ip_routing_strategy" : "spt" + "ip_routing_strategy" : "spt", + "ip6_data_prefix": "2001::/50", + "ip4_data_prefix": "192.168.128.0/24", + "groups": ["topology"] }, { "type" : "CentralICN", - "face_protocol": "udp4" + "face_protocol": "udp4", + "groups": ["topology"] } ], "settings": { - "network": "192.168.133.0/24", + "network": "192.168.128.0/24", "ulimit-n": 10000 } } diff --git a/examples/tutorial/tutorial03-hetnet.json b/examples/tutorial/tutorial03-hetnet.json index a909e8aa..9a9238fb 100644 --- a/examples/tutorial/tutorial03-hetnet.json +++ b/examples/tutorial/tutorial03-hetnet.json @@ -3,51 +3,58 @@ { "type": "Physical", "name": "server", - "hostname": "MY-SERVER" - }, - { - "type": "NetDevice", - "device_name": "br0", - "node": "server", - "managed": false + "hostname": "localhost" }, + { + "type": "Group", + "name": "routed" + }, + { + "type": "GUI", + "groups": [ "routed" ] + }, { "type": "LxcImage", - "name": "ubuntu1604-cicnsuite-rc3", + "name": "cicn-image", + "image": "ubuntu1604-cicnsuite-rc3", "node": "server" }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "name": "cons", "node": "server", + "groups": [ "routed" ], "category": "tablet", "x": 1, "y": 2 }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "name": "wifi", "node": "server", + "groups": [ "routed" ], "category": "wifi", "x": 2, "y": 1 }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "name": "lte", "node": "server", + "groups": [ "routed" ], "category": "lte", "x": 2, "y": 3 }, { "type": "LxcContainer", - "image": "ubuntu1604-cicnsuite-rc3", + "image": "cicn-image", "name": "prod", "node": "server", + "groups": [ "routed" ], "category": "video-server", "x": 3, "y": 2 @@ -77,11 +84,13 @@ }, { "type": "Link", + "groups": [ "routed" ], "src_node": "wifi", "dst_node": "prod" }, { "type": "Link", + "groups": [ "routed" ], "src_node": "lte", "dst_node": "prod" }, @@ -91,6 +100,7 @@ "node": "server", "ap": "wifi", "stations": ["cons"], + "groups": [ "routed" ], "control_port": 30001 }, { @@ -98,20 +108,21 @@ "name": "lch", "node": "server", "ap": "lte", + "groups": [ "routed" ], "stations": ["cons"], "control_port": 30002 }, { "type": "CentralIP", - "ip_routing_strategy": "spt" + "ip_routing_strategy": "spt", + "groups": [ "routed" ], + "ip4_data_prefix": "192.168.42.0/23", + "ip6_data_prefix": "9002::/16" }, { "type": "CentralICN", - "icnip_routing_strategy": "spt", + "groups": [ "routed" ], "face_protocol": "udp4" } - ], - "settings": { - "network": "192.168.2.0/24" - } + ] } diff --git a/examples/tutorial/tutorial04-caching.json b/examples/tutorial/tutorial04-caching.json index eef652cb..1ee63507 100644 --- a/examples/tutorial/tutorial04-caching.json +++ b/examples/tutorial/tutorial04-caching.json @@ -11,19 +11,30 @@ }, { "type": "NetDevice", - "device_name": "eth0", + "device_name": "enp16s0f0", "managed": false, "node": "server" }, + { + "type": "Group", + "name": "routed" + }, + { + "type": "GUI", + "groups": ["routed"], + "port": 8000 + }, { "type": "LxcImage", "name": "ubuntu1604-cicnsuite-rc3", + "image": "ubuntu1604-cicnsuite-rc3", "node": "server" }, { "name": "u1core", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", - "category": "asr", + "category": "ip-router", "type": "LxcContainer", "y": 10, "x": 5, @@ -31,6 +42,7 @@ }, { "name": "u1srv1", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", "category": "user", "type": "LxcContainer", @@ -40,6 +52,7 @@ }, { "name": "u1srv2", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", "category": "user", "type": "LxcContainer", @@ -49,8 +62,9 @@ }, { "name": "u2core", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", - "category": "asr", + "category": "ip-router", "type": "LxcContainer", "y": 6, "x": 9, @@ -58,6 +72,7 @@ }, { "name": "u2srv1", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", "category": "user", "type": "LxcContainer", @@ -67,8 +82,9 @@ }, { "name": "u3core", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", - "category": "asr", + "category": "ip-router", "type": "LxcContainer", "y": 12, "x": 13, @@ -76,8 +92,9 @@ }, { "name": "u3srv1", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", - "category": "serverDemo", + "category": "server", "type": "LxcContainer", "y": 10, "x": 16, @@ -85,8 +102,9 @@ }, { "name": "u3srv2", + "groups": [ "routed" ], "image": "ubuntu1604-cicnsuite-rc3", - "category": "serverDemo", + "category": "server", "type": "LxcContainer", "y": 14, "x": 16, @@ -148,58 +166,70 @@ }, { "type": "Link", + "groups": [ "routed" ], "dst_node": "u1srv1", "capacity": 200, "src_node": "u1core" }, { "type": "Link", + "groups": [ "routed" ], "dst_node": "u1srv2", "capacity": 200, "src_node": "u1core" }, { "type": "Link", + "groups": [ "routed" ], "dst_node": "u2srv1", "capacity": 200, "src_node": "u2core" }, { "type": "Link", + "groups": [ "routed" ], "dst_node": "u3srv1", "capacity": 100, "src_node": "u3core" }, { "type": "Link", + "groups": [ "routed" ], "dst_node": "u3srv2", "capacity": 100, "src_node": "u3core" }, { "type": "Link", + "groups": [ "routed" ], "capacity": 200, "dst_node": "u2core", "src_node": "u1core" }, { "type": "Link", + "groups": [ "routed" ], "capacity": 200, "dst_node": "u3core", "src_node": "u1core" }, { "type": "Link", + "groups": [ "routed" ], "capacity": 200, "dst_node": "u2core", "src_node": "u3core" }, { "type": "CentralIP", - "ip_routing_strategy": "spt" + "groups": [ "routed" ], + "ip_routing_strategy": "spt", + "ip6_data_prefix": "9002::/16", + "ip4_data_prefix": "192.168.15.0/24" }, { "type": "CentralICN", + "groups": [ "routed" ], "face_protocol": "udp4" } ] diff --git a/examples/tutorial/tutorial05-icn2020-emulator.json b/examples/tutorial/tutorial05-icn2020-emulator.json new file mode 100644 index 00000000..e020f232 --- /dev/null +++ b/examples/tutorial/tutorial05-icn2020-emulator.json @@ -0,0 +1,98 @@ +{ + "resources": [ + { + "type": "Group", + "name": "virtual" + }, + { + "type": "Physical", + "name": "server", + "hostname": "localhost" + }, + { + "type": "GUI", + "groups": ["virtual"] + }, + { + "type": "LxcImage", + "name": "cicn-image", + "node": "server", + "image": "ubuntu1604-cicnsuite-rc3" + }, + { + "type" : "LxcContainer", + "image": "cicn-image", + "name" : "cons", + "groups": [ "virtual" ], + "node" : "server", + "category": "smartphone" + }, + { + "type" : "LxcContainer", + "image": "cicn-image", + "name" : "prod", + "groups": [ "virtual" ], + "category": "video-server", + "node" : "server" + }, + { + "type" : "LxcContainer", + "image": "cicn-image", + "name" : "relay", + "groups": [ "virtual" ], + "node" : "server", + "category": "lte-antenna" + }, + { + "type": "EmulatedLteChannel", + "name": "lch", + "ap": "relay", + "stations": [ "cons" ], + "control_port": 30002, + "node": "server", + "groups": [ "virtual" ] + }, + { + "type": "Link", + "src_node": "relay", + "dst_node": "prod", + "groups": [ "virtual" ] + }, + { + "type": "CentralIP", + "ip4_data_prefix": "192.168.42.0/23", + "ip6_data_prefix": "9002::/16", + "ip_routing_strategy": "spt", + "groups": [ + "virtual" + ] + }, + { + "type": "MetisForwarder", + "cache_size": 0, + "node": "cons" + }, + { + "type": "MetisForwarder", + "cache_size": 2000, + "node": "relay" + }, + { + "type": "MetisForwarder", + "cache_size": 0, + "node": "prod" + }, + { + "type": "WebServer", + "prefixes": [ + "/webserver" + ], + "node": "prod" + }, + { + "type": "CentralICN", + "groups": [ "virtual" ], + "face_protocol": "udp4" + } + ] +} diff --git a/examples/tutorial/tutorial05-icn2020.json b/examples/tutorial/tutorial05-icn2020.json new file mode 100644 index 00000000..0eeb59aa --- /dev/null +++ b/examples/tutorial/tutorial05-icn2020.json @@ -0,0 +1,96 @@ +{ + "resources": [ + { + "type": "Group", + "name": "virtual" + }, + { + "type": "Physical", + "name": "server", + "hostname": "localhost" + }, + { + "type": "LxcImage", + "name": "cicn-image", + "node": "server", + "image": "ubuntu1604-cicnsuite-rc3" + }, + { + "type": "GUI", + "port": 8000, + "groups": ["virtual"] + }, + { + "type" : "LxcContainer", + "image": "cicn-image", + "name" : "cons", + "groups": [ "virtual" ], + "category": "pc-video-client", + "node" : "server" + }, + { + "type" : "LxcContainer", + "image": "cicn-image", + "name" : "prod", + "groups": [ "virtual" ], + "category": "video-server", + "node" : "server" + }, + { + "type" : "LxcContainer", + "image": "cicn-image", + "name" : "relay", + "groups": [ "virtual" ], + "category": "icn-router", + "node" : "server" + }, + { + "type": "Link", + "src_node": "cons", + "dst_node": "relay", + "groups": [ "virtual" ] + }, + { + "type": "Link", + "src_node": "relay", + "dst_node": "prod", + "groups": [ "virtual" ] + }, + { + "type": "CentralIP", + "ip4_data_prefix": "192.168.42.0/24", + "ip6_data_prefix": "9002::/16", + "ip_routing_strategy": "spt", + "groups": [ + "virtual" + ] + }, + { + "type": "MetisForwarder", + "cache_size": 0, + "node": "cons" + }, + { + "type": "MetisForwarder", + "cache_size": 2000, + "node": "relay" + }, + { + "type": "MetisForwarder", + "cache_size": 0, + "node": "prod" + }, + { + "type": "WebServer", + "prefixes": [ + "/webserver" + ], + "node": "prod" + }, + { + "type": "CentralICN", + "groups": [ "virtual" ], + "face_protocol": "udp4" + } + ] +} diff --git a/netmodel/interfaces/process/__init__.py b/netmodel/interfaces/process/__init__.py index b985c32f..59bf6f9f 100644 --- a/netmodel/interfaces/process/__init__.py +++ b/netmodel/interfaces/process/__init__.py @@ -16,6 +16,7 @@ # limitations under the License. # +import logging import shlex import socket import subprocess @@ -27,10 +28,13 @@ from netmodel.network.prefix import Prefix from netmodel.model.attribute import Attribute from netmodel.model.filter import Filter from netmodel.model.object import Object -from netmodel.model.query import Query, ACTION_UPDATE -from netmodel.model.query import ACTION_SUBSCRIBE, FUNCTION_SUM +from netmodel.model.query import Query, ACTION_UPDATE, ACTION2STR +from netmodel.model.query import ACTION_SUBSCRIBE, ACTION_UNSUBSCRIBE +from netmodel.model.query import FUNCTION_SUM from netmodel.model.type import String, Integer, Double +log = logging.getLogger(__name__) + DEFAULT_INTERVAL = 1 # s KEY_FIELD = 'device_name' @@ -48,19 +52,19 @@ class Process(threading.Thread): class BWMThread(Process): SEP=';' - CMD="stdbuf -oL bwm-ng -t 1000 -N -o csv -c 0 -C '%s'" + CMD="stdbuf -oL bwm-ng -t 1000 -N -o csv -c 0 -C '%s'" # Parsing information (from README, specs section) # https://github.com/jgjl/bwm-ng/blob/master/README # # Type rate: - FIELDS_RATE = ['timestamp', 'iface_name', 'bytes_out_s', 'bytes_in_s', + FIELDS_RATE = ['timestamp', 'iface_name', 'bytes_out_s', 'bytes_in_s', 'bytes_total_s', 'bytes_in', 'bytes_out', 'packets_out_s', 'packets_in_s', 'packets_total_s', 'packets_in', 'packets_out', 'errors_out_s', 'errors_in_s', 'errors_in', 'errors_out'] # Type svg, sum, max - FIELDS_SUM = ['timestamp', 'iface_name', 'bytes_out', 'bytes_in', - 'bytes_total', 'packets_out', 'packets_in', 'packets_total', + FIELDS_SUM = ['timestamp', 'iface_name', 'bytes_out', 'bytes_in', + 'bytes_total', 'packets_out', 'packets_in', 'packets_total', 'errors_out', 'errors_in'] def __init__(self, interfaces, callback): @@ -74,7 +78,7 @@ class BWMThread(Process): def run(self): cmd = self.CMD % (self.SEP) - p = subprocess.Popen(shlex.split(cmd), stdout = subprocess.PIPE, + p = subprocess.Popen(shlex.split(cmd), stdout = subprocess.PIPE, stderr = subprocess.STDOUT) stdout = [] self._is_running = True @@ -85,18 +89,18 @@ class BWMThread(Process): break if line: record = self._parse_line(line.strip()) - # We use 'total' to push the statistics back to VICN + # We use 'total' to push the statistics back to VICN if record['iface_name'] == 'total': for interfaces in self.groups_of_interfaces: if not len(interfaces) > 1: # If the tuple contains only one interface, grab # the information from bwm_stats and sends it back - # to VICN + # to VICN if interfaces[0] not in self.bwm_stats: continue interface = self.bwm_stats[interfaces[0]] f_list = [[KEY_FIELD, '==', interface.device_name]] - query = Query(ACTION_UPDATE, Interface.__type__, + query = Query(ACTION_UPDATE, Interface.__type__, filter = Filter.from_list(f_list), params = interface.get_attribute_dict()) self._callback(query) @@ -133,7 +137,7 @@ class BWMThread(Process): # sum function specified because it is used to # match the subscribe query attrs = aggregated_interface.get_attribute_dict() - query = Query(ACTION_UPDATE, Interface.__type__, + query = Query(ACTION_UPDATE, Interface.__type__, filter = Filter.from_list([predicate]), params = attrs, aggregate = FUNCTION_SUM) @@ -148,7 +152,7 @@ class BWMThread(Process): bw_upstream = float(record['bytes_out_s']), bw_downstream = float(record['bytes_in_s']), ) - + self.bwm_stats[record['iface_name']] = interface rc = p.poll() @@ -176,17 +180,17 @@ class BWMInterface(BaseInterface): packet = Packet.from_query(reply, reply = True) self.receive(packet) - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- # Router interface - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- def send_impl(self, packet): query = packet.to_query() - assert query.action == ACTION_SUBSCRIBE - interval = query.params.get('interval', DEFAULT_INTERVAL) \ - if query.params else DEFAULT_INTERVAL - assert interval + if query.action not in [ACTION_SUBSCRIBE]: # , ACTION_UNSUBSCRIBE]: + log.warning("Ignore unknown action {}".format( + ACTION2STR[query.action])) + return # TODO: Add the sum operator. If sum the list of interfaces is # added to the BWMThread as a tuple, otherwise every single @@ -194,15 +198,18 @@ class BWMInterface(BaseInterface): # We currently simply extract it from the filter interfaces_list = [p.value for p in query.filter if p.key == KEY_FIELD] - + # interfaces is a list of tuple. If someone sbscribe to mulitple # interfaces interfaces will be a list of 1 tuple. The tuple will # contain the set of interfaces - assert len(interfaces_list) == 1 + if len(interfaces_list) != 1: + log.warning("interfaces_list should have len = 1: {}".format(interfaces_list)) + return + interfaces = interfaces_list[0] \ if isinstance(interfaces_list[0], tuple) \ else tuple([interfaces_list[0]]) - + # Check if interfaces is more than one. In this case, we only support # The SUM function on the list of field. if len(interfaces) > 1: diff --git a/netmodel/interfaces/vicn.py b/netmodel/interfaces/vicn.py index 9ec9672e..80195f9a 100644 --- a/netmodel/interfaces/vicn.py +++ b/netmodel/interfaces/vicn.py @@ -16,16 +16,20 @@ # limitations under the License. # +import logging + from vicn.core.task import BashTask from netmodel.model.object import Object from netmodel.model.attribute import Attribute -from netmodel.model.query import Query, ACTION_INSERT +from netmodel.model.query import Query, ACTION_INSERT, ACTION_SELECT from netmodel.model.type import String from netmodel.network.interface import Interface, InterfaceState from netmodel.network.packet import Packet from netmodel.network.prefix import Prefix from netmodel.util.misc import lookahead +log = logging.getLogger(__name__) + class VICNBaseResource(Object): __type__ = 'vicn/' @@ -50,18 +54,27 @@ class VICNBaseResource(Object): elif query.object_name == 'resource': resources = interface._manager.get_resources() else: - resources = interface._manager.by_type_str(query.object_name) - - for resource, last in lookahead(resources): - params = resource.get_attribute_dict(aggregates = True) - params['id'] = resource._state.uuid._uuid - params['type'] = resource.get_types() - params['state'] = resource._state.state - params['log'] = resource._state.log - reply = Query(ACTION_INSERT, query.object_name, params = params) - reply.last = last - packet = Packet.from_query(reply, reply = True) - cb(packet, ingress_interface = interface) + _resources = interface._manager.by_type_str(query.object_name) + resources = list() + for resource in _resources: + group_names = [r.name for r in resource.groups] + resources.append(resource) + + if query.action == ACTION_SELECT: + for resource, last in lookahead(resources): + params = resource.get_attribute_dict(aggregates = True) + params['id'] = resource._state.uuid._uuid + params['type'] = resource.get_types() + params['state'] = resource._state.state + params['log'] = resource._state.log + reply = Query(ACTION_INSERT, query.object_name, params = params) + reply.last = last + packet = Packet.from_query(reply, reply = True) + cb(packet, ingress_interface = interface) + else: + log.warning("Unknown action in query {}".format(query)) + + interface._manager._broadcast(query) class L2Graph(Object): __type__ = 'vicn/l2graph' diff --git a/netmodel/interfaces/vpp/__init__.py b/netmodel/interfaces/vpp/__init__.py new file mode 100644 index 00000000..34d106fd --- /dev/null +++ b/netmodel/interfaces/vpp/__init__.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 + +# Dependency: vpp-api-python + +import asyncio +import asyncio.subprocess +import collections +import copy +import logging +import pyparsing as pp +import socket +import time + +from netmodel.model.attribute import Attribute +from netmodel.model.filter import Filter +from netmodel.model.query import Query, ACTION2STR, ACTION_UPDATE +from netmodel.model.query import ACTION_SUBSCRIBE, ACTION_UNSUBSCRIBE +from netmodel.model.object import Object +from netmodel.model.type import Double, String +from netmodel.network.interface import Interface as BaseInterface +from netmodel.network.packet import Packet + +log = logging.getLogger(__name__) + +DEFAULT_INTERVAL = 1 # s +KEY_FIELD = 'device_name' + +def parse(s): + kw_name = pp.Keyword('Name') + kw_idx = pp.Keyword('Idx') + kw_state = pp.Keyword('State') + kw_counter = pp.Keyword('Counter') + kw_count = pp.Keyword('Count') + + kw_up = pp.CaselessKeyword('up') + kw_down = pp.CaselessKeyword('down') + kw_rx_packets = pp.CaselessKeyword('rx packets') + kw_rx_bytes = pp.CaselessKeyword('rx bytes') + kw_tx_packets = pp.CaselessKeyword('tx packets') + kw_tx_bytes = pp.CaselessKeyword('tx bytes') + kw_drops = pp.CaselessKeyword('drops') + kw_ip4 = pp.CaselessKeyword('ip4') + kw_ip6 = pp.CaselessKeyword('ip6') + kw_tx_error = pp.CaselessKeyword('tx-error') + kw_rx_miss = pp.CaselessKeyword('rx-miss') + + header = kw_name + kw_idx + kw_state + kw_counter + kw_count + + interface = (pp.Word(pp.alphanums + '/' + '-').setResultsName('device_name') + \ + pp.Word(pp.nums).setResultsName('index') + \ + pp.oneOf(['up', 'down']).setResultsName('state') + \ + pp.Optional(kw_rx_packets + pp.Word(pp.nums).setResultsName('rx_packets')) + \ + pp.Optional(kw_rx_bytes + pp.Word(pp.nums).setResultsName('rx_bytes')) + \ + pp.Optional(kw_tx_packets + pp.Word(pp.nums).setResultsName('tx_packets')) + \ + pp.Optional(kw_tx_bytes + pp.Word(pp.nums).setResultsName('tx_bytes')) + \ + pp.Optional(kw_drops + pp.Word(pp.nums).setResultsName('drops')) + \ + pp.Optional(kw_ip4 + pp.Word(pp.nums).setResultsName('ip4')) + \ + pp.Optional(kw_ip6 + pp.Word(pp.nums).setResultsName('ip6')) + \ + pp.Optional(kw_rx_miss + pp.Word(pp.nums).setResultsName('rx_miss')) + \ + pp.Optional(kw_tx_error + pp.Word(pp.nums).setResultsName('tx_error')) + ).setParseAction(lambda t: t.asDict()) + + bnf = ( + header.suppress() + + pp.OneOrMore(interface) + ).setParseAction(lambda t: t.asList()) + + return bnf.parseString(s, parseAll = True).asList() + +class VPPInterface(Object): + __type__ = 'vpp_interface' + + node = Attribute(String) + device_name = Attribute(String) + bw_upstream = Attribute(Double) # bytes + bw_downstream = Attribute(Double) # bytes + +class VPPCtlInterface(BaseInterface): + __interface__ = 'vppctl' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_object(VPPInterface) + + # Set of monitored interfaces + self._interfaces = collections.defaultdict(int) + self._running = False + + # interface -> (time, rx, tx) + self._last = dict() + + async def _tick(self): + while self._running: + try: + create = asyncio.create_subprocess_exec( + 'vppctl_wrapper', 'show', 'int', + stdout=asyncio.subprocess.PIPE, + ) + proc = await create + await proc.wait() + stdout = await proc.stdout.read() + + if proc.returncode: + log.error("error") + return + + interfaces = parse(stdout.decode()) + last = copy.copy(self._last) + self._last = dict() + now = time.time() + for interface in interfaces: + if not interface['device_name'] in self._interfaces: + continue + tx = float(interface['tx_bytes']) + rx = float(interface['rx_bytes']) + self._last[interface['device_name']] = (now, rx, tx) + + if not interface['device_name'] in last: + continue + prev_now, prev_rx, prev_tx = last[interface['device_name']] + + # Per interface throughput computation + ret = { + 'node' : socket.gethostname(), + 'device_name' : interface['device_name'], + 'bw_upstream' : (tx - prev_tx) / (now - prev_now), + 'bw_downstream' : (rx - prev_rx) / (now - prev_now), + } + + f_list = [[KEY_FIELD, '==', interface['device_name']]] + del interface['device_name'] + query = Query(ACTION_UPDATE, VPPInterface.__type__, + filter = Filter.from_list(f_list), + params = ret) + self.receive(Packet.from_query(query, reply = True)) + except Exception as e: + import traceback; traceback.print_exc() + log.error("Could not perform measurement {}".format(e)) + + await asyncio.sleep(DEFAULT_INTERVAL) + + #-------------------------------------------------------------------------- + # Router interface + #-------------------------------------------------------------------------- + + def send_impl(self, packet): + query = packet.to_query() + + if query.action not in (ACTION_SUBSCRIBE, ACTION_UNSUBSCRIBE): + log.warning("Ignore unknown action {}".format( + ACTION2STR[query.action])) + return + + # We currently simply extract it from the filter + interfaces = set([p.value for p in query.filter if p.key == KEY_FIELD]) + + for interface in interfaces: + if query.action == ACTION_SUBSCRIBE: + self._interfaces[interface] += 1 + else: + self._interfaces[interface] -= 1 + + all_interfaces = set([k for k, v in self._interfaces.items() if v > 0]) + + if all_interfaces and not self._running: + self._running = True + asyncio.ensure_future(self._tick()) + elif not all_interfaces and self._running: + self._running = False + + +#------------------------------------------------------------------------------- + +if __name__ == '__main__': + x=""" Name Idx State Counter Count + TenGigabitEthernetc/0/1 1 up rx packets 3511586 + rx bytes 4785592030 + tx packets 3511678 + tx bytes 313021701 + drops 7 + ip4 161538 + ip6 3350047 + tx-error 2 + host-bh1 4 up rx packets 5 + rx bytes 394 + tx packets 10 + tx bytes 860 + drops 4 + ip6 4 + host-bh2 6 up rx packets 3164301 + rx bytes 287315869 + tx packets 3164238 + tx bytes 4290944332 + drops 4 + ip4 161539 + ip6 3002759 + host-bh3 7 up rx packets 33066 + rx bytes 2446928 + tx packets 33060 + tx bytes 47058708 + drops 5 + ip6 33065 + host-bh4 5 up rx packets 114407 + rx bytes 8466166 + tx packets 114412 + tx bytes 162905294 + drops 7 + ip6 114406 + host-bh5 3 up rx packets 150574 + rx bytes 11142524 + tx packets 150578 + tx bytes 214407016 + drops 7 + ip6 150573 + host-bh6 2 up rx packets 49380 + rx bytes 3654160 + tx packets 49368 + tx bytes 70283976 + drops 9 + ip6 49377 + local0 0 down drops 3 + """ + + r = parse(x) + print(r) diff --git a/netmodel/interfaces/websocket/__init__.py b/netmodel/interfaces/websocket/__init__.py index cb79fc39..b6402aca 100644 --- a/netmodel/interfaces/websocket/__init__.py +++ b/netmodel/interfaces/websocket/__init__.py @@ -97,7 +97,7 @@ class ClientProtocol(WebSocketClientProtocol): Websocket opening handshake has completed. """ self.factory.interface._on_client_open(self) - + def onMessage(self, payload, isBinary): self.factory.interface._on_client_message(self, payload, isBinary) @@ -109,7 +109,7 @@ class ClientProtocol(WebSocketClientProtocol): class WebSocketClientInterface(Interface): """ All messages are exchanged using text (non-binary) mode. - """ + """ __interface__ = 'websocketclient' def __init__(self, *args, **kwargs): @@ -149,6 +149,11 @@ class WebSocketClientInterface(Interface): # Holds the instance of the connect client protocol self._client = None + def __repr__(self): + return ''.format(self._client) + + __str__ = __repr__ + #-------------------------------------------------------------------------- # Interface API #-------------------------------------------------------------------------- @@ -168,10 +173,10 @@ class WebSocketClientInterface(Interface): async def _connect(self): loop = asyncio.get_event_loop() try: - self._instance = await loop.create_connection(self._factory, + self._instance = await loop.create_connection(self._factory, self._address, self._port) except Exception as e: - log.warning('Connect failed : {}'.format(e)) + log.warning('Connect failed on {} : {}'.format(self, e)) self._instance = None # don't await for retry, since it cause an infinite recursion... asyncio.ensure_future(self._retry()) @@ -206,7 +211,7 @@ class WebSocketClientInterface(Interface): query, record = args else: query = args - + if isinstance(query, dict): query = Query.from_dict(query) else: @@ -226,7 +231,7 @@ class WebSocketClientInterface(Interface): asyncio.ensure_future(self._retry()) #------------------------------------------------------------------------------ - + class ServerProtocol(WebSocketServerProtocol, Interface): """ Default WebSocket server protocol. @@ -244,7 +249,7 @@ class ServerProtocol(WebSocketServerProtocol, Interface): Constructor. Args: - callback (Function[ -> ]) : + callback (Function[ -> ]) : hook (Function[->]) : Hook method to be called for every packet to be sent on the interface. Processing continues with the packet returned by this function, or is interrupted in case of a None @@ -252,6 +257,12 @@ class ServerProtocol(WebSocketServerProtocol, Interface): """ WebSocketServerProtocol.__init__(self) Interface.__init__(self, callback=callback, hook=hook) + self._last_peer = None + + def __repr__(self): + return ''.format(self._last_peer if self._last_peer else 'N/A') + + __str__ = __repr__ #-------------------------------------------------------------------------- # Interface API @@ -272,9 +283,10 @@ class ServerProtocol(WebSocketServerProtocol, Interface): # Websocket events def onConnect(self, request): + self._last_peer = request.peer self.factory._instances.append(self) self.set_state(InterfaceState.Up) - + def onOpen(self): #print("WebSocket connection open.") pass @@ -305,7 +317,7 @@ class WebSocketServerInterface(Interface): It is also used to broadcast packets to all connected clients. All messages are exchanged using text (non-binary) mode. - """ + """ __interface__ = 'websocketserver' @@ -331,6 +343,12 @@ class WebSocketServerInterface(Interface): # packets. self._factory._instances = list() + def __repr__(self): + return '>> + # class Group(Resource): + # resources = Attribute(Resource, description = 'Resources belonging to the group', + # multiplicity = Multiplicity.ManyToMany, + # default = [], + # reverse_name = 'groups', + # reverse_description = 'Groups to which the resource belongs') + # <<< + # + # Local variables: + # cls = + # obj = + # obj.type = + # reverse_attribute = + # + # Result: + # 1) Group._reverse_attributes = + # { : [, ...], ...} + # 2) Add attribute to class Resource + # 3) Resource._reverse_attributes = + # { : [ start: + del self[start] + + for i, item in enumerate(value): + self.insert(i + start, item) + else: + rng = list(range(start, stop, step)) + if len(value) != len(rng): + raise ValueError( + "attempt to assign sequence of size %s to " + "extended slice of size %s" % (len(value), + len(rng))) + for i, item in zip(rng, value): + self.__setitem__(i, item) + _tidy(__setitem__) + return __setitem__ + + def __delitem__(fn): + def __delitem__(self, index): + if not isinstance(index, slice): + item = self[index] + try: + self._attribute.do_list_remove(self._instance, item) + fn(self, index) + except : pass + else: + # slice deletion requires __getslice__ and a slice-groking + # __getitem__ for stepped deletion + # note: not breaking this into atomic dels + has_except = False + for item in self[index]: + try: + self._attribute.do_list_remove(self._instance, item) + except : has_except = True + if not has_except: + fn(self, index) + _tidy(__delitem__) + return __delitem__ + + if py2k: + def __setslice__(fn): + def __setslice__(self, start, end, values): + has_except = False + for value in self[start:end]: + try: + self._attribute.do_list_remove(self._instance, value) + except : has_except = True + #values = [self._attribute.do_list_add(self._instance, value) for value in values] + _values = list() + for value in values: + try: + _values.append(self._attribute.do_list_add(self._instance, value)) + except: has_except = True + if not has_except: + fn(self, start, end, _values) + _tidy(__setslice__) + return __setslice__ + + def __delslice__(fn): + def __delslice__(self, start, end): + has_except = False + for value in self[start:end]: + try: + self._attribute.do_list_remove(self._instance, value) + except : has_except = True + if not has_except: + fn(self, start, end) + _tidy(__delslice__) + return __delslice__ + + def extend(fn): + def extend(self, iterable): + for value in iterable: + self.append(value) + _tidy(extend) + return extend + + def __iadd__(fn): + def __iadd__(self, iterable): + # list.__iadd__ takes any iterable and seems to let TypeError + # raise as-is instead of returning NotImplemented + for value in iterable: + self.append(value) + return self + _tidy(__iadd__) + return __iadd__ + + def pop(fn): + def pop(self, index=-1): + try: + self._attribute.do_list_remove(self._instance, item) + item = fn(self, index) + return item + except : return None + _tidy(pop) + return pop + + def __iter__(fn): + def __iter__(self): + for item in fn(self): + yield self._attribute.handle_getitem(self._instance, item) + _tidy(__iter__) + return __iter__ + + def __repr__(fn): + def __repr__(self): + return ''.format(id(self), list.__repr__(self)) + _tidy(__repr__) + return __repr__ + + __str__ = __repr__ + #def __str__(fn): + # def __str__(self): + # return str(list(self)) + # _tidy(__str__) + # return __str__ + + if not py2k: + def clear(fn): + def clear(self, index=-1): + has_except = False + for item in self: + try: + self._attribute.do_list_remove(self._instance, item) + except : has_except = True + if not has_except: + fn(self) + _tidy(clear) + return clear + + # __imul__ : not wrapping this. all members of the collection are already + # present, so no need to fire appends... wrapping it with an explicit + # decorator is still possible, so events on *= can be had if they're + # desired. hard to imagine a use case for __imul__, though. + + l = locals().copy() + l.pop('_tidy') + return l + +def _instrument_list(cls): + # inspired by sqlalchemy + for method, decorator in _list_decorators().items(): + fn = getattr(cls, method, None) + if fn: + #if (fn and method not in methods and + # not hasattr(fn, '_sa_instrumented')): + setattr(cls, method, decorator(fn)) + +class InstrumentedList(list): + + @classmethod + def from_list(cls, value, instance, attribute): + lst = list() + if value: + for x in value: + if isinstance(x, UUID): + x = instance.from_uuid(x) + lst.append(x) + # Having a class method is important for inheritance + value = cls(lst) + value._attribute = attribute + value._instance = instance + return value + + def __contains__(self, key): + from vicn.core.resource import Resource + if isinstance(key, Resource): + key = key.get_uuid() + return list.__contains__(self, key) + + def __lshift__(self, item): + self.append(item) + +_instrument_list(InstrumentedList) diff --git a/netmodel/model/sa_compat.py b/netmodel/model/sa_compat.py new file mode 100644 index 00000000..34211455 --- /dev/null +++ b/netmodel/model/sa_compat.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# This module originates from SQLAlchemy +# +# util/compat.py +# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# + +"""Handle Python version/platform incompatibilities.""" + +import sys + +try: + import threading +except ImportError: + import dummy_threading as threading + +py36 = sys.version_info >= (3, 6) +py33 = sys.version_info >= (3, 3) +py32 = sys.version_info >= (3, 2) +py3k = sys.version_info >= (3, 0) +py2k = sys.version_info < (3, 0) +py265 = sys.version_info >= (2, 6, 5) +jython = sys.platform.startswith('java') +pypy = hasattr(sys, 'pypy_version_info') +win32 = sys.platform.startswith('win') +cpython = not pypy and not jython # TODO: something better for this ? + +import collections +next = next + +if py3k: + import pickle +else: + try: + import cPickle as pickle + except ImportError: + import pickle + +# work around http://bugs.python.org/issue2646 +if py265: + safe_kwarg = lambda arg: arg +else: + safe_kwarg = str + +ArgSpec = collections.namedtuple("ArgSpec", + ["args", "varargs", "keywords", "defaults"]) + +if py3k: + import builtins + + from inspect import getfullargspec as inspect_getfullargspec + from urllib.parse import (quote_plus, unquote_plus, + parse_qsl, quote, unquote) + import configparser + from io import StringIO + + from io import BytesIO as byte_buffer + + def inspect_getargspec(func): + return ArgSpec( + *inspect_getfullargspec(func)[0:4] + ) + + string_types = str, + binary_types = bytes, + binary_type = bytes + text_type = str + int_types = int, + iterbytes = iter + + def u(s): + return s + + def ue(s): + return s + + def b(s): + return s.encode("latin-1") + + if py32: + callable = callable + else: + def callable(fn): + return hasattr(fn, '__call__') + + def cmp(a, b): + return (a > b) - (a < b) + + from functools import reduce + + print_ = getattr(builtins, "print") + + import_ = getattr(builtins, '__import__') + + import itertools + itertools_filterfalse = itertools.filterfalse + itertools_filter = filter + itertools_imap = map + from itertools import zip_longest + + import base64 + + def b64encode(x): + return base64.b64encode(x).decode('ascii') + + def b64decode(x): + return base64.b64decode(x.encode('ascii')) + +else: + from inspect import getargspec as inspect_getfullargspec + inspect_getargspec = inspect_getfullargspec + from urllib import quote_plus, unquote_plus, quote, unquote + from urlparse import parse_qsl + import ConfigParser as configparser + from StringIO import StringIO + from cStringIO import StringIO as byte_buffer + + string_types = basestring, + binary_types = bytes, + binary_type = str + text_type = unicode + int_types = int, long + + def iterbytes(buf): + return (ord(byte) for byte in buf) + + def u(s): + # this differs from what six does, which doesn't support non-ASCII + # strings - we only use u() with + # literal source strings, and all our source files with non-ascii + # in them (all are tests) are utf-8 encoded. + return unicode(s, "utf-8") + + def ue(s): + return unicode(s, "unicode_escape") + + def b(s): + return s + + def import_(*args): + if len(args) == 4: + args = args[0:3] + ([str(arg) for arg in args[3]],) + return __import__(*args) + + callable = callable + cmp = cmp + reduce = reduce + + import base64 + b64encode = base64.b64encode + b64decode = base64.b64decode + + def print_(*args, **kwargs): + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + for arg in enumerate(args): + if not isinstance(arg, basestring): + arg = str(arg) + fp.write(arg) + + import itertools + itertools_filterfalse = itertools.ifilterfalse + itertools_filter = itertools.ifilter + itertools_imap = itertools.imap + from itertools import izip_longest as zip_longest + + +import time +if win32 or jython: + time_func = time.clock +else: + time_func = time.time + +from collections import namedtuple +from operator import attrgetter as dottedgetter + + +if py3k: + def reraise(tp, value, tb=None, cause=None): + if cause is not None: + assert cause is not value, "Same cause emitted" + value.__cause__ = cause + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + # not as nice as that of Py3K, but at least preserves + # the code line where the issue occurred + exec("def reraise(tp, value, tb=None, cause=None):\n" + " if cause is not None:\n" + " assert cause is not value, 'Same cause emitted'\n" + " raise tp, value, tb\n") + + +def raise_from_cause(exception, exc_info=None): + if exc_info is None: + exc_info = sys.exc_info() + exc_type, exc_value, exc_tb = exc_info + cause = exc_value if exc_value is not exception else None + reraise(type(exception), exception, tb=exc_tb, cause=cause) + +if py3k: + exec_ = getattr(builtins, 'exec') +else: + def exec_(func_text, globals_, lcl=None): + if lcl is None: + exec('exec func_text in globals_') + else: + exec('exec func_text in globals_, lcl') + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass. + + Drops the middle class upon creation. + + Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ + + """ + + class metaclass(meta): + __call__ = type.__call__ + __init__ = type.__init__ + + def __new__(cls, name, this_bases, d): + if this_bases is None: + return type.__new__(cls, name, (), d) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) + + +from contextlib import contextmanager + +try: + from contextlib import nested +except ImportError: + # removed in py3k, credit to mitsuhiko for + # workaround + + @contextmanager + def nested(*managers): + exits = [] + vars = [] + exc = (None, None, None) + try: + for mgr in managers: + exit = mgr.__exit__ + enter = mgr.__enter__ + vars.append(enter()) + exits.append(exit) + yield vars + except: + exc = sys.exc_info() + finally: + while exits: + exit = exits.pop() + try: + if exit(*exc): + exc = (None, None, None) + except: + exc = sys.exc_info() + if exc != (None, None, None): + reraise(exc[0], exc[1], exc[2]) diff --git a/netmodel/model/type.py b/netmodel/model/type.py index 20dc2580..9a7b8740 100644 --- a/netmodel/model/type.py +++ b/netmodel/model/type.py @@ -16,27 +16,47 @@ # limitations under the License. # +from socket import inet_pton, inet_ntop, AF_INET6 +from struct import unpack, pack +from abc import ABCMeta + from netmodel.util.meta import inheritors class BaseType: + __choices__ = None + @staticmethod def name(): return self.__class__.__name__.lower() + @classmethod + def restrict(cls, **kwargs): + class BaseType(cls): + __choices__ = kwargs.pop('choices', None) + return BaseType + class String(BaseType): - def __init__(self, *args, **kwargs): - self._min_size = kwargs.pop('min_size', None) - self._max_size = kwargs.pop('max_size', None) - self._ascii = kwargs.pop('ascii', False) - self._forbidden = kwargs.pop('forbidden', None) - super().__init__() + __min_size__ = None + __max_size__ = None + __ascii__ = None + __forbidden__ = None + + @classmethod + def restrict(cls, **kwargs): + base = super().restrict(**kwargs) + class String(base): + __max_size__ = kwargs.pop('max_size', None) + __min_size__ = kwargs.pop('min_size', None) + __ascii__ = kwargs.pop('ascii', None) + __forbidden__ = kwargs.pop('forbidden', None) + return String class Integer(BaseType): def __init__(self, *args, **kwargs): self._min_value = kwargs.pop('min_value', None) self._max_value = kwargs.pop('max_value', None) super().__init__() - + class Double(BaseType): def __init__(self, *args, **kwargs): self._min_value = kwargs.pop('min_value', None) @@ -49,12 +69,161 @@ class Bool(BaseType): class Dict(BaseType): pass +class PrefixTreeException(Exception): pass +class NotEnoughAddresses(PrefixTreeException): pass +class UnassignablePrefix(PrefixTreeException): pass + +class Prefix(BaseType, metaclass=ABCMeta): + + def __init__(self, ip_address, prefix_len=None): + if not prefix_len: + if not isinstance(ip_address, str): + import pdb; pdb.set_trace() + if '/' in ip_address: + ip_address, prefix_len = ip_address.split('/') + prefix_len = int(prefix_len) + else: + prefix_len = self.MAX_PREFIX_SIZE + if isinstance(ip_address, str): + ip_address = self.aton(ip_address) + self.ip_address = ip_address + self.prefix_len = prefix_len + + def __contains__(self, obj): + #it can be an IP as a integer + if isinstance(obj, int): + obj = type(self)(obj, self.MAX_PREFIX_SIZE) + #Or it's an IP string + if isinstance(obj, str): + #It's a prefix as 'IP/prefix' + if '/' in obj: + split_obj = obj.split('/') + obj = type(self)(split_obj[0], int(split_obj[1])) + else: + obj = type(self)(obj, self.MAX_PREFIX_SIZE) + + return self._contains_prefix(obj) + + @classmethod + def mask(cls): + mask_len = cls.MAX_PREFIX_SIZE//8 #Converts from bits to bytes + mask = 0 + for step in range(0,mask_len): + mask = (mask << 8) | 0xff + return mask + + def _contains_prefix(self, prefix): + assert isinstance(prefix, type(self)) + return (prefix.prefix_len >= self.prefix_len and + prefix.ip_address >= self.first_prefix_address() and + prefix.ip_address <= self.last_prefix_address()) + + #Returns the first address of a prefix + def first_prefix_address(self): + return self.ip_address & (self.mask() << (self.MAX_PREFIX_SIZE-self.prefix_len)) + + def canonical_prefix(self): + return type(self)(self.first_prefix_address(), self.prefix_len) + + def last_prefix_address(self): + return self.ip_address | (self.mask() >> self.prefix_len) + + def limits(self): + return self.first_prefix_address(), self.last_prefix_address() + + def __str__(self): + return "{}/{}".format(self.ntoa(self.first_prefix_address()), self.prefix_len) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + return self.get_tuple() == other.get_tuple() + + def get_tuple(self): + return (self.first_prefix_address(), self.prefix_len) + + def __hash__(self): + return hash(self.get_tuple()) + + def __iter__(self): + return self.get_iterator() + + #Iterates by steps of prefix_len, e.g., on all available /31 in a /24 + def get_iterator(self, prefix_len=None): + if prefix_len is None: + prefix_len=self.MAX_PREFIX_SIZE + assert (prefix_len >= self.prefix_len and prefix_len<=self.MAX_PREFIX_SIZE) + step = 2**(self.MAX_PREFIX_SIZE - prefix_len) + for ip in range(self.first_prefix_address(), self.last_prefix_address()+1, step): + yield type(self)(ip, prefix_len) + +class Inet4Prefix(Prefix): + + MAX_PREFIX_SIZE = 32 + + @classmethod + def aton(cls, address): + ret = 0 + components = address.split('.') + for comp in components: + ret = (ret << 8) + int(comp) + return ret + + @classmethod + def ntoa(cls, address): + components = [] + for _ in range(0,4): + components.insert(0,'{}'.format(address % 256)) + address = address >> 8 + return '.'.join(components) + +class Inet6Prefix(Prefix): + + MAX_PREFIX_SIZE = 128 + + @classmethod + def aton (cls, address): + prefix, suffix = unpack(">QQ", inet_pton(AF_INET6, address)) + return (prefix << 64) | suffix + + @classmethod + def ntoa (cls, address): + return inet_ntop(AF_INET6, pack(">QQ", address >> 64, address & ((1 << 64) -1))) + + #skip_internet_address: skip a:b::0, as v6 often use default /64 prefixes + def get_iterator(self, prefix_len=None, skip_internet_address=None): + if skip_internet_address is None: + #We skip the internet address if we iterate over Addresses + if prefix_len is None: + skip_internet_address = True + #But not if we iterate over prefixes + else: + skip_internet_address = False + it = super().get_iterator(prefix_len) + if skip_internet_address: + next(it) + return it + +class InetAddress(Prefix): + + def get_tuple(self): + return (self.ip_address, self.prefix_len) + + def __str__(self): + return self.ntoa(self.ip_address) + +class Inet4Address(InetAddress, Inet4Prefix): + pass + +class Inet6Address(InetAddress, Inet6Prefix): + pass + class Self(BaseType): """Self-reference """ class Type: - BASE_TYPES = (String, Integer, Double, Bool) + BASE_TYPES = (String, Integer, Double, Bool) _registry = dict() @staticmethod diff --git a/netmodel/model/uuid.py b/netmodel/model/uuid.py new file mode 100644 index 00000000..dae51d75 --- /dev/null +++ b/netmodel/model/uuid.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 random +import string + +# Separator for components of the UUID +UUID_SEP = '-' + +# Length of the random component of the UUID +UUID_LEN = 5 + +class UUID: + def __init__(self, name, cls): + self._uuid = self._make_uuid(name, cls) + + def _make_uuid(self, name, cls): + """Generate a unique resource identifier + + The UUID consists in the type of the resource, to which is added a + random identifier of length UUID_LEN. Components of the UUID are + separated by UUID_SEP. + """ + uuid = ''.join(random.choice(string.ascii_uppercase + string.digits) + for _ in range(UUID_LEN)) + if name: + uuid = name # + UUID_SEP + uuid + return UUID_SEP.join([cls.__name__, uuid]) + + def __repr__(self): + return ''.format(self._uuid) + + def __lt__(self, other): + return self._uuid < other._uuid + + __str__ = __repr__ diff --git a/netmodel/network/fib.py b/netmodel/network/fib.py index e6b81607..11b90b22 100644 --- a/netmodel/network/fib.py +++ b/netmodel/network/fib.py @@ -39,7 +39,10 @@ class FIB: self._entries = dict() def add(self, prefix, next_hops = None): - self._entries[prefix] = FIBEntry(prefix, next_hops) + if prefix not in self._entries: + self._entries[prefix] = FIBEntry(prefix, next_hops) + else: + self._entries[prefix].update(next_hops) def update(self, prefix, next_hops = None): entry = self._entries.get(prefix) @@ -54,10 +57,11 @@ class FIB: raise Exception('prefix not found') entry.remove(next_hops) return - + del self._entries[prefix] def get(self, object_name): for entry in self._entries.values(): if entry._prefix.object_name == object_name: - return next(iter(entry._next_hops)) + return entry._next_hops + return None diff --git a/netmodel/network/flow_table.py b/netmodel/network/flow_table.py index 99e42e99..2da40d1c 100644 --- a/netmodel/network/flow_table.py +++ b/netmodel/network/flow_table.py @@ -16,7 +16,10 @@ # limitations under the License. # -from netmodel.model.query import ACTION_SUBSCRIBE, ACTION_UNSUBSCRIBE +import copy +from collections import defaultdict, Counter + +from netmodel.model.query import Query, ACTION_SUBSCRIBE, ACTION_UNSUBSCRIBE # Per-interface flow table class SubFlowTable: @@ -25,21 +28,33 @@ class SubFlowTable: self._interface = interface # Flow -> ingress interface - self._flows = dict() - - # Ingress interface -> list of subscription - self._subscriptions = dict() + self._flows = defaultdict(set) def add(self, packet, ingress_interface): flow = packet.get_flow() - self._flows[flow] = ingress_interface + self._flows[flow].add(ingress_interface) def match(self, packet): flow = packet.get_flow() return self._flows.get(flow.get_reverse()) + def _on_interface_delete(self, interface): + """ + Returns: + False is the flow table is empty. + """ + to_remove = set() + for flow, ingress_interfaces in self._flows.items(): + ingress_interfaces.discard(interface) + if not ingress_interfaces: + to_remove.add(flow) + for flow in to_remove: + del self._flows[flow] + + return len(self._flows) > 0 + class Subscription: - def __init__(self, packet, ingress_list, egress_list): + def __init__(self, packet, ingress_set, egress_set): """ Args: packet : subscription packet @@ -47,8 +62,22 @@ class Subscription: egress_list (List[Interface]) : list of egress interface """ self._packet = packet - self._ingress_list = ingress_list - self._egress_list = egress_list + #self._ingress_set = ingress_set + #self._egress_set = egress_set + + def get_tuple(self): + return (self._packet,) + + def __eq__(self, other): + return self.get_tuple() == other.get_tuple() + + def __hash__(self): + return hash(self.get_tuple()) + + def __repr__(self): + return ''.format(self._packet.to_query()) + + __str__ = __repr__ class FlowTable: """ @@ -76,12 +105,30 @@ class FlowTable: # The Flow Table also maintains a list of subscriptions doubly indexed # by both subscriptors, and egress interfaces - # ingress_interface -> list of subscriptions - self._ingress_subscriptions = dict() - - # egress_interface -> list of subscriptions - self._egress_subscriptions = dict() - + # ingress_interface -> bag of subscriptions + self._ingress_subscriptions = defaultdict(Counter) + + # egress_interface -> bag of subscriptions + self._egress_subscriptions = defaultdict(Counter) + + def dump(self, msg=''): + print("="*80) + print("FLOW TABLE {}".format(msg)) + print("-" * 80) + print("SubFlowTables") + for interface, flow_table in self._sub_flow_tables.items(): + for k, v in flow_table._flows.items(): + print(interface, "\t", k, "\t", v) + print("-" * 80) + print("Ingress subscriptions") + for interface, subscriptions in self._ingress_subscriptions.items(): + print(interface, "\t", subscriptions) + print("-" * 80) + print("Egress subscriptions") + for interface, subscriptions in self._egress_subscriptions.items(): + print(interface, "\t", subscriptions) + print("=" * 80) + print("") def match(self, packet, interface): """ @@ -95,34 +142,30 @@ class FlowTable: if not sub_flow_table: return None return sub_flow_table.match(packet) - - def add(self, packet, ingress_interface, interface): - sub_flow_table = self._sub_flow_tables.get(interface) - if not sub_flow_table: - sub_flow_table = SubFlowTable(interface) - self._sub_flow_tables[interface] = sub_flow_table - sub_flow_table.add(packet, ingress_interface) + + def add(self, packet, ingress_interface, interfaces): + for interface in interfaces: + sub_flow_table = self._sub_flow_tables.get(interface) + if not sub_flow_table: + sub_flow_table = SubFlowTable(interface) + self._sub_flow_tables[interface] = sub_flow_table + sub_flow_table.add(packet, ingress_interface) # If the flow is a subscription, we need to associate it to the list query = packet.to_query() if query.action == ACTION_SUBSCRIBE: # XXX we currently don't merge subscriptions, and assume a single # next hop interface - s = Subscription(packet, [ingress_interface], [interface]) + s = Subscription(packet, set([ingress_interface]), interfaces) if ingress_interface: - if not ingress_interface in self._ingress_subscriptions: - self._ingress_subscriptions[ingress_interface] = list() - self._ingress_subscriptions[ingress_interface].append(s) - - if not interface in self._egress_subscriptions: - self._egress_subscriptions[interface] = list() - self._egress_subscriptions[interface].append(s) + self._ingress_subscriptions[ingress_interface] += Counter([s]) + for interface in interfaces: + self._egress_subscriptions[interface] += Counter([s]) elif query.action == ACTION_UNSUBSCRIBE: raise NotImplementedError - # Events def _on_interface_up(self, interface): @@ -141,9 +184,47 @@ class FlowTable: """ Callback: an interface has been deleted - Cancel all subscriptions that have been issues from + Cancel all subscriptions that have been issues from netmodel.network.interface. Remove all pointers to subscriptions pending on this interface """ + if interface in self._ingress_subscriptions: + # If the interface we delete at the origin of the subscription, + # let's also remove corresponding egress subscriptions + subs = self._ingress_subscriptions[interface] + if not subs: + return + + to_remove = set() + for _interface, subscriptions in self._egress_subscriptions.items(): + + removed = subs & subscriptions + if removed: + for s in removed: + # We found a subscription of this interface on an other + # interface; send unsubscribe... + action, params = s._packet.payload + p = copy.deepcopy(s._packet) + p.payload = (ACTION_UNSUBSCRIBE, params) + _interface.send(p) + # ... and remove them + subscriptions -= removed + + # if the interface has no more subscription remove it. + if not subscriptions: + to_remove.add(_interface) + + for i in to_remove: + del self._egress_subscriptions[i] + del self._ingress_subscriptions[interface] + + # Remove interface from flow table destination + to_remove = set() + for _interface, sub_flow_table in self._sub_flow_tables.items(): + remove = sub_flow_table._on_interface_delete(interface) + if not remove: + to_remove.add(_interface) + for _interface in to_remove: + del self._sub_flow_tables[_interface] diff --git a/netmodel/network/interface.py b/netmodel/network/interface.py index c9e31422..3bad4c41 100644 --- a/netmodel/network/interface.py +++ b/netmodel/network/interface.py @@ -44,7 +44,7 @@ class InterfaceState(enum.Enum): def register_interfaces(): Interface._factory = dict() - for loader, module_name, is_pkg in pkgutil.walk_packages(interfaces.__path__, + for loader, module_name, is_pkg in pkgutil.walk_packages(interfaces.__path__, interfaces.__name__ + '.'): # Note: we cannot skip files using is_pkg otherwise it will ignore # interfaces defined outside of __init__.py @@ -95,12 +95,12 @@ class Interface: self._tx_buffer = list() self._state = InterfaceState.Down - self._error = None + self._error = None self._reconnecting = True self._reconnection_delay = RECONNECTION_DELAY self._registered_objects = dict() - + # Callbacks self._up_callbacks = list() self._down_callbacks = list() @@ -119,7 +119,7 @@ class Interface: def __hash__(self): return hash(self._name) - #--------------------------------------------------------------------------- + #--------------------------------------------------------------------------- def register_object(self, obj): self._registered_objects[obj.__type__] = obj @@ -127,9 +127,9 @@ class Interface: def get_prefixes(self): return [ Prefix(v.__type__) for v in self._registered_objects.values() ] - #--------------------------------------------------------------------------- + #--------------------------------------------------------------------------- # State management, callbacks - #--------------------------------------------------------------------------- + #--------------------------------------------------------------------------- def set_state(self, state): asyncio.ensure_future(self._set_state(state)) @@ -170,7 +170,7 @@ class Interface: for cb, args, kwargs in self._delete_callbacks: cb(interface, *args, **kwargs) - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- def set_reconnecting(self, reconnecting): self._reconnecting = reconnecting @@ -253,13 +253,13 @@ class Interface: def receive_impl(self, packet): ingress_interface = self cb = self._callback - if cb is None: - return if self._hook: new_packet = self._hook(packet) - if new_packet is not None: + if cb is not None and new_packet is not None: cb(new_packet, ingress_interface=ingress_interface) return + if cb is None: + return cb(packet, ingress_interface=ingress_interface) #-------------------------------------------------------------------------- diff --git a/netmodel/network/packet.py b/netmodel/network/packet.py index 9552b0e7..7edcccc4 100644 --- a/netmodel/network/packet.py +++ b/netmodel/network/packet.py @@ -109,7 +109,7 @@ class VICNTLV: @staticmethod def get_type(buf): - (typelen, ) = struct.unpack(NETMODEL_TLV_TYPELEN_STR, + (typelen, ) = struct.unpack(NETMODEL_TLV_TYPELEN_STR, buf[:NETMODEL_TLV_SIZE]) return (typelen & NETMODEL_TLV_TYPE_MASK) >> NETMODEL_TLV_TYPE_SHIFT @@ -179,7 +179,7 @@ class VICNTLV: class ObjectName(VICNTLV): pass @VICNTLV.set_tlv_type(NETMODEL_TLV_FIELD) -class Field(VICNTLV): +class Field(VICNTLV): """Field == STR """ @@ -217,7 +217,7 @@ class Prefix(Object, Prefix_, VICNTLV): def __hash__(self): return hash(self.get_tuple()) - + @VICNTLV.set_tlv_type(NETMODEL_TLV_SRC) class Source(Prefix): """Source address @@ -251,12 +251,12 @@ class Packet(Object, VICNTLV): source = Source() destination = Destination(Prefix) protocol = Protocol(String, default = 'query') - flags = Flags() - payload = Payload() + flags = Flags(String) + payload = Payload(String) # This should be dispatched across L3 L4 L7 - def __init__(self, source = None, destination = None, protocol = None, + def __init__(self, source = None, destination = None, protocol = None, flags = 0, payload = None): self.source = source self.destination = destination @@ -272,8 +272,8 @@ class Packet(Object, VICNTLV): packet = Packet() if src_query: address = Prefix( - object_name = src_query.object_name, - filter = src_query.filter, + object_name = src_query.object_name, + filter = src_query.filter, field_names = src_query.field_names, aggregate = src_query.aggregate) if reply: @@ -283,8 +283,8 @@ class Packet(Object, VICNTLV): if query: address = Prefix( - object_name = query.object_name, - filter = query.filter, + object_name = query.object_name, + filter = query.filter, field_names = query.field_names, aggregate = query.aggregate) @@ -310,7 +310,7 @@ class Packet(Object, VICNTLV): field_names = address.field_names aggregate = address.aggregate - return Query(action, object_name, filter, params, field_names, + return Query(action, object_name, filter, params, field_names, aggregate = aggregate, last = self.last, reply = self.reply) @property @@ -335,6 +335,16 @@ class Packet(Object, VICNTLV): else: self.flags &= ~FLAG_REPLY + def get_tuple(self): + return (self.source, self.destination, self.protocol, self.flags, + self.payload) + + def __eq__(self, other): + return self.get_tuple() == other.get_tuple() + + def __hash__(self): + return hash(self.get_tuple()) + class ErrorPacket(Packet): """ Analog with ICMP errors packets in IP networks @@ -344,7 +354,7 @@ class ErrorPacket(Packet): # Constructor #-------------------------------------------------------------------------- - def __init__(self, type = ERROR, code = ERROR, message = None, + def __init__(self, type = ERROR, code = ERROR, message = None, traceback = None, **kwargs): assert not traceback or isinstance(traceback, str) diff --git a/netmodel/network/prefix.py b/netmodel/network/prefix.py index 00b5db71..d444a56d 100644 --- a/netmodel/network/prefix.py +++ b/netmodel/network/prefix.py @@ -17,20 +17,23 @@ # class Prefix: - def __init__(self, object_name = None, filter = None, field_names = None, + def __init__(self, object_name = None, filter = None, field_names = None, aggregate = None): self.object_name = object_name self.filter = filter self.field_names = field_names self.aggregate = aggregate - def __hash__(self): - return hash(self.get_tuple()) - def get_tuple(self): - return (self.object_name, self.filter, self.field_names, + return (self.object_name, self.filter, self.field_names, self.aggregate) + def __eq__(self, other): + return self.get_tuple() == other.get_tuple() + + def __hash__(self): + return hash(self.get_tuple()) + def __repr__(self): return ''.format(self.get_tuple()) diff --git a/netmodel/network/router.py b/netmodel/network/router.py index 84d69dca..871cefe0 100644 --- a/netmodel/network/router.py +++ b/netmodel/network/router.py @@ -68,7 +68,7 @@ class Router: #-------------------------------------------------------------------------- def register_local_collection(self, cls): - self.get_interface(LOCAL_NAMESPACE).register_collection(cls, + self.get_interface(LOCAL_NAMESPACE).register_collection(cls, LOCAL_NAMESPACE) def register_collection(self, cls, namespace=None): @@ -97,7 +97,7 @@ class Router: interface.set_state(InterfaceState.PendingUp) for prefix in interface.get_prefixes(): - self._fib.add(prefix, [interface]) + self._fib.add(prefix, set([interface])) return interface @@ -112,8 +112,8 @@ class Router: def on_interface_up(self, interface): """ - This callback is triggered when an interface becomes up. - + This callback is triggered when an interface becomes up. + The router will request metadata. The flow table is notified. """ @@ -139,7 +139,7 @@ class Router: # Public API #--------------------------------------------------------------------------- - def add_interface(self, interface_type, name=None, namespace=None, + def add_interface(self, interface_type, name=None, namespace=None, **platform_config): """ namespace is used to force appending of a namespace to the tables. @@ -161,7 +161,7 @@ class Router: interface = interface_cls(self, **platform_config) except Exception as e: traceback.print_exc() - raise Exception("Cannot create interface %s of type %s with parameters %r: %s" + raise Exception("Cannot create interface %s of type %s with parameters %r: %s" % (name, interface_type, platform_config, e)) self._register_interface(interface, name) @@ -231,9 +231,10 @@ class Router: interface flow table - using the FIB if no match is found """ - orig_interface = self._flow_table.match(packet, ingress_interface) - if orig_interface: - orig_interface.send(packet) + orig_interfaces = self._flow_table.match(packet, ingress_interface) + if orig_interfaces: + for orig_interface in orig_interfaces: + orig_interface.send(packet) return if isinstance(packet, str): @@ -247,11 +248,17 @@ class Router: return # Get route from FIB - interface = self._fib.get(packet.destination.object_name) - if not interface: + if packet.destination is None: + log.warning("Ignored reply packet with no match in flow table {}".format( + packet.to_query())) + return + interfaces = self._fib.get(packet.destination.object_name) + if not interfaces: + log.error('No match in FIB for {}'.format( + packet.destination.object_name)) return # Update flow table before sending - self._flow_table.add(packet, ingress_interface, interface) - - interface.send(packet) + self._flow_table.add(packet, ingress_interface, interfaces) + for interface in interfaces: + interface.send(packet) diff --git a/netmodel/util/daemon.py b/netmodel/util/daemon.py index 29683a54..eb8cd1a2 100644 --- a/netmodel/util/daemon.py +++ b/netmodel/util/daemon.py @@ -231,7 +231,7 @@ class Daemon: """ Overload this method if you use twisted (see xmlrpc.py) """ - sys.exit(0) + os._exit(0) # Overload these... diff --git a/netmon/bin/netmon.py b/netmon/bin/netmon.py index 46e6327e..0f96d2b9 100755 --- a/netmon/bin/netmon.py +++ b/netmon/bin/netmon.py @@ -24,7 +24,8 @@ import sys PATH=os.path.join(os.path.dirname(__file__), os.path.pardir, os.path.pardir) sys.path.insert(0, os.path.abspath(PATH)) -import netmodel.network.interfaces +import netmodel.interfaces +from netmodel.network.interface import register_interfaces from netmodel.network.router import Router from netmodel.model.query import Query, ACTION_SELECT, ACTION_SUBSCRIBE from netmodel.util.daemon import Daemon @@ -44,6 +45,7 @@ class RouterDaemon(Daemon): self._router.add_interface('websocketserver') self._router.add_interface('bwm') + self._router.add_interface('vppctl') self._router.add_interface('local', router = self._router) diff --git a/requirements.txt b/requirements.txt index 77b44ead..db0e0699 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ ws4py==0.3.4 pyparsing lockfile -autobahn +#autobahn +websockets diff --git a/scripts/vppctl_wrapper b/scripts/vppctl_wrapper new file mode 100644 index 00000000..940e5784 --- /dev/null +++ b/scripts/vppctl_wrapper @@ -0,0 +1,4 @@ +#!/bin/bash +TIMEOUT=5 + +flock /tmp/vppctl.lock -c "timeout $TIMEOUT vppctl $*" diff --git a/setup.py b/setup.py index 170833c6..4ebd21c4 100755 --- a/setup.py +++ b/setup.py @@ -36,8 +36,16 @@ with open('README.md') as f: # XXX TODO required_modules = list() +data_files = list() + +data_files.extend([ + ("/lib/systemd/system/", ["etc/netmon.service"]), + ("/etc/init/", ["etc/netmon.conf"]), +]) + + setup( - name = 'vICN', + name = 'vicn', version = version, description = 'vICN experiment controller', long_description = long_description, @@ -61,6 +69,7 @@ setup( keywords = 'Experiment Controller; Orchestrator; ICN; LXC; Containers', platforms = "Linux, OSX", packages = find_packages(), + data_files = data_files, install_requires = required_modules, @@ -73,3 +82,43 @@ setup( ], }, ) + +setup( + name = 'netmon', + version = version, + description = 'Netmon', + long_description = long_description, + license = 'Apache 2.0', + + download_url = 'https://gerrit.fd.io/r/cicn', + url = 'https://wiki.fd.io/view/Vicn', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'Topic :: Software Development :: Build Tools', + 'Operating System :: POSIX :: Linux', + 'Operating System :: MacOS :: MacOS X', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + keywords = 'Experiment Controller; Orchestrator; ICN; LXC; Containers', + platforms = "Linux, OSX", + packages = find_packages(), + data_files = data_files, + + install_requires = required_modules, + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points = { + 'console_scripts': [ + 'netmon = netmon.bin.netmon:main', + ], + }, +) + diff --git a/vicn/core/api.py b/vicn/core/api.py index 708e2581..ccbb9b24 100644 --- a/vicn/core/api.py +++ b/vicn/core/api.py @@ -19,6 +19,7 @@ import asyncio import json import logging +import os import resource as ulimit import sys @@ -51,6 +52,7 @@ class API(metaclass = Singleton): def terminate(self): # XXX not valid if nothing has been initialized ResourceManager().terminate() + os._exit(0) def parse_topology_file(self, topology_fn, resources, settings): log.info("Parsing topology file %(topology_fn)s" % locals()) @@ -87,7 +89,7 @@ class API(metaclass = Singleton): if nofile is not None and nofile > 0: if nofile < 1024: log.error('Too few allowed open files for the process') - import os; os._exit(1) + os._exit(1) log.info('Setting open file descriptor limit to {}'.format( nofile)) @@ -95,15 +97,18 @@ class API(metaclass = Singleton): ulimit.RLIMIT_NOFILE, (nofile, nofile)) - ResourceManager(base=scenario[-1], settings=settings) + base = os.path.dirname(scenario_list[-1]) + base = os.path.abspath(base) + ResourceManager(base = base, settings = settings) for resource in resources: try: ResourceManager().create_from_dict(**resource) except Exception as e: + import traceback; traceback.print_exc() log.error("Could not create resource '%r': %r" % \ (resource, e,)) - import os; os._exit(1) + os._exit(1) self._configured = True diff --git a/vicn/core/attribute.py b/vicn/core/attribute.py index 3afe0d6e..02520cbd 100644 --- a/vicn/core/attribute.py +++ b/vicn/core/attribute.py @@ -22,109 +22,34 @@ import logging import operator import types -from netmodel.model.mapper import ObjectSpecification -from netmodel.model.type import Type, Self -from netmodel.util.meta import inheritors -from netmodel.util.misc import is_iterable -from vicn.core.exception import VICNListException -from vicn.core.requirement import Requirement, RequirementList -from vicn.core.sa_collections import InstrumentedList -from vicn.core.state import UUID, NEVER_SET, Operations +from netmodel.model.attribute import Attribute as BaseAttribute, NEVER_SET +from netmodel.model.attribute import Multiplicity, DEFAULT +from netmodel.model.type import Self +from netmodel.model.uuid import UUID +from netmodel.util.misc import is_iterable +from vicn.core.requirement import Requirement, RequirementList +from vicn.core.state import Operations log = logging.getLogger(__name__) -#------------------------------------------------------------------------------ -# Attribute Multiplicity -#------------------------------------------------------------------------------ - -class Multiplicity: - OneToOne = '1_1' - OneToMany = '1_N' - ManyToOne = 'N_1' - ManyToMany = 'N_N' - - @staticmethod - def reverse(value): - reverse_map = { - Multiplicity.OneToOne: Multiplicity.OneToOne, - Multiplicity.OneToMany: Multiplicity.ManyToOne, - Multiplicity.ManyToOne: Multiplicity.OneToMany, - Multiplicity.ManyToMany: Multiplicity.ManyToMany, - } - return reverse_map[value] - - -# Default attribute properties values (default to None) -DEFAULT = { - 'multiplicity' : Multiplicity.OneToOne, - 'mandatory' : False, -} - #------------------------------------------------------------------------------ # Attribute #------------------------------------------------------------------------------ -class Attribute(abc.ABC, ObjectSpecification): - properties = [ - 'name', - 'type', - 'key', - 'description', - 'default', - 'choices', - 'mandatory', - 'multiplicity', - 'ro', - 'auto', - 'func', +class Attribute(BaseAttribute): + properties = BaseAttribute.properties + properties.extend([ 'requirements', - 'reverse_name', - 'reverse_description', - 'reverse_auto' - ] + 'remote_default' + ]) def __init__(self, *args, **kwargs): - for key in Attribute.properties: - value = kwargs.pop(key, NEVER_SET) - setattr(self, key, value) - - if len(args) == 1: - self.type, = args - elif len(args) == 2: - self.name, self.type = args - - # self.type is optional since the type can be inherited. Although we - # will have to verify the attribute is complete at some point - if self.type: - if isinstance(self.type, str): - self.type = Type.from_string(self.type) - assert self.type is Self or Type.exists(self.type) + super().__init__(*args, **kwargs) # Post processing attribute properties if self.requirements is not NEVER_SET: self.requirements = RequirementList(self.requirements) - self.is_aggregate = False - - self._reverse_attributes = list() - - #-------------------------------------------------------------------------- - # Display - #-------------------------------------------------------------------------- - - def __repr__(self): - return ''.format(self.name) - - __str__ = __repr__ - - # The following functions are required to allow comparing attributes, and - # using them as dict keys - - def __eq__(self, other): - return self.name == other.name - - def __hash__(self): - return hash(self.name) #-------------------------------------------------------------------------- # Descriptor protocol @@ -132,6 +57,8 @@ class Attribute(abc.ABC, ObjectSpecification): # see. https://docs.python.org/3/howto/descriptor.html #-------------------------------------------------------------------------- + # XXX Overloaded & simpler + def __get__(self, instance, owner=None): if not instance: return self @@ -144,9 +71,6 @@ class Attribute(abc.ABC, ObjectSpecification): instance.set(self.name, value, blocking=False) - def __delete__(self, instance): - raise NotImplementedError - #-------------------------------------------------------------------------- def do_list_add(self, instance, value): @@ -170,7 +94,7 @@ class Attribute(abc.ABC, ObjectSpecification): value, cur_value) # prevent instrumented list to perform operation - raise VICNListException + raise InstrumentedListException def do_list_remove(self, instance, value): if instance.is_local_attribute(self.name): @@ -187,7 +111,7 @@ class Attribute(abc.ABC, ObjectSpecification): value, cur_value) # prevent instrumented list to perform operation - raise VICNListException + raise InstrumentedListException def do_list_clear(self, instance): if instance.is_local_attribute(self.name): @@ -201,7 +125,7 @@ class Attribute(abc.ABC, ObjectSpecification): value, cur_value) # prevent instrumented list to perform operation - raise VICNListException + raise InstrumentedListException def handle_getitem(self, instance, item): if isinstance(item, UUID): @@ -209,29 +133,6 @@ class Attribute(abc.ABC, ObjectSpecification): return ResourceManager().by_uuid(item) return item - #-------------------------------------------------------------------------- - # Accessors - #-------------------------------------------------------------------------- - - def __getattribute__(self, name): - value = super().__getattribute__(name) - if value is NEVER_SET: - if name == 'default': - return list() if self.is_collection else None - return DEFAULT.get(name, None) - return value - - def has_reverse_attribute(self): - return self.reverse_name and self.multiplicity - - @property - def is_collection(self): - return self.multiplicity in (Multiplicity.OneToMany, - Multiplicity.ManyToMany) - - def is_set(self, instance): - return instance.is_set(self.name) - #-------------------------------------------------------------------------- # Operations #-------------------------------------------------------------------------- @@ -256,6 +157,7 @@ class Attribute(abc.ABC, ObjectSpecification): #------------------------------------------------------------------------------ +# XXX Move this to object, be careful of access to self._reference ! class Reference: """ Value reference. diff --git a/vicn/core/collection.py b/vicn/core/collection.py deleted file mode 100644 index fb222891..00000000 --- a/vicn/core/collection.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (c) 2017 Cisco and/or its affiliates. -# -# 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 vicn.core.sa_collections import InstrumentedList -from netmodel.model.collection import Collection - -class Collection(InstrumentedList, Collection): - pass diff --git a/vicn/core/commands.py b/vicn/core/commands.py index 41c06bf5..c04ab264 100644 --- a/vicn/core/commands.py +++ b/vicn/core/commands.py @@ -65,7 +65,7 @@ class ReturnValue: def _clean(self, value): if value is None or isinstance(value, str): return value - return value.decode('utf-8') + return value.decode('utf-8').strip() def _set_stdout(self, value): self._stdout = self._clean(value) diff --git a/vicn/core/exception.py b/vicn/core/exception.py index 977fc8ad..4389531f 100644 --- a/vicn/core/exception.py +++ b/vicn/core/exception.py @@ -34,8 +34,6 @@ class InitializeException(VICNException): pass class CheckException(VICNException): pass class SetupException(VICNException): pass -class VICNListException(VICNException): pass - class ResourceNotFound(VICNException): pass class VICNWouldBlock(VICNException): diff --git a/vicn/core/resource.py b/vicn/core/resource.py index f92e1255..878a8108 100644 --- a/vicn/core/resource.py +++ b/vicn/core/resource.py @@ -32,14 +32,17 @@ from threading import Event as ThreadEvent # LXD workaround from pylxd.exceptions import NotFound as LXDAPIException +from netmodel.model.collection import Collection +from netmodel.model.key import Key from netmodel.model.mapper import ObjectSpecification +from netmodel.model.object import Object from netmodel.model.type import String, Bool, Integer, Dict from netmodel.model.type import BaseType, Self +from netmodel.model.uuid import UUID from netmodel.util.deprecated import deprecated from netmodel.util.singleton import Singleton from vicn.core.attribute import Attribute, Multiplicity, Reference from vicn.core.attribute import NEVER_SET -from vicn.core.collection import Collection from vicn.core.commands import ReturnValue from vicn.core.event import Event, AttributeChangedEvent from vicn.core.exception import VICNException, ResourceNotFound @@ -47,7 +50,7 @@ from vicn.core.exception import VICNWouldBlock from vicn.core.resource_factory import ResourceFactory from vicn.core.requirement import Requirement, Property from vicn.core.scheduling_algebra import SchedulingAlgebra -from vicn.core.state import ResourceState, UUID +from vicn.core.state import ResourceState from vicn.core.state import Operations, InstanceState from vicn.core.task import run_task, BashTask @@ -73,29 +76,29 @@ class TopLevelResource: pass class FactoryResource(TopLevelResource): pass class CategoryResource(TopLevelResource): pass -#------------------------------------------------------------------------------ - -class ResourceMetaclass(ABCMeta): - def __init__(cls, class_name, parents, attrs): - """ - Args: - cls: The class type we're registering. - class_name: A String containing the class_name. - parents: The parent class types of 'cls'. - attrs: The attribute (members) of 'cls'. - """ - super().__init__(class_name, parents, attrs) - - # We use the metaclass to create attributes for instance, even before - # the Resource Factory is called. They are needed both for initializing - # attributes and reverse attributes, in whatever order. Only class - # creation allow us to clear _attributes, otherwise, we will just add - # those from the parent, siblings, etc... - cls._sanitize() +##------------------------------------------------------------------------------ +# +#class ResourceMetaclass(ABCMeta, ObjectSpecification): +# def __init__(cls, class_name, parents, attrs): +# """ +# Args: +# cls: The class type we're registering. +# class_name: A String containing the class_name. +# parents: The parent class types of 'cls'. +# attrs: The attribute (members) of 'cls'. +# """ +# super().__init__(class_name, parents, attrs) +# +# # We use the metaclass to create attributes for instance, even before +# # the Resource Factory is called. They are needed both for initializing +# # attributes and reverse attributes, in whatever order. Only class +# # creation allow us to clear _attributes, otherwise, we will just add +# # those from the parent, siblings, etc... +# cls._sanitize() #------------------------------------------------------------------------------ -class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass): +class BaseResource(Object): #, ABC, metaclass=ResourceMetaclass): """Base Resource class The base Resource class implements all the logic related to resource @@ -409,6 +412,12 @@ class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass): if isinstance(value, UUID): value = self.from_uuid(value) + # XXX XXX quick fix + from netmodel.model.type import InetAddress + if issubclass(attribute.type, InetAddress) and value is not None \ + and not isinstance(value, InetAddress) and not isinstance(value, Reference): + value = attribute.type(value) + if set_reverse and attribute.reverse_name: for base in self.__class__.mro(): if not hasattr(base, '_reverse_attributes'): @@ -420,7 +429,7 @@ class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass): value.set(ra.name, self, set_reverse = False) elif ra.multiplicity == Multiplicity.ManyToOne: for element in value: - value.set(ra.name, self, set_reverse = False) + element.set(ra.name, self, set_reverse = False) elif ra.multiplicity == Multiplicity.OneToMany: if value is not None: collection = value.get(ra.name) @@ -445,7 +454,7 @@ class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass): return value def set(self, attribute_name, value, current=False, set_reverse=True, - blocking = True): + blocking = None): value = self._set(attribute_name, value, current=current, set_reverse=set_reverse) if self.is_local_attribute(attribute_name) or current: @@ -455,11 +464,10 @@ class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass): vars(self)[attribute_name] = value else: - fut = self._state.manager.attribute_set(self, attribute_name, value) - asyncio.ensure_future(fut) + self._state.manager.attribute_set(self, attribute_name, value) async def async_set(self, attribute_name, value, current=False, - set_reverse=True, blocking=True): + set_reverse=True, blocking=None): """ Example: - setting the ip address on a node's interface @@ -470,8 +478,7 @@ class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass): """ value = self._set(attribute_name, value, current=current, set_reverse=set_reverse) - await self._state.manager.attribute_set(self, attribute_name, value, - blocking=blocking) + await self._state.manager.attribute_set_async(self, attribute_name, value) def set_many(self, attribute_dict, current=False): if not attribute_dict: @@ -541,101 +548,101 @@ class BaseResource(BaseType, ABC, metaclass=ResourceMetaclass): except AttributeError: return None - @classmethod - def _sanitize(cls): - """ - This methods performs sanitization of the object declaration. - - More specifically: - - it goes over all attributes and sets their name based on the python - object attribute name. - - it establishes mutual object relationships through reverse attributes. - - """ - cls._reverse_attributes = dict() - cur_reverse_attributes = dict() - for name, obj in vars(cls).items(): - if not isinstance(obj, ObjectSpecification): - continue - - # XXX it seems obj should always be an attribute, confirm ! - if isinstance(obj, Attribute): - obj.name = name - - # Remember whether a reverse_name is defined before loading - # inherited properties from parent - has_reverse = bool(obj.reverse_name) - - # Handle overloaded attributes - # By recursion, it is sufficient to look into the parent - for base in cls.__bases__: - if hasattr(base, name): - parent_attribute = getattr(base, name) - obj.merge(parent_attribute) - assert obj.type - - # Handle reverse attribute - # - # NOTE: we need to do this after merging to be sure we get all - # properties inherited from parent (eg. multiplicity) - # - # See "Reverse attributes" section in BaseResource docstring. - # - # Continueing with the same example, let's detail how it is handled: - # - # Original declaration: - # >>> - # class Group(Resource): - # resources = Attribute(Resource, description = 'Resources belonging to the group', - # multiplicity = Multiplicity.ManyToMany, - # default = [], - # reverse_name = 'groups', - # reverse_description = 'Groups to which the resource belongs') - # <<< - # - # Local variables: - # cls = - # obj = - # obj.type = - # reverse_attribute = - # - # Result: - # 1) Group._reverse_attributes = - # { : [, ...], ...} - # 2) Add attribute to class Resource - # 3) Resource._reverse_attributes = - # { : [>> +####### # class Group(Resource): +####### # resources = Attribute(Resource, description = 'Resources belonging to the group', +####### # multiplicity = Multiplicity.ManyToMany, +####### # default = [], +####### # reverse_name = 'groups', +####### # reverse_description = 'Groups to which the resource belongs') +####### # <<< +####### # +####### # Local variables: +####### # cls = +####### # obj = +####### # obj.type = +####### # reverse_attribute = +####### # +####### # Result: +####### # 1) Group._reverse_attributes = +####### # { : [, ...], ...} +####### # 2) Add attribute to class Resource +####### # 3) Resource._reverse_attributes = +####### # { : [ base.__subresources__(resource) - + sr = resource.__subresources__() if sr is not None and not isinstance(sr, EmptyResource): resource.set_subresources(sr) pfx_sr = '{}:{}'.format(sr.get_type(), sr.get_uuid()) @@ -713,19 +760,8 @@ class ResourceManager(metaclass=Singleton): """Perform action: __get__, __create__, __delete__ on the full class hierarchy. """ - task = EmptyTask() - for base in reversed(resource.__class__.mro()): - # To avoid adding several times the same task - if action not in vars(base): - continue - func = getattr(base, action, None) - if func is None: - continue - t = func(resource) - - task = task > t - - return task + method = getattr(resource, action, None) + return method() if method else EmptyTask() #-------------------------------------------------------------------------- # Resource model @@ -788,7 +824,11 @@ class ResourceManager(metaclass=Singleton): ret = fut.result() resource._state.attr_change_success[attribute.name] = True resource._state.attr_change_value[attribute.name] = ret + except ResourceNotFound as e: + resource._state.attr_change_success[attribute.name] = False + resource._state.attr_change_value[attribute.name] = e except Exception as e: + import traceback; traceback.print_exc() resource._state.attr_change_success[attribute.name] = False resource._state.attr_change_value[attribute.name] = e resource._state.attr_change_event[attribute.name].set() @@ -899,7 +939,10 @@ class ResourceManager(metaclass=Singleton): attrs = resource._state.attr_change_value[attribute.name] self.attr_log(resource, attribute, 'INIT success. Value = {}'.format(attrs)) - found = self._process_attr_dict(resource, attribute, attrs) + if not isinstance(attrs, ReturnValue): + found = self._process_attr_dict(resource, attribute, attrs) + else: + found = self._process_attr_dict(resource, attribute, attrs.stdout) if not found: log.error('Attribute missing return attrs: {}'.format( attrs)) @@ -923,7 +966,7 @@ class ResourceManager(metaclass=Singleton): if resource._state.attr_change_success[attribute.name] == True: self.attr_log(resource, attribute, 'UPDATE success. Value = {}. Attribute is CLEAN'.format(attrs)) - if attrs != NEVER_SET: + if not isinstance(attrs, ReturnValue) and attrs != NEVER_SET: # None could be interpreted as the return value. Also, # we need not to overwrite the value from get self._process_attr_dict(resource, attribute, attrs) @@ -931,7 +974,7 @@ class ResourceManager(metaclass=Singleton): # We might do this for all returned attributes cur_value = vars(resource)[attribute.name] if attribute.is_collection: - tmp = InstrumentedList(pending_value.value) + tmp = Collection(pending_value.value) tmp._attribute = cur_value._attribute tmp._instance = cur_value._instance else: @@ -974,8 +1017,25 @@ class ResourceManager(metaclass=Singleton): return Query.from_dict(dic) + def _monitor_qtplayer(self, resource): + try: + ip = resource.node.hostname + except: + ip = str(resource.node.management_interface.ip4_address) + + hook = functools.partial(self._on_qtplayer_packet, resource.node.name) + ws = self._router.add_interface('websocketclient', address=ip, + port = DEFAULT_QTPLAYER_PORT, + hook = hook) + q_str = 'SUBSCRIBE * FROM stats' + q = self.parse_query(q_str) + packet = Packet.from_query(q) + self._router._flow_table.add(packet, None, set([ws])) + ws.send(packet) + def _monitor_netmon(self, resource): - ip = resource.node.management_interface.ip4_address + print("MONITOR NODE", resource.node) + ip = str(resource.node.management_interface.ip4_address) if not ip: log.error('IP of monitored Node {} is None'.format(resource.node)) import os; os._exit(1) @@ -986,49 +1046,146 @@ class ResourceManager(metaclass=Singleton): node = resource.node for interface in node.interfaces: if not interface.monitored: + print("non monitored interface", interface) continue + print("NETMON MONITOR INTERFACE", interface) + +# if interface.get_type() == 'dpdkdevice' and hasattr(node,'vpp'): +# +# # Check if vICN has already subscribed for one interface in +# # the channel +# if hasattr(interface.channel,'already_subscribed'): +# continue +# +# channel_id = interface.channel._state.uuid._uuid +# +# update_vpp = functools.partial(self._on_vpp_record, +# pylink_id = channel_id) +# ws_vpp = self._router.add_interface('websocketclient', +# address=ip, hook=update_vpp) +# +# aggregate_interfaces = list() +# for _interface in node.interfaces: +# if not _interface.get_type() == 'dpdkdevice' and \ +# _interface.monitored: +# aggregate_interfaces.append('"' + +# _interface.device_name + '"') +# +# q_str = Q_SUB_VPP.format(','.join(aggregate_interfaces)) +# q = self.parse_query(q_str) +# packet = Packet.from_query(q) +# self._router._flow_table.add(packet, None, ws_vpp) +# ws_vpp.send(packet) +# +# # Prevent vICN to subscribe to other interfaces of the same +# # channel +# interface.channel.already_subscribed = True +# +# else: + if hasattr(node, 'vpp') and node.vpp is not None: + q_str = Q_SUB_VPP_IF.format(interface.vppinterface.device_name) + else: + q_str = Q_SUB_IF.format(interface.device_name) + log.warning(" -- MONITOR {}".format(q_str)) + q = self.parse_query(q_str) + packet = Packet.from_query(q) + self._router._flow_table.add(packet, None, set([ws])) + ws.send(packet) - if interface.get_type() == 'dpdkdevice' and hasattr(node,'vpp'): + def _monitor_vpp_interface(self, vpp_interface): + print("MONITOR interface", vpp_interface) + interface = vpp_interface.parent + node = interface.node + # XXX only monitor in the topology group + if node.get_type() != 'lxccontainer': + print("MONITOR -> Ignored: not in container") + return - # Check if vICN has already subscribed for one interface in - # the channel - if hasattr(interface.channel,'already_subscribed'): - continue + # We only monitor interfaces to provide data for wired channels + channel = interface.channel + if channel is None: + print("MONITOR -> Ignored: no channel") + return + if channel.has_type('emulatedchannel'): + print("MONITOR -> Ignored: belong to wireless channel") + return - channel_id = interface.channel._state.uuid._uuid + # Don't monitor multiple interfaces per channel + if channel in self._monitored_channels: + print("MONITOR -> Ignored: channel already monitored") + return + self._monitored_channels.add(channel) - update_vpp = functools.partial(self._on_vpp_record, - pylink_id = channel_id) - ws_vpp = self._router.add_interface('websocketclient', - address=ip, hook=update_vpp) + ip = str(node.management_interface.ip4_address) + if not ip: + log.error('IP of monitored Node {} is None'.format(resource.node)) + import os; os._exit(1) - aggregate_interfaces = list() - for _interface in node.interfaces: - if not _interface.get_type() == 'dpdkdevice' and \ - _interface.monitored: - aggregate_interfaces.append('"' + - _interface.device_name + '"') + # Reuse existing websockets + ws = self._map_ip_interface.get(ip) + if not ws: + ws = self._router.add_interface('websocketclient', address=ip, + hook=self._on_netmon_record) + self._map_ip_interface[ip] = ws + + q_str = Q_SUB_VPP_IF.format(vpp_interface.device_name) + print("MONITOR -> query= {}".format(q_str)) + q = self.parse_query(q_str) + packet = Packet.from_query(q) + self._router._flow_table.add(packet, None, set([ws])) + ws.send(packet) + + def _monitor_interface(self, interface): + print("MONITOR interface", interface) + node = interface.node + # XXX only monitor in the topology group + if node.get_type() != 'lxccontainer': + print("MONITOR -> Ignored: not in container") + return - q_str = Q_SUB_VPP.format(','.join(aggregate_interfaces)) - q = self.parse_query(q_str) - packet = Packet.from_query(q) - self._router._flow_table.add(packet, None, ws_vpp) - ws_vpp.send(packet) + # Only monitor vpp interfaces on vpp node + if hasattr(node, 'vpp') and node.vpp is not None: + print("MONITOR -> Ignored: non-vpp interface on vpp node") + return - # Prevent vICN to subscribe to other interfaces of the same - # channel - interface.channel.already_subscribed = True + # We only monitor interfaces to provide data for wired channels + channel = interface.channel + if channel is None: + print("MONITOR -> Ignored: no channel") + return + if channel.has_type('emulatedchannel'): + print("MONITOR -> Ignored: belong to wireless channel") + return - else: - q_str = Q_SUB_IF.format(interface.device_name) - q = self.parse_query(q_str) - packet = Packet.from_query(q) - self._router._flow_table.add(packet, None, ws) - ws.send(packet) + # Don't monitor multiple interfaces per channel + if channel in self._monitored_channels: + print("MONITOR -> Ignored: channel already monitored") + return + self._monitored_channels.add(channel) + + ip = str(node.management_interface.ip4_address) + if not ip: + log.error('IP of monitored Node {} is None'.format(resource.node)) + import os; os._exit(1) + + # Reuse existing websockets + ws = self._map_ip_interface.get(ip) + if not ws: + ws = self._router.add_interface('websocketclient', address=ip, + hook=self._on_netmon_record) + self._map_ip_interface[ip] = ws + + q_str = Q_SUB_IF.format(interface.device_name) + print("MONITOR -> query= {}".format(q_str)) + q = self.parse_query(q_str) + packet = Packet.from_query(q) + self._router._flow_table.add(packet, None, set([ws])) + ws.send(packet) def _monitor_emulator(self, resource): ns = resource - ip = ns.node.bridge.ip4_address # management_interface.ip_address + # XXX UGLY, we have no management interface + ip = ns.node.hostname # str(ns.node.interfaces[0].ip4_address) ws_ns = self._router.add_interface('websocketclient', address = ip, port = ns.control_port, @@ -1050,7 +1207,7 @@ class ResourceManager(metaclass=Singleton): q_str = Q_SUB_EMULATOR_IF.format(identifier) q = self.parse_query(q_str) packet = Packet.from_query(q) - self._router._flow_table.add(packet, None, ws_ns) + self._router._flow_table.add(packet, None, set([ws_ns])) ws_ns.send(packet) # We also need to subscribe on the node for the tap interfaces @@ -1059,7 +1216,7 @@ class ResourceManager(metaclass=Singleton): q_str = Q_SUB_EMULATOR.format(tap.device_name) q = self.parse_query(q_str) packet = Packet.from_query(q) - self._router._flow_table.add(packet, None, ws) + self._router._flow_table.add(packet, None, set([ws])) ws.send(packet) def _monitor(self, resource): @@ -1070,29 +1227,37 @@ class ResourceManager(metaclass=Singleton): self._pending_monitoring.clear() return - central_ip = self.by_type_str('centralip') - if not central_ip: - raise NotImplementedError('Missing CentralIP in experiment') - central_ip = central_ip[0] - uuid = resource.get_uuid() - if central_ip._state.state != ResourceState.CLEAN: - self._pending_monitoring.add(uuid) - return + central_ip = self.by_type_str('centralip') + if central_ip: + central_ip = central_ip[0] + + if central_ip._state.state != ResourceState.CLEAN: + self._pending_monitoring.add(uuid) + return if uuid in self._monitored: return self._monitored.add(uuid) - if resource.get_type() == 'netmon': - if resource.node.get_type() != 'lxccontainer': - return - self._monitor_netmon(resource) +# if resource.get_type() == 'netmon': +# if resource.node.get_type() != 'lxccontainer': +# return +# self._monitor_netmon(resource) + + if resource.get_type() == 'qtplayer': + self._monitor_qtplayer(resource) elif resource.has_type('emulatedchannel'): self._monitor_emulator(resource) + elif resource.has_type('interface'): + self._monitor_interface(resource) + + elif resource.has_type('vppinterface'): + self._monitor_vpp_interface(resource) + async def __set_resource_state(self, resource, state): """Sets the resource state (no-lock version) @@ -1128,6 +1293,9 @@ class ResourceManager(metaclass=Singleton): ret = fut.result() resource._state.change_success = True resource._state.change_value = ret + except ResourceNotFound as e: + resource._state.change_success = False + resource._state.change_value = e except Exception as e: resource._state.change_success = False resource._state.change_value = e @@ -1148,7 +1316,7 @@ class ResourceManager(metaclass=Singleton): for attr in resource.iter_attributes(): if resource.is_local_attribute(attr.name): continue - if attr.key: + if resource.has_key_attribute(attr): # Those attributes are already done continue @@ -1182,12 +1350,14 @@ class ResourceManager(metaclass=Singleton): # Monitor all FSM one by one and inform about errors. futs = list() attrs = list() - for attr in resource.get_keys(): - if resource.is_local_attribute(attr.name): - continue - attrs.append(attr) - fut = self.attribute_process(resource, attr) - futs.append(fut) + + for key in resource.get_keys(): + for attr in key: + if resource.is_local_attribute(attr.name): + continue + attrs.append(attr) + fut = self.attribute_process(resource, attr) + futs.append(fut) if not futs: self.log(resource, 'No key attribute to update') @@ -1277,6 +1447,7 @@ class ResourceManager(metaclass=Singleton): print("------") import traceback; traceback.print_tb(e.__traceback__) log.error('Resource: {} - Exception: {}'.format(pfx, e)) + return import os; os._exit(1) elif state == ResourceState.UNINITIALIZED: pending_state = ResourceState.PENDING_DEPS @@ -1398,8 +1569,9 @@ class ResourceManager(metaclass=Singleton): elif pending_state == ResourceState.PENDING_GET: if resource._state.change_success == True: attrs = resource._state.change_value - self.log(resource, S_INIT_DONE.format(attrs)) - self._process_attr_dict(resource, None, attrs) + if not isinstance(attrs, ReturnValue): + self.log(resource, S_INIT_DONE.format(attrs)) + self._process_attr_dict(resource, None, attrs) new_state = ResourceState.CREATED else: e = resource._state.change_value @@ -1452,8 +1624,9 @@ class ResourceManager(metaclass=Singleton): elif pending_state == ResourceState.PENDING_CREATE: if resource._state.change_success == True: attrs = resource._state.change_value - self.log(resource, S_CREATE_OK.format(attrs)) - self._process_attr_dict(resource, None, attrs) + if not isinstance(attrs, ReturnValue): + self.log(resource, S_CREATE_OK.format(attrs)) + self._process_attr_dict(resource, None, attrs) new_state = ResourceState.CREATED else: e = resource._state.change_value diff --git a/vicn/core/sa_collections.py b/vicn/core/sa_collections.py deleted file mode 100644 index a4a24f85..00000000 --- a/vicn/core/sa_collections.py +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# This module is derived from code from SQLAlchemy -# -# orm/collections.py -# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -# - -import logging - -from vicn.core.exception import VICNListException -from vicn.core.sa_compat import py2k -from vicn.core.state import UUID - -log = logging.getLogger(__name__) - -def _list_decorators(): - """Tailored instrumentation wrappers for any list-like class.""" - - def _tidy(fn): - fn._sa_instrumented = True - fn.__doc__ = getattr(list, fn.__name__).__doc__ - - def append(fn): - def append(self, item): - try: - item = self._attribute.do_list_add(self._instance, item) - fn(self, item) - except VICNListException as e: - pass - _tidy(append) - return append - - def remove(fn): - def remove(self, value): - # testlib.pragma exempt:__eq__ - try: - self._attribute.do_list_remove(self._instance, value) - fn(self, value) - except : pass - _tidy(remove) - return remove - - def insert(fn): - def insert(self, index, value): - try: - value = self._attribute.do_list_add(self._instance, item) - fn(self, index, value) - except : pass - _tidy(insert) - return insert - - def __getitem__(fn): - def __getitem__(self, index): - item = fn(self, index) - return self._attribute.handle_getitem(self._instance, item) - _tidy(__getitem__) - return __getitem__ - - def __setitem__(fn): - def __setitem__(self, index, value): - if not isinstance(index, slice): - existing = self[index] - if existing is not None: - try: - self._attribute.do_list_remove(self._instance, existing) - except: pass - try: - value = self._attribute.do_list_add(self._instance, value) - fn(self, index, value) - except: pass - else: - # slice assignment requires __delitem__, insert, __len__ - step = index.step or 1 - start = index.start or 0 - if start < 0: - start += len(self) - if index.stop is not None: - stop = index.stop - else: - stop = len(self) - if stop < 0: - stop += len(self) - - if step == 1: - for i in range(start, stop, step): - if len(self) > start: - del self[start] - - for i, item in enumerate(value): - self.insert(i + start, item) - else: - rng = list(range(start, stop, step)) - if len(value) != len(rng): - raise ValueError( - "attempt to assign sequence of size %s to " - "extended slice of size %s" % (len(value), - len(rng))) - for i, item in zip(rng, value): - self.__setitem__(i, item) - _tidy(__setitem__) - return __setitem__ - - def __delitem__(fn): - def __delitem__(self, index): - if not isinstance(index, slice): - item = self[index] - try: - self._attribute.do_list_remove(self._instance, item) - fn(self, index) - except : pass - else: - # slice deletion requires __getslice__ and a slice-groking - # __getitem__ for stepped deletion - # note: not breaking this into atomic dels - has_except = False - for item in self[index]: - try: - self._attribute.do_list_remove(self._instance, item) - except : has_except = True - if not has_except: - fn(self, index) - _tidy(__delitem__) - return __delitem__ - - if py2k: - def __setslice__(fn): - def __setslice__(self, start, end, values): - has_except = False - for value in self[start:end]: - try: - self._attribute.do_list_remove(self._instance, value) - except : has_except = True - #values = [self._attribute.do_list_add(self._instance, value) for value in values] - _values = list() - for value in values: - try: - _values.append(self._attribute.do_list_add(self._instance, value)) - except: has_except = True - if not has_except: - fn(self, start, end, _values) - _tidy(__setslice__) - return __setslice__ - - def __delslice__(fn): - def __delslice__(self, start, end): - has_except = False - for value in self[start:end]: - try: - self._attribute.do_list_remove(self._instance, value) - except : has_except = True - if not has_except: - fn(self, start, end) - _tidy(__delslice__) - return __delslice__ - - def extend(fn): - def extend(self, iterable): - for value in iterable: - self.append(value) - _tidy(extend) - return extend - - def __iadd__(fn): - def __iadd__(self, iterable): - # list.__iadd__ takes any iterable and seems to let TypeError - # raise as-is instead of returning NotImplemented - for value in iterable: - self.append(value) - return self - _tidy(__iadd__) - return __iadd__ - - def pop(fn): - def pop(self, index=-1): - try: - self._attribute.do_list_remove(self._instance, item) - item = fn(self, index) - return item - except : return None - _tidy(pop) - return pop - - def __iter__(fn): - def __iter__(self): - for item in fn(self): - yield self._attribute.handle_getitem(self._instance, item) - _tidy(__iter__) - return __iter__ - - def __repr__(fn): - def __repr__(self): - return ''.format(id(self), list.__repr__(self)) - _tidy(__repr__) - return __repr__ - - __str__ = __repr__ - #def __str__(fn): - # def __str__(self): - # return str(list(self)) - # _tidy(__str__) - # return __str__ - - if not py2k: - def clear(fn): - def clear(self, index=-1): - has_except = False - for item in self: - try: - self._attribute.do_list_remove(self._instance, item) - except : has_except = True - if not has_except: - fn(self) - _tidy(clear) - return clear - - # __imul__ : not wrapping this. all members of the collection are already - # present, so no need to fire appends... wrapping it with an explicit - # decorator is still possible, so events on *= can be had if they're - # desired. hard to imagine a use case for __imul__, though. - - l = locals().copy() - l.pop('_tidy') - return l - -def _instrument_list(cls): - # inspired by sqlalchemy - for method, decorator in _list_decorators().items(): - fn = getattr(cls, method, None) - if fn: - #if (fn and method not in methods and - # not hasattr(fn, '_sa_instrumented')): - setattr(cls, method, decorator(fn)) - -class InstrumentedList(list): - - @classmethod - def from_list(cls, value, instance, attribute): - lst = list() - if value: - for x in value: - if isinstance(x, UUID): - x = instance.from_uuid(x) - lst.append(x) - # Having a class method is important for inheritance - value = cls(lst) - value._attribute = attribute - value._instance = instance - return value - - def __contains__(self, key): - from vicn.core.resource import Resource - if isinstance(key, Resource): - key = key.get_uuid() - return list.__contains__(self, key) - - def __lshift__(self, item): - self.append(item) - -_instrument_list(InstrumentedList) diff --git a/vicn/core/sa_compat.py b/vicn/core/sa_compat.py deleted file mode 100644 index 34211455..00000000 --- a/vicn/core/sa_compat.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# This module originates from SQLAlchemy -# -# util/compat.py -# Copyright (C) 2005-2016 the SQLAlchemy authors and contributors -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -# - -"""Handle Python version/platform incompatibilities.""" - -import sys - -try: - import threading -except ImportError: - import dummy_threading as threading - -py36 = sys.version_info >= (3, 6) -py33 = sys.version_info >= (3, 3) -py32 = sys.version_info >= (3, 2) -py3k = sys.version_info >= (3, 0) -py2k = sys.version_info < (3, 0) -py265 = sys.version_info >= (2, 6, 5) -jython = sys.platform.startswith('java') -pypy = hasattr(sys, 'pypy_version_info') -win32 = sys.platform.startswith('win') -cpython = not pypy and not jython # TODO: something better for this ? - -import collections -next = next - -if py3k: - import pickle -else: - try: - import cPickle as pickle - except ImportError: - import pickle - -# work around http://bugs.python.org/issue2646 -if py265: - safe_kwarg = lambda arg: arg -else: - safe_kwarg = str - -ArgSpec = collections.namedtuple("ArgSpec", - ["args", "varargs", "keywords", "defaults"]) - -if py3k: - import builtins - - from inspect import getfullargspec as inspect_getfullargspec - from urllib.parse import (quote_plus, unquote_plus, - parse_qsl, quote, unquote) - import configparser - from io import StringIO - - from io import BytesIO as byte_buffer - - def inspect_getargspec(func): - return ArgSpec( - *inspect_getfullargspec(func)[0:4] - ) - - string_types = str, - binary_types = bytes, - binary_type = bytes - text_type = str - int_types = int, - iterbytes = iter - - def u(s): - return s - - def ue(s): - return s - - def b(s): - return s.encode("latin-1") - - if py32: - callable = callable - else: - def callable(fn): - return hasattr(fn, '__call__') - - def cmp(a, b): - return (a > b) - (a < b) - - from functools import reduce - - print_ = getattr(builtins, "print") - - import_ = getattr(builtins, '__import__') - - import itertools - itertools_filterfalse = itertools.filterfalse - itertools_filter = filter - itertools_imap = map - from itertools import zip_longest - - import base64 - - def b64encode(x): - return base64.b64encode(x).decode('ascii') - - def b64decode(x): - return base64.b64decode(x.encode('ascii')) - -else: - from inspect import getargspec as inspect_getfullargspec - inspect_getargspec = inspect_getfullargspec - from urllib import quote_plus, unquote_plus, quote, unquote - from urlparse import parse_qsl - import ConfigParser as configparser - from StringIO import StringIO - from cStringIO import StringIO as byte_buffer - - string_types = basestring, - binary_types = bytes, - binary_type = str - text_type = unicode - int_types = int, long - - def iterbytes(buf): - return (ord(byte) for byte in buf) - - def u(s): - # this differs from what six does, which doesn't support non-ASCII - # strings - we only use u() with - # literal source strings, and all our source files with non-ascii - # in them (all are tests) are utf-8 encoded. - return unicode(s, "utf-8") - - def ue(s): - return unicode(s, "unicode_escape") - - def b(s): - return s - - def import_(*args): - if len(args) == 4: - args = args[0:3] + ([str(arg) for arg in args[3]],) - return __import__(*args) - - callable = callable - cmp = cmp - reduce = reduce - - import base64 - b64encode = base64.b64encode - b64decode = base64.b64decode - - def print_(*args, **kwargs): - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - for arg in enumerate(args): - if not isinstance(arg, basestring): - arg = str(arg) - fp.write(arg) - - import itertools - itertools_filterfalse = itertools.ifilterfalse - itertools_filter = itertools.ifilter - itertools_imap = itertools.imap - from itertools import izip_longest as zip_longest - - -import time -if win32 or jython: - time_func = time.clock -else: - time_func = time.time - -from collections import namedtuple -from operator import attrgetter as dottedgetter - - -if py3k: - def reraise(tp, value, tb=None, cause=None): - if cause is not None: - assert cause is not value, "Same cause emitted" - value.__cause__ = cause - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - -else: - # not as nice as that of Py3K, but at least preserves - # the code line where the issue occurred - exec("def reraise(tp, value, tb=None, cause=None):\n" - " if cause is not None:\n" - " assert cause is not value, 'Same cause emitted'\n" - " raise tp, value, tb\n") - - -def raise_from_cause(exception, exc_info=None): - if exc_info is None: - exc_info = sys.exc_info() - exc_type, exc_value, exc_tb = exc_info - cause = exc_value if exc_value is not exception else None - reraise(type(exception), exception, tb=exc_tb, cause=cause) - -if py3k: - exec_ = getattr(builtins, 'exec') -else: - def exec_(func_text, globals_, lcl=None): - if lcl is None: - exec('exec func_text in globals_') - else: - exec('exec func_text in globals_, lcl') - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass. - - Drops the middle class upon creation. - - Source: http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/ - - """ - - class metaclass(meta): - __call__ = type.__call__ - __init__ = type.__init__ - - def __new__(cls, name, this_bases, d): - if this_bases is None: - return type.__new__(cls, name, (), d) - return meta(name, bases, d) - return metaclass('temporary_class', None, {}) - - -from contextlib import contextmanager - -try: - from contextlib import nested -except ImportError: - # removed in py3k, credit to mitsuhiko for - # workaround - - @contextmanager - def nested(*managers): - exits = [] - vars = [] - exc = (None, None, None) - try: - for mgr in managers: - exit = mgr.__exit__ - enter = mgr.__enter__ - vars.append(enter()) - exits.append(exit) - yield vars - except: - exc = sys.exc_info() - finally: - while exits: - exit = exits.pop() - try: - if exit(*exc): - exc = (None, None, None) - except: - exc = sys.exc_info() - if exc != (None, None, None): - reraise(exc[0], exc[1], exc[2]) diff --git a/vicn/core/state.py b/vicn/core/state.py index a116ba82..aa68341e 100644 --- a/vicn/core/state.py +++ b/vicn/core/state.py @@ -17,17 +17,9 @@ # import asyncio -import random -import string -class NEVER_SET: - pass - -# Separator for components of the UUID -UUID_SEP = '-' - -# Length of the random component of the UUID -UUID_LEN = 5 +from netmodel.model.uuid import UUID +from vicn.core.attribute import NEVER_SET class ResourceState: UNINITIALIZED = 'UNINITIALIZED' @@ -64,31 +56,6 @@ class Operations: LIST_REMOVE = 'remove' LIST_CLEAR = 'clear' -class UUID: - def __init__(self, name, cls): - self._uuid = self._make_uuid(name, cls) - - def _make_uuid(self, name, cls): - """Generate a unique resource identifier - - The UUID consists in the type of the resource, to which is added a - random identifier of length UUID_LEN. Components of the UUID are - separated by UUID_SEP. - """ - uuid = ''.join(random.choice(string.ascii_uppercase + string.digits) - for _ in range(UUID_LEN)) - if name: - uuid = name # + UUID_SEP + uuid - return UUID_SEP.join([cls.__name__, uuid]) - - def __repr__(self): - return ''.format(self._uuid) - - def __lt__(self, other): - return self._uuid < other._uuid - - __str__ = __repr__ - class PendingValue: def __init__(self, value = None): self.clear(value) diff --git a/vicn/core/task.py b/vicn/core/task.py index 49c34b1f..72c80716 100644 --- a/vicn/core/task.py +++ b/vicn/core/task.py @@ -24,11 +24,12 @@ import shlex import subprocess import os -from vicn.core.scheduling_algebra import SchedulingAlgebra +from netmodel.util.process import execute_local + from vicn.core.commands import ReturnValue -from vicn.core.exception import ResourceNotFound from vicn.core.commands import Command, SequentialCommands -from netmodel.util.process import execute_local +from vicn.core.exception import ResourceNotFound +from vicn.core.scheduling_algebra import SchedulingAlgebra log = logging.getLogger(__name__) @@ -150,6 +151,86 @@ async def wait_concurrent_tasks(tasks): wait_task_task = async_task(wait_task) +#------------------------------------------------------------------------------ +# Task inheritance +#------------------------------------------------------------------------------ + +# NOTES: +# - delete is a special case where we have to reverse operations +# - subresources is also a special case, since it deals with resources, but it +# works the same since the algebra is similar + +def inherit_parent(fn): + def f(self, *args, **kwargs): + # Break loops + if fn.__name__ not in vars(self.__class__): + return fn(self, *args, **kwargs) + + parent_tasks = EmptyTask() + for parent in self.__class__.__bases__: + if not fn.__name__ in vars(parent): + continue + for cls in self.__class__.__mro__: + if cls.__dict__.get(fn.__name__) is fn: + break + + parent_task = getattr(super(cls, self), fn.__name__, None) + if not parent_task: + continue + parent_tasks = parent_tasks | parent_task(self) + if fn.__name__ == 'delete': + return fn(*args, **kwargs) > parent_tasks + else: + return parent_tasks > fn(self, *args, **kwargs) + return f + +def override_parent(fn): + def f(self, *args, **kwargs): + # Break loops + if fn.__name__ not in vars(self.__class__): + return fn(self, *args, **kwargs) + + bases = set([ancestor for parent in self.__class__.__bases__ + for ancestor in parent.__bases__]) + + ancestor_tasks = EmptyTask() + for base in bases: + if not fn.__name__ in vars(base): + continue + ancestor_task = getattr(base, fn.__name__, None) + if not ancestor_task: + continue + ancestor_tasks = ancestor_tasks | ancestor_task(self) + if fn.__name__ == 'delete': + return fn(*args, **kwargs) > ancestor_tasks + else: + return ancestor_tasks > fn(self, *args, **kwargs) + return f + +def inherit(*classes): + def decorator(fn): + def f(self, *args, **kwargs): + # Break loops + if fn.__name__ not in vars(self.__class__): + return fn(self, *args, **kwargs) + + parent_tasks = EmptyTask() + for parent in classes: + if not fn.__name__ in vars(parent): + continue + parent_task = getattr(parent, fn.__name__, None) + if not parent_task: + continue + parent_tasks = parent_tasks | parent_task(self) + if fn.__name__ == 'delete': + return fn(*args, **kwargs) > parent_tasks + else: + return parent_tasks > fn(self, *args, **kwargs) + return f + return decorator + +#------------------------------------------------------------------------------ + def get_attribute_task(resource, attribute_name): @async_task async def func(): @@ -308,7 +389,10 @@ class BashTask(Task): def get_full_cmd(self): c = SequentialCommands() desc = None - for line in self._cmd.splitlines(): + cmd = self._cmd + if callable(cmd): + cmd = cmd() + for line in cmd.splitlines(): line = line.strip() if not line: continue @@ -341,10 +425,10 @@ class BashTask(Task): # executed, so that any object passed as parameters is deferenced right # on time. cmd = self.get_full_cmd() - partial = functools.partial(func, cmd, output = bool(self._parse)) + partial = functools.partial(func, cmd, output = self._output or bool(self._parse)) node_str = self._node.name if self._node else '(LOCAL)' - cmd_str = cmd[:77] + '...' if len(cmd) > 80 else cmd + cmd_str = cmd#[:77] + '...' if len(cmd) > 80 else cmd log.info('Execute: {} - {}'.format(node_str, cmd_str)) if self._lock: diff --git a/vicn/resource/application.py b/vicn/resource/application.py index 0f245496..b5502f39 100644 --- a/vicn/resource/application.py +++ b/vicn/resource/application.py @@ -16,9 +16,10 @@ # limitations under the License. # -from vicn.core.attribute import Attribute, Multiplicity -from vicn.core.resource import Resource -from vicn.resource.node import Node +from netmodel.model.key import Key +from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.resource import Resource +from vicn.resource.node import Node class Application(Resource): node = Attribute(Node, @@ -26,5 +27,5 @@ class Application(Resource): mandatory = True, multiplicity = Multiplicity.ManyToOne, reverse_name = 'applications', - key = True, reverse_description = 'Applications installed on node') + __key__ = Key(node) diff --git a/vicn/resource/central.py b/vicn/resource/central.py index bf1c8f7a..d1ef267a 100644 --- a/vicn/resource/central.py +++ b/vicn/resource/central.py @@ -20,43 +20,12 @@ import logging import networkx as nx import os -from netmodel.model.type import String, Integer from netmodel.util.misc import pairwise from vicn.core.attribute import Attribute, Reference -from vicn.core.exception import ResourceNotFound -from vicn.core.resource import Resource -from vicn.core.task import async_task, inline_task -from vicn.core.task import EmptyTask, BashTask from vicn.resource.channel import Channel -from vicn.resource.ip.route import IPRoute -from vicn.resource.group import Group -from vicn.resource.icn.forwarder import Forwarder -from vicn.resource.icn.face import L2Face, L4Face, FaceProtocol -from vicn.resource.icn.producer import Producer -from vicn.resource.icn.route import Route as ICNRoute -from vicn.resource.lxd.lxc_container import LxcContainer -from vicn.resource.node import Node -from vicn.resource.ip_assignment import Ipv4Assignment, Ipv6Assignment log = logging.getLogger(__name__) -TMP_DEFAULT_PORT = 6363 - -CMD_CONTAINER_SET_DNS = 'echo "nameserver {ip_dns}" | ' \ - 'resolvconf -a {interface_name}' - -# For host -CMD_NAT = '\n'.join([ - 'iptables -t nat -A POSTROUTING -o {interface_name} -s {network} ' \ - '! -d {network} -j MASQUERADE', - 'echo 1 > /proc/sys/net/ipv4/ip_forward' -]) - -# For containers -CMD_IP_FORWARD = 'echo 1 > /proc/sys/net/ipv4/ip_forward' - -HOST_FILE = "hosts.vicn" - #------------------------------------------------------------------------------ # Routing strategies #------------------------------------------------------------------------------ @@ -84,6 +53,8 @@ def routing_strategy_spt(G, origins, weight_key = None): origin_nodes = origins.keys() seen = set() for dst_node in origin_nodes: + if not G.has_node(dst_node): + continue sssp = nx.shortest_path(G, target = dst_node) # Notes from the documentation: # - If only the target is specified, return a dictionary keyed by @@ -122,9 +93,13 @@ def routing_strategy_max_flow(G, origins, weight_key = 'capacity'): origin_nodes = origins.keys() for dst_node in origin_nodes: + if not G.has_node(dst_node): + continue for src_node in G.nodes: if src_node == dst_node: continue + if not G.has_node(src_node): + continue _, flow_dict = nx.maximum_flow(G, src_node, dst_node, capacity=weight_key) @@ -149,17 +124,24 @@ MAP_ROUTING_STRATEGY = { # L2 and L4/ICN graphs #------------------------------------------------------------------------------ -def _get_l2_graph(groups, with_managed = False): +def _get_l2_graph(groups): + """ + We iterate on all the channels that belong to the same groups as the + resources. + + NOTE: We have to make sure the nodes also belong to the group. + """ G = nx.Graph() -# for node in manager.by_type(Node): -# G.add_node(node._state.uuid) for group in groups: for channel in group.iter_by_type_str('channel'): if channel.has_type('emulatedchannel'): src = channel._ap_if + # XXX bug in reverse collections, resources and not UUIDs seem to be stored inside + if group.name not in [x.name for x in src.node.groups]: + continue for dst in channel._sta_ifs.values(): - if not with_managed and (not src.managed or not dst.managed): + if group.name not in [x.name for x in dst.node.groups]: continue if G.has_edge(src.node._state.uuid, dst.node._state.uuid): continue @@ -172,15 +154,16 @@ def _get_l2_graph(groups, with_managed = False): # This is for a normal Channel for src_it in range(0, len(channel.interfaces)): src = channel.interfaces[src_it] + if group.name not in [x.name for x in src.node.groups]: + continue # Iterate over the remaining interface to create all the # possible combination for dst_it in range(src_it+1,len(channel.interfaces)): dst = channel.interfaces[dst_it] - - if not with_managed and (not src.managed or - not dst.managed): + if group.name not in [x.name for x in dst.node.groups]: continue + if G.has_edge(src.node._state.uuid, dst.node._state.uuid): continue map_node_interface = { @@ -189,479 +172,3 @@ def _get_l2_graph(groups, with_managed = False): G.add_edge(src.node._state.uuid, dst.node._state.uuid, map_node_interface = map_node_interface) return G - -def _get_icn_graph(manager, groups): - G = nx.Graph() - for group in groups: - # It's safer to iterate on node which we know are in the right groups, - # while it might not be the case for the forwarders... - for node in group.iter_by_type_str('node'): - G.add_node(node._state.uuid) - for face in node.forwarder.faces: - other_face = manager.by_uuid(face._internal_data['sibling_face']) - other_node = other_face.node - if G.has_edge(node._state.uuid, other_node._state.uuid): - continue - map_node_face = { node._state.uuid: face._state.uuid, - other_node._state.uuid: other_face._state.uuid } - G.add_edge(node._state.uuid, other_node._state.uuid, - map_node_face = map_node_face) - - return G - -#------------------------------------------------------------------------------- - -class IPRoutes(Resource): - """ - Resource: IPRoutes - - Centralized IP route computation. - """ - routing_strategy = Attribute(String) - - def __after__(self): - return ("IpAssignment",) - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - - @inline_task - def __get__(self): - raise ResourceNotFound - - @inline_task - def __create__(self): - routes = list() - pre_routes, routes = self._get_ip_routes() - routes.extend(pre_routes) - routes.extend(routes) - for route in routes: - route.node.routing_table.routes << route - - def __delete__(self): - raise NotImplementedError - - #-------------------------------------------------------------------------- - # Internal methods - #-------------------------------------------------------------------------- - - def _get_ip_origins(self): - origins = dict() - for group in self.groups: - for channel in group.iter_by_type_str('channel'): - for interface in channel.interfaces: - node_uuid = interface.node._state.uuid - if not node_uuid in origins: - origins[node_uuid] = list() - ip4 = interface.ip4_address - origins[node_uuid].append(interface.ip4_address) - if interface.ip6_address: - ip6 = interface.ip6_address - origins[node_uuid].append(interface.ip6_address) - return origins - - def _get_ip_routes(self): - if self.routing_strategy == 'pair': - return [], self._get_pair_ip_routes() - - strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy) - - G = _get_l2_graph(self.groups) - origins = self._get_ip_origins() - - # node -> list(origins for which we have routes) - ip_routes = dict() - - pre_routes = list() - routes = list() - for src, prefix, dst in strategy(G, origins): - data = G.get_edge_data(src, dst) - - map_ = data['map_node_interface'] - next_hop_interface = map_[src] - - - next_hop_ingress = self._state.manager.by_uuid(map_[dst]) - src_node = self._state.manager.by_uuid(src) - - mac_addr = None - if ((hasattr(next_hop_ingress, 'vpp') and - next_hop_ingress.vpp is not None) or - (hasattr(src_node, 'vpp') and src_node.vpp is not None)): - mac_addr = next_hop_ingress.mac_address - - # Avoid duplicate routes due to multiple paths in the network - if not src_node in ip_routes: - ip_routes[src_node] = list() - if prefix in ip_routes[src_node]: - continue - - #FIXME: should test for IP format - ip_version = 6 if ":" in prefix else 4 - next_hop_ingress_ip = (next_hop_ingress.ip6_address if ip_version is 6 else - next_hop_ingress.ip4_address) - if prefix == next_hop_ingress_ip: - # Direct route on src_node.name : - # route add [prefix] dev [next_hop_interface_.device_name] - route = IPRoute(node = src_node, - managed = False, - owner = self, - ip_address = prefix, - mac_address = mac_addr, - ip_version = ip_version, - interface = next_hop_interface) - else: - # We need to be sure we have a route to the gw from the node - if not next_hop_ingress_ip in ip_routes[src_node]: - pre_route = IPRoute(node = src_node, - managed = False, - owner = self, - ip_address = next_hop_ingress_ip, - ip_version = ip_version, - mac_address = mac_addr, - interface = next_hop_interface) - ip_routes[src_node].append(next_hop_ingress_ip) - pre_routes.append(pre_route) - - # Route on src_node.name: - # route add [prefix] dev [next_hop_interface_.device_name] - # via [next_hop_ingress.ip_address] - route = IPRoute(node = src_node, - managed = False, - owner = self, - ip_address = prefix, - ip_version = ip_version, - interface = next_hop_interface, - mac_address = mac_addr, - gateway = next_hop_ingress_ip) - ip_routes[src_node].append(prefix) - routes.append(route) - return pre_routes, routes - - def _get_pair_ip_routes(self): - """ - IP routing strategy : direct routes only - """ - routes = list() - G = _get_l2_graph(self.groups) - for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True): - src_node = self._state.manager.by_uuid(src_node_uuid) - dst_node = self._state.manager.by_uuid(dst_node_uuid) - - map_ = data['map_node_interface'] - src = self._state.manager.by_uuid(map_[src_node_uuid]) - dst = self._state.manager.by_uuid(map_[dst_node_uuid]) - - log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format( - src_node.name, src.device_name, src.ip4_address, - dst_node.name, dst.device_name, dst.ip4_address)) - log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format( - dst_node.name, dst.device_name, dst.ip4_address, - src_node.name, src.device_name, src.ip4_address)) - - route = IPRoute(node = src_node, - managed = False, - owner = self, - ip_address = dst.ip4_address, - mac_address = dst.mac_address, - interface = src) - routes.append(route) - route = IPRoute(node = src_node, - managed = False, - owner = self, - ip_address = dst.ip6_address, - ip_version = 6, - mac_address = dst.mac_address, - interface = src) - routes.append(route) - - route = IPRoute(node = dst_node, - managed = False, - owner = self, - ip_address = src.ip4_address, - mac_address = src.mac_address, - interface = dst) - routes.append(route) - route = IPRoute(node = dst_node, - managed = False, - owner = self, - ip_address = src.ip6_address, - ip_version = 6, - mac_address = src.mac_address, - interface = dst) - routes.append(route) - - return routes - -#------------------------------------------------------------------------------- - -class ICNFaces(Resource): - """ - Resource: ICNFaces - - Centralized ICN face creation. - """ - protocol_name = Attribute(String) - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - - @inline_task - def __get__(self): - raise ResourceNotFound # always create faces - - @inline_task - def __create__(self): - icn_faces = self._get_faces() - for face in icn_faces: - face.node.forwarder.faces << face - - def __delete__(self): - raise NotImplementedError - - #-------------------------------------------------------------------------- - # Internal methods - #-------------------------------------------------------------------------- - - def _get_faces(self): - """ - Face creation (heuristic: facemgr) - - Requires: at least direct IP links - """ - protocol = FaceProtocol.from_string(self.protocol_name) - - faces = list() - G = _get_l2_graph(self.groups) - for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True): - src_node = self._state.manager.by_uuid(src_node_uuid) - dst_node = self._state.manager.by_uuid(dst_node_uuid) - - map_ = data['map_node_interface'] - src = self._state.manager.by_uuid(map_[src_node_uuid]) - dst = self._state.manager.by_uuid(map_[dst_node_uuid]) - - log.debug('{} -> {} ({} -> {})'.format(src_node_uuid, - dst_node_uuid, src.device_name, dst.device_name)) - - # XXX This should be moved to the various faces, that register to a - # factory - if protocol == FaceProtocol.ether: - src_face = L2Face(node = src_node, - owner = self, - protocol = protocol, - src_nic = src, - dst_mac = dst.mac_address) - dst_face = L2Face(node = dst_node, - owner = self, - protocol = protocol, - src_nic = dst, - dst_mac = src.mac_address) - - elif protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6, - FaceProtocol.udp4, FaceProtocol.udp6): - src_face = L4Face(node = src_node, - owner = self, - protocol = protocol, - src_ip = src.ip4_address, - dst_ip = dst.ip4_address, - src_port = TMP_DEFAULT_PORT, - dst_port = TMP_DEFAULT_PORT) - dst_face = L4Face(node = dst_node, - owner = self, - protocol = protocol, - src_ip = dst.ip4_address, - dst_ip = src.ip4_address, - src_port = TMP_DEFAULT_PORT, - dst_port = TMP_DEFAULT_PORT) - else: - raise NotImplementedError - - # We key the sibling face for easier building of the ICN graph - src_face._internal_data['sibling_face'] = dst_face._state.uuid - dst_face._internal_data['sibling_face'] = src_face._state.uuid - - faces.append(src_face) - faces.append(dst_face) - - return faces - -#------------------------------------------------------------------------------ - -class ICNRoutes(Resource): - """ - Resource: ICNRoutes - - Centralized ICN route computation. - """ - - routing_strategy = Attribute(String) - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - - @inline_task - def __get__(self): - raise ResourceNotFound # always create routes - - @inline_task - def __create__(self): - icn_routes = self._get_icn_routes() - for route in icn_routes: - route.node.forwarder.routes << route - - def __delete__(self): - raise NotImplementedError - - #-------------------------------------------------------------------------- - # Internal methods - #-------------------------------------------------------------------------- - - def _get_prefix_origins(self): - origins = dict() - for group in self.groups: - for node in group.iter_by_type_str('node'): - node_uuid = node._state.uuid - if not node_uuid in origins: - origins[node_uuid] = list() - for producer in node.producers: - origins[node_uuid].extend(producer.prefixes) - return origins - - def _get_icn_routes(self): - strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy) - - G = _get_icn_graph(self._state.manager, self.groups) - origins = self._get_prefix_origins() - - routes = list() - for src, prefix, dst in strategy(G, origins): - data = G.get_edge_data(src, dst) - - map_ = data['map_node_face'] - next_hop_face = map_[src] - - route = ICNRoute(node = src, - owner = self, - prefix = prefix, - face = next_hop_face) - routes.append(route) - return routes - -#------------------------------------------------------------------------------ - -class DnsServerEntry(Resource): - """ - Resource: DnsServerEntry - - Setup of DNS resolver for LxcContainers - - Todo: - - This should be merged into the LxcContainer resource - """ - - node = Attribute(String) - ip_address = Attribute(String) - interface_name = Attribute(String) - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - - @inline_task - def __get__(self): - raise ResourceNotFound - - def __create__(self): - return BashTask(self.node, CMD_CONTAINER_SET_DNS, - {'ip_dns': self.ip_address, - 'interface_name': self.interface_name}) - - def __delete__(self): - raise NotImplementedError - -#------------------------------------------------------------------------------ - -class CentralIP(Resource): - """ - Resource: CentralIP - - Central IP management (main resource) - """ - - ip_routing_strategy = Attribute(String, description = 'IP routing strategy', - default = 'pair') # spt, pair - ip6_data_prefix = Attribute(String, description="Prefix for IPv6 forwarding", - mandatory = True) - ip4_data_prefix = Attribute(String, description="Prefix for IPv4 forwarding", - mandatory = True) - ip6_max_link_prefix = Attribute(Integer, - description = 'Maximum prefix size assigned to each link', - default = 64) - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - - def __after_init__(self): - return ('Node', 'Channel', 'Interface') - - def __after__(self): - return ('EmulatedChannel') - - def __subresources__(self): - ip4_assign = Ipv4Assignment(prefix = self.ip4_data_prefix, - groups = Reference(self, 'groups')) - ip6_assign = Ipv6Assignment(prefix = self.ip6_data_prefix, - groups = Reference(self, 'groups'), - max_prefix_size = self.ip6_max_link_prefix) - ip_routes = IPRoutes(owner = self, - groups = Reference(self, 'groups'), - routing_strategy = self.ip_routing_strategy) - - return (ip4_assign | ip6_assign) > ip_routes - -#------------------------------------------------------------------------------ - -class CentralICN(Resource): - """ - Resource: CentralICN - - Central ICN management (main resource) - """ - - # Choices: spt, max_flow - icn_routing_strategy = Attribute(String, - description = 'ICN routing strategy', - default = 'spt') - face_protocol = Attribute(String, - description = 'Protocol used to create faces', - default = 'ether') - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - - def __after__(self): - """ - We need to wait for IP configuration in order to be able to build - overload ICN faces, and producers for prefix origins. - """ - return ('CentralIP',) - - def __subresources__(self): - icn_faces = ICNFaces(owner = self, protocol_name = self.face_protocol, - groups = Reference(self, 'groups')) - icn_routes = ICNRoutes(owner = self, - routing_strategy = self.icn_routing_strategy, - groups = Reference(self, 'groups')) - return icn_faces > icn_routes - - @inline_task - def __get__(self): - raise ResourceNotFound - - __delete__ = None diff --git a/vicn/resource/group.py b/vicn/resource/group.py index 1557c42e..a1422c69 100644 --- a/vicn/resource/group.py +++ b/vicn/resource/group.py @@ -36,3 +36,12 @@ class Group(Resource): if not cls: return list() return self.iter_by_type(cls) + +# def get_tuple(self): +# return (self.name,) +# +# def hash(self): +# return hash(self.get_tuple()) +# +# def __eq__(self, other): +# return self.get_tuple() == other.get_tuple() diff --git a/vicn/resource/gui.py b/vicn/resource/gui.py new file mode 100644 index 00000000..26129b3e --- /dev/null +++ b/vicn/resource/gui.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 http.server +import io +import os +import socket +import socketserver +import sys +import threading + +from vicn.core.resource_mgr import ResourceManager +from vicn.helpers.resource_definition import * + +DEFAULT_GUI_ADDRESS = '' +DEFAULT_GUI_PORT = 8000 + +class GUIHandler(http.server.SimpleHTTPRequestHandler): + def send_head(self): + if self.path == '/js/settings.js': + return self.get_settings() + return super().send_head() + + def get_settings(self): + host = self.request.getsockname()[0] + port = ResourceManager().get('websocket_port'); + r = [] + r.append("var URL='ws://{}:{}';\n".format(host, port)) + enc = sys.getfilesystemencoding() + encoded = '\n'.join(r).encode(enc, 'surrogateescape') + f = io.BytesIO() + f.write(encoded) + f.seek(0) + self.send_response(http.server.HTTPStatus.OK) + self.send_header("Content-type", "text/javascript;") + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + return f + + +class GUI(Resource): + """ + Resource: GUI + + This resource is empty on purpose. It is a temporary resource used as a + placeholder for controlling the GUI and should be deprecated in future + releases. + """ + address = Attribute(String, description = 'Address on which the Webserver listens', + default = DEFAULT_GUI_ADDRESS) + port = Attribute(Integer, description = 'Port on which the Webserver listens', + default = DEFAULT_GUI_PORT) + path = Attribute(String, default='www') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + thread = threading.Thread(target = self.run) + thread.daemon = True + # XXX vICN should expose internal resources for interaction + try: + thread.start() + except KeyboardInterrupt: + server.shutdown() + sys.exit(0) + + def run(self): + if not self.path.startswith(os.path.sep): + # XXX we might also search in the experiment folder + base_dir = os.path.join(os.path.dirname(__file__), os.path.pardir, + os.path.pardir) + web_dir = os.path.join(base_dir, self.path) + else: + web_dir = self.path + os.chdir(web_dir) + socketserver.TCPServer.allow_reuse_address = True + httpd = socketserver.TCPServer((self.address, self.port), GUIHandler) + httpd.serve_forever() + + def __del__(self): + pass diff --git a/vicn/resource/icn/ccnx_keystore.py b/vicn/resource/icn/ccnx_keystore.py index ddd87019..ceff1647 100644 --- a/vicn/resource/icn/ccnx_keystore.py +++ b/vicn/resource/icn/ccnx_keystore.py @@ -18,7 +18,7 @@ from netmodel.model.type import String, Integer from vicn.core.attribute import Attribute, Reference -from vicn.core.task import BashTask +from vicn.core.task import BashTask, inherit_parent from vicn.resource.linux.file import File from vicn.resource.linux.package_manager import Packages @@ -39,13 +39,13 @@ class MetisKeystore(File): filename = Attribute(String, description = "File containing the keystore", default = DEFAULT_KEYSTORE_FILE, mandatory=False) - password = Attribute(String, + password = Attribute(String, description = "Password for the keystore file", default = DEFAULT_KEYSTORE_PASSWD) - subject_name = Attribute(String, + subject_name = Attribute(String, description = "Subject name for the keystore", default = DEFAULT_KEYSTORE_SUBJ) - validity = Attribute(String, + validity = Attribute(String, description = "Validity period of the keystore", default = DEFAULT_KEYSTORE_VALIDITY) size = Attribute(Integer, description = 'Length of the keys', @@ -62,6 +62,7 @@ class MetisKeystore(File): names=self._get_package_names(), owner=self) return packages + @inherit_parent def __create__(self): args = {'filename' : self.filename, 'password' : self.password, 'subject_name' : self.subject_name, 'validity' : self.validity, diff --git a/vicn/resource/icn/ccnx_metis.py b/vicn/resource/icn/ccnx_metis.py index ead9b9bf..418c7683 100644 --- a/vicn/resource/icn/ccnx_metis.py +++ b/vicn/resource/icn/ccnx_metis.py @@ -23,9 +23,9 @@ from vicn.core.attribute import Attribute from vicn.core.exception import ResourceNotFound from vicn.core.resource_mgr import wait_resource_task from vicn.core.task import BashTask, EmptyTask, task +from vicn.core.task import inherit_parent from vicn.resource.icn.ccnx_keystore import MetisKeystore -from vicn.resource.icn.face import L2Face, L4Face, FaceProtocol -from vicn.resource.icn.face import DEFAULT_ETHER_PROTO +from vicn.resource.icn.face import L2Face, L4Face from vicn.resource.icn.forwarder import Forwarder from vicn.resource.linux.file import TextFile from vicn.resource.linux.service import Service @@ -37,10 +37,12 @@ CMD_ADD_LISTENER_ETHER = ( 'add listener ether ether{conn_id} {listener.src_nic.device_name} ' '{listener.ether_proto}') CMD_ADD_LISTENER_L4 = 'add listener {protocol} transport{conn_id} {infos}' -CMD_ADD_CONNECTION_ETHER = ('add connection ether {face.id} {face.dst_mac} ' +CMD_ADD_CX_ETHER = ('add connection ether {face.id} {face.dst_mac} ' '{face.src_nic.device_name}') -CMD_ADD_CONNECTION_L4 = ('add connection {protocol} {face.id} {face.dst_ip} ' - '{face.dst_port} {face.src_ip} {face.src_port}') +CMD_ADD_CX_L4_IPV4 = ('add connection {protocol} {face.id} {face.dst.ip4_address} ' + '{face.dst_port} {face.src.ip4_address} {face.src_port}') +CMD_ADD_CX_L4_IPV6 = ('add connection {protocol} {face.id} {face.dst.ip6_address} ' + '{face.dst_port} {face.src.ip6_address} {face.src_port}') CMD_ADD_ROUTE = 'add route {route.face.id} ccnx:{route.prefix} {route.cost}' METIS_DAEMON_BOOTSTRAP = ( 'metis_daemon --port {port} --daemon --log-file {log_file} ' @@ -64,24 +66,25 @@ class MetisListener: @staticmethod def listener_from_face(face): - if face.protocol is FaceProtocol.ether: - return MetisEtherListener(face.protocol, face.src_nic, + if face.protocol == 'ether': + return MetisEtherListener(face.protocol, face.src_nic, face.ether_proto) - elif face.protocol in [FaceProtocol.tcp4, FaceProtocol.tcp6, - FaceProtocol.udp4, FaceProtocol.udp6]: - return MetisL4Listener(face.protocol, face.src_ip, face.src_port) + elif face.protocol in ('tcp4', 'udp4'): + return MetisL4Listener(face.protocol, face.src.ip4_address, face.src_port) + elif face.protocol in ('tcp6', 'udp6'): + return MetisL4Listener(face.protocol, face.src.ip6_address, face.src_port) else: raise ValueError("Metis only supports Ethernet and TCP/UDP faces") class MetisEtherListener(MetisListener): - def __init__(self, protocol, src_nic, ether_proto=DEFAULT_ETHER_PROTO): + def __init__(self, protocol, src_nic, ether_proto): super().__init__(protocol) self.src_nic = src_nic self.ether_proto = ether_proto def get_setup_command(self, conn_id): - return CMD_ADD_LISTENER_ETHER.format(listener = self, + return CMD_ADD_LISTENER_ETHER.format(listener = self, conn_id = conn_id) def __eq__(self, other): @@ -102,9 +105,9 @@ class MetisL4Listener(MetisListener): self.src_port = src_port def _get_proto_as_str(self): - if self.protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6): + if self.protocol in ('tcp4', 'tcp6'): return "tcp" - elif self.protocol in (FaceProtocol.udp4, FaceProtocol.udp6): + elif self.protocol in ('udp4', 'udp6'): return "udp" def get_setup_command(self, conn_id): @@ -126,14 +129,14 @@ class MetisForwarder(Forwarder, Service): __package_names__ = ['metis-forwarder'] __service_name__ = "metis-forwarder" - log_file = Attribute(String, description = 'File for metis logging', + log_file = Attribute(String, description = 'File for metis logging', default = '/tmp/ccnx-metis.log') # '/dev/null') - port = Attribute(Integer, description = 'TCP port for metis', + port = Attribute(Integer, description = 'TCP port for metis', default = 9695) - gen_config = Attribute(Bool, + gen_config = Attribute(Bool, description = 'Set to record all metis commands in a config file', default = True) - config_file = Attribute(String, default = '/root/.ccnx_metis.conf') + config_file = Attribute(String, default = '/root/.ccnx_metis.conf') #-------------------------------------------------------------------------- # Constructor and Accessors @@ -160,15 +163,18 @@ class MetisForwarder(Forwarder, Service): def __after__(self): return ('CentralICN',) + @inherit_parent def __subresources__(self): self.keystore = MetisKeystore(node = self.node, owner = self) self.env_file = self._write_environment_file() return self.keystore | self.env_file + @inherit_parent @task def __get__(self): raise ResourceNotFound + @inherit_parent def __create__(self): # Alternatively, we might put all commands in a configuration file @@ -222,7 +228,7 @@ class MetisForwarder(Forwarder, Service): """ command = METIS_DAEMON_BOOTSTRAP - args = {'port' : self.port, 'log_file' : self.log_file, + args = {'port' : self.port, 'log_file' : self.log_file, 'cs_size' : self.cache_size, 'config' : self.config_file} return BashTask(self.node, command, parameters = args) @@ -232,7 +238,7 @@ class MetisForwarder(Forwarder, Service): """ command = METIS_DAEMON_STOP + '; ' + METIS_DAEMON_BOOTSTRAP - args = {'port' : self.port, 'log_file' : self.log_file, + args = {'port' : self.port, 'log_file' : self.log_file, 'cs_size' : self.cache_size, 'config' : self.config_file} return BashTask(self.node, command, parameters = args) @@ -287,23 +293,16 @@ class MetisForwarder(Forwarder, Service): face.id = 'conn{}'.format(self._nb_conn) self._nb_conn += 1 - if face.protocol is FaceProtocol.ether: - assert isinstance(face, L2Face), \ - 'Ethernet face should be instance of L2Face' - cmd = CMD_ADD_CONNECTION_ETHER.format(face = face) - - elif face.protocol in (FaceProtocol.tcp4, FaceProtocol.tcp6): - assert isinstance(face, L4Face), \ - "TCP/UDP face should be instance of L4Face" - cmd = CMD_ADD_CONNECTION_L4.format(face = face, - protocol = 'tcp') - - elif face.protocol in (FaceProtocol.udp4, FaceProtocol.udp6): - assert isinstance(face, L4Face), \ - 'TCP/UDP face should be instance of L4Face' - cmd = CMD_ADD_CONNECTION_L4.format(face = face, - protocol = 'udp') - + if face.protocol == 'ether': + cmd = CMD_ADD_CX_ETHER.format(face = face) + elif face.protocol == 'tcp4': + cmd = CMD_ADD_CX_L4_IPV4.format(face = face, protocol = 'tcp') + elif face.protocol == 'tcp6': + cmd = CMD_ADD_CX_L4_IPV6.format(face = face, protocol = 'tcp') + elif face.protocol == 'udp4': + cmd = CMD_ADD_CX_L4_IPV4.format(face = face, protocol = 'udp') + elif face.protocol == 'udp6': + cmd = CMD_ADD_CX_L4_IPV6.format(face = face, protocol = 'udp') else: raise ValueError('Unsupported face type for Metis') @@ -324,13 +323,13 @@ class MetisForwarder(Forwarder, Service): delete_task = EmptyTask() if len(delete_cmds) > 0: - cmds = '\n'.join('{} {}'.format(self._baseline, command) + cmds = '\n'.join('{} {}'.format(self._baseline, command) for command in delete_cmds) delete_task = BashTask(self.node, cmds) create_task = EmptyTask() if len(create_cmds) > 0: - cmds = '\n'.join('{} {}'.format(self._baseline, command) + cmds = '\n'.join('{} {}'.format(self._baseline, command) for command in create_cmds) create_task = BashTask(self.node, cmds) @@ -348,16 +347,16 @@ class MetisForwarder(Forwarder, Service): create_task = BashTask(self.node, '\n'.join(create_cmds)) return delete_task > create_task - + def _write_environment_file(self): param_port = "PORT={port}" param_log_file = "LOG_FILE={log_file}" param_cs_capacity = "CS_SIZE={cs_size}" param_config = "CONFIG={config}" - env = [param_port.format(port = self.port), + env = [param_port.format(port = self.port), param_log_file.format(log_file = self.log_file), - param_cs_capacity.format(cs_size = self.cache_size), + param_cs_capacity.format(cs_size = self.cache_size), param_config.format(config = self.config_file)] environment_file = TextFile(filename = METIS_ETC_DEFAULT, diff --git a/vicn/resource/icn/ccnx_simpleTrafficGenerator.py b/vicn/resource/icn/ccnx_simpleTrafficGenerator.py index 221298fc..41a1b855 100644 --- a/vicn/resource/icn/ccnx_simpleTrafficGenerator.py +++ b/vicn/resource/icn/ccnx_simpleTrafficGenerator.py @@ -19,7 +19,7 @@ from netmodel.model.type import String from vicn.core.attribute import Attribute, Multiplicity from vicn.core.resource import Resource, EmptyResource -from vicn.core.task import EmptyTask +from vicn.core.task import EmptyTask, inherit_parent from vicn.resource.icn.icn_application import ICN_SUITE_CCNX_1_0 from vicn.resource.node import Node @@ -28,7 +28,7 @@ from vicn.resource.icn.ccnx_consumer_producer_test import CcnxProducerTest class CcnxSimpleTrafficGenerator(Resource): - prefix = Attribute(String, + prefix = Attribute(String, description = "Routable prefix for the applications", default = lambda self: self.default_name(), mandatory = False) @@ -44,11 +44,12 @@ class CcnxSimpleTrafficGenerator(Resource): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._sr = None - + #-------------------------------------------------------------------------- # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __subresources__(self): """ Create the list of consumers and producers. @@ -57,7 +58,7 @@ class CcnxSimpleTrafficGenerator(Resource): sr = EmptyResource() for producer in self.producers: - producer = CcnxProducerTest(node = producer, + producer = CcnxProducerTest(node = producer, owner = self, prefixes = [self.prefix]) sr = sr | producer diff --git a/vicn/resource/icn/central.py b/vicn/resource/icn/central.py new file mode 100644 index 00000000..aa8ea357 --- /dev/null +++ b/vicn/resource/icn/central.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 +import networkx as nx + +from netmodel.model.type import String +from vicn.core.attribute import Attribute, Reference +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import Resource +from vicn.core.task import inline_task +from vicn.resource.central import _get_l2_graph, MAP_ROUTING_STRATEGY +from vicn.resource.icn.face import Face +from vicn.resource.icn.route import Route + +log = logging.getLogger(__name__) + + +def _get_icn_graph(manager, groups): + G = nx.Graph() + for group in groups: + # It's safer to iterate on node which we know are in the right groups, + # while it might not be the case for the forwarders... + for node in group.iter_by_type_str('node'): + G.add_node(node._state.uuid) + try: + forwarder = node.forwarder + except ResourceNotFound: + continue + for face in forwarder.faces: + other_face = manager.by_uuid(face._internal_data['sibling_face']) + other_node = other_face.node + if G.has_edge(node._state.uuid, other_node._state.uuid): + continue + map_node_face = { node._state.uuid: face._state.uuid, + other_node._state.uuid: other_face._state.uuid } + G.add_edge(node._state.uuid, other_node._state.uuid, + map_node_face = map_node_face) + + return G + +#------------------------------------------------------------------------------- + +class ICNFaces(Resource): + """ + Resource: ICNFaces + + Centralized ICN face creation. + """ + protocol_name = Attribute(String) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inline_task + def __get__(self): + raise ResourceNotFound # always create faces + + @inline_task + def __create__(self): + icn_faces = self._get_faces() + for face in icn_faces: + face.node.forwarder.faces << face + + def __delete__(self): + raise NotImplementedError + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + def _get_faces(self): + """ + Face creation (heuristic: facemgr) + + Requires: at least direct IP links + """ + faces = list() + G = _get_l2_graph(self.groups) + for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True): + src_node = self._state.manager.by_uuid(src_node_uuid) + dst_node = self._state.manager.by_uuid(dst_node_uuid) + + if not src_node.managed or not dst_node.managed: + continue + + map_ = data['map_node_interface'] + src = self._state.manager.by_uuid(map_[src_node_uuid]) + dst = self._state.manager.by_uuid(map_[dst_node_uuid]) + + log.debug('{} -> {} ({} -> {})'.format(src_node_uuid, + dst_node_uuid, src.device_name, dst.device_name)) + + face_cls = Face.from_protocol(self.protocol_name) + if face_cls is None: + raise NotImplementedError + + src_face = face_cls(protocol = self.protocol_name, + owner = self, + node = src_node, + src = src, + dst = dst) + dst_face = face_cls(protocol = self.protocol_name, + owner = self, + node = dst_node, + src = dst, + dst = src) + + + # We key the sibling face for easier building of the ICN graph + src_face._internal_data['sibling_face'] = dst_face._state.uuid + dst_face._internal_data['sibling_face'] = src_face._state.uuid + + faces.append(src_face) + faces.append(dst_face) + + return faces + +#------------------------------------------------------------------------------ + +class ICNRoutes(Resource): + """ + Resource: Routes + + Centralized ICN route computation. + """ + + routing_strategy = Attribute(String) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inline_task + def __get__(self): + raise ResourceNotFound # always create routes + + @inline_task + def __create__(self): + icn_routes = self._get_icn_routes() + for route in icn_routes: + route.node.forwarder.routes << route + + def __delete__(self): + raise NotImplementedError + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + def _get_prefix_origins(self): + origins = dict() + for group in self.groups: + for node in group.iter_by_type_str('node'): + node_uuid = node._state.uuid + if not node_uuid in origins: + origins[node_uuid] = list() + for producer in node.producers: + origins[node_uuid].extend(producer.prefixes) + return origins + + def _get_icn_routes(self): + strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy) + + G = _get_icn_graph(self._state.manager, self.groups) + origins = self._get_prefix_origins() + + routes = list() + for src, prefix, dst in strategy(G, origins): + src_node = self._state.manager.by_uuid(src) + if not src_node.managed: + continue + data = G.get_edge_data(src, dst) + + map_ = data['map_node_face'] + next_hop_face = map_[src] + + route = Route(node = src, + owner = self, + prefix = prefix, + face = next_hop_face) + routes.append(route) + + return routes + +#------------------------------------------------------------------------------ + +class CentralICN(Resource): + """ + Resource: CentralICN + + Central ICN management (main resource) + """ + + # Choices: spt, max_flow + icn_routing_strategy = Attribute(String, + description = 'ICN routing strategy', + default = 'spt') + face_protocol = Attribute(String, + description = 'Protocol used to create faces', + default = 'ether') + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __after__(self): + """ + We need to wait for IP configuration in order to be able to build + overload ICN faces, and producers for prefix origins. + """ + return ('CentralIP',) + + def __subresources__(self): + icn_faces = ICNFaces(owner = self, protocol_name = self.face_protocol, + groups = Reference(self, 'groups')) + icn_routes = ICNRoutes(owner = self, + routing_strategy = self.icn_routing_strategy, + groups = Reference(self, 'groups')) + return icn_faces > icn_routes diff --git a/vicn/resource/icn/cicn.py b/vicn/resource/icn/cicn.py new file mode 100644 index 00000000..76dafe0e --- /dev/null +++ b/vicn/resource/icn/cicn.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 re + +from netmodel.model.type import Integer, Bool +from vicn.core.attribute import Attribute +from vicn.core.exception import ResourceNotFound +from vicn.core.requirement import Requirement +from vicn.core.resource_mgr import wait_resource_task +from vicn.core.task import async_task, task, BashTask, EmptyTask +from vicn.core.task import inherit_parent +from vicn.resource.icn.forwarder import Forwarder +from vicn.resource.node import Node +from vicn.resource.vpp.vpp_commands import CMD_VPP_ENABLE_PLUGIN + +CMD_VPP_CICN_GET = "vppctl_wrapper cicn show" +CMD_VPP_ADD_ICN_ROUTE = 'vppctl_wrapper cicn cfg fib add prefix {route.prefix} face {route.face.id}' +CMD_VPP_ADD_ICN_FACE = 'vppctl_wrapper cicn cfg face add local {face.src_ip}:{face.src_port} remote {face.dst_ip}:{face.dst_port}' + +CMD_VPP_CICN_GET_CACHE_SIZE = 'vppctl_wrapper cicn show | grep "CS entries" | grep -Eo "[0-9]+"' +CMD_VPP_CICN_SET_CACHE_SIZE = 'vppctl_wrapper cicn control param cs size {self.cache_size}' +_ADD_FACE_RETURN_FORMAT = "Face ID: [0-9]+" + +def check_face_id_return_format(data): + prog = re.compile(_ADD_FACE_RETURN_FORMAT) + return prog.match(data) + +def parse_face_id(data): + return data.partition(':')[2] + +class CICNForwarder(Forwarder): + """ + NOTE: Based on the Vagrantfile, we recommend a node with mem=4096, cpu=4 + """ + + node = Attribute(Node, + mandatory=True, + requirements = [Requirement('vpp')], + reverse_name='cicn') + numa_node = Attribute(Integer, + description = 'Numa node on which vpp will run', + default = None) + core = Attribute(Integer, + description = 'Core belonging the numa node on which vpp will run', + default = None) + enable_worker = Attribute(Bool, + description = 'Enable one worker for packet processing', + default = False) + + #__packages__ = ['vpp-plugin-cicn'] + + def __after__(self): + return ['CentralICN'] + + @inherit_parent + def __get__(self): + def parse(rv): + if rv.return_value > 0 or 'cicn: not enabled' in rv.stdout: + raise ResourceNotFound + return BashTask(self.node, CMD_VPP_CICN_GET, + lock = self.node.vpp.vppctl_lock, parse=parse) + + @inherit_parent + def __create__(self): + + #self.node.vpp.plugins.append("cicn") + lock = self.node.vpp.vppctl_lock + create_task = BashTask(self.node, CMD_VPP_ENABLE_PLUGIN, + {'plugin' : 'cicn'}, lock = lock) + + face_task = EmptyTask() + route_task = EmptyTask() + + def parse_face(rv, face): + if check_face_id_return_format(rv.stdout): + face.id = parse_face_id(rv.stdout) + return {} + + for face in self.faces: + face_task = face_task > BashTask(self.node, CMD_VPP_ADD_ICN_FACE, + {'face':face}, + parse = (lambda x : parse_face(x, face)), lock = lock) + + if not self.routes: + from vicn.resource.icn.route import Route + for route in self._state.manager.by_type(Route): + if route.node is self.node: + self.routes.append(route) + for route in self.routes: + route_task = route_task > BashTask(self.node, + CMD_VPP_ADD_ICN_ROUTE, {'route' : route}, lock = lock) + + return (wait_resource_task(self.node.vpp) > create_task) > (face_task > route_task) + + #-------------------------------------------------------------------------- + # Attributes + #-------------------------------------------------------------------------- + + # Force local update + + _add_faces = None + _remove_faces = None + _get_faces = None + _set_faces = None + + _add_routes = None + _remove_routes = None + _get_routes = None + _set_routes = None + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + def _set_cache_size(self): + return BashTask(self.node, CMD_VPP_CICN_SET_CACHE_SIZE, {'self': self}, + lock = self.node.vpp.vppctl_lock) + + def _get_cache_size(self): + def parse(rv): + return int(rv.stdout) + return BashTask(self.node, CMD_VPP_CICN_GET_CACHE_SIZE, parse=parse, + lock = self.node.vpp.vppctl_lock) diff --git a/vicn/resource/icn/face.py b/vicn/resource/icn/face.py index 641d10e7..4fd07883 100644 --- a/vicn/resource/icn/face.py +++ b/vicn/resource/icn/face.py @@ -16,36 +16,31 @@ # limitations under the License. # -from enum import Enum - -from netmodel.model.type import Integer, String, Bool -from vicn.core.attribute import Attribute -from vicn.core.requirement import Requirement -from vicn.core.resource import Resource -from vicn.resource.node import Node -from vicn.resource.interface import Interface +from netmodel.model.type import Integer, String, Bool, InetAddress +from netmodel.model.object import ObjectMetaclass +from vicn.core.attribute import Attribute +from vicn.core.requirement import Requirement +from vicn.core.resource import Resource +from vicn.resource.interface import Interface +from vicn.resource.node import Node DEFAULT_ETHER_PROTO = 0x0801 -FMT_L4FACE = '{protocol.name}://{dst_ip}:{dst_port}/' -FMT_L2FACE = '{protocol.name}://[{dst_mac}]/{src_nic.device_name}' - -class FaceProtocol(Enum): - ether = 0 - ip4 = 1 - ip6 = 2 - tcp4 = 3 - tcp6 = 4 - udp4 = 5 - udp6 = 7 - app = 8 - - @staticmethod - def from_string(protocol): - return getattr(FaceProtocol, protocol) +DEFAULT_PORT = 6363 + +FMT_L4FACE_IPV4 = '{protocol}://{dst.ip4_address}:{dst_port}/' +FMT_L4FACE_IPV6 = '{protocol}://{dst.ip6_address}:{dst_port}/' +FMT_L2FACE = '{protocol}://[{dst.mac_address}]/{src.device_name}' #------------------------------------------------------------------------------ -class Face(Resource): +class FaceMetaclass(ObjectMetaclass): + def __new__(mcls, name, bases, attrs): + cls = super(FaceMetaclass, mcls).__new__(mcls, name, bases, attrs) + if name != 'Face': + cls.register() + return cls + +class Face(Resource, metaclass=FaceMetaclass): """ Resource: Face """ @@ -57,6 +52,10 @@ class Face(Resource): protocol = Attribute(String, description = 'Face underlying protocol', mandatory = True) + + src = Attribute(Interface, mandatory = True) + dst = Attribute(Interface, mandatory = True) + id = Attribute(String, description = 'Local face ID', ro = True) @@ -75,6 +74,18 @@ class Face(Resource): description = 'Flags for face creation with NFDC', func = lambda self : self._lambda_nfdc_flags()) + # map protocol -> face class + _map_protocol = dict() + + @classmethod + def register(cls): + for protocol in cls.__protocols__: + Face._map_protocol[protocol] = cls + + @classmethod + def from_protocol(cls, protocol): + return cls._map_protocol.get(protocol) + def __repr__(self): flags = '' if self.permanent: @@ -95,7 +106,7 @@ class Face(Resource): # NFD specifics def _lambda_nfd_uri(self): - raise NotImplementedError + return 'N/A' # raise NotImplementedError def _lambda_nfdc_flags(self): flags = '' @@ -111,14 +122,11 @@ class Face(Resource): class L2Face(Face): - src_nic = Attribute(Interface, - description = "Name of the network interface linked to the face", - mandatory=True) - dst_mac = Attribute(String, description = "destination MAC address", - mandatory=True) + __protocols__ = ['ether'] + ether_proto = Attribute(String, description = "Ethernet protocol number used by the face", - default=DEFAULT_ETHER_PROTO) + default = DEFAULT_ETHER_PROTO) def _lambda_nfd_uri(self): return self.format(FMT_L2FACE) @@ -127,14 +135,13 @@ class L2Face(Face): class L4Face(Face): - ip_version = Attribute(Integer, description = "IPv4 or IPv6", default = 4) - src_ip = Attribute(String, description = "local IP address", - mandatory = True) - src_port = Attribute(Integer, description = "local TCP/UDP port") - dst_ip = Attribute(String, descrition = "remote IP address", - mandatory=True) + __protocols__ = ['tcp4', 'tcp6', 'udp4', 'udp6'] + + src_port = Attribute(Integer, description = "local TCP/UDP port", + default = DEFAULT_PORT) dst_port = Attribute(Integer, description = "remote TCP/UDP port", - mandatory=True) + default = DEFAULT_PORT) def _lambda_nfd_uri(self): - return self.format(FMT_L4FACE) + fmt = FMT_L4FACE_IPV4 if self.protocol in ['tcp4', 'udp4'] else FMT_L4FACE_IPV6 + return self.format(fmt) diff --git a/vicn/resource/icn/forwarder.py b/vicn/resource/icn/forwarder.py index 748532cf..e2ad944f 100644 --- a/vicn/resource/icn/forwarder.py +++ b/vicn/resource/icn/forwarder.py @@ -30,7 +30,7 @@ DEFAULT_CACHE_SIZE = 1000 # pk DEFAULT_CACHE_POLICY = 'LRU' DEFAULT_STRATEGY = 'best-route' -class Forwarder(ICNApplication, ABC): +class Forwarder(ICNApplication): """ Resource: Forwarder """ diff --git a/vicn/resource/icn/icn_application.py b/vicn/resource/icn/icn_application.py index 817d9403..e8b6e19c 100644 --- a/vicn/resource/icn/icn_application.py +++ b/vicn/resource/icn/icn_application.py @@ -20,6 +20,7 @@ from vicn.resource.linux.application import LinuxApplication from vicn.core.attribute import Attribute from netmodel.model.type import Integer +ICN_SUITE_HICN_1_0=0 ICN_SUITE_CCNX_1_0=1 ICN_SUITE_NDN=2 diff --git a/vicn/resource/icn/iping.py b/vicn/resource/icn/iping.py index 0e04eadc..4494c79f 100644 --- a/vicn/resource/icn/iping.py +++ b/vicn/resource/icn/iping.py @@ -36,28 +36,28 @@ class IPing(ICNApplication): __package_names__ = ["libicnet"] - prefixes = Attribute(String, + prefixes = Attribute(String, description = "name served by the ping server", default = lambda self: self.default_name(), mandatory = False, multiplicity = Multiplicity.OneToMany) node = Attribute(Node, requirements=[ - Requirement("forwarder", - capabilities = set(['ICN_SUITE_CCNX_1_0']), + Requirement("forwarder", + capabilities = set(['ICN_SUITE_CCNX_1_0']), properties = {"protocol_suites" : ICN_SUITE_CCNX_1_0}) ]) - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- # Methods - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- def __method_start__(self): return self._build_command() - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- # Internal methods - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- def default_name(self): return ['/iping'] @@ -72,16 +72,16 @@ class IPingClient(IPing, Producer): Resource: IPingClient """ - flood = Attribute(Bool, description = 'enable flood mode', + flood = Attribute(Bool, description = 'enable flood mode', default = False) count = Attribute(Integer, description = 'number of ping to send') - interval = Attribute(Integer, + interval = Attribute(Integer, description = 'interval between interests in ping mode') size = Attribute(Integer, description = 'size of the interests') - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- # Internal methods - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- def _build_command(self): template = ["iPing_Client", "-l ccnx:{prefix}"] @@ -110,9 +110,9 @@ class IPingServer(IPing, Consumer): size = Attribute(Integer, description = "size of the payload") - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- # Internal methods - #-------------------------------------------------------------------------- + #-------------------------------------------------------------------------- def _build_command(self): template = ["iPing_Server", "-l ccnx:{prefix}"] diff --git a/vicn/resource/icn/ndnpingserver.py b/vicn/resource/icn/ndnpingserver.py index f9cfa7cc..8fbb5fb6 100644 --- a/vicn/resource/icn/ndnpingserver.py +++ b/vicn/resource/icn/ndnpingserver.py @@ -54,7 +54,7 @@ class NDNPingServerBase(Producer): default = lambda self: self._default_prefixes()) node = Attribute(requirements = [ - Requirement("forwarder", + Requirement("forwarder", capabilities = set(['ICN_SUITE_NDN_1_0'])) ]) __package_names__ = ['ndnping'] diff --git a/vicn/resource/icn/nfd.py b/vicn/resource/icn/nfd.py index c65fdeb8..1b7f39f7 100644 --- a/vicn/resource/icn/nfd.py +++ b/vicn/resource/icn/nfd.py @@ -22,6 +22,7 @@ import re from vicn.core.exception import ResourceNotFound from vicn.core.task import inline_task, BashTask from vicn.core.task import ParseRegexTask +from vicn.core.task import inherit_parent from vicn.resource.icn.forwarder import Forwarder from vicn.resource.icn.icn_application import ICN_SUITE_NDN @@ -74,17 +75,20 @@ class NFD(Forwarder): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent @inline_task def __get__(self): # NFD is assumed not to exist raise ResourceNotFound + @inherit_parent def __create__(self): # Modify the configuration file before running the forwarder service conf = BashTask(self.node, CMD_SET_STRATEGY_CACHE, {'nfd': self}) - forwarder = Forwarder.__create__(self) - return conf.then(forwarder) + forwarder = super().__create__() + return conf > forwarder + @inherit_parent def __delete__(self): raise NotImplementedError diff --git a/vicn/resource/icn/producer.py b/vicn/resource/icn/producer.py index 131097f2..36cbea77 100644 --- a/vicn/resource/icn/producer.py +++ b/vicn/resource/icn/producer.py @@ -16,6 +16,7 @@ # limitations under the License. # +from netmodel.model.key import Key from netmodel.model.type import String from vicn.resource.icn.icn_application import ICNApplication from vicn.core.attribute import Attribute, Multiplicity @@ -33,5 +34,6 @@ class Producer(ICNApplication): description = 'Node on which the producer is installed', mandatory = True, multiplicity = Multiplicity.ManyToOne, - reverse_name = 'producers', - key = True) + reverse_name = 'producers') + + __key__ = Key(node) diff --git a/vicn/resource/icn/route.py b/vicn/resource/icn/route.py index 0dc2ed2f..6efd909b 100644 --- a/vicn/resource/icn/route.py +++ b/vicn/resource/icn/route.py @@ -25,12 +25,10 @@ from vicn.resource.node import Node class Route(Resource): node = Attribute(Node, mandatory = True) prefix = Attribute(String, mandatory = True) - face = Attribute(Face, description = "face used to forward interests", + face = Attribute(Face, description = "face used to forward interests", mandatory=True) cost = Attribute(Integer, default=1) def __repr__(self): - return ''.format(self.prefix, self.face, + return ''.format(self.prefix, self.face, self.node.name) - - __str__ = __repr__ diff --git a/vicn/resource/interface.py b/vicn/resource/interface.py index 0ae2dc94..3aa40fb4 100644 --- a/vicn/resource/interface.py +++ b/vicn/resource/interface.py @@ -16,8 +16,9 @@ # limitations under the License. # -from vicn.core.attribute import Attribute, Multiplicity +from netmodel.model.key import Key from netmodel.model.type import Bool +from vicn.core.attribute import Attribute, Multiplicity from vicn.core.resource import Resource from vicn.resource.node import Node from vicn.resource.channel import Channel @@ -30,8 +31,7 @@ class Interface(Resource): node = Attribute(Node, description = 'Node to which the interface belongs', multiplicity = Multiplicity.ManyToOne, reverse_name = 'interfaces', - mandatory = True, - key = True) + mandatory = True) channel = Attribute(Channel, description = 'Channel to which the interface is attached', multiplicity = Multiplicity.ManyToOne, reverse_name = 'interfaces') @@ -41,6 +41,8 @@ class Interface(Resource): default = True) monitored = Attribute(Bool, default = True) + __key__ = Key(node, Resource.name) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/vicn/resource/ip/central.py b/vicn/resource/ip/central.py new file mode 100644 index 00000000..0c397728 --- /dev/null +++ b/vicn/resource/ip/central.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 +import networkx as nx +import os + +from netmodel.model.type import String, Integer, Bool +from vicn.core.attribute import Attribute, Reference +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import Resource +from vicn.core.task import inline_task, BashTask +from vicn.resource.central import _get_l2_graph, MAP_ROUTING_STRATEGY +from vicn.resource.channel import Channel +from vicn.resource.ip_assignment import Ipv4Assignment, Ipv6Assignment +from vicn.resource.ip.route import IPRoute + +log = logging.getLogger(__name__) + +CMD_CONTAINER_SET_DNS = 'echo "nameserver {ip_dns}" | ' \ + 'resolvconf -a {interface_name}' + +#------------------------------------------------------------------------------- + +class IPRoutes(Resource): + """ + Resource: IPRoutes + + Centralized IP route computation. + """ + routing_strategy = Attribute(String) + + def __after__(self): + return ("IpAssignment",) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inline_task + def __get__(self): + raise ResourceNotFound + + @inline_task + def __create__(self): + routes = list() + pre_routes, routes = self._get_ip_routes() + routes.extend(pre_routes) + routes.extend(routes) + for route in routes: + route.node.routing_table.routes << route + + def __delete__(self): + raise NotImplementedError + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + def _get_ip_origins(self): + origins = dict() + for group in self.groups: + for channel in group.iter_by_type_str('channel'): + for interface in channel.interfaces: + if group.name not in [x.name for x in interface.node.groups]: + continue + node_uuid = interface.node._state.uuid + if not node_uuid in origins: + origins[node_uuid] = list() + origins[node_uuid].append(interface.ip4_address.canonical_prefix()) + if interface.ip6_address: + origins[node_uuid].append(interface.ip6_address.canonical_prefix()) + return origins + + def _get_ip_routes(self): + if self.routing_strategy == 'pair': + return [], self._get_pair_ip_routes() + + strategy = MAP_ROUTING_STRATEGY.get(self.routing_strategy) + + G = _get_l2_graph(self.groups) + origins = self._get_ip_origins() + + # node -> list(origins for which we have routes) + ip_routes = dict() + + pre_routes = list() + routes = list() + for src, prefix, dst in strategy(G, origins): + data = G.get_edge_data(src, dst) + + map_ = data['map_node_interface'] + next_hop_interface = map_[src] + + next_hop_ingress = self._state.manager.by_uuid(map_[dst]) + src_node = self._state.manager.by_uuid(src) + + # Avoid duplicate routes due to multiple paths in the network + if not src_node in ip_routes: + ip_routes[src_node] = set() + #XXX Untested for VPP + #When you set up an IP address ip/prefix_len (ip addr add), you already create a route + #towards ip/prefix_len + for interface in src_node.interfaces: + ip_routes[src_node].add(interface.ip4_address.canonical_prefix()) + ip_routes[src_node].add(interface.ip6_address.canonical_prefix()) + if prefix in ip_routes[src_node]: + continue + ip_routes[src_node].add(prefix) + + ip_version = 6 if ":" in str(prefix) else 4 + next_hop_ingress_ip = (next_hop_ingress.ip6_address if ip_version is 6 else + next_hop_ingress.ip4_address) + if prefix == next_hop_ingress_ip: + # Direct route on src_node.name : + # route add [prefix] dev [next_hop_interface_.device_name] + route = IPRoute(node = src_node, + managed = False, + owner = self, + ip_address = prefix, + ip_version = ip_version, + interface = next_hop_interface) + else: + # We need to be sure we have a route to the gw from the node + if not next_hop_ingress_ip.canonical_prefix() in ip_routes[src_node]: + pre_route = IPRoute(node = src_node, + managed = False, + owner = self, + ip_address = next_hop_ingress_ip.canonical_prefix(), + ip_version = ip_version, + interface = next_hop_interface) + ip_routes[src_node].add(next_hop_ingress_ip.canonical_prefix()) + pre_routes.append(pre_route) + + # Route on src_node.name: + # route add [prefix] dev [next_hop_interface_.device_name] + # via [next_hop_ingress.ip_address] + route = IPRoute(node = src_node, + managed = False, + owner = self, + ip_address = prefix, + ip_version = ip_version, + interface = next_hop_interface, + gateway = next_hop_ingress_ip) + routes.append(route) + return pre_routes, routes + + def _get_pair_ip_routes(self): + """ + IP routing strategy : direct routes only + """ + routes = list() + G = _get_l2_graph(self.groups) + for src_node_uuid, dst_node_uuid, data in G.edges_iter(data = True): + src_node = self._state.manager.by_uuid(src_node_uuid) + dst_node = self._state.manager.by_uuid(dst_node_uuid) + + map_ = data['map_node_interface'] + src = self._state.manager.by_uuid(map_[src_node_uuid]) + dst = self._state.manager.by_uuid(map_[dst_node_uuid]) + + log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format( + src_node.name, src.device_name, src.ip4_address, + dst_node.name, dst.device_name, dst.ip4_address)) + log.debug('[IP ROUTE] NODES {}/{}/{} -> {}/{}/{}'.format( + dst_node.name, dst.device_name, dst.ip4_address, + src_node.name, src.device_name, src.ip4_address)) + + route = IPRoute(node = src_node, + managed = False, + owner = self, + ip_address = dst.ip4_address, + mac_address = dst.mac_address, + interface = src) + routes.append(route) + route = IPRoute(node = src_node, + managed = False, + owner = self, + ip_address = dst.ip6_address, + ip_version = 6, + mac_address = dst.mac_address, + interface = src) + routes.append(route) + + route = IPRoute(node = dst_node, + managed = False, + owner = self, + ip_address = src.ip4_address, + mac_address = src.mac_address, + interface = dst) + routes.append(route) + route = IPRoute(node = dst_node, + managed = False, + owner = self, + ip_address = src.ip6_address, + ip_version = 6, + mac_address = src.mac_address, + interface = dst) + routes.append(route) + + return routes + +#------------------------------------------------------------------------------ + +class DnsServerEntry(Resource): + """ + Resource: DnsServerEntry + + Setup of DNS resolver for LxcContainers + + Todo: + - This should be merged into the LxcContainer resource + """ + + node = Attribute(String) + ip_address = Attribute(String) + interface_name = Attribute(String) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inline_task + def __get__(self): + raise ResourceNotFound + + def __create__(self): + return BashTask(self.node, CMD_CONTAINER_SET_DNS, + {'ip_dns': str(self.ip_address), + 'interface_name': self.interface_name}) + + def __delete__(self): + raise NotImplementedError + +#------------------------------------------------------------------------------ + +class CentralIP(Resource): + """ + Resource: CentralIP + + Central IP management (main resource) + """ + + ip_routing_strategy = Attribute(String, description = 'IP routing strategy', + default = 'pair') # spt, pair + ip6_data_prefix = Attribute(String, description="Prefix for IPv6 forwarding", + mandatory = True) + ip4_data_prefix = Attribute(String, description="Prefix for IPv4 forwarding", + mandatory = True) + ip6_max_link_prefix = Attribute(Integer, + description = 'Maximum prefix size assigned to each link', + default = 64) + ip6_disjoint_addresses = Attribute(Bool, description="assign disjoint addresses spanning over the whole prefix", default=False) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __after_init__(self): + return ('Node', 'Channel', 'Interface', 'EmulatedChannel') + + def __subresources__(self): + ip4_assign = Ipv4Assignment(prefix = self.ip4_data_prefix, + groups = Reference(self, 'groups')) + ip6_assign = Ipv6Assignment(prefix = self.ip6_data_prefix, + groups = Reference(self, 'groups'), + max_prefix_len = self.ip6_max_link_prefix, + disjoint_addresses = self.ip6_disjoint_addresses) + ip_routes = IPRoutes(owner = self, + groups = Reference(self, 'groups'), + routing_strategy = self.ip_routing_strategy) + + return (ip4_assign | ip6_assign) > ip_routes diff --git a/vicn/resource/ip/prefix_tree.py b/vicn/resource/ip/prefix_tree.py index 34af1d14..1fec2d47 100644 --- a/vicn/resource/ip/prefix_tree.py +++ b/vicn/resource/ip/prefix_tree.py @@ -16,134 +16,7 @@ # limitations under the License. # -from socket import inet_pton, inet_ntop, AF_INET6 -from struct import unpack, pack -from abc import ABCMeta - -class PrefixTreeException(Exception): pass -class NotEnoughAddresses(PrefixTreeException): pass -class UnassignablePrefix(PrefixTreeException): pass - -class Prefix(metaclass=ABCMeta): - - def __init__(self, ip_address, prefix_size=None): - if not prefix_size: - ip_address, prefix_size = ip_address.split('/') - prefix_size = int(prefix_size) - if isinstance(ip_address, str): - ip_address = self.aton(ip_address) - self.ip_address = ip_address - self.prefix_size = prefix_size - - def __contains__(self, obj): - #it can be an IP as a integer - if isinstance(obj, int): - obj = type(self)(obj, self.MAX_PREFIX_SIZE) - #Or it's an IP string - if isinstance(obj, str): - #It's a prefix as 'IP/prefix' - if '/' in obj: - split_obj = obj.split('/') - obj = type(self)(split_obj[0], int(split_obj[1])) - else: - obj = type(self)(obj, self.MAX_PREFIX_SIZE) - - return self._contains_prefix(obj) - - @classmethod - def mask(cls): - mask_len = cls.MAX_PREFIX_SIZE//8 #Converts from bits to bytes - mask = 0 - for step in range(0,mask_len): - mask = (mask << 8) | 0xff - return mask - - def _contains_prefix(self, prefix): - assert isinstance(prefix, type(self)) - return (prefix.prefix_size >= self.prefix_size and - prefix.ip_address >= self.first_prefix_address() and - prefix.ip_address <= self.last_prefix_address()) - - #Returns the first address of a prefix - def first_prefix_address(self): - return self.ip_address & (self.mask() << (self.MAX_PREFIX_SIZE-self.prefix_size)) - - def last_prefix_address(self): - return self.ip_address | (self.mask() >> self.prefix_size) - - def limits(self): - return self.first_prefix_address(), self.last_prefix_address() - - def __str__(self): - return "{}/{}".format(self.ntoa(self.first_prefix_address()), self.prefix_size) - - def __eq__(self, other): - return (self.first_prefix_address() == other.first_prefix_address() and - self.prefix_size == other.prefix_size) - - def __hash__(self): - return hash(str(self)) - - def __iter__(self): - return self.get_iterator() - - #Iterates by steps of prefix_size, e.g., on all available /31 in a /24 - def get_iterator(self, prefix_size=None): - if prefix_size is None: - prefix_size=self.MAX_PREFIX_SIZE - assert (prefix_size >= self.prefix_size and prefix_size<=self.MAX_PREFIX_SIZE) - step = 2**(self.MAX_PREFIX_SIZE - prefix_size) - for ip in range(self.first_prefix_address(), self.last_prefix_address()+1, step): - yield type(self)(ip, prefix_size) - -class Inet4Prefix(Prefix): - - MAX_PREFIX_SIZE = 32 - - @classmethod - def aton(cls, address): - ret = 0 - components = address.split('.') - for comp in components: - ret = (ret << 8) + int(comp) - return ret - - @classmethod - def ntoa(cls, address): - components = [] - for _ in range(0,4): - components.insert(0,'{}'.format(address % 256)) - address = address >> 8 - return '.'.join(components) - -class Inet6Prefix(Prefix): - - MAX_PREFIX_SIZE = 128 - - @classmethod - def aton (cls, address): - prefix, suffix = unpack(">QQ", inet_pton(AF_INET6, address)) - return (prefix << 64) | suffix - - @classmethod - def ntoa (cls, address): - return inet_ntop(AF_INET6, pack(">QQ", address >> 64, address & ((1 << 64) -1))) - - #skip_internet_address: skip a:b::0, as v6 often use default /64 prefixes - def get_iterator(self, prefix_size=None, skip_internet_address=None): - if skip_internet_address is None: - #We skip the internet address if we iterate over Addresses - if prefix_size is None: - skip_internet_address = True - #But not if we iterate over prefixes - else: - skip_internet_address = False - it = super().get_iterator(prefix_size) - if skip_internet_address: - next(it) - return it - -###### PREFIX TREE ###### +from netmodel.model.type import Inet4Prefix, Inet6Prefix class PrefixTree: @@ -160,21 +33,21 @@ class PrefixTree: self.full = False - def find_prefix(self, prefix_size): + def find_prefix(self, prefix_len): ret, lret, rret = [None]*3 - if prefix_size > self.prefix.prefix_size and not self.full: + if prefix_len > self.prefix.prefix_len and not self.full: if self.left is None: - lret = self.prefix_cls(self.prefix.first_prefix_address(), self.prefix.prefix_size+1) + lret = self.prefix_cls(self.prefix.first_prefix_address(), self.prefix.prefix_len+1) else: - lret = self.left.find_prefix(prefix_size) + lret = self.left.find_prefix(prefix_len) if self.right is None: - rret = self.prefix_cls(self.prefix.last_prefix_address(), self.prefix.prefix_size+1) + rret = self.prefix_cls(self.prefix.last_prefix_address(), self.prefix.prefix_len+1) else: - rret = self.right.find_prefix(prefix_size) + rret = self.right.find_prefix(prefix_len) #Now we look for the longer prefix to assign - if not lret or (rret and rret.prefix_size > lret.prefix_size): + if not lret or (rret and rret.prefix_len > lret.prefix_len): ret = rret else: ret = lret @@ -183,9 +56,9 @@ class PrefixTree: def assign_prefix(self, prefix): assert prefix in self.prefix - if prefix.prefix_size > self.prefix.prefix_size: + if prefix.prefix_len > self.prefix.prefix_len: #Existing prefix on the left - lp = self.prefix_cls(self.prefix.first_prefix_address(), self.prefix.prefix_size+1) + lp = self.prefix_cls(self.prefix.first_prefix_address(), self.prefix.prefix_len+1) #It's on the left branch if prefix in lp: if not self.left: @@ -193,7 +66,7 @@ class PrefixTree: self.left.assign_prefix(prefix) #It's on the right branch else: - rp = self.prefix_cls(self.prefix.last_prefix_address(), self.prefix.prefix_size+1) + rp = self.prefix_cls(self.prefix.last_prefix_address(), self.prefix.prefix_len+1) if not self.right: self.right = PrefixTree(rp) self.right.assign_prefix(prefix) @@ -202,13 +75,14 @@ class PrefixTree: else: raise RuntimeError("And you may ask yourself, well, how did I get here?") - def get_prefix(self, prefix_size): - ret = self.find_prefix(prefix_size) + def get_prefix(self, prefix_len): + ret = self.find_prefix(prefix_len) if not ret: raise NotEnoughAddresses #find_prefix returns the size of the largest available prefix in our space #not necessarily the one we asked for - ret.prefix_size = prefix_size + ret.ip_address = ret.first_prefix_address() + ret.prefix_len = prefix_len self.assign_prefix(ret) return ret diff --git a/vicn/resource/ip/route.py b/vicn/resource/ip/route.py index b9f82960..2f5807b6 100644 --- a/vicn/resource/ip/route.py +++ b/vicn/resource/ip/route.py @@ -16,7 +16,7 @@ # limitations under the License. # -from netmodel.model.type import String, Integer +from netmodel.model.type import String, Integer, InetAddress from vicn.resource.node import Node from vicn.core.attribute import Attribute from vicn.core.resource import Resource @@ -24,10 +24,8 @@ from vicn.resource.interface import Interface class IPRoute(Resource): node = Attribute(Node, mandatory = True) - ip_address = Attribute(String, mandatory = True) + ip_address = Attribute(InetAddress, mandatory = True) interface = Attribute(Interface, mandatory = True) - gateway = Attribute(String) + gateway = Attribute(InetAddress) ip_version = Attribute(Integer, default=4) - - # FIXME Temp hack for VPP, migrate this to an ARP table resource - mac_address = Attribute(String) + metric = Attribute(Integer) diff --git a/vicn/resource/ip/routing_table.py b/vicn/resource/ip/routing_table.py index e45793cc..ce16524c 100644 --- a/vicn/resource/ip/routing_table.py +++ b/vicn/resource/ip/routing_table.py @@ -20,28 +20,31 @@ from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.resource import Resource from vicn.core.task import EmptyTask, BashTask +from vicn.core.task import inherit_parent from vicn.resource.ip.route import IPRoute from vicn.resource.node import Node from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ROUTE from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ROUTE_GW -CMD_ADD_ROUTE = ('ip -{route.ip_version} route add {route.ip_address} ' - 'dev {route.interface.device_name} || true') -CMD_ADD_ROUTE_GW = ('ip -{route.ip_version} route add {route.ip_address} ' - 'dev {route.interface.device_name} via {route.gateway} || true') -CMD_DEL_ROUTE = ('ip -{route.ip_version} route del {route.ip_address} ' +CMD_ADD_ROUTE = ('ip -{route.ip_version} route add {route.ip_address}/{route.ip_address.prefix_len} ' + 'dev {route.interface.device_name}') +CMD_ADD_ROUTE_GW = ('ip -{route.ip_version} route add {route.ip_address}/{route.ip_address.prefix_len} ' + 'dev {route.interface.device_name} via {route.gateway}') +CMD_ADD_ROUTE_GWM = ('ip -{route.ip_version} route add {route.ip_address}/{route.ip_address.prefix_len} ' + 'dev {route.interface.device_name} via {route.gateway} metric {route.metric}') +CMD_DEL_ROUTE = ('ip -{route.ip_version} route del {route.ip_address}/{route.ip_address.prefix_len} ' 'dev {route.interface.device_name}') CMD_SHOW_ROUTES = 'ip route show' -CMD_ADD_ARP_ENTRY = 'arp -s {route.ip_address} {route.mac_address}' -CMD_ADD_NDB_ENTRY = ('ip -6 neigh add {route.ip_address} lladr {route.mac_address} ' - 'dev {route.interface.device_name}') +#CMD_ADD_ARP_ENTRY = 'arp -s {route.ip_address} {route.mac_address}' +#CMD_ADD_NDB_ENTRY = ('ip -6 neigh add {route.ip_address} lladr {route.mac_address} ' +# 'dev {route.interface.device_name}') # Populate arp table too. The current configuration with one single bridge # connecting every container and vpp nodes seem to create loops that prevent # vpp from netmodel.network.interface for routing ip packets. -VPP_ARP_FIX = True +#VPP_ARP_FIX = True def _iter_routes(out): for line in out.splitlines(): @@ -97,6 +100,7 @@ class RoutingTable(Resource): def __after__(self): return ('CentralIP', 'VPPInterface', 'ContainerSetup') + @inherit_parent def __get__(self): def cache(rv): for route in _iter_routes(rv.stdout): @@ -106,6 +110,7 @@ class RoutingTable(Resource): raise ResourceNotFound return BashTask(self.node, CMD_SHOW_ROUTES, parse=cache) + @inherit_parent def __create__(self): """ Create a single BashTask for all routes @@ -114,7 +119,6 @@ class RoutingTable(Resource): done = set() routes_cmd = list() routes_via_cmd = list() - arp_cmd = list() # vppctl lock # NOTE: we currently lock vppctl during the whole route update @@ -122,7 +126,7 @@ class RoutingTable(Resource): routes_via_lock = None for route in self.routes: - if route.ip_address in self._routes: + if str(route.ip_address) in self._routes: continue if route.ip_address in done: continue @@ -135,21 +139,22 @@ class RoutingTable(Resource): cmd = CMD_ADD_ROUTE.format(route = route) routes_cmd.append(cmd) else: - cmd = CMD_ADD_ROUTE_GW.format(route = route) - routes_via_cmd.append(cmd) - if VPP_ARP_FIX and route.mac_address: - if route.ip_address != "default": - cmd = CMD_ADD_ARP_ENTRY.format(route = route) - arp_cmd.append(cmd) + if route.metric is None: + cmd = CMD_ADD_ROUTE_GW.format(route = route) + routes_via_cmd.append(cmd) + else: + cmd = CMD_ADD_ROUTE_GWM.format(route = route) + routes_via_cmd.append(cmd) else: + if route.metric == 1025: + continue if route.gateway is None: cmd = CMD_VPP_ADD_ROUTE.format(route = route) - routes_cmd.append(cmd) routes_lock = route.node.vpp.vppctl_lock else: cmd = CMD_VPP_ADD_ROUTE_GW.format(route = route) - routes_via_cmd.append(cmd) routes_via_lock = route.node.vpp.vppctl_lock + routes_via_cmd.append(cmd) # TODO: checks clean_routes_task = EmptyTask() @@ -166,12 +171,8 @@ class RoutingTable(Resource): else: routes_via_task = EmptyTask() - if len(arp_cmd) > 0: - arp_task = BashTask(self.node, '\n'.join(arp_cmd)) - else: - arp_task = EmptyTask() - - return ((clean_routes_task > routes_task) > routes_via_task) > arp_task + return ((clean_routes_task > routes_task) > routes_via_task) + @inherit_parent def __delete__(self): raise NotImplementedError diff --git a/vicn/resource/ip_assignment.py b/vicn/resource/ip_assignment.py index 55401ecd..3bb16ff9 100644 --- a/vicn/resource/ip_assignment.py +++ b/vicn/resource/ip_assignment.py @@ -20,10 +20,12 @@ import math import logging from vicn.core.resource import Resource -from netmodel.model.type import String +from netmodel.model.type import String, Bool from vicn.core.attribute import Attribute from vicn.resource.ip.prefix_tree import Inet6Prefix, PrefixTree, Inet4Prefix +from netmodel.model.type import Inet4Address, Inet6Address from vicn.core.task import inline_task, async_task, EmptyTask +from vicn.core.task import inherit_parent from vicn.core.exception import ResourceNotFound log = logging.getLogger(__name__) @@ -31,8 +33,9 @@ log = logging.getLogger(__name__) class IpAssignment(Resource): prefix = Attribute(String, mandatory=True) control_prefix = Attribute(String, description="prefix for control plane") - max_prefix_size = Attribute(String, + max_prefix_len = Attribute(String, description="Maximum assigned prefix size for a link") + disjoint_addresses = Attribute(Bool, default=False) PrefixClass = None @@ -41,20 +44,20 @@ class IpAssignment(Resource): self._prefix = self.PrefixClass(self.prefix) self._prefix_tree = PrefixTree(self._prefix) self._assigned_prefixes = {} - if not self.max_prefix_size: - self.max_prefix_size = self.PrefixClass.MAX_PREFIX_SIZE + if not self.max_prefix_len: + self.max_prefix_len = self.PrefixClass.MAX_PREFIX_SIZE if self.control_prefix: self._ctrl_prefix = self.PrefixClass(self.control_prefix) self._ctrl_prefix_it = iter(self._ctrl_prefix) next(self._ctrl_prefix_it) #Removes internet address self._assigned_addresses = {} - def get_prefix(self, obj, prefix_size): + def get_prefix(self, obj, prefix_len): ret = None if obj in self._assigned_prefixes: ret = self._assigned_prefixes[obj] else: - ret = self._prefix_tree.get_prefix(prefix_size) + ret = self._prefix_tree.get_prefix(prefix_len) self._assigned_prefixes[obj] = ret return ret @@ -69,10 +72,12 @@ class IpAssignment(Resource): self._assigned_addresses[obj] = ret return ret + @inherit_parent @inline_task def __get__(self): raise ResourceNotFound + @inherit_parent #@inline_task def __create__(self): # XXX code from Channel.__create__, until Events are properly implemented. @@ -80,38 +85,77 @@ class IpAssignment(Resource): task = EmptyTask() for group in self.groups: for channel in group.iter_by_type_str('channel'): - interfaces = sorted(channel.interfaces, key = lambda x : x.device_name) - if not interfaces: - continue - - min_prefix_size = math.ceil(math.log(len(channel.interfaces), 2)) - prefix_size = min(self.max_prefix_size, - self.PrefixClass.MAX_PREFIX_SIZE - min_prefix_size) - prefix = self.get_prefix(channel, prefix_size) - - it = prefix.get_iterator() + if channel.has_type("emulatedchannel"): + interfaces = [channel._ap_if] + interfaces.extend(channel._sta_ifs.values()) + else: + interfaces = sorted(channel.interfaces, key = lambda x : x.device_name) + if not interfaces: + continue + + if self.PrefixClass is Inet6Prefix: + # 0 is forbidden + num_required_ip = len(interfaces) + 1 + elif channel.has_type("emulatedchannel"): #EmulatedChannel + IPv4 + num_required_ip = len(interfaces) + 2 + channel.nb_base_stations #Internet address + bcast + else: + num_required_ip = len(interfaces) + min_prefix_len = math.ceil(math.log(num_required_ip, 2)) + + prefix_len = min(self.max_prefix_len, + self.PrefixClass.MAX_PREFIX_SIZE - min_prefix_len) + + #XXX lte-emulator expects /24 + if channel.has_type("emulatedltechannel") and self.PrefixClass is Inet4Prefix: + prefix_len = 24 + + # Retrieve a prefix for the whole channel + prefix = self.get_prefix(channel, prefix_len) + + # by default iterate on /32 or /128, unless we require to + # iterate on less specific + it_prefix_len = self.PrefixClass.MAX_PREFIX_SIZE + + if self.disjoint_addresses and prefix_len+min_prefix_len <= self.PrefixClass.MAX_PREFIX_SIZE: + it_prefix_len = min_prefix_len + prefix_len + + # Use an iterator to assign IPs from the prefix to the + # interfaces + it = prefix.get_iterator(it_prefix_len) + # XXX MACCHA it is a prefix + + if channel.has_type("emulatedchannel"): + #Internet address + next(it) for interface in interfaces: - ip = next(it) - interface.set(self.ATTR_PREFIX, prefix_size) - #XXX Why do we need to create that async task? - #XXX Probably because the PendingValue is not created - #XXX in the main thread + if_prefix = next(it) + #XXX We cannot assign prefix::0 as a valid address to an interface. + #XXX For each interface with an ip6 address belonging to prefix, + #XXX linux add prefix::0 to the local routing table. Therefore prefix::0 cannot be + #XXX the address of a non local interface + ip = if_prefix.ip_address + if ip == prefix.first_prefix_address() and self.PrefixClass is Inet6Prefix: + if if_prefix.prefix_len < if_prefix.MAX_PREFIX_SIZE: + if_prefix.ip_address = ip+1 + else: + if_prefix = next(it) + + if_prefix.prefix_len = prefix_len + if self.PrefixClass is Inet6Prefix: + address = Inet6Address(if_prefix.ip_address, prefix_len) + else: + address = Inet4Address(if_prefix.ip_address, prefix_len) @async_task - async def set_ip(interface, ip): - await interface.async_set(self.ATTR_ADDRESS, self.PrefixClass.ntoa(ip.ip_address)) - task = task | set_ip(interface, ip) - + async def set_ip(interface, address): + await interface.async_set(self.ATTR_ADDRESS, address) + task = task | set_ip(interface, address) return task - __delete__ = None - class Ipv6Assignment(IpAssignment): PrefixClass = Inet6Prefix - ATTR_ADDRESS = 'ip6_address' - ATTR_PREFIX = 'ip6_prefix' + ATTR_ADDRESS = 'ip6_address' class Ipv4Assignment(IpAssignment): PrefixClass = Inet4Prefix - ATTR_ADDRESS = 'ip4_address' - ATTR_PREFIX = 'ip4_prefix' + ATTR_ADDRESS = 'ip4_address' diff --git a/vicn/resource/linux/bridge.py b/vicn/resource/linux/bridge.py index 7b5ceed7..edc8acdc 100644 --- a/vicn/resource/linux/bridge.py +++ b/vicn/resource/linux/bridge.py @@ -22,18 +22,15 @@ from vicn.core.address_mgr import AddressManager from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.requirement import Requirement -from vicn.core.task import inline_task +from vicn.core.task import inline_task, inherit from vicn.resource.channel import Channel +from vicn.resource.interface import Interface from vicn.resource.linux.bridge_mgr import BridgeManager -from vicn.resource.linux.net_device import BaseNetDevice +from vicn.resource.linux.net_device import NetDevice log = logging.getLogger(__name__) -# FIXME This should use the AddressManager to get allocated a name that does -# not exist -DEFAULT_BRIDGE_NAME = 'br0' - -class Bridge(Channel, BaseNetDevice): +class Bridge(Channel, NetDevice): """ Resource: Bridge """ @@ -62,21 +59,10 @@ class Bridge(Channel, BaseNetDevice): # Resource lifecycle #-------------------------------------------------------------------------- - @inline_task - def __get__(self): - # FIXME we currently force the recreation of the bridge, delegating the - # check to the creation function - raise ResourceNotFound - + @inherit(Channel, Interface) def __create__(self): - # FIXME : reserves .1 IP address for the bridge, provided no other - # class uses this trick - AddressManager().get_ip(self) return self.node.bridge_manager.add_bridge(self.device_name) - # Everything should be handled by BaseNetDevice - __delete__ = None - #-------------------------------------------------------------------------- # Resource lifecycle #-------------------------------------------------------------------------- @@ -86,7 +72,7 @@ class Bridge(Channel, BaseNetDevice): Returns: Task """ - return self.node.bridge_manager.add_interface(self.device_name, + return self.node.bridge_manager.add_interface(self.device_name, interface.device_name, vlan) def __method_add_interface__(self, interface, vlan=None): @@ -99,6 +85,6 @@ class Bridge(Channel, BaseNetDevice): """ log.info('Removing interface {} from bridge {}'.format( interface.device_name, self.name)) - return self.node.bridge_manager.del_interface(self.device_name, + return self.node.bridge_manager.del_interface(self.device_name, interface.device_name) diff --git a/vicn/resource/linux/certificate.py b/vicn/resource/linux/certificate.py index dd451770..2cc6c9b0 100644 --- a/vicn/resource/linux/certificate.py +++ b/vicn/resource/linux/certificate.py @@ -23,6 +23,7 @@ from vicn.core.attribute import Attribute, Multiplicity, Reference from vicn.core.exception import ResourceNotFound from vicn.core.resource import Resource from vicn.core.task import task, inline_task, BashTask +from vicn.core.task import inherit_parent from vicn.resource.linux.file import File from vicn.resource.node import Node @@ -49,29 +50,40 @@ class Certificate(Resource): multiplicity = Multiplicity.ManyToOne) cert = Attribute(String, description = 'Certificate path', mandatory = True) - key = Attribute(String, description = 'Key path', - mandatory = True) + key = Attribute(String, description = 'Key path') #-------------------------------------------------------------------------- # Resource lifecycle #-------------------------------------------------------------------------- - @inline_task - def __initialize__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self._cert_file = File(node = Reference(self, 'node'), filename = Reference(self, 'cert'), managed = False) - self._key_file = File(node = Reference(self, 'node'), - filename = Reference(self, 'key'), - managed = False) + if self.key: + self._key_file = File(node = Reference(self, 'node'), + filename = Reference(self, 'key'), + managed = False) + else: + self._key_file = None + @inherit_parent def __get__(self): - return self._cert_file.__get__() | self._key_file.__get__() + if self.key: + return self._cert_file.__get__() | self._key_file.__get__() + else: + return self._cert_file.__get__() + @inherit_parent def __create__(self): return BashTask(self.node, CMD_CREATE, {'self': self}) + @inherit_parent def __delete__(self): - return self._cert_file.__delete__() | self._key_file.__delete__() + if self.key: + return self._cert_file.__delete__() | self._key_file.__delete__() + else: + return self._cert_file.__delete__() diff --git a/vicn/resource/linux/certificate_store.py b/vicn/resource/linux/certificate_store.py new file mode 100644 index 00000000..59d9a239 --- /dev/null +++ b/vicn/resource/linux/certificate_store.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 vicn.core.attribute import Attribute, Multiplicity +from vicn.core.resource import Resource +from vicn.resource.linux.certificate import Certificate +from vicn.resource.node import Node + +PACKAGE = 'ca-certificates' + +CMD_ADD_CERTIFICATE = ''' +cp {certificate.cert} {store.PATH} +dpkg-reconfigure ca-certificates +''' + +class CertificateStore(Resource): + """ + Resource: System-wide Certificate Store + + This resource allows manipulation of the trusted certificates on the system. + Use with care. + + TODO: + - Ensure ca-certificates package is installed. + - Issue a warning to the user when it is used. + """ + + PATH = '/usr/share/ca-certificates' + + certificates = Attribute(Certificate, multiplicity = Multiplicity.OneToMany) + node = Attribute(Node, mandatory = True, requirements = [ + #Requirement(PACKAGE, 'in', 'packages') + ]) + + def _add_certificate(self): + # Return a task that takes a certificate as parameter + PARAM = None + return BashTask(self.node, CMD_ADD_CERTIFICATE, {'store': self, 'certificate': PARAM}, + root = True) diff --git a/vicn/resource/linux/dnsmasq.py b/vicn/resource/linux/dnsmasq.py index b5aa8053..70358ecb 100644 --- a/vicn/resource/linux/dnsmasq.py +++ b/vicn/resource/linux/dnsmasq.py @@ -24,6 +24,7 @@ from string import Template from netmodel.model.type import String, Bool from vicn.core.attribute import Attribute from vicn.core.resource import EmptyResource +from vicn.core.task import inherit_parent, override_parent from vicn.resource.dns_server import DnsServer from vicn.resource.interface import Interface from vicn.resource.linux.file import TextFile @@ -72,15 +73,16 @@ class DnsMasq(Service, DnsServer): log_dhcp = Attribute(Bool, description = 'Flag: log DHCP queries', default = True) - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.interface: raise Exception("Cannot initialize bridge without interface") + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inherit_parent def __subresources__(self): # Overwrite configuration file flags = list() @@ -112,3 +114,7 @@ class DnsMasq(Service, DnsServer): return TextFile(node = self.node, owner = self, filename = FN_CONF, content = conf, overwrite = True) + + @override_parent + def __create__(self): + return self.__method_stop_start() diff --git a/vicn/resource/linux/file.py b/vicn/resource/linux/file.py index 44b4b5be..2a62f60e 100644 --- a/vicn/resource/linux/file.py +++ b/vicn/resource/linux/file.py @@ -16,11 +16,12 @@ # limitations under the License. # +from netmodel.model.key import Key from netmodel.model.type import String, Bool from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.resource import Resource -from vicn.core.task import BashTask, inline_task +from vicn.core.task import BashTask, inline_task, inherit_parent from vicn.resource.node import Node CREATE_DIR_CMD = "mkdir -p {dir}" @@ -38,22 +39,23 @@ class File(Resource): Resource: File """ filename = Attribute(String, description = 'Path to the file', - key = True, mandatory = True) node = Attribute(Node, description = 'Node on which the file is created', mandatory = True, multiplicity = Multiplicity.ManyToOne, reverse_name = 'files', - key = True, reverse_description = 'Files created on the node') overwrite = Attribute(Bool, description = 'Determines whether an existing file is overwritten', default = False) + __key__ = Key(node, filename) + #-------------------------------------------------------------------------- # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __get__(self): # UGLY @inline_task @@ -72,12 +74,14 @@ class File(Resource): test = BashTask(self.node, GET_FILE_CMD, {"file": self}, parse=is_path) return test + @inherit_parent def __create__(self): ctask = BashTask(self.node, CREATE_FILE_CMD, {"file": self}) if self.overwrite: ctask = BashTask(self.node, DELETE_FILE_CMD, {'file': self}) > ctask return ctask + @inherit_parent def __delete__(self): return BashTask(self.node, DELETE_FILE_CMD, { "file" : self}) @@ -96,6 +100,8 @@ class TextFile(File): # Resource lifecycle #-------------------------------------------------------------------------- + # XXX REDUNDANT !!! + @inherit_parent def __create__(self): return BashTask(self.node, CMD_PRINT_TO_FILE, {'file': self}) diff --git a/vicn/resource/linux/folder.py b/vicn/resource/linux/folder.py new file mode 100644 index 00000000..636ecbcd --- /dev/null +++ b/vicn/resource/linux/folder.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 netmodel.model.key import Key +from netmodel.model.type import String, Bool, Integer +from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import Resource +from vicn.core.task import BashTask, inline_task, EmptyTask +from vicn.resource.node import Node + +CREATE_FOLDER_CMD = "mkdir -p {folder.foldername}" +DELETE_FOLDER_CMD = "rm -f {folder.foldername}" + +GET_FOLDER_CMD = 'test -d {folder.foldername} && readlink -e {folder.foldername}' + +SET_FOLDER_PERMISSION_CMD = 'chmod {folder.permission} {folder.foldername}' + +class Folder(Resource): + """ + Resource: Folder + """ + foldername = Attribute(String, description = 'Path to the folder', + mandatory = True) + node = Attribute(Node, description = 'Node on which the directory is created', + mandatory = True, + multiplicity = Multiplicity.ManyToOne, + reverse_name = 'folders', + reverse_description = 'Folders created on the node') + overwrite = Attribute(Bool, + description = 'Determines whether an existing folder is overwritten', + default = False) + permission = Attribute(Integer, + description = 'Permission to set in the folder', + default = 775) + + __key__ = Key(node, foldername) + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + @inline_task + def __get__(self): + # UGLY +# @inline_task +# def not_found(): + raise ResourceNotFound + + # if self.overwrite: + # return not_found() + + # def is_path (rv): + # if rv is None or rv.stdout is None or len(rv.stdout) == 0 or \ + # rv.return_value != 0: + # raise ResourceNotFound + # return {} # 'filename': rv.stdout} + + # create = BashTask(self.node, GET_FOLDER_CMD, {"folder": self}, parse=is_path) + + # return create + + def __create__(self): + ctask = BashTask(self.node, CREATE_FOLDER_CMD, {"folder": self}) + + if self.overwrite: + ctask = BashTask(self.node, DELETE_FOLDER_CMD, {'folder': self}) > ctask + + set_permission = BashTask(self.node, SET_FOLDER_PERMISSION_CMD, {"folder": self}) + + return ctask > set_permission + + def __delete__(self): + return BashTask(self.node, DELETE_FOLFER_CMD, { "folder" : self}) diff --git a/vicn/resource/linux/gre_tunnel.py b/vicn/resource/linux/gre_tunnel.py new file mode 100644 index 00000000..c5a25307 --- /dev/null +++ b/vicn/resource/linux/gre_tunnel.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 string + +from netmodel.model.type import Integer, String +from vicn.core.attribute import Attribute +from vicn.core.resource import Resource +from vicn.core.task import BashTask, inherit_parent, override_parent +from vicn.resource.channel import Channel +from vicn.resource.linux.net_device import NetDevice, SlaveNetDevice + +CMD_CREATE_GRE_TUNNEL=''' +ip tunnel add {device_name} mode gre remote {dst} local {src} ttl 255 +''' + +class GREChannel(Channel): + """ + Resource: GRETunnel + """ + pass + +class GREInterface(SlaveNetDevice): + + remote_address = Attribute(String, description ='', + mandatory = True) + + @override_parent + def __create__(self): + return BashTask(self.src_interface.node, CMD_CREATE_GRE_TUNNEL, { + 'device_name': self.device_name, + 'src': str(self.parent.ip4_address), + 'dst': self.remote_address}) + +class GRETunnel(Resource): + + src_interface = Attribute(NetDevice, description = 'source interface', + mandatory = True) + dst_interface = Attribute(NetDevice, description = 'destination interface', + mandatory = True) + + @inherit_parent + def __subresources__(self): + channel = GREChannel() + src = GREInterface(node=src_interface.node, device_name="gre0", + parent=src_interface, channel=channel) + dst = GREInterface(node=dst_interface.node, device_name="gre0", + parent=dst_interface, channel=channel) + return (src | dst) | channel diff --git a/vicn/resource/linux/iperf.py b/vicn/resource/linux/iperf.py index a0780a1c..e4b8e94c 100644 --- a/vicn/resource/linux/iperf.py +++ b/vicn/resource/linux/iperf.py @@ -16,12 +16,10 @@ # limitations under the License. # -from abc import ABC - from netmodel.model.type import Integer from vicn.core.attribute import Attribute from vicn.resource.linux.application import LinuxApplication -class Iperf3(LinuxApplication, ABC): +class Iperf3(LinuxApplication): __package_names__ = ['iperf3'] diff --git a/vicn/resource/linux/keypair.py b/vicn/resource/linux/keypair.py index 66c98e5b..b748b756 100644 --- a/vicn/resource/linux/keypair.py +++ b/vicn/resource/linux/keypair.py @@ -23,6 +23,7 @@ from vicn.core.attribute import Attribute, Multiplicity, Reference from vicn.core.exception import ResourceNotFound from vicn.core.resource import Resource from vicn.core.task import task, inline_task, BashTask +from vicn.core.task import inherit_parent from vicn.resource.linux.file import File from vicn.resource.node import Node @@ -48,8 +49,8 @@ class Keypair(Resource): # Resource lifecycle #-------------------------------------------------------------------------- - @inline_task - def __initialize__(self): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self._pubkey_file = File(node = Reference(self, 'node'), filename = self.key + '.pub', managed = False) @@ -57,14 +58,17 @@ class Keypair(Resource): filename = self.key, managed = False) + @inherit_parent def __get__(self): return self._pubkey_file.__get__() | self._key_file.__get__() + @inherit_parent def __create__(self): return BashTask(self.node, CMD_CREATE, { 'dirname': os.path.dirname(self.key), 'self': self}) + @inherit_parent def __delete__(self): return self._pubkey_file.__delete__() | self._key_file.__delete__() diff --git a/vicn/resource/linux/link.py b/vicn/resource/linux/link.py index da41fbe1..3ffaae97 100644 --- a/vicn/resource/linux/link.py +++ b/vicn/resource/linux/link.py @@ -20,12 +20,14 @@ import random import string import logging +from netmodel.model.key import Key from netmodel.model.type import Integer, String from vicn.core.attribute import Attribute, Reference from vicn.core.exception import ResourceNotFound from vicn.core.state import ResourceState, AttributeState from vicn.core.task import inline_task, async_task, run_task from vicn.core.task import get_attributes_task, BashTask +from vicn.core.task import inherit_parent from vicn.resource.channel import Channel from vicn.resource.interface import Interface from vicn.resource.linux.net_device import NonTapBaseNetDevice @@ -53,7 +55,7 @@ ip link set dev {tmp_src} netns {pid} name {interface.device_name} ip link set dev {tmp_dst} up ovs-vsctl add-port {host.bridge.device_name} {tmp_dst}''' -CMD_UP=''' +CMD_SET_UP=''' ip link set dev {interface.device_name} up ''' @@ -73,18 +75,19 @@ class Link(Channel): delay = Attribute(String, description = 'Link propagation delay') src_node = Attribute(Node, description = 'Source node', - key = True, mandatory = True) dst_node = Attribute(Node, description = 'Destination node', - key = True, mandatory = True) + __key__ = Key(src_node, dst_node) + def __init__(self, *args, **kwargs): assert 'src_node' in kwargs and 'dst_node' in kwargs self._src = None self._dst = None super().__init__(*args, **kwargs) + @inherit_parent @inline_task def __initialize__(self): # We create two managed net devices that are pre-setup @@ -124,23 +127,27 @@ class Link(Channel): vpp_src = VPPInterface(parent = self._src, vpp = self.src_node.vpp, ip4_address = Reference(self._src, 'ip4_address'), - device_name = 'vpp' + self._src.device_name) + ip6_address = Reference(self._src, 'ip6_address'), + device_name = 'host-' + self._src.device_name) manager.commit_resource(vpp_src) if hasattr(self.dst_node, 'vpp') and not self.dst_node.vpp is None: vpp_dst = VPPInterface(parent = self._dst, vpp = self.dst_node.vpp, ip4_address = Reference(self._dst, 'ip4_address'), - device_name = 'vpp' + self._dst.device_name) + ip6_address = Reference(self._dst, 'ip6_address'), + device_name = 'host-' + self._dst.device_name) manager.commit_resource(vpp_dst) #-------------------------------------------------------------------------- # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __get__(self): return (self._src.__get__() | self._dst.__get__()) > async_task(self._commit)() + @inherit_parent def __create__(self): assert self.src_node.get_type() == 'lxccontainer' assert self.dst_node.get_type() == 'lxccontainer' @@ -168,8 +175,8 @@ class Link(Channel): host = src_host create = BashTask(host, CMD_CREATE, {'interface': self, 'tmp_src': tmp_src, 'tmp_dst': tmp_dst}) - up_src = BashTask(self.src_node, CMD_UP, {'interface': self._src}) - up_dst = BashTask(self.dst_node, CMD_UP, {'interface': self._dst}) + up_src = BashTask(self.src_node, CMD_SET_UP, {'interface': self._src}) + up_dst = BashTask(self.dst_node, CMD_SET_UP, {'interface': self._dst}) up = up_src | up_dst delif = delif_src | delif_dst return ((delif > (pid @ create)) > up) > async_task(self._commit)() @@ -178,10 +185,11 @@ class Link(Channel): 'tmp_src': tmp_src, 'tmp_dst': tmp_dst, 'host' : src_host}) create2 = BashTask(dst_host, CMD_CREATE_BR_TO_LXC, {'interface': self._dst, 'tmp_src': tmp_dst, 'tmp_dst': tmp_src, 'host' : dst_host}) - up_src = BashTask(self.src_node, CMD_UP, {'interface': self._src}) - up_dst = BashTask(self.dst_node, CMD_UP, {'interface': self._dst}) + up_src = BashTask(self.src_node, CMD_SET_UP, {'interface': self._src}) + up_dst = BashTask(self.dst_node, CMD_SET_UP, {'interface': self._dst}) return (((pid_src @ create) | (pid_dst @ create2)) > (up_src | up_dst)) > async_task(self._commit)() + @inherit_parent def __delete__(self): return self._src.__delete__() | self._dst.__delete__() diff --git a/vicn/resource/linux/macvlan.py b/vicn/resource/linux/macvlan.py index ea9c37c1..3c81cde1 100644 --- a/vicn/resource/linux/macvlan.py +++ b/vicn/resource/linux/macvlan.py @@ -18,14 +18,15 @@ from netmodel.model.type import String from vicn.core.attribute import Attribute -from vicn.core.task import BashTask -from vicn.resource.linux.net_device import SlaveBaseNetDevice +from vicn.core.task import BashTask, inherit +from vicn.resource.interface import Interface +from vicn.resource.linux.net_device import SlaveNetDevice CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \ 'link {netdevice.parent.device_name} ' \ 'type {netdevice.netdevice_type} mode {netdevice.mode}' -class MacVlan(SlaveBaseNetDevice): +class MacVlan(SlaveNetDevice): """ Resource: MacVlan @@ -48,5 +49,6 @@ class MacVlan(SlaveBaseNetDevice): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit(Interface) def __create__(self): return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self}) diff --git a/vicn/resource/linux/macvtap.py b/vicn/resource/linux/macvtap.py index 82002e02..23176763 100644 --- a/vicn/resource/linux/macvtap.py +++ b/vicn/resource/linux/macvtap.py @@ -18,14 +18,15 @@ from netmodel.model.type import String from vicn.core.attribute import Attribute -from vicn.core.task import BashTask -from vicn.resource.linux.net_device import SlaveBaseNetDevice +from vicn.core.task import BashTask, inherit +from vicn.resource.interface import Interface +from vicn.resource.linux.net_device import SlaveNetDevice CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \ 'link {netdevice.parent.device_name} ' \ 'type {netdevice.netdevice_type} mode {netdevice.mode}' -class MacVtap(SlaveBaseNetDevice): +class MacVtap(SlaveNetDevice): """ Resource: MacVtap @@ -48,5 +49,6 @@ class MacVtap(SlaveBaseNetDevice): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit(Interface) def __create__(self): return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self}) diff --git a/vicn/resource/linux/net_device.py b/vicn/resource/linux/net_device.py index c393ac1a..f9ab40b1 100644 --- a/vicn/resource/linux/net_device.py +++ b/vicn/resource/linux/net_device.py @@ -23,11 +23,13 @@ import random import string from netmodel.model.type import Integer, String, Bool +from netmodel.model.type import Inet4Address, Inet6Address from vicn.core.address_mgr import AddressManager from vicn.core.attribute import Attribute from vicn.core.exception import ResourceNotFound from vicn.core.resource import BaseResource -from vicn.core.task import BashTask, task, EmptyTask +from vicn.core.task import BashTask, task, EmptyTask, inherit +from vicn.core.task import inherit_parent, override_parent from vicn.resource.linux.application import LinuxApplication as Application from vicn.resource.interface import Interface @@ -59,11 +61,11 @@ CMD_SET_MAC_ADDRESS = 'ip link set dev {netdevice.device_name} ' \ 'address {netdevice.mac_address}' CMD_GET_IP_ADDRESS = 'ip addr show {netdevice.device_name}' CMD_SET_IP4_ADDRESS = 'ip addr add dev {netdevice.device_name} ' \ - '{netdevice.ip4_address} brd + || true' + '{netdevice.ip4_address}/{netdevice.ip4_address.prefix_len} brd + || true' CMD_SET_IP6_ADDRESS = 'ip addr add dev {netdevice.device_name} ' \ - '{netdevice.ip6_address}/{netdevice.ip6_prefix} || true' + '{netdevice.ip6_address}/{netdevice.ip6_address.prefix_len} || true' CMD_SET_PROMISC = 'ip link set dev {netdevice.device_name} promisc {on_off}' -CMD_SET_UP = 'ip link set {netdevice.device_name} {up_down}' +CMD_SET_UP = 'ip link set {netdevice.device_name} {state}' CMD_SET_CAPACITY='\n'.join([ 'tc qdisc del dev {netdevice.device_name} root || true', 'tc qdisc add dev {netdevice.device_name} root handle 1: tbf rate ' @@ -92,6 +94,10 @@ CMD_UNSET_IP6_FWD = 'sysctl -w net.ipv6.conf.{netdevice.device_name}.forwarding= CMD_SET_IP6_FWD = 'sysctl -w net.ipv6.conf.{netdevice.device_name}.forwarding=1' CMD_GET_IP6_FWD = 'sysctl -n net.ipv6.conf.{netdevice.device_name}.forwarding' +DEFAULT_IP4_PREFIX_LEN = 31 +DEFAULT_IP6_PREFIX_LEN = 64 + +NetDeviceName = String.restrict(max_size = MAX_DEVICE_NAME_SIZE) #------------------------------------------------------------------------------- @@ -265,22 +271,20 @@ def parse_ip_addr(data): #------------------------------------------------------------------------------ -class BaseNetDevice(Interface, Application): +class NetDevice(Interface, Application): __type__ = BaseResource # XXX note: ethtool only required if we need to get the pci address __package_names__ = ['ethtool'] - device_name = Attribute(String, description = 'Name of the NetDevice', - default = lambda x : x._default_device_name(), - max_size = MAX_DEVICE_NAME_SIZE) + device_name = Attribute(NetDeviceName, description = 'Name of the NetDevice', + default = lambda x : x._default_device_name()) capacity = Attribute(Integer, - description = 'Capacity for interface shaping (Mb/s)') + description = 'Capacity for interface shaping (Mb/s)', + default = None) mac_address = Attribute(String, description = 'Mac address of the device') - ip4_address = Attribute(String, description = 'IP address of the device') - ip4_prefix = Attribute(Integer, description = 'Prefix for the IPv4link', default=31) #XXX 31? - ip6_address = Attribute(String, description = 'IPv6 address of the device') - ip6_prefix = Attribute(Integer, description = 'Prefix for the IPv6 link', default=64) + ip4_address = Attribute(Inet4Address, description = 'IP address of the device') + ip6_address = Attribute(Inet6Address, description = 'IPv6 address of the device') ip6_forwarding = Attribute(Bool, description = 'IPv6 forwarding', default = True) pci_address = Attribute(String, description = 'PCI bus address of the device', @@ -308,6 +312,7 @@ class BaseNetDevice(Interface, Application): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __get__(self): def check(rv): if not bool(rv): @@ -315,8 +320,11 @@ class BaseNetDevice(Interface, Application): return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True, parse=check) - __create__ = None + @inherit_parent + def __create__(self): + return BashTask(self.node, CMD_CREATE, {'netdevice': self}) + @inherit_parent def __delete__(self): return BashTask(self.node, CMD_DELETE, {'netdevice': self}) @@ -369,7 +377,7 @@ class BaseNetDevice(Interface, Application): if len(ips) > 1: log.warning('Keeping only first of many IP addresses...') ip = ips[0] - attrs['ip4_address'] = ip['ip-address'] + attrs['ip4_address'] = Inet4Address(ip['ip-address'], DEFAULT_IP4_PREFIX_LEN) else: attrs['ip4_address'] = None return attrs @@ -413,8 +421,7 @@ class BaseNetDevice(Interface, Application): if len(ips) > 1: log.warning('Keeping only first of many IPv6 addresses...') ip = ips[0] - attrs['ip6_address'] = ip['ip-address'] - attrs['ip6_prefix'] = ip['prefix'] + attrs['ip6_address'] = Inet6Address(ip_address = ip['ip-address'], prefix_len = ip['prefix']) else: attrs['ip6_address'] = None return attrs @@ -457,9 +464,9 @@ class BaseNetDevice(Interface, Application): return {'up': False} def _set_up(self): - up_down = 'up' if self.up else 'down' + state = 'up' if self.up else 'down' return BashTask(self.node, CMD_SET_UP, - {'netdevice': self, 'up_down': up_down}) + {'netdevice': self, 'state': state}) @task def _get_capacity(self): @@ -526,7 +533,7 @@ class BaseNetDevice(Interface, Application): #------------------------------------------------------------------------------ -class NonTapBaseNetDevice(BaseNetDevice): +class NonTapBaseNetDevice(NetDevice): # Tap devices for instance don't have offload offload = Attribute(Bool, description = 'Offload', default=True) @@ -534,59 +541,34 @@ class NonTapBaseNetDevice(BaseNetDevice): # Attributes #-------------------------------------------------------------------------- - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def _get_offload(self): return BashTask(self.node, CMD_GET_OFFLOAD, {'netdevice': self}, parse = lambda rv : rv.stdout.strip() == 'on') def _set_offload(self): - cmd = None - if self.offload: - cmd = CMD_SET_OFFLOAD - else: - cmd = CMD_UNSET_OFFLOAD + cmd = CMD_SET_OFFLOAD if self.offload else CMD_UNSET_OFFLOAD return BashTask(self.node, cmd, {'netdevice' : self}) #------------------------------------------------------------------------------ -class NetDevice(NonTapBaseNetDevice): - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- - - def __create__(self): - return BashTask(self.node, CMD_CREATE, {'netdevice': self}) - -#------------------------------------------------------------------------------ - -class SlaveBaseNetDevice(BaseNetDevice): +class SlaveNetDevice(NetDevice): parent = Attribute(NetDevice, description = 'Parent NetDevice') - host = Attribute(NetDevice, description = 'Host interface', - default = lambda x : x._default_host()) - - def _default_host(self): - if self.node.__class__.__name__ == 'LxcContainer': - host = self.node.node - else: - host = self.node - max_len = MAX_DEVICE_NAME_SIZE - len(self.node.name) - 1 - device_name = self.device_name[:max_len] - - return NetDevice(node = host, - device_name = '{}-{}'.format(self.node.name, device_name), - managed = False) - -#------------------------------------------------------------------------------ - -class SlaveNetDevice(SlaveBaseNetDevice): - - #-------------------------------------------------------------------------- - # Resource lifecycle - #-------------------------------------------------------------------------- +# host = Attribute(NetDevice, description = 'Host interface', +# default = lambda x : x._default_host()) +# +# def _default_host(self): +# if self.node.__class__.__name__ == 'LxcContainer': +# host = self.node.node +# else: +# host = self.node +# max_len = MAX_DEVICE_NAME_SIZE - len(self.node.name) - 1 +# device_name = self.device_name[:max_len] +# +# return NetDevice(node = host, +# device_name = '{}-{}'.format(self.node.name, device_name), +# managed = False) + @override_parent def __create__(self): return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self}) diff --git a/vicn/resource/linux/netmon.py b/vicn/resource/linux/netmon.py index 8472f308..9c5c97fd 100644 --- a/vicn/resource/linux/netmon.py +++ b/vicn/resource/linux/netmon.py @@ -25,5 +25,5 @@ class NetMon(Service): Generic network monitoring daemon, used internally by VICN for resource monitoring. """ - __package_names__ = ['netmon'] + __package_names__ = [] # XXX transition 'netmon'] __service_name__ = 'netmon' diff --git a/vicn/resource/linux/ovs.py b/vicn/resource/linux/ovs.py index d67e4bca..c0b57151 100644 --- a/vicn/resource/linux/ovs.py +++ b/vicn/resource/linux/ovs.py @@ -46,12 +46,12 @@ class OVS(BridgeManager): #--------------------------------------------------------------------------- def add_bridge(self, bridge_name): - return BashTask(self.node, CMD_ADD_BRIDGE, + return BashTask(self.node, CMD_ADD_BRIDGE, {'bridge_name': bridge_name}, output = False, as_root = True) def del_bridge(self, bridge_name): - return BashTask(self.node, CMD_DEL_BRIDGE, + return BashTask(self.node, CMD_DEL_BRIDGE, {'bridge_name': bridge_name}, output = False, as_root = True) @@ -62,7 +62,7 @@ class OVS(BridgeManager): output = False, as_root = True) def del_interface(self, bridge_name, interface_name, vlan=None): - return BashTask(self.node, CMD_DEL_INTERFACE, + return BashTask(self.node, CMD_DEL_INTERFACE, {'bridge_name': bridge_name, 'interface_name': interface_name, 'vlan': vlan}, output = False, as_root = True) diff --git a/vicn/resource/linux/package_manager.py b/vicn/resource/linux/package_manager.py index 93241502..04a47986 100644 --- a/vicn/resource/linux/package_manager.py +++ b/vicn/resource/linux/package_manager.py @@ -19,13 +19,14 @@ import asyncio import logging +from netmodel.model.key import Key from netmodel.model.type import String, Bool from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.requirement import Requirement from vicn.core.resource import Resource from vicn.core.task import BashTask, EmptyTask, async_task -from vicn.core.task import inline_task, run_task +from vicn.core.task import inline_task, run_task, inherit_parent from vicn.resource.node import Node log = logging.getLogger(__name__) @@ -78,12 +79,13 @@ class PackageManager(Resource): reverse_name = 'package_manager', reverse_auto = True, mandatory = True, - key = True, multiplicity = Multiplicity.OneToOne) trusted = Attribute(Bool, description="Force repository trust", default=False) + __key__ = Key(node) + #-------------------------------------------------------------------------- # Constructor and Accessors #-------------------------------------------------------------------------- @@ -100,6 +102,7 @@ class PackageManager(Resource): def __after__(self): return ('Repository',) + @inherit_parent @inline_task def __get__(self): raise ResourceNotFound @@ -182,21 +185,25 @@ class Package(Resource): package_name = Attribute(String, mandatory = True) node = Attribute(Node, mandatory = True, - key = True, requirements=[ Requirement('package_manager') ]) + __key__ = Key(node) + #--------------------------------------------------------------------------- # Resource lifecycle #--------------------------------------------------------------------------- + @inherit_parent def __get__(self): return BashTask(self.node, CMD_PKG_TEST, {'self': self}) + @inherit_parent def __create__(self): return self.node.package_manager.__method_install__(self.package_name) + @inherit_parent @async_task async def __delete__(self): with await self.node.package_manager._lock: @@ -218,15 +225,17 @@ class Packages(Resource): names = Attribute(String, multiplicity = Multiplicity.OneToMany) node = Attribute(Node, mandatory = True, - key = True, requirements=[ Requirement('package_manager') ]) + __key__ = Key(node) + #--------------------------------------------------------------------------- # Resource lifecycle #--------------------------------------------------------------------------- + @inherit_parent def __subresources__(self): """ Note: Although packages are (rightfully) specified concurrent, apt tasks diff --git a/vicn/resource/linux/phy_interface.py b/vicn/resource/linux/phy_interface.py index 81d2950c..8d7f02c8 100644 --- a/vicn/resource/linux/phy_interface.py +++ b/vicn/resource/linux/phy_interface.py @@ -16,7 +16,8 @@ # limitations under the License. # -from netmodel.model.type import String +from netmodel.model.type import String, Integer +from netmodel.model.type import Inet4Address, Inet6Address from vicn.core.attribute import Attribute from vicn.core.resource import BaseResource from vicn.resource.interface import Interface @@ -34,17 +35,6 @@ class PhyInterface(Interface): mandatory = True) pci_address = Attribute(String, description = "Device's PCI bus address", mandatory = True) - mac_address = Attribute(String, description = "Device's MAC address", - mandatory=True) - ip4_address = Attribute(String, description = "Device's IP address") - ip6_address = Attribute(String, description = "Device's IP address") - - #-------------------------------------------------------------------------- - # Constructor and Accessors - #-------------------------------------------------------------------------- - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if not self.name: - self.name = self.node.name + '-' + self.device_name + mac_address = Attribute(String, description = "Device's MAC address") + ip4_address = Attribute(Inet4Address, description = "Device's IP address") + ip6_address = Attribute(Inet6Address, description = "Device's IP address") diff --git a/vicn/resource/linux/phy_link.py b/vicn/resource/linux/phy_link.py index 878cf7c6..2976b7f0 100644 --- a/vicn/resource/linux/phy_link.py +++ b/vicn/resource/linux/phy_link.py @@ -16,10 +16,12 @@ # limitations under the License. # -from vicn.core.attribute import Attribute -from vicn.core.task import inline_task +from vicn.core.attribute import Attribute, Reference +from vicn.core.task import inline_task, async_task +from vicn.core.task import inherit_parent from vicn.resource.channel import Channel from vicn.resource.linux.phy_interface import PhyInterface +from vicn.resource.vpp.interface import VPPInterface class PhyLink(Channel): """ @@ -28,12 +30,53 @@ class PhyLink(Channel): Physical Link to inform the orchestrator about Layer2 connectivity. """ - src = Attribute(PhyInterface, description = 'Source interface', + src = Attribute(PhyInterface, description = 'Source interface', mandatory = True) - dst = Attribute(PhyInterface, description = 'Destination interface', + dst = Attribute(PhyInterface, description = 'Destination interface', mandatory = True) + @inherit_parent @inline_task def __initialize__(self): self.src.set('channel', self) self.dst.set('channel', self) + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + async def _commit(self): + manager = self._state.manager + + # Disable rp_filtering + # self.src.rp_filter = False + # self.dst.rp_filter = False + + #XXX VPP + vpp_src = VPPInterface(parent = self.src, + vpp = self.src.node.vpp, + ip4_address = Reference(self.src, 'ip4_address'), + ip6_address = Reference(self.src, 'ip6_address'), + device_name = self.src.device_name) + manager.commit_resource(vpp_src) + + + vpp_dst = VPPInterface(parent = self.dst, + vpp = self.dst.node.vpp, + ip4_address = Reference(self.dst, 'ip4_address'), + ip6_address = Reference(self.dst, 'ip6_address'), + device_name = self.dst.device_name) + manager.commit_resource(vpp_dst) + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __get__(self): + return async_task(self._commit)() + + def __create__(self): + assert self.src.node.get_type() == 'lxccontainer' + assert self.dst.node.get_type() == 'lxccontainer' + + return async_task(self._commit)() diff --git a/vicn/resource/linux/physical.py b/vicn/resource/linux/physical.py index f71b5856..c058ff16 100644 --- a/vicn/resource/linux/physical.py +++ b/vicn/resource/linux/physical.py @@ -29,6 +29,7 @@ from vicn.core.attribute import Attribute from vicn.core.commands import Command, ReturnValue from vicn.core.exception import ResourceNotFound, VICNException from vicn.core.task import Task, task, EmptyTask +from vicn.core.task import inherit_parent from vicn.resource.linux.keypair import Keypair from vicn.resource.node import Node, DEFAULT_USERNAME from vicn.resource.node import DEFAULT_SSH_PUBLIC_KEY @@ -71,14 +72,16 @@ class Physical(Node): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __subresources__(self): """ Require a SSH keypair to be present for authentication on nodes """ return Keypair(node = self, key = FN_KEY) + @inherit_parent def __initialize__(self): - if not is_local_host(self.hostname): + if self.managed and not is_local_host(self.hostname): """ Initialization require the ssh port to be open on the node, and the ssh public key to be copied on the remote node. @@ -124,6 +127,7 @@ class Physical(Node): p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) if output: out, err = p.communicate() + #print('error {}, output {}'.format(err,out)) return ReturnValue(p.returncode, stdout=out, stderr=err) p.wait() diff --git a/vicn/resource/linux/qtplayer.py b/vicn/resource/linux/qtplayer.py new file mode 100644 index 00000000..44b10ccf --- /dev/null +++ b/vicn/resource/linux/qtplayer.py @@ -0,0 +1,5 @@ +from vicn.resource.linux.application import LinuxApplication + +class QtPlayer(LinuxApplication): + pass + diff --git a/vicn/resource/linux/repository.py b/vicn/resource/linux/repository.py index cd740d38..f07421ba 100644 --- a/vicn/resource/linux/repository.py +++ b/vicn/resource/linux/repository.py @@ -31,14 +31,14 @@ class Repository(Application): part of any basic distribution install. """ - repo_name = Attribute(String, description = 'Name of the repository', + repo_name = Attribute(String, description = 'Name of the repository', default = 'vicn') - directory = Attribute(String, description = 'Directory holding packages', + directory = Attribute(String, description = 'Directory holding packages', default = '') sections = Attribute(String, description = 'Sections', multiplicity = Multiplicity.OneToMany, default = []) - distributions = Attribute(String, + distributions = Attribute(String, description = 'List of distributions served by this repository', multiplicity = Multiplicity.ManyToMany, default = ['sid', 'trusty', 'xenial']) diff --git a/vicn/resource/linux/service.py b/vicn/resource/linux/service.py index 3eb753fc..1143461a 100644 --- a/vicn/resource/linux/service.py +++ b/vicn/resource/linux/service.py @@ -21,6 +21,7 @@ import logging from vicn.core.exception import ResourceNotFound from vicn.core.resource import CategoryResource from vicn.core.task import inline_task, BashTask, EmptyTask +from vicn.core.task import inherit_parent from vicn.resource.linux.application import LinuxApplication log = logging.getLogger(__name__) @@ -51,33 +52,33 @@ class Service(LinuxApplication): __type__ = CategoryResource - + @inherit_parent @inline_task def __get__(self): raise ResourceNotFound - + def __method_restart__(self): - return BashTask(self.node, CMD_RESTART, + return BashTask(self.node, CMD_RESTART, {'service_name': self.__service_name__}) - + def __method_start__(self): return BashTask(self.node, CMD_START, {'service_name': self.__service_name__}) - - def __create__(self): - if self.__service_name__ == 'lxd': - log.warning('Not restarting LXD') - return EmptyTask() + def __method_stop__(self): + return BashTask(self.node, CMD_STOP, + {'service_name': self.__service_name__}) - if self.__service_name__ == 'dnsmasq': - return BashTask(self.node, CMD_STOP_START, - {'service_name': self.__service_name__}) + def __method_stop_start(self): + return BashTask(self.node, CMD_STOP_START, + {'service_name': self.__service_name__}) + @inherit_parent + def __create__(self): return self.__method_restart__() - + @inherit_parent def __delete__(self): - return BashTask(self.node, CMD_STOP, + return BashTask(self.node, CMD_STOP, {'service_name': self.__service_name__}) diff --git a/vicn/resource/linux/sym_veth_pair.py b/vicn/resource/linux/sym_veth_pair.py index bf79a69b..ebfe2968 100644 --- a/vicn/resource/linux/sym_veth_pair.py +++ b/vicn/resource/linux/sym_veth_pair.py @@ -26,12 +26,12 @@ from vicn.core.resource import Resource from vicn.core.state import ResourceState, AttributeState from vicn.core.task import BashTask, get_attributes_task from vicn.core.task import async_task, task, inline_task -from vicn.core.task import run_task +from vicn.core.task import run_task, inherit_parent from vicn.resource.interface import Interface from vicn.resource.node import Node from vicn.resource.linux.net_device import NonTapBaseNetDevice from vicn.resource.linux.link import CMD_DELETE_IF_EXISTS -from vicn.resource.linux.link import CMD_UP +from vicn.resource.linux.link import CMD_SET_UP CMD_CREATE=''' # Create veth pair in the host node @@ -48,13 +48,13 @@ class SymVethPair(Resource): resource is that is it not a channel. """ - node1 = Attribute(Node, + node1 = Attribute(Node, description = 'Node on which one side of the veth will sit', mandatory = True) - node2 = Attribute(Node, + node2 = Attribute(Node, description = 'Node on which the other side of the veth will sit', mandatory = True) - capacity = Attribute(Integer, + capacity = Attribute(Integer, description = 'Capacity of the veth pair (Mb/s)') side1 = Attribute(Interface, description = 'Source interface') side2 = Attribute(Interface, description = 'Destination interface') @@ -66,21 +66,22 @@ class SymVethPair(Resource): async def _commit(self): # see link.py for explanations manager = self._state.manager - await manager._set_resource_state(self.side1, + await manager._set_resource_state(self.side1, ResourceState.INITIALIZED) - await manager._set_resource_state(self.side2, + await manager._set_resource_state(self.side2, ResourceState.INITIALIZED) await manager._set_resource_state(self.side1, ResourceState.CREATED) await manager._set_resource_state(self.side2, ResourceState.CREATED) await manager._set_attribute_state(self, 'side1', AttributeState.CLEAN) await manager._set_attribute_state(self, 'side2', AttributeState.CLEAN) manager.commit_resource(self.side1) - manager.commit_resource(self.side2) + manager.commit_resource(self.side2) #-------------------------------------------------------------------------- # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent @inline_task def __initialize__(self): self.side1 = NonTapBaseNetDevice(node = self.node1, @@ -94,6 +95,7 @@ class SymVethPair(Resource): self.side1.remote = self.side2 self.side2.remote = self.side1 + @inherit_parent @async_task async def __get__(self): manager = self._state.manager @@ -106,46 +108,48 @@ class SymVethPair(Resource): await self._commit() + @inherit_parent def __create__(self): assert self.node1.get_type() == 'lxccontainer' assert self.node2.get_type() == 'lxccontainer' - + node1_host = self.node1.node node2_host = self.node2.node - + assert node1_host == node2_host host = node1_host - + # Sometimes a down interface persists on one side - delif_side1 = BashTask(self.node1, CMD_DELETE_IF_EXISTS, + delif_side1 = BashTask(self.node1, CMD_DELETE_IF_EXISTS, {'interface': self.side1}) - delif_side2 = BashTask(self.node2, CMD_DELETE_IF_EXISTS, + delif_side2 = BashTask(self.node2, CMD_DELETE_IF_EXISTS, {'interface': self.side2}) - + pid_node1 = get_attributes_task(self.node1, ['pid']) pid_node2 = get_attributes_task(self.node2, ['pid']) - + tmp_side1 = 'tmp-veth-' + ''.join(random.choice( string.ascii_uppercase + string.digits) for _ in range(5)) tmp_side2 = 'tmp-veth-' + ''.join(random.choice( string.ascii_uppercase + string.digits) for _ in range(5)) - - create = BashTask(host, CMD_CREATE, + + create = BashTask(host, CMD_CREATE, {'side1_device_name': self.side1.device_name, - 'side2_device_name': self.side2.device_name, + 'side2_device_name': self.side2.device_name, 'tmp_side1': tmp_side1, 'tmp_side2': tmp_side2}) - - up_side1 = BashTask(self.node1, CMD_UP, {'interface': self.side1}) - up_side2 = BashTask(self.node2, CMD_UP, {'interface': self.side2}) - + + up_side1 = BashTask(self.node1, CMD_SET_UP, {'interface': self.side1}) + up_side2 = BashTask(self.node2, CMD_SET_UP, {'interface': self.side2}) + @async_task async def set_state(): await self._commit() - + delif = delif_side1 | delif_side2 up = up_side1 | up_side2 pid = pid_node1 | pid_node2 return ((delif > (pid @ create)) > up) > set_state() - + + @inherit_parent def __delete__(self): raise NotImplementedError diff --git a/vicn/resource/linux/tap_device.py b/vicn/resource/linux/tap_device.py index b7c9f967..88ca055d 100644 --- a/vicn/resource/linux/tap_device.py +++ b/vicn/resource/linux/tap_device.py @@ -18,39 +18,22 @@ from netmodel.model.type import String from vicn.core.attribute import Attribute -from vicn.core.task import BashTask -from vicn.resource.linux.net_device import BaseNetDevice, IPV4, IPV6, CMD_FLUSH_IP +from vicn.core.task import BashTask, override_parent +from vicn.resource.linux.net_device import NetDevice, IPV4, IPV6, CMD_FLUSH_IP CMD_CREATE='ip tuntap add name {netdevice.device_name} mode tap' -#CMD_SET_IP_ADDRESS='ip -{version} addr add dev {netdevice.device_name} 0.0.0.0' -class TapDevice(BaseNetDevice): +class TapDevice(NetDevice): def __init__(self, *args, **kwargs): super().__init__(self, *args, **kwargs) self.prefix = 'tap' self.netdevice_type = 'tap' + @override_parent def __create__(self): return BashTask(self.node, CMD_CREATE, {'netdevice': self}) -##mengueha: do we actually need that? -# def _set_ip4_address(self): -# if self.ip4_address is None: -# # Unset IP -# return BashTask(self.node, CMD_FLUSH_IP, -# {'device_name': self.device_name}) -# return BashTask(self.node, CMD_SET_IP_ADDRESS, -# {'netdevice': self}) -# -# def _set_ip6_address(self): -# if self.ip6_address is None: -# # Unset IP -# return BashTask(self.node, CMD_FLUSH_IP, -# {'ip_version': IPV6, 'device_name': self.device_name}) -# return BashTask(self.node, CMD_SET_IP_ADDRESS, -# {'netdevice': self}) - class TapChannel(TapDevice): station_name = Attribute(String) channel_name = Attribute(String) diff --git a/vicn/resource/linux/veth_pair.py b/vicn/resource/linux/veth_pair.py index 53fa9bf8..52050074 100644 --- a/vicn/resource/linux/veth_pair.py +++ b/vicn/resource/linux/veth_pair.py @@ -15,48 +15,82 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import random -import string -from vicn.resource.linux.net_device import SlaveBaseNetDevice -from vicn.core.task import BashTask, get_attributes_task - -# ip link add veth0 type veth peer name veth1 +from netmodel.model.key import Key +from netmodel.model.type import String, Bool +from vicn.core.attribute import Attribute, Reference +from vicn.core.task import BashTask, inline_task, get_attributes_task +from vicn.resource.linux.net_device import NetDevice, NetDeviceName +from vicn.resource.node import Node +from vicn.resource.symmetric_channel import SymmetricChannel CMD_CREATE=''' # Create veth pair in the host node -ip link add name {interface.host.device_name} type veth peer name {tmp_name} +ip link add name {self.device_name} type veth peer name {self.peer_device_name} +''' + +DEPRECATED_CMD_UP=''' # The host interface will always be up... ip link set dev {interface.host.device_name} up + # Move interface into container and rename it ip link set dev {tmp_name} netns {pid} name {interface.device_name} -''' -CMD_UP=''' ip link set dev {interface.device_name} up ''' -# see: -# http://stackoverflow.com/questions/22780927/lxc-linux-containers-add-new-network-interface-without-restarting +# Forward declaration +class VethPair(SymmetricChannel): + pass + +class VethNetDevice(NetDevice): + parent = Attribute(VethPair, mandatory = True, ro = True) -class VethPair(SlaveBaseNetDevice): - # Do not need the parent attribute... + __get__ = None + __create__ = None + __delete__ = None - def __init__(self, *args, **kwargs): - super().__init__(self, *args, **kwargs) - self.prefix = 'veth' - self.netdevice_type = 'veth' +class VethPair(SymmetricChannel): + # Mimics NetDevice for using its __get__ and __delete__ functions + node = Attribute(Node) + device_name = Attribute(NetDeviceName) + peer_device_name = Attribute(NetDeviceName) + capacity = Attribute(String) + src = Attribute(ro = True, mandatory = False) + dst = Attribute(ro = True, mandatory = False) + auto_commit = Attribute(Bool, description = 'Auto commit interfaces') + + __key1__ = Key(node, device_name) + __key2__ = Key(node, peer_device_name) + + @inline_task + def _commit(self): + if self.auto_commit: + manager = self._state.manager + + manager.commit_resource(self.src) + manager.commit_resource(self.dst) + + def __initialize__(self): + # XXX owner prevents the resource to be committed + self.src = VethNetDevice(node = self.node, + parent = self, + device_name = self.device_name, + channel = self, + capacity = Reference(self, 'capacity'), + owner = self) + self.dst = VethNetDevice(node = self.node, + parent = self, + device_name = self.peer_device_name, + channel = self, + capacity = Reference(self, 'capacity'), + owner = self) def __create__(self): - assert self.node.__class__.__name__ == 'LxcContainer' - host = self.node.node - pid = get_attributes_task(self.node, ['pid']) - tmp_name = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase \ - + string.digits) for _ in range(5)) - create = BashTask(host, CMD_CREATE, {'tmp_name': tmp_name, - 'interface': self}) - up = BashTask(self.node, CMD_UP, {'interface': self}) - bridge = host.bridge_manager.add_interface(host.bridge.device_name, - self.host.device_name) - return ((pid @ create) > up) > bridge - - # ... IP and UP missing... + veth = BashTask(self.node, CMD_CREATE, {'self': self}) + return (veth > super().__create__()) > self._commit() + + def __get__(self): + return NetDevice.__get__(self) > self._commit() + + def __delete__(self): + return NetDevice.__delete__(self) diff --git a/vicn/resource/linux/veth_pair_lxc.py b/vicn/resource/linux/veth_pair_lxc.py new file mode 100644 index 00000000..dd26b7bb --- /dev/null +++ b/vicn/resource/linux/veth_pair_lxc.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 random +import string + +from vicn.core.attribute import Attribute +from vicn.resource.linux.net_device import NetDevice, SlaveNetDevice +from vicn.core.task import BashTask, get_attributes_task +from vicn.core.task import override_parent +from vicn.core.attribute import Attribute + +# ip link add veth0 type veth peer name veth1 + +CMD_CREATE=''' +# Create veth pair in the host node +ip link add name {interface.host.device_name} type veth peer name {tmp_name} +# The host interface will always be up... +ip link set dev {interface.host.device_name} up +# Move interface into container and rename it +ip link set dev {tmp_name} netns {pid} name {interface.device_name} +''' +CMD_UP=''' +ip link set dev {interface.device_name} up +''' + +# see: +# http://stackoverflow.com/questions/22780927/lxc-linux-containers-add-new-network-interface-without-restarting + +class VethPairLxc(SlaveNetDevice): + + host = Attribute(NetDevice, description = 'Host interface', + default = lambda x : x._default_host()) + + def _default_host(self): + if self.node.__class__.__name__ == 'LxcContainer': + host = self.node.node + else: + host = self.node + max_len = MAX_DEVICE_NAME_SIZE - len(self.node.name) - 1 + device_name = self.device_name[:max_len] + + return NetDevice(node = host, + device_name = '{}-{}'.format(self.node.name, device_name), + managed = False) + + def __create__(self): + assert self.node.__class__.__name__ == 'LxcContainer' + host = self.node.node + pid = get_attributes_task(self.node, ['pid']) + tmp_name = 'tmp-veth-' + ''.join(random.choice(string.ascii_uppercase \ + + string.digits) for _ in range(5)) + create = BashTask(host, CMD_CREATE, {'tmp_name': tmp_name, + 'interface': self}) + up = BashTask(self.node, CMD_UP, {'interface': self}) + bridge = host.bridge_manager.add_interface(host.bridge.device_name, + self.host.device_name) + return ((pid @ create) > up) > bridge + + # ... IP and UP missing... diff --git a/vicn/resource/lxd/lxc_container.py b/vicn/resource/lxd/lxc_container.py index 8c8b4816..5b1d4e3b 100644 --- a/vicn/resource/lxd/lxc_container.py +++ b/vicn/resource/lxd/lxc_container.py @@ -32,9 +32,11 @@ from vicn.core.exception import ResourceNotFound from vicn.core.requirement import Requirement from vicn.core.resource_mgr import wait_resource_task from vicn.core.task import task, inline_task, BashTask, EmptyTask +from vicn.core.task import inherit_parent from vicn.resource.linux.net_device import NetDevice from vicn.resource.node import Node from vicn.resource.vpp.scripts import APPARMOR_VPP_PROFILE +from vicn.resource.lxd.lxc_image import LxcImage from vicn.resource.lxd.lxd_profile import LXD_PROFILE_DEFAULT_IFNAME log = logging.getLogger(__name__) @@ -54,9 +56,20 @@ CMD_GET_IP6_FWD = 'sysctl -n net.ipv6.conf.all.forwarding' CMD_NETWORK_DHCP='dhclient {container.management_interface.device_name}' +CMD_MOUNT_FOLDER=''' +lxc config device add {container.name} {device-name} disk source={host_path} path={container_path} +sleep 1 +''' + # Type: ContainerName -ContainerName = String(max_size = 64, ascii = True, - forbidden = ('/', ',', ':')) +# https://github.com/lxc/lxd/issues/1431 +# [...] all container names must be a valid hostname under the most +#restrictive definition of this, that is, maximum 63 characters, may not contain +#dots, may not start by a digit or dash, may not end by a dash and must be made +#entirely of letters, digits or hyphens. +# XXX better have a allowed property +ContainerName = String.restrict(max_size = 63, ascii = True, + forbidden = ('/', ',', ':', '_')) class LxcContainer(Node): """ @@ -93,9 +106,9 @@ class LxcContainer(Node): ]) profiles = Attribute(String, multiplicity = Multiplicity.OneToMany, default = ['vicn']) - image = Attribute(String, description = 'image', default = None) - is_image = Attribute(Bool, defaut = False) - pid = Attribute(Integer, description = 'PID of the container') + image = Attribute(LxcImage, description = 'image', default = None) + is_image = Attribute(Bool, default = False) + pid = Attribute(Integer, description = 'PID of the container', ro = True) ip6_forwarding = Attribute(Bool, default=True) #-------------------------------------------------------------------------- @@ -110,6 +123,7 @@ class LxcContainer(Node): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent @inline_task def __initialize__(self): """ @@ -128,6 +142,7 @@ class LxcContainer(Node): if iface.get_type() == "dpdkdevice": self.node.vpp_host.dpdk_devices.append(iface.pci_address) + @inherit_parent @task def __get__(self): client = self.node.lxd_hypervisor.client @@ -136,6 +151,7 @@ class LxcContainer(Node): except pylxd.exceptions.NotFound: raise ResourceNotFound + @inherit_parent def __create__(self): """ Make sure vpp_host is instanciated before starting the container. @@ -153,6 +169,7 @@ class LxcContainer(Node): def _create_container(self): container = self._get_container_description() log.debug('Container description: {}'.format(container)) + print('Container description: {}'.format(container)) client = self.node.lxd_hypervisor.client self._container = client.containers.create(container, wait=True) @@ -188,13 +205,13 @@ class LxcContainer(Node): # SOURCE image_names = [alias['name'] for alias in self.node.lxd_hypervisor.aliases] - image_exists = self.image is not None and self.image in image_names + image_exists = self.image.image is not None and self.image.image in image_names if image_exists: container['source'] = { 'type' : 'image', 'mode' : 'local', - 'alias' : self.image, + 'alias' : self.image.image, } else: container['source'] = { @@ -234,6 +251,7 @@ class LxcContainer(Node): Method: Start the container """ self._container.start(wait = True) + import time; time.sleep(1) @task def __method_stop__(self): @@ -288,10 +306,35 @@ class LxcContainer(Node): """ if not self._container: - log.error("Executing command on uninitialized container", self, command) + log.error("Executing command on uninitialized container {} {}".format(self, command)) import os; os._exit(1) - ret = self._container.execute(shlex.split(command)) + if 'vppctl_wrapper' in command: + vpp_log = '{}/vpp-{}.sh'.format(self._state.manager._base, self.name) + with open(vpp_log, 'a') as f: + print("lxc exec {} -- {}".format(self.name, command), file=f) + + # XXX Workaround: pylxd 2.2.3 buggy (w/ lxd 2.14) ? + # But this workaround is broken with lxd 2.15 and pylxd 2.2.4 works + # lxc exec freezes + #return self.node.execute('lxc exec {} -- {}'.format(self.name, command), + # output = output, as_root = as_root) + + print("lxc exec {} -- {}".format(self.name, command)) + while True: + try: + ret = self._container.execute(shlex.split(command)) + break + except pylxd.exceptions.NotFound: + print("=====> pylxd not found during {}".format(command)) + time.sleep(1) + except pylxd.exceptions.ClientConnectionFailed: + print("=====> pylxd connection failed during {}".format(command)) + time.sleep(1) + except requests.exceptions.SSLError: + print("=====> ssl error during {}".format(command)) + time.sleep(1) + # NOTE: pylxd documents the return value as a tuple, while it is in # fact a ContainerExecuteResult object @@ -306,12 +349,8 @@ class LxcContainer(Node): return ReturnValue(*args) def _get_ip6_forwarding(self): - def parse(rv): - ret = {"ip6_forwarding" : False} - if rv.stdout == "1": - ret["ip6_forwarding"] = True - return ret - return BashTask(self, CMD_GET_IP6_FWD, parse=parse) + return BashTask(self, CMD_GET_IP6_FWD, + parse = lambda rv: {'ip6_forwarding' : rv.stdout == "1"}) def _set_ip6_forwarding(self): cmd = CMD_SET_IP6_FWD if self.ip6_forwarding else CMD_UNSET_IP6_FWD diff --git a/vicn/resource/lxd/lxc_image.py b/vicn/resource/lxd/lxc_image.py index a3a03245..f630fe2f 100644 --- a/vicn/resource/lxd/lxc_image.py +++ b/vicn/resource/lxd/lxc_image.py @@ -24,7 +24,7 @@ from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.requirement import Requirement from vicn.core.resource import Resource -from vicn.core.task import task, inline_task +from vicn.core.task import task, inline_task, inherit_parent from vicn.resource.linux.application import LinuxApplication as Application from vicn.resource.node import Node @@ -56,8 +56,11 @@ class LxcImage(Resource): # Resource lifecycle #--------------------------------------------------------------------------- + @inherit_parent @task def __get__(self): + log.warning('Image test is currently disabled') + return aliases = [alias['name'] for images in self.node.lxd_hypervisor.client.images.all() for alias in images.aliases] if not self.image in aliases: @@ -69,6 +72,7 @@ class LxcImage(Resource): return + @inherit_parent @task def __create_DISABLED__(self): """ @@ -97,6 +101,7 @@ class LxcImage(Resource): tmp_container.delete() + @inherit_parent @task def __delete__(self): self.node.lxd_hypervisor.client.images.delete(self.name) diff --git a/vicn/resource/lxd/lxd_certificate_store.py b/vicn/resource/lxd/lxd_certificate_store.py new file mode 100644 index 00000000..97a8cada --- /dev/null +++ b/vicn/resource/lxd/lxd_certificate_store.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 vicn.core.attribute import Attribute, Multiplicity +from vicn.core.resource import Resource +from vicn.resource.linux.certificate import Certificate +from vicn.resource.node import Node + +PACKAGE = 'ca-certificates' + +CMD_ADD_CERTIFICATE = 'cp {certificate.cert} {store.PATH}' + +class LxdCertificateStore(Resource): + """ + Resource: System-wide Certificate Store + + This resource allows manipulation of the trusted certificates on the system. + Use with care. + + See. vicn.resource.linux.certificate_store + """ + + PATH = '~/.config/lxc/servercerts/' + + certificates = Attribute(Certificate, multiplicity = Multiplicity.OneToMany) + node = Attribute(Node, mandatory = True, requirements = [ + #Requirement(PACKAGE, 'in', 'packages') + ]) + + def _add_certificate(self): + # Return a task that takes a certificate as parameter + PARAM = None + return BashTask(self.node, CMD_ADD_CERTIFICATE, {'store': self, 'certificate': PARAM}) diff --git a/vicn/resource/lxd/lxd_hypervisor.py b/vicn/resource/lxd/lxd_hypervisor.py index bbdba7c6..fa63b96b 100644 --- a/vicn/resource/lxd/lxd_hypervisor.py +++ b/vicn/resource/lxd/lxd_hypervisor.py @@ -16,15 +16,6 @@ # limitations under the License. # -#------------------------------------------------------------------------------- -# NOTES -#------------------------------------------------------------------------------- -# - lxd >= 2.0.4 is required -# daemon/container: Remember the return code in the non wait-for-websocket -# case (Issue #2243) -# - Reference: https://github.com/lxc/lxd/tree/master/doc -#------------------------------------------------------------------------------- - import logging import os from pylxd import Client @@ -34,7 +25,8 @@ from netmodel.model.type import String, Integer from vicn.core.attribute import Attribute, Multiplicity, Reference from vicn.core.exception import ResourceNotFound from vicn.core.resource import Resource -from vicn.core.task import BashTask, task +from vicn.core.task import EmptyTask, BashTask, task +from vicn.core.task import inherit_parent, override_parent from vicn.resource.linux.application import LinuxApplication as Application from vicn.resource.linux.service import Service from vicn.resource.linux.certificate import Certificate @@ -74,10 +66,12 @@ CMD_LXD_NETWORK_SET = 'lxc network create {lxd_hypervisor.network} || true' class LxdInit(Application): __package_names__ = ['lxd', 'zfsutils-linux', 'lsof'] + @inherit_parent def __get__(self): return BashTask(self.owner.node, CMD_LXD_CHECK_INIT, {'lxd': self.owner}) + @inherit_parent def __create__(self): cmd_params = { 'storage-backend' : self.owner.storage_backend, @@ -107,12 +101,14 @@ class LxdInit(Application): # zfs-dkms in the host return BashTask(self.owner.node, cmd, as_root = True) + @inherit_parent def __delete__(self): raise NotImplementedError class LxdInstallCert(Resource): certificate = Attribute(Certificate, mandatory = True) + @inherit_parent @task def __get__(self): try: @@ -126,6 +122,7 @@ class LxdInstallCert(Resource): raise ResourceNotFound + @inherit_parent @task def __create__(self): """ @@ -140,6 +137,8 @@ class LxdInstallCert(Resource): #------------------------------------------------------------------------------ +LxdStorageType = String.restrict(choices=('zfs')) + class LxdHypervisor(Service): """ Resource: LxdHypervisor @@ -150,9 +149,8 @@ class LxdHypervisor(Service): lxd_port = Attribute(Integer, description = 'LXD REST API port', default = 8443) - storage_backend = Attribute(String, description = 'Storage backend', - default = 'zfs', - choices = ['zfs']) + storage_backend = Attribute(LxdStorageType, description = 'Storage backend', + default = 'zfs') storage_size = Attribute(Integer, description = 'Storage size', default = LXD_STORAGE_SIZE_DEFAULT) # GB zfs_pool = Attribute(String, description = 'ZFS pool', @@ -190,6 +188,7 @@ class LxdHypervisor(Service): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __subresources__(self): lxd_init = LxdInit(owner=self, node = self.node) lxd_local_cert = Certificate(node = Reference(self, 'node'), @@ -208,6 +207,11 @@ class LxdHypervisor(Service): return (lxd_init | lxd_local_cert) > (lxd_vicn_profile | lxd_cert_install) + @override_parent + def __create__(self): + log.warning('Not restarting LXD') + return EmptyTask() + #-------------------------------------------------------------------------- # Private methods #-------------------------------------------------------------------------- diff --git a/vicn/resource/lxd/lxd_profile.py b/vicn/resource/lxd/lxd_profile.py index db871671..e8e022d4 100644 --- a/vicn/resource/lxd/lxd_profile.py +++ b/vicn/resource/lxd/lxd_profile.py @@ -19,7 +19,7 @@ from vicn.core.resource import Resource from netmodel.model.type import String from vicn.core.attribute import Attribute, Multiplicity -from vicn.core.task import BashTask +from vicn.core.task import BashTask, inherit_parent from vicn.core.exception import ResourceNotFound CMD_LXD_PROFILE_CREATE = ''' @@ -41,18 +41,20 @@ LXD_PROFILE_DEFAULT_IFNAME = 'vicn_mgmt' class LxdProfile(Resource): - description = Attribute(String, descr="profile description", mandatory=True) - pool = Attribute(String, descr="ZFS pool used by the containers", mandatory=True) - network = Attribute(String, description='Network on which to attach', mandatory=True) - iface_name = Attribute(String, description='Default interface name', + description = Attribute(String, description = "profile description", mandatory=True) + pool = Attribute(String, description = "ZFS pool used by the containers", mandatory=True) + network = Attribute(String, description = 'Network on which to attach', mandatory=True) + iface_name = Attribute(String, description = 'Default interface name', default = LXD_PROFILE_DEFAULT_IFNAME) node = Attribute(Resource, mandatory=True) + @inherit_parent def __get__(self): def parse(rv): if not rv.stdout: raise ResourceNotFound return BashTask(self.node, CMD_LXD_PROFILE_GET, {'profile':self}, parse=parse) + @inherit_parent def __create__(self): return BashTask(self.node, CMD_LXD_PROFILE_CREATE, {'profile':self}) diff --git a/vicn/resource/lxd/lxd_remote.py b/vicn/resource/lxd/lxd_remote.py new file mode 100644 index 00000000..b5f8454a --- /dev/null +++ b/vicn/resource/lxd/lxd_remote.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 vicn.core.resource import Resource +from netmodel.model.type import String, Bool +from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.task import BashTask, EmptyTask, inherit_parent +from vicn.core.exception import ResourceNotFound + +# From LXD 2.14 +# +# lxc remote add [] [--accept-certificate] [--password=PASSWORD] [--public] [--protocol=PROTOCOL] +# lxc remote remove +# lxc remote list +# lxc remote rename +# lxc remote set-url +# lxc remote set-default +# lxc remote get-default + +CMD_GET = 'lxc remote list | grep {remote.name}' + +CMD_CREATE = 'lxc remote add {remote.name} {remote.url}{public}' +CMD_CREATE += ' --protocol {remote.protocol}' + +CMD_DELETE = 'lxc remote remove {remote.name}' + +CMD_SET_URL = 'lxc remote set-url {remote.name} {remote.url}' + +CMD_GET_DEFAULT = 'lxc remote get-default' +CMD_SET_DEFAULT = 'lxc remote set-default {remote.name}' + +LxdProtocol = String.restrict(choices = ('simplestreams', 'lxd')) + +class LxdRemote(Resource): + # name (inherited) + url = Attribute(String, mandatory = True) + protocol = Attribute(LxdProtocol, default='lxd') + public = Attribute(Bool, default = True) + static = Attribute(Bool, default = False) + default = Attribute(Bool, default = False) + + # Used to identify the LXD instance + node = Attribute(Resource, mandatory=True) + + @inherit_parent + def __get__(self): + def parse(rv): + if not rv.stdout: + raise ResourceNotFound + return { + 'url': self.url, + 'protocol': self.protocol, + 'public': self.public, + 'static': self.static, + 'default': self.default, + } + return BashTask(self.node, CMD_GET, {'remote': self}, parse = parse) + + @inherit_parent + def __create__(self): + public = ' --public' if self.public else '' + return BashTask(self.node, CMD_CREATE, {'remote': self, 'public': public}) + + @inherit_parent + def __delete__(self): + return BashTask(self.node, CMD_DELETE, {'remote': self}) + + def _get_url(self): + return None + + def _set_url(self): + return BashTask(self.node, CMD_SET_URL, {'remote': self}) + + def _get_default(self): + def parse(rv): + return {'default': rv.stdout == self.name} + return BashTask(self.node, CMD_GET_DEFAULT, {'remote': self}, + parse = parse) + + def _set_default(self): + if self.default: + return BashTask(self.node, CMD_SET_DEFAULT, {'remote': self}) + else: + return EmptyTask() diff --git a/vicn/resource/node.py b/vicn/resource/node.py index c785e32b..3805677e 100644 --- a/vicn/resource/node.py +++ b/vicn/resource/node.py @@ -32,6 +32,10 @@ DEFAULT_SSH_PRIVATE_KEY = os.path.expanduser(os.path.join( DEFAULT_SSH_PUBLIC_KEY = os.path.expanduser(os.path.join( '~', '.vicn', 'ssh_client_cert', 'ssh_client_key.pub')) +OS = String.restrict(choices=('debian', 'ubuntu')) +Distribution = String.restrict(choices=('trusty', 'xenial', 'sid')) +Architecture = String.restrict(choices=('amd64')) + class Node(Resource): """ Resource: Node @@ -42,15 +46,13 @@ class Node(Resource): y = Attribute(Double, description = 'Y coordinate', default = 0.0) category = Attribute(String) - os = Attribute(String, description = 'OS', - default = 'ubuntu', - choices = ['debian', 'ubuntu']) - dist = Attribute(String, description = 'Distribution name', - default = 'xenial', - choices = ['trusty', 'xenial', 'sid']) - arch = Attribute(String, description = 'Architecture', - default = 'amd64', - choices = ['amd64']) + scale = Attribute(Double, default = 1) + os = Attribute(OS, description = 'OS', + default = 'ubuntu') + dist = Attribute(Distribution, description = 'Distribution name', + default = 'xenial') + arch = Attribute(Architecture, description = 'Architecture', + default = 'amd64') node_with_kernel = Attribute(Self, description = 'Node on which the kernel sits', ro = True) diff --git a/vicn/resource/ns3/emulated_channel.py b/vicn/resource/ns3/emulated_channel.py index 5f61960c..3c090a9e 100644 --- a/vicn/resource/ns3/emulated_channel.py +++ b/vicn/resource/ns3/emulated_channel.py @@ -16,30 +16,33 @@ # limitations under the License. # +import functools import logging import random +from netmodel.model.key import Key from netmodel.model.type import Integer from netmodel.util.socket import check_port from vicn.core.address_mgr import AddressManager -from vicn.core.attribute import Attribute, Multiplicity +from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.requirement import Requirement from vicn.core.resource import BaseResource from vicn.core.resource_mgr import wait_resources from vicn.core.task import inline_task, async_task, task from vicn.core.task import BashTask, run_task +from vicn.core.task import inherit_parent from vicn.resource.channel import Channel -from vicn.resource.linux.application import LinuxApplication as Application +from vicn.resource.linux.application import LinuxApplication from vicn.resource.linux.net_device import NetDevice from vicn.resource.linux.tap_device import TapDevice -from vicn.resource.linux.veth_pair import VethPair +from vicn.resource.linux.veth_pair_lxc import VethPairLxc from vicn.resource.lxd.lxc_container import LxcContainer from vicn.resource.node import Node log = logging.getLogger(__name__) -class EmulatedChannel(Channel, Application): +class EmulatedChannel(Channel, LinuxApplication): """EmulatedChannel resource This resources serves as a base class for wireless channels emulated by @@ -56,7 +59,7 @@ class EmulatedChannel(Channel, Application): traffic and prevent loops on the bridge. - We also need that all interfaces related to ap and stations are created before we run the commandline (currently, dynamically adding/removing - AP and stations is not supported by the emulator). This is made + AP and stations is not supported by the emulator). This is made possible thanks to the key=True parameter, which makes sure the attributes are processed before the __create__ is called. @@ -66,11 +69,15 @@ class EmulatedChannel(Channel, Application): __resource_type__ = BaseResource - ap = Attribute(Node, description = 'AP', key = True) + ap = Attribute(Node, description = 'AP') stations = Attribute(Node, description = 'List of stations', - multiplicity = Multiplicity.OneToMany, key = True) + multiplicity = Multiplicity.OneToMany) control_port = Attribute(Integer, description = 'Control port for the simulation') + nb_base_stations = Attribute(Integer, description='Number of nodes emulated by the AP', + default=1) + + __key__ = Key(ap, stations) # Overloaded attributes node = Attribute(requirements = [ @@ -101,20 +108,29 @@ class EmulatedChannel(Channel, Application): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent + def __initialize__(self): + return self.__set_ap() > self.__set_stations() + + @inherit_parent @inline_task def __get__(self): raise ResourceNotFound + + @inherit_parent def __create__(self): # NOTE: http://stackoverflow.com/questions/21141352/python-subprocess- # calling-a-script-which-runs-a-background-process-hanging # The output of the background scripts is still going to the same file # descriptor as the child script, thus the parent script waits for it # to finish. - cmd = '(' + self.__app_name__ + ' ' + self._get_cmdline_params() + \ - '>/dev/null 2>&1) &' - return BashTask(self.node, cmd) + def get_cmdline(channel): + return '(' + channel.__app_name__ + ' ' + \ + channel._get_cmdline_params() + '>/dev/null 2>&1) &' + return BashTask(self.node, functools.partial(get_cmdline, self)) + @inherit_parent def __delete__(self): raise NotImplementedError @@ -123,9 +139,8 @@ class EmulatedChannel(Channel, Application): #-------------------------------------------------------------------------- @async_task - async def _set_ap(self, ap=None): - if ap is None: - ap = self.ap + async def __set_ap(self): + ap = self.ap if ap is None: log.info('Ignored setting ap to None...') return @@ -133,13 +148,13 @@ class EmulatedChannel(Channel, Application): # Add a WiFi interface for the AP... interfaces = list() if isinstance(ap, LxcContainer): - # Ideally, We need to create a VethPair for each station + # Ideally, We need to create a VethPairLxc for each station # This should be monitored for the total channel bw host = NetDevice(node = ap.node, device_name='vhh-' + ap.name + '-' + self.name, monitored = False, managed = False) - self._ap_if = VethPair(node = self.ap, + self._ap_if = VethPairLxc(node = self.ap, name = 'vh-' + ap.name + '-' + self.name, device_name = 'vh-' + ap.name + '-' + self.name, host = host, @@ -171,37 +186,35 @@ class EmulatedChannel(Channel, Application): # Add interfaces to bridge vlan = AddressManager().get('vlan', self, tag='ap') - # AS the container has created the VethPair already without Vlan, we + # AS the container has created the VethPairLxc already without Vlan, we # need to delete and recreate it task = self.node.bridge._remove_interface(self._ap_bridged) await run_task(task, self._state.manager) task = self.node.bridge._add_interface(self._ap_bridged, vlan = vlan) await run_task(task, self._state.manager) + task = self.node.bridge._remove_interface(self._ap_tap) + await run_task(task, self._state.manager) task = self.node.bridge._add_interface(self._ap_tap, vlan = vlan) await run_task(task, self._state.manager) @inline_task - def _get_ap(self): + def __get_ap(self): return {'ap': None} @inline_task - def _get_stations(self): + def __get_stations(self): return {'stations': list()} @async_task - async def _set_stations(self, stations=None): - print('adding stations...') - if stations is None: - stations = self.stations - - for station in stations: + async def __set_stations(self): + for station in self.stations: await self._add_station(station) - def _add_stations(self, stations): + def __add_stations(self, stations): raise NotImplementedError @inline_task - def _remove_stations(self, station): + def __remove_stations(self, station): raise NotImplementedError diff --git a/vicn/resource/ns3/emulated_lte_channel.py b/vicn/resource/ns3/emulated_lte_channel.py index bf0f7097..0847a403 100644 --- a/vicn/resource/ns3/emulated_lte_channel.py +++ b/vicn/resource/ns3/emulated_lte_channel.py @@ -16,11 +16,15 @@ # limitations under the License. # +import math + from vicn.core.address_mgr import AddressManager -from vicn.core.resource_mgr import wait_resources -from vicn.core.task import run_task +from vicn.core.resource_mgr import wait_resources, wait_resource_task +from vicn.core.task import run_task, EmptyTask from vicn.resource.ns3.emulated_channel import EmulatedChannel from vicn.resource.linux.net_device import NetDevice +from vicn.core.attribute import Attribute +from netmodel.model.type import Integer DEFAULT_FADING_ENABLED = True DEFAULT_TW_BUFFER = 800000 @@ -48,13 +52,24 @@ class EmulatedLteChannel(EmulatedChannel): __package_names__ = ['lte-emulator'] __app_name__ = 'lte_emulator' + nb_base_stations = Attribute(Integer, description='Number of nodes emulated by the AP', + default=8) + + def __create__(self): + task = EmptyTask() + for group in self.groups: + ip4_assigns = group.iter_by_type_str("ipv4assignment") + for ip4_assign in ip4_assigns: + task = task | wait_resource_task(ip4_assign) + + return task > super().__create__() #--------------------------------------------------------------------------- # Attribute handlers #--------------------------------------------------------------------------- async def _add_station(self, station): from vicn.resource.lxd.lxc_container import LxcContainer - from vicn.resource.linux.veth_pair import VethPair + from vicn.resource.linux.veth_pair_lxc import VethPairLxc from vicn.resource.linux.tap_device import TapChannel interfaces = list() @@ -66,9 +81,9 @@ class EmulatedLteChannel(EmulatedChannel): host = NetDevice(node = station.node, device_name='vhh-' + station.name + '-' + self.name, managed = False) - sta_if = VethPair(node = station, + sta_if = VethPairLxc(node = station, name = 'vh-' + station.name + '-' + self.name, - device_name = 'vh-' + station.name + '-' + self.name, + device_name = 'vh-' + station.name + '-' + self.name, host = host, owner = self) bridged_sta = sta_if.host @@ -104,31 +119,24 @@ class EmulatedLteChannel(EmulatedChannel): task = self.node.bridge._remove_interface(bridged_sta) await run_task(task, self._state.manager) - task = self.node.bridge._add_interface(bridged_sta, - vlan = vlan) + task = self.node.bridge._add_interface(bridged_sta, vlan = vlan) await run_task(task, self._state.manager) + task = self.node.bridge._remove_interface(sta_tap) + await run_task(task, self._state.manager) task = self.node.bridge._add_interface(sta_tap, vlan = vlan) await run_task(task, self._state.manager) def _get_cmdline_params(self): - # IP have not been assign, use AddressManager for simplicity since it - # will remember the assignment - # NOTE: here the IP address passed to emulator program is hardcoded with - # a /24 mask(even if the associated IP with the station does not have a - # /24 mask). This is not a problem at all because the netmask passed to - # the emulator program has no impact on configuration in the emulator - # program. Indeed, the IP routing table in the emulator program are - # configured on a per address basis(one route per IP address) instead of - # on a per prefix basis(one route per prefix). This guarantees the IP - # routing will not change regardless of what netmask is. That is why we - # can always safely pass a hardcoded /24 mask to the emulator program. - sta_list = list() # list of identifiers sta_macs = list() # list of macs sta_taps = list() sta_ips = list() + + bs_ip_addr = self._ap_if.ip4_address + bs_ip = str(bs_ip_addr) + '/' + str(bs_ip_addr.prefix_len) + for station in self.stations: if not station.managed: interface = [i for i in station.interfaces if i.channel == self] @@ -137,17 +145,15 @@ class EmulatedLteChannel(EmulatedChannel): sta_list.append(interface.name) sta_macs.append(interface.mac_address) - sta_ips.append(interface.ip4_address + '/24') + sta_ips.append(str(interface.ip4_address)+'/'+str(prefix_len)) else: identifier = self._sta_ifs[station]._state.uuid._uuid sta_list.append(identifier) mac = self._sta_ifs[station].mac_address sta_macs.append(mac) - - # Preallocate IP address - ip = AddressManager().get_ip(self._sta_ifs[station]) + '/24' - sta_ips.append(ip) + ip = self._sta_ifs[station].ip4_address + sta_ips.append(str(ip)+'/'+str(ip.prefix_len)) tap = self._sta_taps[station].device_name sta_taps.append(tap) @@ -178,8 +184,7 @@ class EmulatedLteChannel(EmulatedChannel): # Coma-separated list of stations' IP/netmask len 'sta-ips' : ','.join(sta_ips), # Base station IP/netmask len - 'bs-ip' : AddressManager().get_ip(self._ap_if) + '/' + - str(DEFAULT_NETMASK), + 'bs-ip' : bs_ip, 'txBuffer' : '800000', 'isFading' : 'true' if DEFAULT_FADING_ENABLED else 'false', } diff --git a/vicn/resource/ns3/emulated_wifi_channel.py b/vicn/resource/ns3/emulated_wifi_channel.py index 088d4444..d8838e47 100644 --- a/vicn/resource/ns3/emulated_wifi_channel.py +++ b/vicn/resource/ns3/emulated_wifi_channel.py @@ -38,7 +38,7 @@ class EmulatedWiFiChannel(EmulatedChannel): async def _add_station(self, station): from vicn.resource.lxd.lxc_container import LxcContainer - from vicn.resource.linux.veth_pair import VethPair + from vicn.resource.linux.veth_pair_lxc import VethPairLxc from vicn.resource.linux.tap_device import TapChannel from vicn.resource.linux.macvlan import MacVlan @@ -46,18 +46,21 @@ class EmulatedWiFiChannel(EmulatedChannel): if not station.managed: sta_if = None else: + # To connect a container to the EmulatedWifiChannel, we use a + # VethPairLxc connected to the bridge, that will be in the same VLAN as + # the station TAP entering the emulator if isinstance(station, LxcContainer): host = NetDevice(node = station.node, device_name='vhh-' + station.name + '-' + self.name, managed = False) - sta_if = VethPair(node = station, + sta_if = VethPairLxc(node = station, name = 'vh-' + station.name + '-' + self.name, - device_name = 'vh-' + station.name + '-' + self.name, + device_name = 'vh-' + station.name + '-' + self.name, host = host, owner = self) bridged_sta = sta_if.host else: - raise NotImplementedError + raise NotImplementedError if sta_if: self._sta_ifs[station] = sta_if @@ -65,7 +68,7 @@ class EmulatedWiFiChannel(EmulatedChannel): interfaces.append(sta_if) self._state.manager.commit_resource(sta_if) - sta_tap = TapChannel(node = self.node, + sta_tap = TapChannel(node = self.node, owner = self, device_name = 'tap-' + station.name + '-' + self.name, up = True, @@ -81,23 +84,23 @@ class EmulatedWiFiChannel(EmulatedChannel): # Add interfaces to bridge # One vlan per station is needed to avoid broadcast loops - vlan = AddressManager().get('vlan', sta_tap) + vlan = AddressManager().get('vlan', sta_tap) # sta_tap choosen because always there if sta_if: sta_if.set('channel', self) task = self.node.bridge._remove_interface(bridged_sta) await run_task(task, self._state.manager) - task = self.node.bridge._add_interface(bridged_sta, - vlan = vlan) + task = self.node.bridge._add_interface(bridged_sta, vlan = vlan) await run_task(task, self._state.manager) + task = self.node.bridge._remove_interface(sta_tap) + await run_task(task, self._state.manager) task = self.node.bridge._add_interface(sta_tap, vlan = vlan) await run_task(task, self._state.manager) def _get_cmdline_params(self, ): - # sta-macs and sta-list for unmanaged stations sta_list = list() # list of identifiers sta_macs = list() # list of macs @@ -136,7 +139,7 @@ class EmulatedWiFiChannel(EmulatedChannel): # Y position of the Base Station 'bs-y' : 0, #self.ap.y, # Experiment ID - 'experiment-id' : 'vicn', + 'experiment-id' : 'vicn', # Index of the base station 'bs-name' : self._ap_tap.device_name, # Base station MAC address diff --git a/vicn/resource/symmetric_channel.py b/vicn/resource/symmetric_channel.py new file mode 100644 index 00000000..d8a35030 --- /dev/null +++ b/vicn/resource/symmetric_channel.py @@ -0,0 +1,16 @@ +from netmodel.model.key import Key +from vicn.core.attribute import Attribute +from vicn.core.task import inherit_parent +from vicn.resource.interface import Interface +from vicn.resource.channel import Channel + +class SymmetricChannel(Channel): + src = Attribute(Interface, mandatory = True) + dst = Attribute(Interface, mandatory = True) + + __key__ = Key(src, dst) + + @inherit_parent + def __create__(self): + self.interfaces << self.src + self.interfaces << self.dst diff --git a/vicn/resource/vpp/cicn.py b/vicn/resource/vpp/cicn.py deleted file mode 100644 index 1a68f11f..00000000 --- a/vicn/resource/vpp/cicn.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (c) 2017 Cisco and/or its affiliates. -# -# 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 re - -from netmodel.model.type import Integer, Bool -from vicn.core.attribute import Attribute -from vicn.core.exception import ResourceNotFound -from vicn.core.requirement import Requirement -from vicn.core.resource_mgr import wait_resource_task -from vicn.core.task import async_task, task, BashTask, EmptyTask -from vicn.resource.icn.forwarder import Forwarder -from vicn.resource.node import Node -from vicn.resource.vpp.vpp_commands import CMD_VPP_ENABLE_PLUGIN -from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_GET -from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ICN_FACE -from vicn.resource.vpp.vpp_commands import CMD_VPP_ADD_ICN_ROUTE -from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_GET_CACHE_SIZE -from vicn.resource.vpp.vpp_commands import CMD_VPP_CICN_SET_CACHE_SIZE - -_ADD_FACE_RETURN_FORMAT = "Face ID: [0-9]+" - -def check_face_id_return_format(data): - prog = re.compile(_ADD_FACE_RETURN_FORMAT) - return prog.match(data) - -def parse_face_id(data): - return data.partition(':')[2] - -class CICNForwarder(Forwarder): - """ - NOTE: Based on the Vagrantfile, we recommend a node with mem=4096, cpu=4 - """ - - node = Attribute(Node, - mandatory=True, - requirements = [Requirement('vpp')], - reverse_name='cicn') - numa_node = Attribute(Integer, - description = 'Numa node on which vpp will run', - default = None) - core = Attribute(Integer, - description = 'Core belonging the numa node on which vpp will run', - default = None) - enable_worker = Attribute(Bool, - description = 'Enable one worker for packet processing', - default = False) - - #__packages__ = ['vpp-plugin-cicn'] - - def __after__(self): - return ['CentralICN'] - - def __get__(self): - def parse(rv): - if rv.return_value > 0 or 'cicn: not enabled' in rv.stdout: - raise ResourceNotFound - return BashTask(self.node, CMD_VPP_CICN_GET, - lock = self.node.vpp.vppctl_lock, parse=parse) - - def __create__(self): - - #self.node.vpp.plugins.append("cicn") - lock = self.node.vpp.vppctl_lock - create_task = BashTask(self.node, CMD_VPP_ENABLE_PLUGIN, - {'plugin' : 'cicn'}, lock = lock) - - face_task = EmptyTask() - route_task = EmptyTask() - - def parse_face(rv, face): - if check_face_id_return_format(rv.stdout): - face.id = parse_face_id(rv.stdout) - return {} - - for face in self.faces: - face_task = face_task > BashTask(self.node, CMD_VPP_ADD_ICN_FACE, - {'face':face}, - parse = (lambda x : parse_face(x, face)), lock = lock) - - if not self.routes: - from vicn.resource.icn.route import Route - for route in self._state.manager.by_type(Route): - if route.node is self.node: - self.routes.append(route) - for route in self.routes: - route_task = route_task > BashTask(self.node, - CMD_VPP_ADD_ICN_ROUTE, {'route' : route}, lock = lock) - - return (wait_resource_task(self.node.vpp) > create_task) > (face_task > route_task) - - # Nothing to do - __delete__ = None - - #-------------------------------------------------------------------------- - # Attributes - #-------------------------------------------------------------------------- - - # Force local update - - _add_faces = None - _remove_faces = None - _get_faces = None - _set_faces = None - - _add_routes = None - _remove_routes = None - _get_routes = None - _set_routes = None - - #-------------------------------------------------------------------------- - # Internal methods - #-------------------------------------------------------------------------- - - def _set_cache_size(self): - return BashTask(self.node, CMD_VPP_CICN_SET_CACHE_SIZE, {'self': self}, - lock = self.node.vpp.vppctl_lock) - - def _get_cache_size(self): - def parse(rv): - return int(rv.stdout) - return BashTask(self.node, CMD_VPP_CICN_GET_CACHE_SIZE, parse=parse, - lock = self.node.vpp.vppctl_lock) diff --git a/vicn/resource/vpp/dpdk_device.py b/vicn/resource/vpp/dpdk_device.py index 472ee26f..76659129 100644 --- a/vicn/resource/vpp/dpdk_device.py +++ b/vicn/resource/vpp/dpdk_device.py @@ -19,6 +19,7 @@ from netmodel.model.type import Integer, String from vicn.core.attribute import Attribute from vicn.resource.linux.phy_interface import PhyInterface +from vicn.core.task import BashTask, task class DpdkDevice(PhyInterface): """ @@ -32,4 +33,3 @@ class DpdkDevice(PhyInterface): socket_mem = Attribute(Integer, description = 'Memory used by the vpp forwarder', default = 512) - mac_address = Attribute(String) diff --git a/vicn/resource/vpp/interface.py b/vicn/resource/vpp/interface.py index 5f8f5018..d0538661 100644 --- a/vicn/resource/vpp/interface.py +++ b/vicn/resource/vpp/interface.py @@ -16,17 +16,63 @@ # limitations under the License. # +import pyparsing as pp + +from netmodel.model.key import Key from netmodel.model.type import Integer, String, Bool +from netmodel.model.type import Inet4Address, Inet6Address from vicn.core.resource import Resource from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.task import inline_task, BashTask, task +from vicn.core.task import inherit_parent from vicn.core.task import EmptyTask from vicn.resource.interface import Interface from vicn.resource.linux.net_device import NonTapBaseNetDevice from vicn.resource.vpp.vpp import VPP -from vicn.resource.vpp.vpp_commands import CMD_VPP_CREATE_IFACE +from vicn.resource.vpp.vpp_commands import CMD_VPP_CREATE_IFACE, CMD_VPP_CREATE_MEMIFACE from vicn.resource.vpp.vpp_commands import CMD_VPP_SET_IP, CMD_VPP_SET_UP +from vicn.resource.vpp.memif_device import MemifDevice + +GREP_MEMIF_INFO = 'vppctl_wrapper show memif | grep interface --no-group-separator -A 1' + +def parse_memif(rv, vppinterface): + kw_interface = pp.CaselessKeyword('interface') + kw_key = pp.CaselessKeyword('key') + kw_file = pp.CaselessKeyword('file') + kw_listener = pp.CaselessKeyword('listener') + kw_connfd = pp.CaselessKeyword('conn-fd') + kw_intfd = pp.CaselessKeyword('int-fd') + kw_ringsize = pp.CaselessKeyword('ring-size') + kw_numc2srings = pp.CaselessKeyword('num-c2s-rings') + kw_nums2crings = pp.CaselessKeyword('num-s2c-rings') + kw_buffersize = pp.CaselessKeyword('buffer_size') + + r_path = ' *(/[a-zA-Z0-9_\-]*)*\.[a-zA-Z0-9_\-]*' + r_id = ' *-+[0-9]*' + + single = kw_interface.suppress() + pp.Word(pp.alphanums).setResultsName('interface') + \ + kw_key.suppress() + pp.Word(pp.alphanums).setResultsName('key') + \ + kw_file.suppress() + pp.Regex(r_path).setResultsName('path') # + \ + # kw_listener.suppress() + pp.Word(pp.alphanums).setResultsName('listener') + \ + # kw_connfd.suppress() + pp.Regex(r_id).setResultsName('conn-fd') + \ + # kw_intfd.suppress() + pp.Regex(r_id).setResultsName('int-fd') + \ + # kw_ringsize.suppress() + pp.Word(pp.nums).setResultsName('ring-size') + \ + # kw_numc2srings.suppress() + pp.Word(pp.nums).setResultsName('num-c2s-rings') + \ + # kw_nums2crings.suppress() + pp.Word(pp.nums).setResultsName('num-s2c-rings') + \ + # kw_buffersize.suppress() + pp.Word(pp.nums).setResultsName('buffer-size') + + multiple = pp.OneOrMore(pp.Group(single)) + + results = multiple.parseString(rv.stdout) + + for interface in results: + if interface['path'] == vppinterface.parent.path_unix_socket + vppinterface.parent.socket_name: + vppinterface.device_name = interface['interface'] + vppinterface.parent.device_name = interface['interface'] + + return vppinterface.device_name + class VPPInterface(Resource): """ @@ -39,18 +85,21 @@ class VPPInterface(Resource): description = 'Forwarder to which this interface belong to', mandatory = True, multiplicity = Multiplicity.ManyToOne, - key = True, reverse_name = 'interfaces') parent = Attribute(Interface, description = 'parent', mandatory = True, reverse_name = 'vppinterface') - ip4_address = Attribute(String) - ip6_address = Attribute(String) - prefix_len = Attribute(Integer, default = 31) + ip4_address = Attribute(Inet4Address) + ip6_address = Attribute(Inet6Address) up = Attribute(Bool, description = 'Interface up/down status') monitored = Attribute(Bool, default = True) device_name = Attribute(String) + __key__ = Key(vpp, Resource.name) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + #-------------------------------------------------------------------------- # Resource lifecycle #-------------------------------------------------------------------------- @@ -61,10 +110,12 @@ class VPPInterface(Resource): """ return ['CentralIP'] + @inherit_parent @inline_task def __get__(self): raise ResourceNotFound + @inherit_parent def __create__(self): # We must control what is the type of the parent netDevice (currently # supported only veths, physical nics are coming) @@ -73,15 +124,27 @@ class VPPInterface(Resource): # We must let the routing algorithm know that the parent interface # belongs to vpp self.parent.has_vpp_child = True - - self.ip4_address = self.parent.ip4_address - self.ip6_address = self.parent.ip6_address self.up = True - if isinstance(self.parent,NonTapBaseNetDevice): + if isinstance(self.parent, MemifDevice): + #TODO: add output parsing to get the interface name + create_task = BashTask(self.vpp.node, CMD_VPP_CREATE_MEMIFACE, { + 'key': hex(self.parent.key), + 'vpp_interface': self, + 'master_slave': 'master' if self.parent.master else 'slave'}, + lock = self.vpp.vppctl_lock) + fill_name = BashTask(self.vpp.node, GREP_MEMIF_INFO, + parse = (lambda x, y=self : parse_memif(x, y)), + lock = self.vpp.vppctl_lock) + + create_task = create_task > fill_name + + elif isinstance(self.parent,NonTapBaseNetDevice): # Remove ip address in the parent device, it must only be set in # the vpp interface otherwise vpp and the linux kernel will reply # to non-icn request (e.g., ARP replies, port ureachable etc) + self.ip4_address = self.parent.ip4_address + self.ip6_address = self.parent.ip6_address self.device_name = 'host-' + self.parent.device_name create_task = BashTask(self.vpp.node, CMD_VPP_CREATE_IFACE, @@ -94,6 +157,8 @@ class VPPInterface(Resource): self.parent.remote.set('offload', False) elif self.parent.get_type() == 'dpdkdevice': + self.ip4_address = self.parent.ip4_address + self.ip6_address = self.parent.ip6_address self.device_name = self.parent.device_name else : # Currently assume naively that everything else will be a physical @@ -112,13 +177,27 @@ class VPPInterface(Resource): def _set_ip4_address(self): if self.ip4_address: - return BashTask(self.vpp.node, CMD_VPP_SET_IP, {'netdevice': self}, + return BashTask(self.vpp.node, CMD_VPP_SET_IP, { + 'device_name': self.device_name, + 'ip_address': str(self.ip4_address), + 'prefix_len': self.ip4_address.prefix_len}, lock = self.vpp.vppctl_lock) - def _set_up(self): - return BashTask(self.vpp.node, CMD_VPP_SET_UP, {'netdevice': self}, + def _set_ip6_address(self): + if self.ip6_address: + return BashTask(self.vpp.node, CMD_VPP_SET_IP, { + 'device_name': self.device_name, + 'ip_address': str(self.ip6_address), + 'prefix_len': self.ip6_address.prefix_len}, lock = self.vpp.vppctl_lock) + def _set_up(self): + state = 'up' if self.up else 'down' + return BashTask(self.vpp.node, CMD_VPP_SET_UP, { + 'netdevice': self, + 'state': state}, + lock = self.vpp.vppctl_lock) + @task def _get_up(self): return {'up' : False} diff --git a/vicn/resource/vpp/memif_device.py b/vicn/resource/vpp/memif_device.py new file mode 100644 index 00000000..a114900a --- /dev/null +++ b/vicn/resource/vpp/memif_device.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 netmodel.model.key import Key +from netmodel.model.type import Bool, String, Integer +from netmodel.model.type import Inet4Address, Inet6Address +from vicn.core.attribute import Attribute +from vicn.resource.interface import Interface +from vicn.resource.lxd.lxc_container import CMD_MOUNT_FOLDER +from vicn.resource.linux.folder import Folder +from vicn.core.task import inline_task, BashTask +from vicn.core.task import inherit_parent +from vicn.core.exception import ResourceNotFound +from vicn.core.address_mgr import AddressManager + +class MemifDevice(Interface): + """ + Resource: Memory interface device + + A MemifDevice is device build on top of a the memory interface provided by vpp. + It uses a unix socket to connect the two vpp-s (one master and one slave). + The unix socket must be shared between the two vpp-s. + """ + path_unix_socket = Attribute(String, + mandatory = True, + description = 'Path to the shared folder holding the unix socket') + socket_name = Attribute(String, + mandatory = True, + description = 'Path to the shared folder holding the unix socket') + folder_host = Attribute(Folder, + mandatory = True, + description = 'Folder in the host to be mounted in the container ih path_unix_socket') + master= Attribute(Bool, + description = 'True if this interface is connected to the master vpp', + default = False) + # We need to automatically assign a mac address to the memif so that we can + # recreate it after vpp reboots thanks to a config file or bash script. + # Just reading the actual value would not work since we need to use an + # external script and this mac address would thus not be prevent in the list + # of executed commands. + mac_address = Attribute(String, description = 'Mac address of the device', + default = lambda self: AddressManager().get_mac(self)) + ip4_address = Attribute(Inet4Address, description = "Device's IP address") + ip6_address = Attribute(Inet6Address, description = "Device's IP address") + device_name = Attribute(String) + key = Attribute(Integer) + + __key__ = Key(folder_host) + + @inline_task + def __get__(self): + raise ResourceNotFound + + @inherit_parent + def __create__(self): + return BashTask(self.node.node_with_kernel, CMD_MOUNT_FOLDER, { + 'container': self.node, + 'device-name': self.socket_name, + 'host_path': self.folder_host.foldername, + 'container_path': self.path_unix_socket}, output=True, + lock=self.node.vpp.memif_lock) + diff --git a/vicn/resource/vpp/memif_link.py b/vicn/resource/vpp/memif_link.py new file mode 100644 index 00000000..62de03c6 --- /dev/null +++ b/vicn/resource/vpp/memif_link.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 random +import string +import logging +import asyncio + +from netmodel.model.type import Integer, String +from netmodel.model.key import Key +from vicn.core.attribute import Attribute, Reference +from vicn.core.exception import ResourceNotFound +from vicn.core.state import ResourceState, AttributeState +from vicn.core.task import inline_task, async_task, run_task +from vicn.core.task import get_attributes_task, BashTask +from vicn.core.task import inherit_parent +from vicn.resource.channel import Channel +from vicn.resource.interface import Interface +from vicn.resource.linux.net_device import NonTapBaseNetDevice +from vicn.resource.node import Node +from vicn.resource.lxd.lxc_container import CMD_MOUNT_FOLDER, LxcContainer +from vicn.resource.vpp.memif_device import MemifDevice +from vicn.resource.linux.folder import Folder + +# FIXME remove VPP specific code +from vicn.resource.vpp.interface import VPPInterface + +log = logging.getLogger(__name__) + +CONTAINER_SOCKET_PATH='/root/{}' +SHARED_FOLDER_PATH='/tmp/{}' + +class MemifLink(Channel): + """ + Resource: MemifLink + + Implements a virtual wired link between containers. It is made + with a pair of MemifDevice which are created in the two containers. + + Because of this, the resource only supports passing source and destination + containers, and not interfaces. It also explains the relative complexity of + the current implementation. + """ + + src_node = Attribute(LxcContainer, description = 'Source node', + mandatory = True) + dst_node = Attribute(LxcContainer, description = 'Destination node', + mandatory = True) + folder = Attribute(Folder, description = 'Shared folder holding the socket used by the MemifDevices') + key = Attribute(Integer) + + __key__ = Key(src_node, dst_node) + + def __init__(self, *args, **kwargs): + assert 'src_node' in kwargs and 'dst_node' in kwargs + self._src = None + self._dst = None + self._folder = None + super().__init__(*args, **kwargs) + + @inherit_parent + def __subresources__(self): + assert self.src_node.node_with_kernel == self.dst_node.node_with_kernel + + host = self.src_node.node_with_kernel + # We create two managed net devices that are pre-setup + # but the resource manager has to take over for IP addresses etc. + # Being done in initialize, those attributes won't be considered as + # dependencies and will thus not block the resource state machine. + + socket_name = 'socket-' + ''.join(random.choice(string.ascii_uppercase + + string.digits) for _ in range(5)) +'.file' + + _folder = Folder(node = host, + foldername = SHARED_FOLDER_PATH.format(self.src_node.name + '-' + self.dst_node.name), + permission = 777) + + self.key = random.randint(0, 2**64) + + self._src = MemifDevice(node = self.src_node, + channel = self, + owner = self, + path_unix_socket = CONTAINER_SOCKET_PATH.format(self.src_node.name + '-' + + self.dst_node.name) + '/', + folder_host = _folder, + socket_name = socket_name, + device_name = 'memif-'+self.dst_node.name, + key = Reference(self, 'key'), + master = False) + self._dst = MemifDevice(node = self.dst_node, + channel = self, + owner = self, + path_unix_socket = CONTAINER_SOCKET_PATH.format(self.src_node.name + '-' + + self.dst_node.name) + '/', + socket_name = socket_name, + folder_host = _folder, + device_name = 'memif-'+self.src_node.name, + key = Reference(self, 'key'), + master = True) + self._dst.remote = self._src + self._src.remote = self._dst + + return _folder | (self._src | self._dst) + + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + async def _commit(self): + manager = self._state.manager + + if hasattr(self.src_node, 'vpp') and not self.src_node.vpp is None: + vpp_src = VPPInterface(parent = self._src, + vpp = self.src_node.vpp, + ip4_address = Reference(self._src, 'ip4_address'), + ip6_address = Reference(self._src, 'ip6_address')) + manager.commit_resource(vpp_src) + + if hasattr(self.dst_node, 'vpp') and not self.dst_node.vpp is None: + vpp_dst = VPPInterface(parent = self._dst, + vpp = self.dst_node.vpp, + ip4_address = Reference(self._dst, 'ip4_address'), + ip6_address = Reference(self._dst, 'ip6_address')) + manager.commit_resource(vpp_dst) + + @inherit_parent + def __get__(self): + return async_task(self._commit)() diff --git a/vicn/resource/vpp/scripts.py b/vicn/resource/vpp/scripts.py index 3a3d5e8f..d5130212 100644 --- a/vicn/resource/vpp/scripts.py +++ b/vicn/resource/vpp/scripts.py @@ -282,6 +282,6 @@ api-segment { ''' APPARMOR_VPP_PROFILE = ''' -lxc.aa_profile = lxc-dpdk +lxc.aa_profile = lxc-dpdk lxc.mount.entry = hugetlbfs dev/hugepages hugetlbfs rw,relatime,create=dir 0 0 lxc.mount.auto = sys:rw''' diff --git a/vicn/resource/vpp/vpp.py b/vicn/resource/vpp/vpp.py index 9edcfea3..8250f99a 100644 --- a/vicn/resource/vpp/vpp.py +++ b/vicn/resource/vpp/vpp.py @@ -23,6 +23,7 @@ from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.resource import Resource from vicn.core.task import BashTask, task, inline_task +from vicn.core.task import inherit_parent, EmptyTask from vicn.resource.lxd.lxc_container import LxcContainer from vicn.resource.node import Node from vicn.resource.linux.application import LinuxApplication @@ -33,6 +34,7 @@ from vicn.resource.vpp.scripts import TPL_VPP_DPDK_DAEMON_SCRIPT from vicn.resource.vpp.vpp_commands import CMD_VPP_DISABLE, CMD_VPP_STOP from vicn.resource.vpp.vpp_commands import CMD_VPP_START from vicn.resource.vpp.vpp_commands import CMD_VPP_ENABLE_PLUGIN +from vicn.resource.vpp.vpp_commands import CMD_REMOVE_DPDK_PLUGIN from vicn.resource.vpp.vpp_host import VPPHost #------------------------------------------------------------------------------ @@ -50,7 +52,7 @@ class VPP(LinuxApplication): start and stop commands """ - __package_names__ = ['vpp', 'vpp-dbg', 'vpp-dpdk-dev'] + __package_names__ = ['vpp', 'vpp-dpdk-dev'] plugins = Attribute(String, multiplicity = Multiplicity.OneToMany) @@ -72,12 +74,15 @@ class VPP(LinuxApplication): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.vppctl_lock = asyncio.Lock() + self.vppctl_lock = asyncio.Lock() + + # needed by "lxc config device add" + self.memif_lock = asyncio.Lock() - self.dpdk_setup_file = None if isinstance(self.node, LxcContainer): if not 'vpp' in self.node.profiles: self.node.profiles.append('vpp') + self.dpdk_setup_file = None #-------------------------------------------------------------------------- # Resource lifecycle @@ -86,16 +91,14 @@ class VPP(LinuxApplication): def __after__(self): return ['BaseNetDevice'] + @inherit_parent + @task def __get__(self): + raise ResourceNotFound return BashTask(self.node, CMD_GET) + @inherit_parent def __subresources__(self): - self.dpdk_setup_file = TextFile(node = self.node, - filename = FN_VPP_DPDK_SCRIPT, - overwrite = True) - return self.dpdk_setup_file - - def __create__(self): socket_mem = dict() numa_mgr = self.node.node_with_kernel.numa_mgr @@ -117,25 +120,6 @@ class VPP(LinuxApplication): self.numa_node, self.core = \ numa_mgr.get_numa_core(numa_node = self.numa_node) - dpdk_list = list() - - # On numa architecture socket-mem requires to set the amount of memory - # to be reserved on each numa node - socket_mem_str = 'socket-mem ' - for numa in range (0,numa_mgr.get_number_of_numa()): - if numa in socket_mem: - socket_mem_str = socket_mem_str + str(socket_mem[numa]) - else: - socket_mem_str = socket_mem_str + '0' - - if numa < numa_mgr.get_number_of_numa()-1: - socket_mem_str = socket_mem_str + ',' - - dpdk_list.append(socket_mem_str) - - for interface in self.node.interfaces: - if isinstance(interface, DpdkDevice): - dpdk_list.append('dev ' + interface.pci_address) # Add the core on which running vpp and the dpdk parameters setup = TPL_VPP_DPDK_DAEMON_SCRIPT + 'cpu {' @@ -146,19 +130,42 @@ class VPP(LinuxApplication): self.numa_node, cpu_worker =numa_mgr.get_numa_core(self.numa_node) setup = setup + '''\n corelist-workers ''' + str(cpu_worker) - setup = setup + '''\n}\n\n dpdk { ''' - - for dpdk_dev in dpdk_list: - setup = setup + ''' \n ''' + dpdk_dev - - setup = setup + '\n}' + dpdk_list = list() + for interface in self.node.interfaces: + if isinstance(interface, DpdkDevice): + dpdk_list.append('dev ' + interface.pci_address) + setup = setup + '''\n}\n\n''' + if dpdk_list: + setup = setup + 'dpdk {' + # add socket_mem + # On numa architecture socket-mem requires to set the amount of memory + # to be reserved on each numa node + socket_mem_str = 'socket-mem ' + for numa in range (0,numa_mgr.get_number_of_numa()): + if numa in socket_mem: + socket_mem_str = socket_mem_str + str(socket_mem[numa]) + else: + socket_mem_str = socket_mem_str + '0' + + if numa < numa_mgr.get_number_of_numa()-1: + socket_mem_str = socket_mem_str + ',' + + dpdk_list.append(socket_mem_str) + + for dpdk_dev in dpdk_list: + setup = setup + ''' \n ''' + dpdk_dev + setup = setup + '''\n}''' + + dpdk_setup_file = TextFile(node = self.node, + filename = FN_VPP_DPDK_SCRIPT, + content = setup, + overwrite = True) - if any([isinstance(interface,DpdkDevice) for interface in self.node.interfaces]): - self.dpdk_setup_file.content = setup - else: - self.dpdk_setup_file.content = TPL_VPP_DPDK_DAEMON_SCRIPT + return dpdk_setup_file + @inherit_parent + def __create__(self): lock = self.node.node_with_kernel.vpp_host.vppstart_lock vpp_disable = BashTask(self.node, CMD_VPP_DISABLE, lock = lock) @@ -166,8 +173,19 @@ class VPP(LinuxApplication): enable_ip_forward = BashTask(self.node, CMD_DISABLE_IP_FORWARD) start_vpp = BashTask(self.node, CMD_VPP_START, lock = lock) - return ((vpp_disable > vpp_stop) | enable_ip_forward) > start_vpp + found = False + for iface in self.interfaces: + if isinstance(iface.parent, DpdkDevice): + found = True + break + + remove_dpdk_plugin = EmptyTask() + if (not found): + remove_dpdk_plugin = BashTask(self.node, CMD_REMOVE_DPDK_PLUGIN, lock = lock) + + return (((vpp_disable > vpp_stop) | enable_ip_forward) > (remove_dpdk_plugin > start_vpp)) + @inherit_parent def __delete__(self): return BashTask(self.node, CMD_VPP_STOP) diff --git a/vicn/resource/vpp/vpp_bridge.py b/vicn/resource/vpp/vpp_bridge.py index 612145d9..53523c17 100644 --- a/vicn/resource/vpp/vpp_bridge.py +++ b/vicn/resource/vpp/vpp_bridge.py @@ -24,6 +24,7 @@ from vicn.core.requirement import Requirement from vicn.core.resource_mgr import wait_resource_task from vicn.core.resource import Resource from vicn.core.task import task, BashTask, EmptyTask +from vicn.core.task import inherit_parent from vicn.resource.channel import Channel from vicn.resource.linux.application import LinuxApplication from vicn.resource.linux.sym_veth_pair import SymVethPair @@ -33,7 +34,7 @@ from vicn.resource.vpp.dpdk_device import DpdkDevice from vicn.resource.vpp.interface import VPPInterface from vicn.resource.vpp.vpp import VPP -CMD_ADD_INTERFACE_TO_BR = ('vppctl set interface l2 bridge ' +CMD_ADD_INTERFACE_TO_BR = ('vppctl_wrapper set interface l2 bridge ' '{interface.device_name} {br_domain}') class VPPBridge(Channel, LinuxApplication): @@ -69,6 +70,7 @@ class VPPBridge(Channel, LinuxApplication): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __subresources__ (self): # We don't need any reference to the list of SymVethPair because each # side of a veth will be included in the node.interfaces list @@ -77,12 +79,14 @@ class VPPBridge(Channel, LinuxApplication): return Resource.__concurrent__(*self._veths) + @inherit_parent @task def __initialize__ (self): # Add the veth side on the connected_nodes to the set of interfaces of # the channel self.interfaces.extend([veth.side2 for veth in self._veths]) + @inherit_parent @task def __get__(self): # Forces creation @@ -91,6 +95,7 @@ class VPPBridge(Channel, LinuxApplication): # Nothing to do __delete__ = None + @inherit_parent def __create__(self): manager = self._state.manager diff --git a/vicn/resource/vpp/vpp_commands.py b/vicn/resource/vpp/vpp_commands.py index 40315c19..30898eae 100644 --- a/vicn/resource/vpp/vpp_commands.py +++ b/vicn/resource/vpp/vpp_commands.py @@ -5,37 +5,41 @@ CMD_VPP_DISABLE = 'systemctl disable vpp.service' # 'sleep 1' ensures that VPP has enough time to start CMD_VPP_START = ''' -systemctl start vpp -sleep 1 +flock /tmp/vppctl.lock -c "systemctl start vpp" ''' CMD_VPP_STOP = ''' -systemctl stop vpp -killall -9 vpp_main || true +flock /tmp/vppctl.lock -c "systemctl stop vpp" +''' +#killall -9 vpp_main || true +CMD_VPP_ENABLE_PLUGIN = 'vppctl_wrapper {plugin} control start' + +CMD_REMOVE_DPDK_PLUGIN = ''' +rm /usr/lib/vpp_api_test_plugins/dpdk_test_plugin.so +rm /usr/lib/vpp_plugins/dpdk_plugin.so ''' -CMD_VPP_ENABLE_PLUGIN = 'vppctl {plugin} enable' ##### VPP INTERFACES ##### CMD_VPP_CREATE_IFACE = ''' -# Create vpp interface from netmodel.network.interface.device_name} with mac {self.parent.mac_address} -vppctl create host-interface name {vpp_interface.parent.device_name} hw-addr {vpp_interface.parent.mac_address} -vppctl set interface state {vpp_interface.device_name} up +# Create vpp interface from {vpp_interface.parent.device_name} with mac {vpp_interface.parent.mac_address} +vppctl_wrapper create host-interface name {vpp_interface.parent.device_name} hw-addr {vpp_interface.parent.mac_address} +vppctl_wrapper set interface state {vpp_interface.device_name} up ''' -CMD_VPP_SET_IP = 'vppctl set int ip address {netdevice.device_name} {netdevice.ip4_address}/{netdevice.prefix_len}' -CMD_VPP_SET_UP = 'vppctl set int state {netdevice.device_name} up' - -##### VPP IP ROUTING ##### -CMD_VPP_ADD_ROUTE = 'vppctl set ip arp static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}' -CMD_VPP_DEL_ROUTE = 'vppctl set ip arp del static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}' -CMD_VPP_ADD_ROUTE_GW = 'vppctl ip route add {route.ip_address}/32 via {route.gateway} {route.interface.vppinterface.device_name}' -CMD_VPP_DEL_ROUTE_GW = 'vppctl ip route del {route.ip_address}/32 via {route.gateway} {route.interface.vppinterface.device_name}' - -##### VPP CICN PLUGIN ##### +# It is important to pass the mac address so that it does not get randomly +# generated by VPP, preventing any reboot of VPP and recreation of commands +CMD_VPP_CREATE_MEMIFACE = ''' +# Create vpp interface from shared_memory +vppctl_wrapper create memif key {key} socket {vpp_interface.parent.path_unix_socket}{vpp_interface.parent.socket_name} hw-addr {vpp_interface.parent.mac_address} {master_slave} +''' +CMD_VPP_SET_IP = 'vppctl_wrapper set int ip address {device_name} {ip_address}/{prefix_len}' +CMD_VPP_SET_UP = 'vppctl_wrapper set int state {netdevice.device_name} {state}' -CMD_VPP_CICN_GET = "timeout 1 vppctl cicn show" #We timeout if vpp is not started -CMD_VPP_ADD_ICN_ROUTE = 'vppctl cicn cfg fib add prefix {route.prefix} face {route.face.id}' -CMD_VPP_ADD_ICN_FACE = 'vppctl cicn cfg face add local {face.src_ip}:{face.src_port} remote {face.dst_ip}:{face.dst_port}' +##### VPP IP ROUTING ##### -CMD_VPP_CICN_GET_CACHE_SIZE = 'vppctl cicn show | grep "CS entries" | grep -Eo "[0-9]+"' -CMD_VPP_CICN_SET_CACHE_SIZE = 'vppctl cicn control param cs size {self.cache_size}' +CMD_VPP_ADD_ARP = 'vppctl_wrapper set ip arp static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}' +CMD_VPP_DEL_ARP = 'vppctl_wrapper set ip arp del static {route.interface.vppinterface.device_name} {route.ip_address} {route.mac_address}' +CMD_VPP_ADD_ROUTE = 'vppctl_wrapper ip route add {route.ip_address}/{route.ip_address.prefix_len} via {route.interface.vppinterface.device_name}' +CMD_VPP_DEL_ROUTE = 'vppctl_wrapper ip route del {route.ip_address}/{route.ip_address.prefix_len} via {route.interface.vppinterface.device_name}' +CMD_VPP_ADD_ROUTE_GW = 'vppctl_wrapper ip route add {route.ip_address}/{route.ip_address.prefix_len} via {route.gateway} {route.interface.vppinterface.device_name}' +CMD_VPP_DEL_ROUTE_GW = 'vppctl_wrapper ip route del {route.ip_address}/{route.ip_address.prefix_len} via {route.gateway} {route.interface.vppinterface.device_name}' diff --git a/vicn/resource/vpp/vpp_host.py b/vicn/resource/vpp/vpp_host.py index 954d1d32..29094451 100644 --- a/vicn/resource/vpp/vpp_host.py +++ b/vicn/resource/vpp/vpp_host.py @@ -23,6 +23,7 @@ from vicn.core.attribute import Attribute, Multiplicity from vicn.core.exception import ResourceNotFound from vicn.core.requirement import Requirement from vicn.core.task import BashTask, task, EmptyTask +from vicn.core.task import inherit_parent from vicn.resource.linux.application import LinuxApplication from vicn.resource.linux.file import TextFile from vicn.resource.node import Node @@ -41,7 +42,7 @@ CMD_APP_ARMOR_RELOAD = ''' CMD_SYSCTL_HUGEPAGES = 'sysctl -w vm.nr_hugepages={nb_hp}' DEFAULT_NB_HUGEPAGES = 1024 CMD_GREP_UIO_DEV = 'ls /dev | grep uio' -CMD_CREATE_UIO_DEVICES = "dpdk_nic_bind --bind=igb_uio {pci_address}" +CMD_CREATE_UIO_DEVICES = "dpdk-devbind --bind=igb_uio {pci_address}" class VPPHost(LinuxApplication): """ @@ -75,7 +76,7 @@ class VPPHost(LinuxApplication): description = 'Dpdk devices on the node', multiplicity = Multiplicity.OneToMany) - __package_names__ = ['dpdk', 'vpp-dpdk-dkms'] + __package_names__ = ['vpp-dpdk-dkms', 'vpp-dpdk-dev'] #-------------------------------------------------------------------------- # Constructor and Accessors @@ -89,6 +90,7 @@ class VPPHost(LinuxApplication): # Resource lifecycle #-------------------------------------------------------------------------- + @inherit_parent def __subresources__(self): app_armor_file = TextFile(node = self.node, filename = FN_APPARMOR_DPDK_SCRIPT, @@ -100,6 +102,7 @@ class VPPHost(LinuxApplication): overwrite = True) return app_armor_file | startup_conf + @inherit_parent @task def __get__(self): """ @@ -108,6 +111,7 @@ class VPPHost(LinuxApplication): """ raise ResourceNotFound + @inherit_parent def __create__(self): modules = BashTask(self.node, CMD_INSERT_MODULES) app_armor_reload = BashTask(self.node, CMD_APP_ARMOR_RELOAD) @@ -132,8 +136,6 @@ class VPPHost(LinuxApplication): return ((modules | app_armor_reload) | sysctl_hugepages) > \ (disable_vpp > create_uio) - __delete__ = None - #-------------------------------------------------------------------------- # Attributes #-------------------------------------------------------------------------- diff --git a/www/css/contrib/bootstrap-dropmenu.min.css b/www/css/contrib/bootstrap-dropmenu.min.css new file mode 100644 index 00000000..3378c140 --- /dev/null +++ b/www/css/contrib/bootstrap-dropmenu.min.css @@ -0,0 +1,5 @@ +/*! + * bootstrap-dropmenu v0.9.0 (https://skywalkapps.github.io/bootstrap-dropmenu) + * Copyright 2016 Martin Staněk + * Licensed under MIT + */.navbar-toggle{line-height:1}.navbar-toggle.navbar-toggle-left{float:left;margin-left:15px}.navbar-toggle-label{text-transform:uppercase;display:inline-block;vertical-align:top;margin-right:4px;color:#888;font-size:14px;font-weight:bold}.navbar-toggle-icon{display:inline-block}.navbar-default .navbar-toggle{border-color:#e7e7e7}.navbar-inverse .navbar-toggle{border-color:#080808}.navbar-inverse .navbar-toggle-label{color:#fff}.sw-example>nav .navbar-toggle{display:block !important} \ No newline at end of file diff --git a/www/css/contrib/bootstrap-table.min.css b/www/css/contrib/bootstrap-table.min.css new file mode 100644 index 00000000..d72d0655 --- /dev/null +++ b/www/css/contrib/bootstrap-table.min.css @@ -0,0 +1 @@ +.fixed-table-container .bs-checkbox,.fixed-table-container .no-records-found{text-align:center}.fixed-table-body thead th .th-inner,.table td,.table th{box-sizing:border-box}.bootstrap-table .table{margin-bottom:0!important;border-bottom:1px solid #ddd;border-collapse:collapse!important;border-radius:1px}.bootstrap-table .table:not(.table-condensed),.bootstrap-table .table:not(.table-condensed)>tbody>tr>td,.bootstrap-table .table:not(.table-condensed)>tbody>tr>th,.bootstrap-table .table:not(.table-condensed)>tfoot>tr>td,.bootstrap-table .table:not(.table-condensed)>tfoot>tr>th,.bootstrap-table .table:not(.table-condensed)>thead>tr>td{padding:8px}.bootstrap-table .table.table-no-bordered>tbody>tr>td,.bootstrap-table .table.table-no-bordered>thead>tr>th{border-right:2px solid transparent}.bootstrap-table .table.table-no-bordered>tbody>tr>td:last-child{border-right:none}.fixed-table-container{position:relative;clear:both;border:1px solid #ddd;border-radius:4px;-webkit-border-radius:4px;-moz-border-radius:4px}.fixed-table-container.table-no-bordered{border:1px solid transparent}.fixed-table-footer,.fixed-table-header{overflow:hidden}.fixed-table-footer{border-top:1px solid #ddd}.fixed-table-body{overflow-x:auto;overflow-y:auto;height:100%}.fixed-table-container table{width:100%}.fixed-table-container thead th{height:0;padding:0;margin:0;border-left:1px solid #ddd}.fixed-table-container thead th:focus{outline:transparent solid 0}.fixed-table-container thead th:first-child{border-left:none;border-top-left-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px}.fixed-table-container tbody td .th-inner,.fixed-table-container thead th .th-inner{padding:8px;line-height:24px;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fixed-table-container thead th .sortable{cursor:pointer;background-position:right;background-repeat:no-repeat;padding-right:30px}.fixed-table-container thead th .both{background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC')}.fixed-table-container thead th .asc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==)}.fixed-table-container thead th .desc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII=)}.fixed-table-container th.detail{width:30px}.fixed-table-container tbody td{border-left:1px solid #ddd}.fixed-table-container tbody tr:first-child td{border-top:none}.fixed-table-container tbody td:first-child{border-left:none}.fixed-table-container tbody .selected td{background-color:#f5f5f5}.fixed-table-container .bs-checkbox .th-inner{padding:8px 0}.fixed-table-container input[type=radio],.fixed-table-container input[type=checkbox]{margin:0 auto!important}.fixed-table-pagination .pagination-detail,.fixed-table-pagination div.pagination{margin-top:10px;margin-bottom:10px}.fixed-table-pagination div.pagination .pagination{margin:0}.fixed-table-pagination .pagination a{padding:6px 12px;line-height:1.428571429}.fixed-table-pagination .pagination-info{line-height:34px;margin-right:5px}.fixed-table-pagination .btn-group{position:relative;display:inline-block;vertical-align:middle}.fixed-table-pagination .dropup .dropdown-menu{margin-bottom:0}.fixed-table-pagination .page-list{display:inline-block}.fixed-table-toolbar .columns-left{margin-right:5px}.fixed-table-toolbar .columns-right{margin-left:5px}.fixed-table-toolbar .columns label{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.428571429}.fixed-table-toolbar .bs-bars,.fixed-table-toolbar .columns,.fixed-table-toolbar .search{position:relative;margin-top:10px;margin-bottom:10px;line-height:34px}.fixed-table-pagination li.disabled a{pointer-events:none;cursor:default}.fixed-table-loading{display:none;position:absolute;top:42px;right:0;bottom:0;left:0;z-index:99;background-color:#fff;text-align:center}.fixed-table-body .card-view .title{font-weight:700;display:inline-block;min-width:30%;text-align:left!important}.table td,.table th{vertical-align:middle}.fixed-table-toolbar .dropdown-menu{text-align:left;max-height:300px;overflow:auto}.fixed-table-toolbar .btn-group>.btn-group{display:inline-block;margin-left:-1px!important}.fixed-table-toolbar .btn-group>.btn-group>.btn{border-radius:0}.fixed-table-toolbar .btn-group>.btn-group:first-child>.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.fixed-table-toolbar .btn-group>.btn-group:last-child>.btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.bootstrap-table .table>thead>tr>th{vertical-align:bottom;border-bottom:1px solid #ddd}.bootstrap-table .table thead>tr>th{padding:0;margin:0}.bootstrap-table .fixed-table-footer tbody>tr>td{padding:0!important}.bootstrap-table .fixed-table-footer .table{border-bottom:none;border-radius:0;padding:0!important}.pull-right .dropdown-menu{right:0;left:auto}p.fixed-table-scroll-inner{width:100%;height:200px}div.fixed-table-scroll-outer{top:0;left:0;visibility:hidden;width:200px;height:150px;overflow:hidden} \ No newline at end of file diff --git a/www/css/contrib/bootstrap.min.css b/www/css/contrib/bootstrap.min.css new file mode 100644 index 00000000..a9f35cee --- /dev/null +++ b/www/css/contrib/bootstrap.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.2.0 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;width:100% \9;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;width:100% \9;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px;line-height:1.42857143 \0}input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],input[type=radio].disabled,input[type=checkbox].disabled,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm,.form-horizontal .form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{position:absolute;z-index:-1;filter:alpha(opacity=0);opacity:0}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#777}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#777}.navbar-inverse .navbar-nav>li>a{color:#777}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#777}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#777}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#428bca;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar[aria-valuenow="1"],.progress-bar[aria-valuenow="2"]{min-width:30px}.progress-bar[aria-valuenow="0"]{min-width:30px;color:#777;background-color:transparent;background-image:none;-webkit-box-shadow:none;box-shadow:none}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#428bca}.panel-primary>.panel-heading .badge{color:#428bca;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate3d(0,-25%,0);-o-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0)}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-size:12px;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/www/css/contrib/jasny-bootstrap.min.css b/www/css/contrib/jasny-bootstrap.min.css new file mode 100644 index 00000000..04af127a --- /dev/null +++ b/www/css/contrib/jasny-bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Jasny Bootstrap v3.1.3 (http://jasny.github.io/bootstrap) + * Copyright 2012-2014 Arnold Daniels + * Licensed under Apache-2.0 (https://github.com/jasny/bootstrap/blob/master/LICENSE) + */ + +.container-smooth{max-width:1170px}@media (min-width:1px){.container-smooth{width:auto}}.btn-labeled{padding-top:0;padding-bottom:0}.btn-label{position:relative;background:0 0;background:rgba(0,0,0,.15);display:inline-block;padding:6px 12px;left:-12px;border-radius:3px 0 0 3px}.btn-label.btn-label-right{left:auto;right:-12px;border-radius:0 3px 3px 0}.btn-lg .btn-label{padding:10px 16px;left:-16px;border-radius:5px 0 0 5px}.btn-lg .btn-label.btn-label-right{left:auto;right:-16px;border-radius:0 5px 5px 0}.btn-sm .btn-label{padding:5px 10px;left:-10px;border-radius:2px 0 0 2px}.btn-sm .btn-label.btn-label-right{left:auto;right:-10px;border-radius:0 2px 2px 0}.btn-xs .btn-label{padding:1px 5px;left:-5px;border-radius:2px 0 0 2px}.btn-xs .btn-label.btn-label-right{left:auto;right:-5px;border-radius:0 2px 2px 0}.nav-tabs-bottom{border-bottom:0;border-top:1px solid #ddd}.nav-tabs-bottom>li{margin-bottom:0;margin-top:-1px}.nav-tabs-bottom>li>a{border-radius:0 0 4px 4px}.nav-tabs-bottom>li>a:hover,.nav-tabs-bottom>li>a:focus,.nav-tabs-bottom>li.active>a,.nav-tabs-bottom>li.active>a:hover,.nav-tabs-bottom>li.active>a:focus{border:1px solid #ddd;border-top-color:transparent}.nav-tabs-left{border-bottom:0;border-right:1px solid #ddd}.nav-tabs-left>li{margin-bottom:0;margin-right:-1px;float:none}.nav-tabs-left>li>a{border-radius:4px 0 0 4px;margin-right:0;margin-bottom:2px}.nav-tabs-left>li>a:hover,.nav-tabs-left>li>a:focus,.nav-tabs-left>li.active>a,.nav-tabs-left>li.active>a:hover,.nav-tabs-left>li.active>a:focus{border:1px solid #ddd;border-right-color:transparent}.row>.nav-tabs-left{padding-right:0;padding-left:15px;margin-right:-1px;position:relative;z-index:1}.row>.nav-tabs-left+.tab-content{border-left:1px solid #ddd}.nav-tabs-right{border-bottom:0;border-left:1px solid #ddd}.nav-tabs-right>li{margin-bottom:0;margin-left:-1px;float:none}.nav-tabs-right>li>a{border-radius:0 4px 4px 0;margin-left:0;margin-bottom:2px}.nav-tabs-right>li>a:hover,.nav-tabs-right>li>a:focus,.nav-tabs-right>li.active>a,.nav-tabs-right>li.active>a:hover,.nav-tabs-right>li.active>a:focus{border:1px solid #ddd;border-left-color:transparent}.row>.nav-tabs-right{padding-left:0;padding-right:15px}.navmenu,.navbar-offcanvas{width:300px;height:auto;border-width:1px;border-style:solid;border-radius:4px}.navmenu-fixed-left,.navmenu-fixed-right,.navbar-offcanvas{position:fixed;z-index:1050;top:0;bottom:0;overflow-y:auto;border-radius:0}.navmenu-fixed-left,.navbar-offcanvas.navmenu-fixed-left{left:0;right:auto;border-width:0 1px 0 0}.navmenu-fixed-right,.navbar-offcanvas{left:auto;right:0;border-width:0 0 0 1px}.navmenu-nav{margin-bottom:10px}.navmenu-nav.dropdown-menu{position:static;margin:0;padding-top:0;float:none;border:none;-webkit-box-shadow:none;box-shadow:none;border-radius:0}.navbar-offcanvas .navbar-nav{margin:0}@media (min-width:768px){.navbar-offcanvas{width:auto;border-top:0;box-shadow:none}.navbar-offcanvas.offcanvas{position:static;display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-offcanvas .navbar-nav.navbar-left:first-child{margin-left:-15px}.navbar-offcanvas .navbar-nav.navbar-right:last-child{margin-right:-15px}.navbar-offcanvas .navmenu-brand{display:none}}.navmenu-brand{display:block;font-size:18px;line-height:20px;padding:10px 15px;margin:10px 0}.navmenu-brand:hover,.navmenu-brand:focus{text-decoration:none}.navmenu-default,.navbar-default .navbar-offcanvas{background-color:#f8f8f8;border-color:#e7e7e7}.navmenu-default .navmenu-brand,.navbar-default .navbar-offcanvas .navmenu-brand{color:#777}.navmenu-default .navmenu-brand:hover,.navbar-default .navbar-offcanvas .navmenu-brand:hover,.navmenu-default .navmenu-brand:focus,.navbar-default .navbar-offcanvas .navmenu-brand:focus{color:#5e5e5e;background-color:transparent}.navmenu-default .navmenu-text,.navbar-default .navbar-offcanvas .navmenu-text{color:#777}.navmenu-default .navmenu-nav>.dropdown>a:hover .caret,.navbar-default .navbar-offcanvas .navmenu-nav>.dropdown>a:hover .caret,.navmenu-default .navmenu-nav>.dropdown>a:focus .caret,.navbar-default .navbar-offcanvas .navmenu-nav>.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navmenu-default .navmenu-nav>.open>a,.navbar-default .navbar-offcanvas .navmenu-nav>.open>a,.navmenu-default .navmenu-nav>.open>a:hover,.navbar-default .navbar-offcanvas .navmenu-nav>.open>a:hover,.navmenu-default .navmenu-nav>.open>a:focus,.navbar-default .navbar-offcanvas .navmenu-nav>.open>a:focus{background-color:#e7e7e7;color:#555}.navmenu-default .navmenu-nav>.open>a .caret,.navbar-default .navbar-offcanvas .navmenu-nav>.open>a .caret,.navmenu-default .navmenu-nav>.open>a:hover .caret,.navbar-default .navbar-offcanvas .navmenu-nav>.open>a:hover .caret,.navmenu-default .navmenu-nav>.open>a:focus .caret,.navbar-default .navbar-offcanvas .navmenu-nav>.open>a:focus .caret{border-top-color:#555;border-bottom-color:#555}.navmenu-default .navmenu-nav>.dropdown>a .caret,.navbar-default .navbar-offcanvas .navmenu-nav>.dropdown>a .caret{border-top-color:#777;border-bottom-color:#777}.navmenu-default .navmenu-nav.dropdown-menu,.navbar-default .navbar-offcanvas .navmenu-nav.dropdown-menu{background-color:#e7e7e7}.navmenu-default .navmenu-nav.dropdown-menu>.divider,.navbar-default .navbar-offcanvas .navmenu-nav.dropdown-menu>.divider{background-color:#f8f8f8}.navmenu-default .navmenu-nav.dropdown-menu>.active>a,.navbar-default .navbar-offcanvas .navmenu-nav.dropdown-menu>.active>a,.navmenu-default .navmenu-nav.dropdown-menu>.active>a:hover,.navbar-default .navbar-offcanvas .navmenu-nav.dropdown-menu>.active>a:hover,.navmenu-default .navmenu-nav.dropdown-menu>.active>a:focus,.navbar-default .navbar-offcanvas .navmenu-nav.dropdown-menu>.active>a:focus{background-color:#d7d7d7}.navmenu-default .navmenu-nav>li>a,.navbar-default .navbar-offcanvas .navmenu-nav>li>a{color:#777}.navmenu-default .navmenu-nav>li>a:hover,.navbar-default .navbar-offcanvas .navmenu-nav>li>a:hover,.navmenu-default .navmenu-nav>li>a:focus,.navbar-default .navbar-offcanvas .navmenu-nav>li>a:focus{color:#333;background-color:transparent}.navmenu-default .navmenu-nav>.active>a,.navbar-default .navbar-offcanvas .navmenu-nav>.active>a,.navmenu-default .navmenu-nav>.active>a:hover,.navbar-default .navbar-offcanvas .navmenu-nav>.active>a:hover,.navmenu-default .navmenu-nav>.active>a:focus,.navbar-default .navbar-offcanvas .navmenu-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navmenu-default .navmenu-nav>.disabled>a,.navbar-default .navbar-offcanvas .navmenu-nav>.disabled>a,.navmenu-default .navmenu-nav>.disabled>a:hover,.navbar-default .navbar-offcanvas .navmenu-nav>.disabled>a:hover,.navmenu-default .navmenu-nav>.disabled>a:focus,.navbar-default .navbar-offcanvas .navmenu-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navmenu-inverse,.navbar-inverse .navbar-offcanvas{background-color:#222;border-color:#080808}.navmenu-inverse .navmenu-brand,.navbar-inverse .navbar-offcanvas .navmenu-brand{color:#999}.navmenu-inverse .navmenu-brand:hover,.navbar-inverse .navbar-offcanvas .navmenu-brand:hover,.navmenu-inverse .navmenu-brand:focus,.navbar-inverse .navbar-offcanvas .navmenu-brand:focus{color:#fff;background-color:transparent}.navmenu-inverse .navmenu-text,.navbar-inverse .navbar-offcanvas .navmenu-text{color:#999}.navmenu-inverse .navmenu-nav>.dropdown>a:hover .caret,.navbar-inverse .navbar-offcanvas .navmenu-nav>.dropdown>a:hover .caret,.navmenu-inverse .navmenu-nav>.dropdown>a:focus .caret,.navbar-inverse .navbar-offcanvas .navmenu-nav>.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navmenu-inverse .navmenu-nav>.open>a,.navbar-inverse .navbar-offcanvas .navmenu-nav>.open>a,.navmenu-inverse .navmenu-nav>.open>a:hover,.navbar-inverse .navbar-offcanvas .navmenu-nav>.open>a:hover,.navmenu-inverse .navmenu-nav>.open>a:focus,.navbar-inverse .navbar-offcanvas .navmenu-nav>.open>a:focus{background-color:#080808;color:#fff}.navmenu-inverse .navmenu-nav>.open>a .caret,.navbar-inverse .navbar-offcanvas .navmenu-nav>.open>a .caret,.navmenu-inverse .navmenu-nav>.open>a:hover .caret,.navbar-inverse .navbar-offcanvas .navmenu-nav>.open>a:hover .caret,.navmenu-inverse .navmenu-nav>.open>a:focus .caret,.navbar-inverse .navbar-offcanvas .navmenu-nav>.open>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navmenu-inverse .navmenu-nav>.dropdown>a .caret,.navbar-inverse .navbar-offcanvas .navmenu-nav>.dropdown>a .caret{border-top-color:#999;border-bottom-color:#999}.navmenu-inverse .navmenu-nav.dropdown-menu,.navbar-inverse .navbar-offcanvas .navmenu-nav.dropdown-menu{background-color:#080808}.navmenu-inverse .navmenu-nav.dropdown-menu>.divider,.navbar-inverse .navbar-offcanvas .navmenu-nav.dropdown-menu>.divider{background-color:#222}.navmenu-inverse .navmenu-nav.dropdown-menu>.active>a,.navbar-inverse .navbar-offcanvas .navmenu-nav.dropdown-menu>.active>a,.navmenu-inverse .navmenu-nav.dropdown-menu>.active>a:hover,.navbar-inverse .navbar-offcanvas .navmenu-nav.dropdown-menu>.active>a:hover,.navmenu-inverse .navmenu-nav.dropdown-menu>.active>a:focus,.navbar-inverse .navbar-offcanvas .navmenu-nav.dropdown-menu>.active>a:focus{background-color:#000}.navmenu-inverse .navmenu-nav>li>a,.navbar-inverse .navbar-offcanvas .navmenu-nav>li>a{color:#999}.navmenu-inverse .navmenu-nav>li>a:hover,.navbar-inverse .navbar-offcanvas .navmenu-nav>li>a:hover,.navmenu-inverse .navmenu-nav>li>a:focus,.navbar-inverse .navbar-offcanvas .navmenu-nav>li>a:focus{color:#fff;background-color:transparent}.navmenu-inverse .navmenu-nav>.active>a,.navbar-inverse .navbar-offcanvas .navmenu-nav>.active>a,.navmenu-inverse .navmenu-nav>.active>a:hover,.navbar-inverse .navbar-offcanvas .navmenu-nav>.active>a:hover,.navmenu-inverse .navmenu-nav>.active>a:focus,.navbar-inverse .navbar-offcanvas .navmenu-nav>.active>a:focus{color:#fff;background-color:#080808}.navmenu-inverse .navmenu-nav>.disabled>a,.navbar-inverse .navbar-offcanvas .navmenu-nav>.disabled>a,.navmenu-inverse .navmenu-nav>.disabled>a:hover,.navbar-inverse .navbar-offcanvas .navmenu-nav>.disabled>a:hover,.navmenu-inverse .navmenu-nav>.disabled>a:focus,.navbar-inverse .navbar-offcanvas .navmenu-nav>.disabled>a:focus{color:#444;background-color:transparent}.alert-fixed-top,.alert-fixed-bottom{position:fixed;width:100%;z-index:1035;border-radius:0;margin:0;left:0}@media (min-width:992px){.alert-fixed-top,.alert-fixed-bottom{width:992px;left:50%;margin-left:-496px}}.alert-fixed-top{top:0;border-width:0 0 1px}@media (min-width:992px){.alert-fixed-top{border-bottom-right-radius:4px;border-bottom-left-radius:4px;border-width:0 1px 1px}}.alert-fixed-bottom{bottom:0;border-width:1px 0 0}@media (min-width:992px){.alert-fixed-bottom{border-top-right-radius:4px;border-top-left-radius:4px;border-width:1px 1px 0}}.offcanvas{display:none}.offcanvas.in{display:block}@media (max-width:767px){.offcanvas-xs{display:none}.offcanvas-xs.in{display:block}}@media (max-width:991px){.offcanvas-sm{display:none}.offcanvas-sm.in{display:block}}@media (max-width:1199px){.offcanvas-md{display:none}.offcanvas-md.in{display:block}}.offcanvas-lg{display:none}.offcanvas-lg.in{display:block}.canvas-sliding{-webkit-transition:top .35s,left .35s,bottom .35s,right .35s;transition:top .35s,left .35s,bottom .35s,right .35s}.offcanvas-clone{height:0!important;width:0!important;overflow:hidden!important;border:none!important;margin:0!important;padding:0!important;position:absolute!important;top:auto!important;left:auto!important;bottom:0!important;right:0!important;opacity:0!important}.table.rowlink td:not(.rowlink-skip),.table .rowlink td:not(.rowlink-skip){cursor:pointer}.table.rowlink td:not(.rowlink-skip) a,.table .rowlink td:not(.rowlink-skip) a{color:inherit;font:inherit;text-decoration:inherit}.table-hover.rowlink tr:hover td,.table-hover .rowlink tr:hover td{background-color:#cfcfcf}.btn-file{overflow:hidden;position:relative;vertical-align:middle}.btn-file>input{position:absolute;top:0;right:0;margin:0;opacity:0;filter:alpha(opacity=0);font-size:23px;height:100%;width:100%;direction:ltr;cursor:pointer}.fileinput{margin-bottom:9px;display:inline-block}.fileinput .form-control{padding-top:7px;padding-bottom:5px;display:inline-block;margin-bottom:0;vertical-align:middle;cursor:text}.fileinput .thumbnail{overflow:hidden;display:inline-block;margin-bottom:5px;vertical-align:middle;text-align:center}.fileinput .thumbnail>img{max-height:100%}.fileinput .btn{vertical-align:middle}.fileinput-exists .fileinput-new,.fileinput-new .fileinput-exists{display:none}.fileinput-inline .fileinput-controls{display:inline}.fileinput-filename{vertical-align:middle;display:inline-block;overflow:hidden}.form-control .fileinput-filename{vertical-align:bottom}.fileinput.input-group{display:table}.fileinput.input-group>*{position:relative;z-index:2}.fileinput.input-group>.btn-file{z-index:1}.fileinput-new.input-group .btn-file,.fileinput-new .input-group .btn-file{border-radius:0 4px 4px 0}.fileinput-new.input-group .btn-file.btn-xs,.fileinput-new .input-group .btn-file.btn-xs,.fileinput-new.input-group .btn-file.btn-sm,.fileinput-new .input-group .btn-file.btn-sm{border-radius:0 3px 3px 0}.fileinput-new.input-group .btn-file.btn-lg,.fileinput-new .input-group .btn-file.btn-lg{border-radius:0 6px 6px 0}.form-group.has-warning .fileinput .fileinput-preview{color:#8a6d3b}.form-group.has-warning .fileinput .thumbnail{border-color:#faebcc}.form-group.has-error .fileinput .fileinput-preview{color:#a94442}.form-group.has-error .fileinput .thumbnail{border-color:#ebccd1}.form-group.has-success .fileinput .fileinput-preview{color:#3c763d}.form-group.has-success .fileinput .thumbnail{border-color:#d6e9c6}.input-group-addon:not(:first-child){border-left:0} \ No newline at end of file diff --git a/www/css/contrib/led.css b/www/css/contrib/led.css new file mode 100644 index 00000000..07a1e3b9 --- /dev/null +++ b/www/css/contrib/led.css @@ -0,0 +1,37 @@ +/* Adapted from http://cssdeck.com/labs/css-leds */ + +.led-red { + /*margin: 20px auto;*/ + width: 12px; + height: 12px; + background-color: #940; + border-radius: 50%; + box-shadow: #000 0 -1px 7px 1px, inset #600 0 -1px 9px, #F00 0 2px 12px; +} + +.led-yellow { + /*margin: 20px auto;*/ + width: 12px; + height: 12px; + background-color: #A90; + border-radius: 50%; + box-shadow: #000 0 -1px 7px 1px, inset #660 0 -1px 9px, #DD0 0 2px 12px; +} + +.led-green { + /*margin: 20px auto;*/ + width: 12px; + height: 12px; + background-color: #690; + border-radius: 50%; + box-shadow: #000 0 -1px 7px 1px, inset #460 0 -1px 9px, #7D0 0 2px 12px; +} + +.led-blue { + /*margin: 20px auto;*/ + width: 12px; + height: 12px; + background-color: #4AB; + border-radius: 50%; + box-shadow: #000 0 -1px 7px 1px, inset #006 0 -1px 9px, #06F 0 2px 14px; +} diff --git a/www/css/contrib/vis-timeline-graph2d.min.css b/www/css/contrib/vis-timeline-graph2d.min.css new file mode 100644 index 00000000..67fec46c --- /dev/null +++ b/www/css/contrib/vis-timeline-graph2d.min.css @@ -0,0 +1 @@ +.vis .overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-active{box-shadow:0 0 10px #86d5f8}.vis [class*=span]{min-height:0;width:auto}div.vis-configuration{position:relative;display:block;float:left;font-size:12px}div.vis-configuration-wrapper{display:block;width:700px}div.vis-configuration-wrapper::after{clear:both;content:"";display:block}div.vis-configuration.vis-config-option-container{display:block;width:495px;background-color:#fff;border:2px solid #f7f8fa;border-radius:4px;margin-top:20px;left:10px;padding-left:5px}div.vis-configuration.vis-config-button{display:block;width:495px;height:25px;vertical-align:middle;line-height:25px;background-color:#f7f8fa;border:2px solid #ceced0;border-radius:4px;margin-top:20px;left:10px;padding-left:5px;cursor:pointer;margin-bottom:30px}div.vis-configuration.vis-config-button.hover{background-color:#4588e6;border:2px solid #214373;color:#fff}div.vis-configuration.vis-config-item{display:block;float:left;width:495px;height:25px;vertical-align:middle;line-height:25px}div.vis-configuration.vis-config-item.vis-config-s2{left:10px;background-color:#f7f8fa;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s3{left:20px;background-color:#e4e9f0;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s4{left:30px;background-color:#cfd8e6;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-header{font-size:18px;font-weight:700}div.vis-configuration.vis-config-label{width:120px;height:25px;line-height:25px}div.vis-configuration.vis-config-label.vis-config-s3{width:110px}div.vis-configuration.vis-config-label.vis-config-s4{width:100px}div.vis-configuration.vis-config-colorBlock{top:1px;width:30px;height:19px;border:1px solid #444;border-radius:2px;padding:0;margin:0;cursor:pointer}input.vis-configuration.vis-config-checkbox{left:-5px}input.vis-configuration.vis-config-rangeinput{position:relative;top:-5px;width:60px;padding:1px;margin:0;pointer-events:none}input.vis-configuration.vis-config-range{-webkit-appearance:none;border:0 solid #fff;background-color:rgba(0,0,0,0);width:300px;height:20px}input.vis-configuration.vis-config-range::-webkit-slider-runnable-track{width:300px;height:5px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(to bottom,#dedede 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8', GradientType=0 );border:1px solid #999;box-shadow:#aaa 0 0 3px 0;border-radius:3px}input.vis-configuration.vis-config-range::-webkit-slider-thumb{-webkit-appearance:none;border:1px solid #14334b;height:17px;width:17px;border-radius:50%;background:#3876c2;background:-moz-linear-gradient(top,#3876c2 0,#385380 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#3876c2),color-stop(100%,#385380));background:-webkit-linear-gradient(top,#3876c2 0,#385380 100%);background:-o-linear-gradient(top,#3876c2 0,#385380 100%);background:-ms-linear-gradient(top,#3876c2 0,#385380 100%);background:linear-gradient(to bottom,#3876c2 0,#385380 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3876c2', endColorstr='#385380', GradientType=0 );box-shadow:#111927 0 0 1px 0;margin-top:-7px}input.vis-configuration.vis-config-range:focus{outline:0}input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track{background:#9d9d9d;background:-moz-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#9d9d9d),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-o-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:linear-gradient(to bottom,#9d9d9d 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#9d9d9d', endColorstr='#c8c8c8', GradientType=0 )}input.vis-configuration.vis-config-range::-moz-range-track{width:300px;height:10px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(to bottom,#dedede 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8', GradientType=0 );border:1px solid #999;box-shadow:#aaa 0 0 3px 0;border-radius:3px}input.vis-configuration.vis-config-range::-moz-range-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:-moz-focusring{outline:#fff solid 1px;outline-offset:-1px}input.vis-configuration.vis-config-range::-ms-track{width:300px;height:5px;background:0 0;border-color:transparent;border-width:6px 0;color:transparent}input.vis-configuration.vis-config-range::-ms-fill-lower{background:#777;border-radius:10px}input.vis-configuration.vis-config-range::-ms-fill-upper{background:#ddd;border-radius:10px}input.vis-configuration.vis-config-range::-ms-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:focus::-ms-fill-lower{background:#888}input.vis-configuration.vis-config-range:focus::-ms-fill-upper{background:#ccc}.vis-configuration-popup{position:absolute;background:rgba(57,76,89,.85);border:2px solid #f2faff;line-height:30px;height:30px;width:150px;text-align:center;color:#fff;font-size:14px;border-radius:4px;-webkit-transition:opacity .3s ease-in-out;-moz-transition:opacity .3s ease-in-out;transition:opacity .3s ease-in-out}.vis-configuration-popup:after,.vis-configuration-popup:before{left:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}.vis-configuration-popup:after{border-color:rgba(136,183,213,0);border-left-color:rgba(57,76,89,.85);border-width:8px;margin-top:-8px}.vis-configuration-popup:before{border-color:rgba(194,225,245,0);border-left-color:#f2faff;border-width:12px;margin-top:-12px}div.vis-tooltip{position:absolute;visibility:hidden;padding:5px;white-space:nowrap;font-family:verdana;font-size:14px;color:#000;background-color:#f5f4ed;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;border:1px solid #808074;box-shadow:3px 3px 10px rgba(0,0,0,.2);pointer-events:none;z-index:5}.vis-current-time{background-color:#FF7F6E;width:2px;z-index:1;pointer-events:none}.vis-rolling-mode-btn{height:40px;width:40px;position:absolute;top:7px;right:20px;border-radius:50%;font-size:28px;cursor:pointer;opacity:.8;color:#fff;font-weight:700;text-align:center;background:#3876c2}.vis-rolling-mode-btn:before{content:"\26F6"}.vis-rolling-mode-btn:hover{opacity:1}.vis-custom-time{background-color:#6E94FF;width:2px;cursor:move;z-index:1}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-horizontal{position:absolute;width:100%;height:0;border-bottom:1px solid}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-minor{border-color:#e5e5e5}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-major{border-color:#bfbfbf}.vis-data-axis .vis-y-axis.vis-major{width:100%;position:absolute;color:#4d4d4d;white-space:nowrap}.vis-data-axis .vis-y-axis.vis-major.vis-measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-minor{position:absolute;width:100%;color:#bebebe;white-space:nowrap}.vis-data-axis .vis-y-axis.vis-minor.vis-measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-title{position:absolute;color:#4d4d4d;white-space:nowrap;bottom:20px;text-align:center}.vis-data-axis .vis-y-axis.vis-title.vis-measure{padding:0;margin:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-title.vis-left{bottom:0;-webkit-transform-origin:left top;-moz-transform-origin:left top;-ms-transform-origin:left top;-o-transform-origin:left top;transform-origin:left bottom;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.vis-data-axis .vis-y-axis.vis-title.vis-right{bottom:0;-webkit-transform-origin:right bottom;-moz-transform-origin:right bottom;-ms-transform-origin:right bottom;-o-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.vis-legend{background-color:rgba(247,252,255,.65);padding:5px;border:1px solid #b3b3b3;box-shadow:2px 2px 10px rgba(154,154,154,.55)}.vis-legend-text{white-space:nowrap;display:inline-block}.vis-item{position:absolute;color:#1A1A1A;border-color:#97B0F8;border-width:1px;background-color:#D5DDF6;display:inline-block}.vis-item.vis-point.vis-selected,.vis-item.vis-selected{background-color:#FFF785}.vis-item.vis-selected{border-color:#FFC200;z-index:2}.vis-editable.vis-selected{cursor:move}.vis-item.vis-box{text-align:center;border-style:solid;border-radius:2px}.vis-item.vis-point{background:0 0}.vis-item.vis-dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}.vis-item.vis-range{border-style:solid;border-radius:2px;box-sizing:border-box}.vis-item.vis-background{border:none;background-color:rgba(213,221,246,.4);box-sizing:border-box;padding:0;margin:0}.vis-item .vis-item-overflow{position:relative;width:100%;height:100%;padding:0;margin:0;overflow:hidden}.vis-item-visible-frame{white-space:nowrap}.vis-item.vis-range .vis-item-content{position:relative;display:inline-block}.vis-item.vis-background .vis-item-content{position:absolute;display:inline-block}.vis-item.vis-line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis-item .vis-item-content{white-space:nowrap;box-sizing:border-box;padding:5px}.vis-item .vis-onUpdateTime-tooltip{position:absolute;background:#4f81bd;color:#fff;width:200px;text-align:center;white-space:nowrap;padding:5px;border-radius:1px}.vis-item .vis-delete,.vis-item .vis-delete-rtl{position:absolute;top:0;width:24px;height:24px;box-sizing:border-box;padding:0 5px;cursor:pointer;-webkit-transition:background .2s linear;-moz-transition:background .2s linear;-ms-transition:background .2s linear;-o-transition:background .2s linear;transition:background .2s linear}.vis-item .vis-delete{right:-24px}.vis-item .vis-delete-rtl{left:-24px}.vis-item .vis-delete-rtl:after,.vis-item .vis-delete:after{content:"\00D7";color:red;font-family:arial,sans-serif;font-size:22px;font-weight:700;-webkit-transition:color .2s linear;-moz-transition:color .2s linear;-ms-transition:color .2s linear;-o-transition:color .2s linear;transition:color .2s linear}.vis-item .vis-delete-rtl:hover,.vis-item .vis-delete:hover{background:red}.vis-item .vis-delete-rtl:hover:after,.vis-item .vis-delete:hover:after{color:#fff}.vis-item .vis-drag-center{position:absolute;width:100%;height:100%;top:0;left:0;cursor:move}.vis-item.vis-range .vis-drag-left,.vis-item.vis-range .vis-drag-right{position:absolute;width:24px;max-width:20%;min-width:2px;height:100%;top:0}.vis-item.vis-range .vis-drag-left{left:-4px;cursor:w-resize}.vis-item.vis-range .vis-drag-right{right:-4px;cursor:e-resize}.vis-range.vis-item.vis-readonly .vis-drag-left,.vis-range.vis-item.vis-readonly .vis-drag-right{cursor:auto}.vis-labelset .vis-label.draggable,.vis-nesting-group{cursor:pointer}.vis-itemset{position:relative;padding:0;margin:0;box-sizing:border-box}.vis-itemset .vis-background,.vis-itemset .vis-foreground{position:absolute;width:100%;height:100%;overflow:visible}.vis-axis{position:absolute;width:100%;height:0;left:0;z-index:1}.vis-foreground .vis-group{position:relative;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis-foreground .vis-group:last-child{border-bottom:none}.vis-nested-group{background:#f5f5f5}.vis-label.vis-nesting-group.expanded:before{content:"\25BC"}.vis-label.vis-nesting-group.collapsed-rtl:before{content:"\25C0"}.vis-label.vis-nesting-group.collapsed:before{content:"\25B6"}.vis-overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-labelset,.vis-labelset .vis-label{position:relative;box-sizing:border-box}.vis-labelset{overflow:hidden}.vis-labelset .vis-label{left:0;top:0;width:100%;color:#4d4d4d;border-bottom:1px solid #bfbfbf}.vis-labelset .vis-label:last-child{border-bottom:none}.vis-labelset .vis-label .vis-inner{display:inline-block;padding:5px}.vis-labelset .vis-label .vis-inner.vis-hidden{padding:0}.vis-panel{position:absolute;padding:0;margin:0;box-sizing:border-box}.vis-panel.vis-bottom,.vis-panel.vis-center,.vis-panel.vis-left,.vis-panel.vis-right,.vis-panel.vis-top{border:1px #bfbfbf}.vis-panel.vis-center,.vis-panel.vis-left,.vis-panel.vis-right{border-top-style:solid;border-bottom-style:solid;overflow:hidden}.vis-left.vis-panel.vis-vertical-scroll,.vis-right.vis-panel.vis-vertical-scroll{height:100%;overflow-x:hidden;overflow-y:scroll}.vis-background,.vis-time-axis,.vis-timeline{overflow:hidden}.vis-left.vis-panel.vis-vertical-scroll{direction:rtl}.vis-left.vis-panel.vis-vertical-scroll .vis-content,.vis-right.vis-panel.vis-vertical-scroll{direction:ltr}.vis-right.vis-panel.vis-vertical-scroll .vis-content{direction:rtl}.vis-panel.vis-bottom,.vis-panel.vis-center,.vis-panel.vis-top{border-left-style:solid;border-right-style:solid}.vis-panel>.vis-content{position:relative}.vis-panel .vis-shadow{position:absolute;width:100%;height:1px;box-shadow:0 0 10px rgba(0,0,0,.8)}.vis-panel .vis-shadow.vis-top{top:-1px;left:0}.vis-panel .vis-shadow.vis-bottom{bottom:-1px;left:0}.vis-graph-group0{fill:#4f81bd;fill-opacity:0;stroke-width:2px;stroke:#4f81bd}.vis-graph-group1{fill:#f79646;fill-opacity:0;stroke-width:2px;stroke:#f79646}.vis-graph-group2{fill:#8c51cf;fill-opacity:0;stroke-width:2px;stroke:#8c51cf}.vis-graph-group3{fill:#75c841;fill-opacity:0;stroke-width:2px;stroke:#75c841}.vis-graph-group4{fill:#ff0100;fill-opacity:0;stroke-width:2px;stroke:#ff0100}.vis-graph-group5{fill:#37d8e6;fill-opacity:0;stroke-width:2px;stroke:#37d8e6}.vis-graph-group6{fill:#042662;fill-opacity:0;stroke-width:2px;stroke:#042662}.vis-graph-group7{fill:#00ff26;fill-opacity:0;stroke-width:2px;stroke:#00ff26}.vis-graph-group8{fill:#f0f;fill-opacity:0;stroke-width:2px;stroke:#f0f}.vis-graph-group9{fill:#8f3938;fill-opacity:0;stroke-width:2px;stroke:#8f3938}.vis-timeline .vis-fill{fill-opacity:.1;stroke:none}.vis-timeline .vis-bar{fill-opacity:.5;stroke-width:1px}.vis-timeline .vis-point{stroke-width:2px;fill-opacity:1}.vis-timeline .vis-legend-background{stroke-width:1px;fill-opacity:.9;fill:#fff;stroke:#c2c2c2}.vis-timeline .vis-outline{stroke-width:1px;fill-opacity:1;fill:#fff;stroke:#e5e5e5}.vis-timeline .vis-icon-fill{fill-opacity:.3;stroke:none}.vis-time-axis{position:relative}.vis-time-axis.vis-foreground{top:0;left:0;width:100%}.vis-time-axis.vis-background{position:absolute;top:0;left:0;width:100%;height:100%}.vis-time-axis .vis-text{position:absolute;color:#4d4d4d;padding:3px;overflow:hidden;box-sizing:border-box;white-space:nowrap}.vis-time-axis .vis-text.vis-measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis-time-axis .vis-grid.vis-vertical{position:absolute;border-left:1px solid}.vis-time-axis .vis-grid.vis-vertical-rtl{position:absolute;border-right:1px solid}.vis-time-axis .vis-grid.vis-minor{border-color:#e5e5e5}.vis-time-axis .vis-grid.vis-major{border-color:#bfbfbf}.vis-timeline{position:relative;border:1px solid #bfbfbf;padding:0;margin:0;box-sizing:border-box} \ No newline at end of file diff --git a/www/css/contrib/vis.css b/www/css/contrib/vis.css new file mode 100644 index 00000000..23cd790f --- /dev/null +++ b/www/css/contrib/vis.css @@ -0,0 +1,1295 @@ +.vis .overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + /* Must be displayed above for example selected Timeline items */ + z-index: 10; +} + +.vis-active { + box-shadow: 0 0 10px #86d5f8; +} + +/* override some bootstrap styles screwing up the timelines css */ + +.vis [class*="span"] { + min-height: 0; + width: auto; +} + +div.vis-configuration { + position:relative; + display:block; + float:left; + font-size:12px; +} + +div.vis-configuration-wrapper { + display:block; + width:700px; +} + + +div.vis-configuration.vis-config-option-container{ + display:block; + width:495px; + background-color: #ffffff; + border:2px solid #f7f8fa; + border-radius:4px; + margin-top:20px; + left:10px; + padding-left:5px; +} + +div.vis-configuration.vis-config-button{ + display:block; + width:495px; + height:25px; + vertical-align: middle; + line-height:25px; + background-color: #f7f8fa; + border:2px solid #ceced0; + border-radius:4px; + margin-top:20px; + left:10px; + padding-left:5px; + cursor: pointer; + margin-bottom:30px; +} + +div.vis-configuration.vis-config-button.hover{ + background-color: #4588e6; + border:2px solid #214373; + color:#ffffff; +} + +div.vis-configuration.vis-config-item{ + display:block; + float:left; + width:495px; + height:25px; + vertical-align: middle; + line-height:25px; +} + + +div.vis-configuration.vis-config-item.vis-config-s2{ + left:10px; + background-color: #f7f8fa; + padding-left:5px; + border-radius:3px; +} +div.vis-configuration.vis-config-item.vis-config-s3{ + left:20px; + background-color: #e4e9f0; + padding-left:5px; + border-radius:3px; +} +div.vis-configuration.vis-config-item.vis-config-s4{ + left:30px; + background-color: #cfd8e6; + padding-left:5px; + border-radius:3px; +} + +div.vis-configuration.vis-config-header{ + font-size:18px; + font-weight: bold; +} + +div.vis-configuration.vis-config-label{ + width:120px; + height:25px; + line-height: 25px; +} + +div.vis-configuration.vis-config-label.vis-config-s3{ + width:110px; +} +div.vis-configuration.vis-config-label.vis-config-s4{ + width:100px; +} + +div.vis-configuration.vis-config-colorBlock{ + top:1px; + width:30px; + height:19px; + border:1px solid #444444; + border-radius:2px; + padding:0px; + margin:0px; + cursor:pointer; +} + +input.vis-configuration.vis-config-checkbox { + left:-5px; +} + + +input.vis-configuration.vis-config-rangeinput{ + position:relative; + top:-5px; + width:60px; + height:13px; + padding:1px; + margin:0; + pointer-events:none; +} + +input.vis-configuration.vis-config-range{ + /*removes default webkit styles*/ + -webkit-appearance: none; + + /*fix for FF unable to apply focus style bug */ + border: 0px solid white; + background-color:rgba(0,0,0,0); + + /*required for proper track sizing in FF*/ + width: 300px; + height:20px; +} +input.vis-configuration.vis-config-range::-webkit-slider-runnable-track { + width: 300px; + height: 5px; + background: #dedede; /* Old browsers */ + background: -moz-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dedede), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* IE10+ */ + background: linear-gradient(to bottom, #dedede 0%,#c8c8c8 99%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ + + border: 1px solid #999999; + box-shadow: #aaaaaa 0px 0px 3px 0px; + border-radius: 3px; +} +input.vis-configuration.vis-config-range::-webkit-slider-thumb { + -webkit-appearance: none; + border: 1px solid #14334b; + height: 17px; + width: 17px; + border-radius: 50%; + background: #3876c2; /* Old browsers */ + background: -moz-linear-gradient(top, #3876c2 0%, #385380 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#3876c2), color-stop(100%,#385380)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #3876c2 0%,#385380 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #3876c2 0%,#385380 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #3876c2 0%,#385380 100%); /* IE10+ */ + background: linear-gradient(to bottom, #3876c2 0%,#385380 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3876c2', endColorstr='#385380',GradientType=0 ); /* IE6-9 */ + box-shadow: #111927 0px 0px 1px 0px; + margin-top: -7px; +} +input.vis-configuration.vis-config-range:focus { + outline: none; +} +input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track { + background: #9d9d9d; /* Old browsers */ + background: -moz-linear-gradient(top, #9d9d9d 0%, #c8c8c8 99%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#9d9d9d), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* IE10+ */ + background: linear-gradient(to bottom, #9d9d9d 0%,#c8c8c8 99%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#9d9d9d', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ +} + +input.vis-configuration.vis-config-range::-moz-range-track { + width: 300px; + height: 10px; + background: #dedede; /* Old browsers */ + background: -moz-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dedede), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* IE10+ */ + background: linear-gradient(to bottom, #dedede 0%,#c8c8c8 99%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ + + border: 1px solid #999999; + box-shadow: #aaaaaa 0px 0px 3px 0px; + border-radius: 3px; +} +input.vis-configuration.vis-config-range::-moz-range-thumb { + border: none; + height: 16px; + width: 16px; + + border-radius: 50%; + background: #385380; +} + +/*hide the outline behind the border*/ +input.vis-configuration.vis-config-range:-moz-focusring{ + outline: 1px solid white; + outline-offset: -1px; +} + +input.vis-configuration.vis-config-range::-ms-track { + width: 300px; + height: 5px; + + /*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */ + background: transparent; + + /*leave room for the larger thumb to overflow with a transparent border */ + border-color: transparent; + border-width: 6px 0; + + /*remove default tick marks*/ + color: transparent; +} +input.vis-configuration.vis-config-range::-ms-fill-lower { + background: #777; + border-radius: 10px; +} +input.vis-configuration.vis-config-range::-ms-fill-upper { + background: #ddd; + border-radius: 10px; +} +input.vis-configuration.vis-config-range::-ms-thumb { + border: none; + height: 16px; + width: 16px; + border-radius: 50%; + background: #385380; +} +input.vis-configuration.vis-config-range:focus::-ms-fill-lower { + background: #888; +} +input.vis-configuration.vis-config-range:focus::-ms-fill-upper { + background: #ccc; +} + +.vis-configuration-popup { + position: absolute; + background: rgba(57, 76, 89, 0.85); + border: 2px solid #f2faff; + line-height:30px; + height:30px; + width:150px; + text-align:center; + color: #ffffff; + font-size:14px; + border-radius:4px; + -webkit-transition: opacity 0.3s ease-in-out; + -moz-transition: opacity 0.3s ease-in-out; + transition: opacity 0.3s ease-in-out; +} +.vis-configuration-popup:after, .vis-configuration-popup:before { + left: 100%; + top: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +.vis-configuration-popup:after { + border-color: rgba(136, 183, 213, 0); + border-left-color: rgba(57, 76, 89, 0.85); + border-width: 8px; + margin-top: -8px; +} +.vis-configuration-popup:before { + border-color: rgba(194, 225, 245, 0); + border-left-color: #f2faff; + border-width: 12px; + margin-top: -12px; +} + +.vis-timeline { + position: relative; + border: 1px solid #bfbfbf; + + overflow: hidden; + padding: 0; + margin: 0; + + box-sizing: border-box; +} + + +.vis-panel { + position: absolute; + + padding: 0; + margin: 0; + + box-sizing: border-box; +} + +.vis-panel.vis-center, +.vis-panel.vis-left, +.vis-panel.vis-right, +.vis-panel.vis-top, +.vis-panel.vis-bottom { + border: 1px #bfbfbf; +} + +.vis-panel.vis-center, +.vis-panel.vis-left, +.vis-panel.vis-right { + border-top-style: solid; + border-bottom-style: solid; + overflow: hidden; +} + +.vis-panel.vis-center, +.vis-panel.vis-top, +.vis-panel.vis-bottom { + border-left-style: solid; + border-right-style: solid; +} + +.vis-background { + overflow: hidden; +} + +.vis-panel > .vis-content { + position: relative; +} + +.vis-panel .vis-shadow { + position: absolute; + width: 100%; + height: 1px; + box-shadow: 0 0 10px rgba(0,0,0,0.8); + /* TODO: find a nice way to ensure vis-shadows are drawn on top of items + z-index: 1; + */ +} + +.vis-panel .vis-shadow.vis-top { + top: -1px; + left: 0; +} + +.vis-panel .vis-shadow.vis-bottom { + bottom: -1px; + left: 0; +} + +.vis-labelset { + position: relative; + + overflow: hidden; + + box-sizing: border-box; +} + +.vis-labelset .vis-label { + position: relative; + left: 0; + top: 0; + width: 100%; + color: #4d4d4d; + + box-sizing: border-box; +} + +.vis-labelset .vis-label { + border-bottom: 1px solid #bfbfbf; +} + +.vis-labelset .vis-label.draggable { + cursor: pointer; +} + +.vis-labelset .vis-label:last-child { + border-bottom: none; +} + +.vis-labelset .vis-label .vis-inner { + display: inline-block; + padding: 5px; +} + +.vis-labelset .vis-label .vis-inner.vis-hidden { + padding: 0; +} + + +.vis-itemset { + position: relative; + padding: 0; + margin: 0; + + box-sizing: border-box; +} + +.vis-itemset .vis-background, +.vis-itemset .vis-foreground { + position: absolute; + width: 100%; + height: 100%; + overflow: visible; +} + +.vis-axis { + position: absolute; + width: 100%; + height: 0; + left: 0; + z-index: 1; +} + +.vis-foreground .vis-group { + position: relative; + box-sizing: border-box; + border-bottom: 1px solid #bfbfbf; +} + +.vis-foreground .vis-group:last-child { + border-bottom: none; +} + +.vis-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; +} + +.vis-item { + position: absolute; + color: #1A1A1A; + border-color: #97B0F8; + border-width: 1px; + background-color: #D5DDF6; + display: inline-block; + /*overflow: hidden;*/ +} + +.vis-item.vis-selected { + border-color: #FFC200; + background-color: #FFF785; + + /* z-index must be higher than the z-index of custom time bar and current time bar */ + z-index: 2; +} + +.vis-editable.vis-selected { + cursor: move; +} + +.vis-item.vis-point.vis-selected { + background-color: #FFF785; +} + +.vis-item.vis-box { + text-align: center; + border-style: solid; + border-radius: 2px; +} + +.vis-item.vis-point { + background: none; +} + +.vis-item.vis-dot { + position: absolute; + padding: 0; + border-width: 4px; + border-style: solid; + border-radius: 4px; +} + +.vis-item.vis-range { + border-style: solid; + border-radius: 2px; + box-sizing: border-box; +} + +.vis-item.vis-background { + border: none; + background-color: rgba(213, 221, 246, 0.4); + box-sizing: border-box; + padding: 0; + margin: 0; +} + +.vis-item .vis-item-overflow { + position: relative; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + overflow: hidden; +} + +.vis-item.vis-range .vis-item-content { + position: relative; + display: inline-block; +} + +.vis-item.vis-background .vis-item-content { + position: absolute; + display: inline-block; +} + +.vis-item.vis-line { + padding: 0; + position: absolute; + width: 0; + border-left-width: 1px; + border-left-style: solid; +} + +.vis-item .vis-item-content { + white-space: nowrap; + box-sizing: border-box; + padding: 5px; +} + +.vis-item .vis-delete { + background: url('img/timeline/delete.png') no-repeat center; + position: absolute; + width: 24px; + height: 24px; + top: -4px; + right: -24px; + cursor: pointer; +} + +.vis-item.vis-range .vis-drag-left { + position: absolute; + width: 24px; + max-width: 20%; + min-width: 2px; + height: 100%; + top: 0; + left: -4px; + + cursor: w-resize; +} + +.vis-item.vis-range .vis-drag-right { + position: absolute; + width: 24px; + max-width: 20%; + min-width: 2px; + height: 100%; + top: 0; + right: -4px; + + cursor: e-resize; +} + +.vis-time-axis { + position: relative; + overflow: hidden; +} + +.vis-time-axis.vis-foreground { + top: 0; + left: 0; + width: 100%; +} + +.vis-time-axis.vis-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.vis-time-axis .vis-text { + position: absolute; + color: #4d4d4d; + padding: 3px; + overflow: hidden; + box-sizing: border-box; + + white-space: nowrap; +} + +.vis-time-axis .vis-text.vis-measure { + position: absolute; + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + visibility: hidden; +} + +.vis-time-axis .vis-grid.vis-vertical { + position: absolute; + border-left: 1px solid; +} + +.vis-time-axis .vis-grid.vis-minor { + border-color: #e5e5e5; +} + +.vis-time-axis .vis-grid.vis-major { + border-color: #bfbfbf; +} + +.vis-current-time { + background-color: #FF7F6E; + width: 2px; + z-index: 1; +} +.vis-custom-time { + background-color: #6E94FF; + width: 2px; + cursor: move; + z-index: 1; +} +.vis-timeline { + /* + -webkit-transition: height .4s ease-in-out; + transition: height .4s ease-in-out; + */ +} + +.vis-panel { + /* + -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; + transition: height .4s ease-in-out, top .4s ease-in-out; + */ +} + +.vis-axis { + /* + -webkit-transition: top .4s ease-in-out; + transition: top .4s ease-in-out; + */ +} + +/* TODO: get animation working nicely + +.vis-item { + -webkit-transition: top .4s ease-in-out; + transition: top .4s ease-in-out; +} + +.vis-item.line { + -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; + transition: height .4s ease-in-out, top .4s ease-in-out; +} +/**/ + +.vis-panel.vis-background.vis-horizontal .vis-grid.vis-horizontal { + position: absolute; + width: 100%; + height: 0; + border-bottom: 1px solid; +} + +.vis-panel.vis-background.vis-horizontal .vis-grid.vis-minor { + border-color: #e5e5e5; +} + +.vis-panel.vis-background.vis-horizontal .vis-grid.vis-major { + border-color: #bfbfbf; +} + + +.vis-data-axis .vis-y-axis.vis-major { + width: 100%; + position: absolute; + color: #4d4d4d; + white-space: nowrap; +} + +.vis-data-axis .vis-y-axis.vis-major.vis-measure { + padding: 0; + margin: 0; + border: 0; + visibility: hidden; + width: auto; +} + + +.vis-data-axis .vis-y-axis.vis-minor { + position: absolute; + width: 100%; + color: #bebebe; + white-space: nowrap; +} + +.vis-data-axis .vis-y-axis.vis-minor.vis-measure { + padding: 0; + margin: 0; + border: 0; + visibility: hidden; + width: auto; +} + +.vis-data-axis .vis-y-axis.vis-title { + position: absolute; + color: #4d4d4d; + white-space: nowrap; + bottom: 20px; + text-align: center; +} + +.vis-data-axis .vis-y-axis.vis-title.vis-measure { + padding: 0; + margin: 0; + visibility: hidden; + width: auto; +} + +.vis-data-axis .vis-y-axis.vis-title.vis-left { + bottom: 0; + -webkit-transform-origin: left top; + -moz-transform-origin: left top; + -ms-transform-origin: left top; + -o-transform-origin: left top; + transform-origin: left bottom; + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + transform: rotate(-90deg); +} + +.vis-data-axis .vis-y-axis.vis-title.vis-right { + bottom: 0; + -webkit-transform-origin: right bottom; + -moz-transform-origin: right bottom; + -ms-transform-origin: right bottom; + -o-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg); +} + +.vis-legend { + background-color: rgba(247, 252, 255, 0.65); + padding: 5px; + border: 1px solid #b3b3b3; + box-shadow: 2px 2px 10px rgba(154, 154, 154, 0.55); +} + +.vis-legend-text { + /*font-size: 10px;*/ + white-space: nowrap; + display: inline-block +} +.vis-graph-group0 { + fill:#4f81bd; + fill-opacity:0; + stroke-width:2px; + stroke: #4f81bd; +} + +.vis-graph-group1 { + fill:#f79646; + fill-opacity:0; + stroke-width:2px; + stroke: #f79646; +} + +.vis-graph-group2 { + fill: #8c51cf; + fill-opacity:0; + stroke-width:2px; + stroke: #8c51cf; +} + +.vis-graph-group3 { + fill: #75c841; + fill-opacity:0; + stroke-width:2px; + stroke: #75c841; +} + +.vis-graph-group4 { + fill: #ff0100; + fill-opacity:0; + stroke-width:2px; + stroke: #ff0100; +} + +.vis-graph-group5 { + fill: #37d8e6; + fill-opacity:0; + stroke-width:2px; + stroke: #37d8e6; +} + +.vis-graph-group6 { + fill: #042662; + fill-opacity:0; + stroke-width:2px; + stroke: #042662; +} + +.vis-graph-group7 { + fill:#00ff26; + fill-opacity:0; + stroke-width:2px; + stroke: #00ff26; +} + +.vis-graph-group8 { + fill:#ff00ff; + fill-opacity:0; + stroke-width:2px; + stroke: #ff00ff; +} + +.vis-graph-group9 { + fill: #8f3938; + fill-opacity:0; + stroke-width:2px; + stroke: #8f3938; +} + +.vis-timeline .vis-fill { + fill-opacity:0.1; + stroke: none; +} + + +.vis-timeline .vis-bar { + fill-opacity:0.5; + stroke-width:1px; +} + +.vis-timeline .vis-point { + stroke-width:2px; + fill-opacity:1.0; +} + + +.vis-timeline .vis-legend-background { + stroke-width:1px; + fill-opacity:0.9; + fill: #ffffff; + stroke: #c2c2c2; +} + + +.vis-timeline .vis-outline { + stroke-width:1px; + fill-opacity:1; + fill: #ffffff; + stroke: #e5e5e5; +} + +.vis-timeline .vis-icon-fill { + fill-opacity:0.3; + stroke: none; +} + +div.vis-network div.vis-manipulation { + border-width: 0; + border-bottom: 1px; + border-style:solid; + border-color: #d6d9d8; + background: #ffffff; /* Old browsers */ + background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */ + background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */ + + padding-top:4px; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 28px; +} + +div.vis-network div.vis-edit-mode { + position:absolute; + left: 0; + top: 5px; + height: 30px; +} + +/* FIXME: shouldn't the vis-close button be a child of the vis-manipulation div? */ + +div.vis-network div.vis-close { + position:absolute; + right: 0; + top: 0; + width: 30px; + height: 30px; + + background-position: 20px 3px; + background-repeat: no-repeat; + background-image: url("img/network/cross.png"); + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.vis-network div.vis-close:hover { + opacity: 0.6; +} + +div.vis-network div.vis-manipulation div.vis-button, +div.vis-network div.vis-edit-mode div.vis-button { + float:left; + font-family: verdana; + font-size: 12px; + -moz-border-radius: 15px; + border-radius: 15px; + display:inline-block; + background-position: 0px 0px; + background-repeat:no-repeat; + height:24px; + margin-left: 10px; + /*vertical-align:middle;*/ + cursor: pointer; + padding: 0px 8px 0px 8px; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.vis-network div.vis-manipulation div.vis-button:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); +} + +div.vis-network div.vis-manipulation div.vis-button:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); +} + +div.vis-network div.vis-manipulation div.vis-button.vis-back { + background-image: url("img/network/backIcon.png"); +} + +div.vis-network div.vis-manipulation div.vis-button.vis-none:hover { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); + cursor: default; +} +div.vis-network div.vis-manipulation div.vis-button.vis-none:active { + box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); +} +div.vis-network div.vis-manipulation div.vis-button.vis-none { + padding: 0; +} +div.vis-network div.vis-manipulation div.notification { + margin: 2px; + font-weight: bold; +} + +div.vis-network div.vis-manipulation div.vis-button.vis-add { + background-image: url("img/network/addNodeIcon.png"); +} + +div.vis-network div.vis-manipulation div.vis-button.vis-edit, +div.vis-network div.vis-edit-mode div.vis-button.vis-edit { + background-image: url("img/network/editIcon.png"); +} + +div.vis-network div.vis-edit-mode div.vis-button.vis-edit.vis-edit-mode { + background-color: #fcfcfc; + border: 1px solid #cccccc; +} + +div.vis-network div.vis-manipulation div.vis-button.vis-connect { + background-image: url("img/network/connectIcon.png"); +} + +div.vis-network div.vis-manipulation div.vis-button.vis-delete { + background-image: url("img/network/deleteIcon.png"); +} +/* top right bottom left */ +div.vis-network div.vis-manipulation div.vis-label, +div.vis-network div.vis-edit-mode div.vis-label { + margin: 0 0 0 23px; + line-height: 25px; +} +div.vis-network div.vis-manipulation div.vis-separator-line { + float:left; + display:inline-block; + width:1px; + height:21px; + background-color: #bdbdbd; + margin: 0px 7px 0 15px; /*top right bottom left*/ +} + +/* TODO: is this redundant? +div.network-navigation_wrapper { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} +*/ +div.vis-network-tooltip { + position: absolute; + visibility: hidden; + padding: 5px; + white-space: nowrap; + + font-family: verdana; + font-size:14px; + font-color:#000000; + background-color: #f5f4ed; + + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + border: 1px solid #808074; + + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2); + pointer-events: none; +} +div.vis-network div.vis-navigation div.vis-button { + width:34px; + height:34px; + -moz-border-radius: 17px; + border-radius: 17px; + position:absolute; + display:inline-block; + background-position: 2px 2px; + background-repeat:no-repeat; + cursor: pointer; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div.vis-network div.vis-navigation div.vis-button:hover { + box-shadow: 0 0 3px 3px rgba(56, 207, 21, 0.30); +} + +div.vis-network div.vis-navigation div.vis-button:active { + box-shadow: 0 0 1px 3px rgba(56, 207, 21, 0.95); +} + +div.vis-network div.vis-navigation div.vis-button.vis-up { + background-image: url("img/network/upArrow.png"); + bottom:50px; + left:55px; +} +div.vis-network div.vis-navigation div.vis-button.vis-down { + background-image: url("img/network/downArrow.png"); + bottom:10px; + left:55px; +} +div.vis-network div.vis-navigation div.vis-button.vis-left { + background-image: url("img/network/leftArrow.png"); + bottom:10px; + left:15px; +} +div.vis-network div.vis-navigation div.vis-button.vis-right { + background-image: url("img/network/rightArrow.png"); + bottom:10px; + left:95px; +} +div.vis-network div.vis-navigation div.vis-button.vis-zoomIn { + background-image: url("img/network/plus.png"); + bottom:10px; + right:15px; +} +div.vis-network div.vis-navigation div.vis-button.vis-zoomOut { + background-image: url("img/network/minus.png"); + bottom:10px; + right:55px; +} +div.vis-network div.vis-navigation div.vis-button.vis-zoomExtends { + background-image: url("img/network/zoomExtends.png"); + bottom:50px; + right:15px; +} + +div.vis-color-picker { + position:absolute; + margin-top:-140px; + margin-left:30px; + width:293px; + height:425px; + padding: 10px; + border-radius:15px; + background-color:#ffffff; + display:none; + box-shadow: rgba(0,0,0,0.5) 0px 0px 10px 0px; +} + +div.vis-color-picker div.vis-arrow { + position: absolute; + top:147px; + left:5px; +} + +div.vis-color-picker div.vis-arrow:after, +div.vis-color-picker div.vis-arrow:before { + right: 100%; + top: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +div.vis-color-picker div.vis-arrow:after { + border-color: rgba(255, 255, 255, 0); + border-right-color: #ffffff; + border-width: 30px; + margin-top: -30px; +} + +div.vis-color-picker div.vis-color { + position:absolute; + width: 289px; + height: 289px; + cursor: pointer; +} + + + +div.vis-color-picker div.vis-brightness { + position: absolute; + top:313px; +} + +div.vis-color-picker div.vis-opacity { + position:absolute; + top:350px; +} + +div.vis-color-picker div.vis-selector { + position:absolute; + top:137px; + left:137px; + width:15px; + height:15px; + border-radius:15px; + border:1px solid #ffffff; + background: #4c4c4c; /* Old browsers */ + background: -moz-linear-gradient(top, #4c4c4c 0%, #595959 12%, #666666 25%, #474747 39%, #2c2c2c 50%, #000000 51%, #111111 60%, #2b2b2b 76%, #1c1c1c 91%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(12%,#595959), color-stop(25%,#666666), color-stop(39%,#474747), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(60%,#111111), color-stop(76%,#2b2b2b), color-stop(91%,#1c1c1c), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* IE10+ */ + background: linear-gradient(to bottom, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313',GradientType=0 ); /* IE6-9 */ +} + + + +div.vis-color-picker div.vis-new-color { + position:absolute; + width:140px; + height:20px; + border:1px solid rgba(0,0,0,0.1); + border-radius:5px; + top:380px; + left:159px; + text-align:right; + padding-right:2px; + font-size:10px; + color:rgba(0,0,0,0.4); + vertical-align:middle; + line-height:20px; + +} + +div.vis-color-picker div.vis-initial-color { + position:absolute; + width:140px; + height:20px; + border:1px solid rgba(0,0,0,0.1); + border-radius:5px; + top:380px; + left:10px; + text-align:left; + padding-left:2px; + font-size:10px; + color:rgba(0,0,0,0.4); + vertical-align:middle; + line-height:20px; +} + +div.vis-color-picker div.vis-label { + position:absolute; + width:300px; + left:10px; +} + +div.vis-color-picker div.vis-label.vis-brightness { + top:300px; +} + +div.vis-color-picker div.vis-label.vis-opacity { + top:338px; +} + +div.vis-color-picker div.vis-button { + position:absolute; + width:68px; + height:25px; + border-radius:10px; + vertical-align: middle; + text-align:center; + line-height: 25px; + top:410px; + border:2px solid #d9d9d9; + background-color: #f7f7f7; + cursor:pointer; +} + +div.vis-color-picker div.vis-button.vis-cancel { + /*border:2px solid #ff4e33;*/ + /*background-color: #ff7761;*/ + left:5px; +} +div.vis-color-picker div.vis-button.vis-load { + /*border:2px solid #a153e6;*/ + /*background-color: #cb8dff;*/ + left:82px; +} +div.vis-color-picker div.vis-button.vis-apply { + /*border:2px solid #4588e6;*/ + /*background-color: #82b6ff;*/ + left:159px; +} +div.vis-color-picker div.vis-button.vis-save { + /*border:2px solid #45e655;*/ + /*background-color: #6dff7c;*/ + left:236px; +} + + +div.vis-color-picker input.vis-range { + width: 290px; + height:20px; +} + +/* TODO: is this redundant? +div.vis-color-picker input.vis-range-brightness { + width: 289px !important; +} + + +div.vis-color-picker input.vis-saturation-range { + width: 289px !important; +}*/ \ No newline at end of file diff --git a/www/css/contrib/vis.min.css b/www/css/contrib/vis.min.css new file mode 100644 index 00000000..4c4f34be --- /dev/null +++ b/www/css/contrib/vis.min.css @@ -0,0 +1 @@ +.vis-background,.vis-labelset,.vis-timeline{overflow:hidden}.vis .overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-active{box-shadow:0 0 10px #86d5f8}.vis [class*=span]{min-height:0;width:auto}div.vis-configuration{position:relative;display:block;float:left;font-size:12px}div.vis-configuration-wrapper{display:block;width:700px}div.vis-configuration.vis-config-option-container{display:block;width:495px;background-color:#fff;border:2px solid #f7f8fa;border-radius:4px;margin-top:20px;left:10px;padding-left:5px}div.vis-configuration.vis-config-button{display:block;width:495px;height:25px;vertical-align:middle;line-height:25px;background-color:#f7f8fa;border:2px solid #ceced0;border-radius:4px;margin-top:20px;left:10px;padding-left:5px;cursor:pointer;margin-bottom:30px}div.vis-configuration.vis-config-button.hover{background-color:#4588e6;border:2px solid #214373;color:#fff}div.vis-configuration.vis-config-item{display:block;float:left;width:495px;height:25px;vertical-align:middle;line-height:25px}div.vis-configuration.vis-config-item.vis-config-s2{left:10px;background-color:#f7f8fa;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s3{left:20px;background-color:#e4e9f0;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-item.vis-config-s4{left:30px;background-color:#cfd8e6;padding-left:5px;border-radius:3px}div.vis-configuration.vis-config-header{font-size:18px;font-weight:700}div.vis-configuration.vis-config-label{width:120px;height:25px;line-height:25px}div.vis-configuration.vis-config-label.vis-config-s3{width:110px}div.vis-configuration.vis-config-label.vis-config-s4{width:100px}div.vis-configuration.vis-config-colorBlock{top:1px;width:30px;height:19px;border:1px solid #444;border-radius:2px;padding:0;margin:0;cursor:pointer}input.vis-configuration.vis-config-checkbox{left:-5px}input.vis-configuration.vis-config-rangeinput{position:relative;top:-5px;width:60px;height:13px;padding:1px;margin:0;pointer-events:none}.vis-panel,.vis-timeline{padding:0;box-sizing:border-box}input.vis-configuration.vis-config-range{-webkit-appearance:none;border:0 solid #fff;background-color:rgba(0,0,0,0);width:300px;height:20px}input.vis-configuration.vis-config-range::-webkit-slider-runnable-track{width:300px;height:5px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(to bottom,#dedede 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8', GradientType=0 );border:1px solid #999;box-shadow:#aaa 0 0 3px 0;border-radius:3px}input.vis-configuration.vis-config-range::-webkit-slider-thumb{-webkit-appearance:none;border:1px solid #14334b;height:17px;width:17px;border-radius:50%;background:#3876c2;background:-moz-linear-gradient(top,#3876c2 0,#385380 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#3876c2),color-stop(100%,#385380));background:-webkit-linear-gradient(top,#3876c2 0,#385380 100%);background:-o-linear-gradient(top,#3876c2 0,#385380 100%);background:-ms-linear-gradient(top,#3876c2 0,#385380 100%);background:linear-gradient(to bottom,#3876c2 0,#385380 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3876c2', endColorstr='#385380', GradientType=0 );box-shadow:#111927 0 0 1px 0;margin-top:-7px}input.vis-configuration.vis-config-range:focus{outline:0}input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track{background:#9d9d9d;background:-moz-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#9d9d9d),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-o-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#9d9d9d 0,#c8c8c8 99%);background:linear-gradient(to bottom,#9d9d9d 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#9d9d9d', endColorstr='#c8c8c8', GradientType=0 )}input.vis-configuration.vis-config-range::-moz-range-track{width:300px;height:10px;background:#dedede;background:-moz-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#dedede),color-stop(99%,#c8c8c8));background:-webkit-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-o-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:-ms-linear-gradient(top,#dedede 0,#c8c8c8 99%);background:linear-gradient(to bottom,#dedede 0,#c8c8c8 99%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8', GradientType=0 );border:1px solid #999;box-shadow:#aaa 0 0 3px 0;border-radius:3px}input.vis-configuration.vis-config-range::-moz-range-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:-moz-focusring{outline:#fff solid 1px;outline-offset:-1px}input.vis-configuration.vis-config-range::-ms-track{width:300px;height:5px;background:0 0;border-color:transparent;border-width:6px 0;color:transparent}input.vis-configuration.vis-config-range::-ms-fill-lower{background:#777;border-radius:10px}input.vis-configuration.vis-config-range::-ms-fill-upper{background:#ddd;border-radius:10px}input.vis-configuration.vis-config-range::-ms-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#385380}input.vis-configuration.vis-config-range:focus::-ms-fill-lower{background:#888}input.vis-configuration.vis-config-range:focus::-ms-fill-upper{background:#ccc}.vis-configuration-popup{position:absolute;background:rgba(57,76,89,.85);border:2px solid #f2faff;line-height:30px;height:30px;width:150px;text-align:center;color:#fff;font-size:14px;border-radius:4px;-webkit-transition:opacity .3s ease-in-out;-moz-transition:opacity .3s ease-in-out;transition:opacity .3s ease-in-out}.vis-configuration-popup:after,.vis-configuration-popup:before{left:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}.vis-configuration-popup:after{border-color:rgba(136,183,213,0);border-left-color:rgba(57,76,89,.85);border-width:8px;margin-top:-8px}.vis-configuration-popup:before{border-color:rgba(194,225,245,0);border-left-color:#f2faff;border-width:12px;margin-top:-12px}.vis-timeline{position:relative;border:1px solid #bfbfbf;margin:0}.vis-panel{position:absolute;margin:0}.vis-panel.vis-bottom,.vis-panel.vis-center,.vis-panel.vis-left,.vis-panel.vis-right,.vis-panel.vis-top{border:1px #bfbfbf}.vis-panel.vis-center,.vis-panel.vis-left,.vis-panel.vis-right{border-top-style:solid;border-bottom-style:solid;overflow:hidden}.vis-panel.vis-bottom,.vis-panel.vis-center,.vis-panel.vis-top{border-left-style:solid;border-right-style:solid}.vis-panel>.vis-content{position:relative}.vis-panel .vis-shadow{position:absolute;width:100%;height:1px;box-shadow:0 0 10px rgba(0,0,0,.8)}.vis-itemset,.vis-labelset,.vis-labelset .vis-label{position:relative;box-sizing:border-box}.vis-panel .vis-shadow.vis-top{top:-1px;left:0}.vis-panel .vis-shadow.vis-bottom{bottom:-1px;left:0}.vis-labelset .vis-label{left:0;top:0;width:100%;color:#4d4d4d;border-bottom:1px solid #bfbfbf}.vis-labelset .vis-label.draggable{cursor:pointer}.vis-labelset .vis-label:last-child{border-bottom:none}.vis-labelset .vis-label .vis-inner{display:inline-block;padding:5px}.vis-labelset .vis-label .vis-inner.vis-hidden{padding:0}.vis-itemset{padding:0;margin:0}.vis-itemset .vis-background,.vis-itemset .vis-foreground{position:absolute;width:100%;height:100%;overflow:visible}.vis-axis{position:absolute;width:100%;height:0;left:0;z-index:1}.vis-foreground .vis-group{position:relative;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis-foreground .vis-group:last-child{border-bottom:none}.vis-overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}.vis-item{position:absolute;color:#1A1A1A;border-color:#97B0F8;border-width:1px;background-color:#D5DDF6;display:inline-block}.vis-item.vis-point.vis-selected,.vis-item.vis-selected{background-color:#FFF785}.vis-item.vis-selected{border-color:#FFC200;z-index:2}.vis-editable.vis-selected{cursor:move}.vis-item.vis-box{text-align:center;border-style:solid;border-radius:2px}.vis-item.vis-point{background:0 0}.vis-item.vis-dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}.vis-item.vis-range{border-style:solid;border-radius:2px;box-sizing:border-box}.vis-item.vis-background{border:none;background-color:rgba(213,221,246,.4);box-sizing:border-box;padding:0;margin:0}.vis-item .vis-item-overflow{position:relative;width:100%;height:100%;padding:0;margin:0;overflow:hidden}.vis-item.vis-range .vis-item-content{position:relative;display:inline-block}.vis-item.vis-background .vis-item-content{position:absolute;display:inline-block}.vis-item.vis-line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis-item .vis-item-content{white-space:nowrap;box-sizing:border-box;padding:5px}.vis-item .vis-delete{background:url(img/timeline/delete.png) center no-repeat;position:absolute;width:24px;height:24px;top:-4px;right:-24px;cursor:pointer}.vis-item.vis-range .vis-drag-left{position:absolute;width:24px;max-width:20%;min-width:2px;height:100%;top:0;left:-4px;cursor:w-resize}.vis-item.vis-range .vis-drag-right{position:absolute;width:24px;max-width:20%;min-width:2px;height:100%;top:0;right:-4px;cursor:e-resize}.vis-time-axis{position:relative;overflow:hidden}.vis-time-axis.vis-foreground{top:0;left:0;width:100%}.vis-time-axis.vis-background{position:absolute;top:0;left:0;width:100%;height:100%}.vis-time-axis .vis-text{position:absolute;color:#4d4d4d;padding:3px;overflow:hidden;box-sizing:border-box;white-space:nowrap}.vis-time-axis .vis-text.vis-measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis-time-axis .vis-grid.vis-vertical{position:absolute;border-left:1px solid}.vis-time-axis .vis-grid.vis-minor{border-color:#e5e5e5}.vis-time-axis .vis-grid.vis-major{border-color:#bfbfbf}.vis-current-time{background-color:#FF7F6E;width:2px;z-index:1}.vis-custom-time{background-color:#6E94FF;width:2px;cursor:move;z-index:1}div.vis-network div.vis-close,div.vis-network div.vis-edit-mode div.vis-button,div.vis-network div.vis-manipulation div.vis-button{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-touch-callout:none;-khtml-user-select:none}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-horizontal{position:absolute;width:100%;height:0;border-bottom:1px solid}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-minor{border-color:#e5e5e5}.vis-panel.vis-background.vis-horizontal .vis-grid.vis-major{border-color:#bfbfbf}.vis-data-axis .vis-y-axis.vis-major{width:100%;position:absolute;color:#4d4d4d;white-space:nowrap}.vis-data-axis .vis-y-axis.vis-major.vis-measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-minor{position:absolute;width:100%;color:#bebebe;white-space:nowrap}.vis-data-axis .vis-y-axis.vis-minor.vis-measure{padding:0;margin:0;border:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-title{position:absolute;color:#4d4d4d;white-space:nowrap;bottom:20px;text-align:center}.vis-data-axis .vis-y-axis.vis-title.vis-measure{padding:0;margin:0;visibility:hidden;width:auto}.vis-data-axis .vis-y-axis.vis-title.vis-left{bottom:0;-webkit-transform-origin:left top;-moz-transform-origin:left top;-ms-transform-origin:left top;-o-transform-origin:left top;transform-origin:left bottom;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.vis-data-axis .vis-y-axis.vis-title.vis-right{bottom:0;-webkit-transform-origin:right bottom;-moz-transform-origin:right bottom;-ms-transform-origin:right bottom;-o-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.vis-legend{background-color:rgba(247,252,255,.65);padding:5px;border:1px solid #b3b3b3;box-shadow:2px 2px 10px rgba(154,154,154,.55)}.vis-legend-text{white-space:nowrap;display:inline-block}.vis-graph-group0{fill:#4f81bd;fill-opacity:0;stroke-width:2px;stroke:#4f81bd}.vis-graph-group1{fill:#f79646;fill-opacity:0;stroke-width:2px;stroke:#f79646}.vis-graph-group2{fill:#8c51cf;fill-opacity:0;stroke-width:2px;stroke:#8c51cf}.vis-graph-group3{fill:#75c841;fill-opacity:0;stroke-width:2px;stroke:#75c841}.vis-graph-group4{fill:#ff0100;fill-opacity:0;stroke-width:2px;stroke:#ff0100}.vis-graph-group5{fill:#37d8e6;fill-opacity:0;stroke-width:2px;stroke:#37d8e6}.vis-graph-group6{fill:#042662;fill-opacity:0;stroke-width:2px;stroke:#042662}.vis-graph-group7{fill:#00ff26;fill-opacity:0;stroke-width:2px;stroke:#00ff26}.vis-graph-group8{fill:#f0f;fill-opacity:0;stroke-width:2px;stroke:#f0f}.vis-graph-group9{fill:#8f3938;fill-opacity:0;stroke-width:2px;stroke:#8f3938}.vis-timeline .vis-fill{fill-opacity:.1;stroke:none}.vis-timeline .vis-bar{fill-opacity:.5;stroke-width:1px}.vis-timeline .vis-point{stroke-width:2px;fill-opacity:1}.vis-timeline .vis-legend-background{stroke-width:1px;fill-opacity:.9;fill:#fff;stroke:#c2c2c2}.vis-timeline .vis-outline{stroke-width:1px;fill-opacity:1;fill:#fff;stroke:#e5e5e5}.vis-timeline .vis-icon-fill{fill-opacity:.3;stroke:none}div.vis-network div.vis-manipulation{border-width:0;border-bottom:1px;border-style:solid;border-color:#d6d9d8;background:#fff;background:-moz-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#fff),color-stop(48%,#fcfcfc),color-stop(50%,#fafafa),color-stop(100%,#fcfcfc));background:-webkit-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-o-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-ms-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:linear-gradient(to bottom,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc', GradientType=0 );padding-top:4px;position:absolute;left:0;top:0;width:100%;height:28px}div.vis-network div.vis-edit-mode{position:absolute;left:0;top:5px;height:30px}div.vis-network div.vis-close{position:absolute;right:0;top:0;width:30px;height:30px;background-position:20px 3px;background-repeat:no-repeat;background-image:url(img/network/cross.png);user-select:none}div.vis-network div.vis-close:hover{opacity:.6}div.vis-network div.vis-edit-mode div.vis-button,div.vis-network div.vis-manipulation div.vis-button{float:left;font-family:verdana;font-size:12px;-moz-border-radius:15px;border-radius:15px;display:inline-block;background-position:0 0;background-repeat:no-repeat;height:24px;margin-left:10px;padding:0 8px;user-select:none}div.vis-network div.vis-manipulation div.vis-button:hover{box-shadow:1px 1px 8px rgba(0,0,0,.2)}div.vis-network div.vis-manipulation div.vis-button:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}div.vis-network div.vis-manipulation div.vis-button.vis-back{background-image:url(img/network/backIcon.png)}div.vis-network div.vis-manipulation div.vis-button.vis-none:hover{box-shadow:1px 1px 8px transparent;cursor:default}div.vis-network div.vis-manipulation div.vis-button.vis-none:active{box-shadow:1px 1px 8px transparent}div.vis-network div.vis-manipulation div.vis-button.vis-none{padding:0}div.vis-network div.vis-manipulation div.notification{margin:2px;font-weight:700}div.vis-network div.vis-manipulation div.vis-button.vis-add{background-image:url(img/network/addNodeIcon.png)}div.vis-network div.vis-edit-mode div.vis-button.vis-edit,div.vis-network div.vis-manipulation div.vis-button.vis-edit{background-image:url(img/network/editIcon.png)}div.vis-network div.vis-edit-mode div.vis-button.vis-edit.vis-edit-mode{background-color:#fcfcfc;border:1px solid #ccc}div.vis-network div.vis-manipulation div.vis-button.vis-connect{background-image:url(img/network/connectIcon.png)}div.vis-network div.vis-manipulation div.vis-button.vis-delete{background-image:url(img/network/deleteIcon.png)}div.vis-network div.vis-edit-mode div.vis-label,div.vis-network div.vis-manipulation div.vis-label{margin:0 0 0 23px;line-height:25px}div.vis-network div.vis-manipulation div.vis-separator-line{float:left;display:inline-block;width:1px;height:21px;background-color:#bdbdbd;margin:0 7px 0 15px}div.vis-network-tooltip{position:absolute;visibility:hidden;padding:5px;white-space:nowrap;font-family:verdana;font-size:14px;font-color:#000;background-color:#f5f4ed;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;border:1px solid #808074;box-shadow:3px 3px 10px rgba(0,0,0,.2);pointer-events:none}div.vis-network div.vis-navigation div.vis-button{width:34px;height:34px;-moz-border-radius:17px;border-radius:17px;position:absolute;display:inline-block;background-position:2px 2px;background-repeat:no-repeat;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.vis-network div.vis-navigation div.vis-button:hover{box-shadow:0 0 3px 3px rgba(56,207,21,.3)}div.vis-network div.vis-navigation div.vis-button:active{box-shadow:0 0 1px 3px rgba(56,207,21,.95)}div.vis-network div.vis-navigation div.vis-button.vis-up{background-image:url(img/network/upArrow.png);bottom:50px;left:55px}div.vis-network div.vis-navigation div.vis-button.vis-down{background-image:url(img/network/downArrow.png);bottom:10px;left:55px}div.vis-network div.vis-navigation div.vis-button.vis-left{background-image:url(img/network/leftArrow.png);bottom:10px;left:15px}div.vis-network div.vis-navigation div.vis-button.vis-right{background-image:url(img/network/rightArrow.png);bottom:10px;left:95px}div.vis-network div.vis-navigation div.vis-button.vis-zoomIn{background-image:url(img/network/plus.png);bottom:10px;right:15px}div.vis-network div.vis-navigation div.vis-button.vis-zoomOut{background-image:url(img/network/minus.png);bottom:10px;right:55px}div.vis-network div.vis-navigation div.vis-button.vis-zoomExtends{background-image:url(img/network/zoomExtends.png);bottom:50px;right:15px}div.vis-color-picker{position:absolute;margin-top:-140px;margin-left:30px;width:293px;height:425px;padding:10px;border-radius:15px;background-color:#fff;display:none;box-shadow:rgba(0,0,0,.5) 0 0 10px 0}div.vis-color-picker div.vis-arrow{position:absolute;top:147px;left:5px}div.vis-color-picker div.vis-arrow:after,div.vis-color-picker div.vis-arrow:before{right:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}div.vis-color-picker div.vis-arrow:after{border-color:rgba(255,255,255,0);border-right-color:#fff;border-width:30px;margin-top:-30px}div.vis-color-picker div.vis-color{position:absolute;width:289px;height:289px;cursor:pointer}div.vis-color-picker div.vis-brightness{position:absolute;top:313px}div.vis-color-picker div.vis-opacity{position:absolute;top:350px}div.vis-color-picker div.vis-selector{position:absolute;top:137px;left:137px;width:15px;height:15px;border-radius:15px;border:1px solid #fff;background:#4c4c4c;background:-moz-linear-gradient(top,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4c4c4c),color-stop(12%,#595959),color-stop(25%,#666),color-stop(39%,#474747),color-stop(50%,#2c2c2c),color-stop(51%,#000),color-stop(60%,#111),color-stop(76%,#2b2b2b),color-stop(91%,#1c1c1c),color-stop(100%,#131313));background:-webkit-linear-gradient(top,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);background:-o-linear-gradient(top,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);background:-ms-linear-gradient(top,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);background:linear-gradient(to bottom,#4c4c4c 0,#595959 12%,#666 25%,#474747 39%,#2c2c2c 50%,#000 51%,#111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313', GradientType=0 )}div.vis-color-picker div.vis-initial-color,div.vis-color-picker div.vis-new-color{width:140px;height:20px;top:380px;font-size:10px;color:rgba(0,0,0,.4);line-height:20px;position:absolute;vertical-align:middle}div.vis-color-picker div.vis-new-color{border:1px solid rgba(0,0,0,.1);border-radius:5px;left:159px;text-align:right;padding-right:2px}div.vis-color-picker div.vis-initial-color{border:1px solid rgba(0,0,0,.1);border-radius:5px;left:10px;text-align:left;padding-left:2px}div.vis-color-picker div.vis-label{position:absolute;width:300px;left:10px}div.vis-color-picker div.vis-label.vis-brightness{top:300px}div.vis-color-picker div.vis-label.vis-opacity{top:338px}div.vis-color-picker div.vis-button{position:absolute;width:68px;height:25px;border-radius:10px;vertical-align:middle;text-align:center;line-height:25px;top:410px;border:2px solid #d9d9d9;background-color:#f7f7f7;cursor:pointer}div.vis-color-picker div.vis-button.vis-cancel{left:5px}div.vis-color-picker div.vis-button.vis-load{left:82px}div.vis-color-picker div.vis-button.vis-apply{left:159px}div.vis-color-picker div.vis-button.vis-save{left:236px}div.vis-color-picker input.vis-range{width:290px;height:20px} \ No newline at end of file diff --git a/www/css/main.css b/www/css/main.css new file mode 100644 index 00000000..c27855e8 --- /dev/null +++ b/www/css/main.css @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2017 Cisco and/or its affiliates. + * + * 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. + * + */ + +ul, a { + font-size: 20px !important; +} + +.navbar-nav>li.leftmenu { + width: 100%; +} + +.navbar-default { + opacity: 0.95 !important; +} + +.buttons { +position: relative; +margin: 50px; +top: 20px; +} + +network { + width: 100%; + margin: 0; + padding: 1em; +} + +graphs { + padding: 1em; + overflow: hidden; +} + +#canvas { + top: 0px; + bottom: 50px; +} + +#viewport { + top: 50px; + height: 94%; + position: relative; +} + + +.slidebar-nav { + left: 250px; + list-style: none; + height: 95%; + margin: 0; + margin-left: -250px; + overflow-y: auto; + padding: 0; + position: fixed; + top: 50px; + width: 0; + z-index: 1000; +} +#wrapper.toggled .slidebar-nav { + width: 250px; +} +.slidebar-nav .navbar-collapse { + padding: 0; + max-height: none; +} + +.slidebar-nav ul { + float: none; + width: 100%; +} +.slidebar-nav ul:not { + display: block; +} +.slidebar-nav li { + float: none; + display: block; +} +.slidebar-nav li a { + padding-top: 12px; + padding-bottom: 12px; +} +.slidebar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin: 0; + padding: 5px 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +.slidebar-nav .open .dropdown-menu > li > a { + padding: 5px 15px 5px 25px; +} +.slidebar-nav .navbar-brand { + width: 100%; +} +.slidebar-nav .open > a > b.caret { + border-top: none; + border-bottom: 4px solid; +} +.slidebar-nav .navbar-nav { + margin: 0; +} + +#wrapper { + padding-left: 0; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; +} +#wrapper.toggled { + padding-left: 250px; +} +#page-wrapper6 { + width: 100%; + position: absolute; + padding: 15px; + top: 50px; +} +#wrapper.toggled #page-wrapper6 { + position: absolute; + margin-right: -250px; +} +@media(min-width:768px) { + #wrapper { + padding-left: 250px; + } + #wrapper.toggled { + padding-left: 0; + } + #page-wrapper6 { + padding: 20px; + position: relative; + } + #wrapper.toggled #page-wrapper6 { + position: relative; + margin-right: 0; + } + .slidebar-nav { + width: 250px;; + } + #wrapper.toggled .slidebar-nav { + width: 0; + } +} + +.slidebar-toggle { + position: relative; + float: left; + padding: 9px 10px; + margin-top: 8px; + margin-left: 15px; +/* + margin-bottom: 8px; + */ + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.slidebar-toggle:focus { + outline: 0; +} +.slidebar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.slidebar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +.navbar-header { + height: 50px; +} + +.navbar-default .slidebar-toggle { + /* Border of hamburger button */ + width: 75px; + + float: left; + background: url(../img/vicn-logo.png); + background-size: 60px; + height: 60px; + top: 6px; + + background-repeat: no-repeat; +} diff --git a/www/favicon.ico b/www/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/www/fonts/glyphicons-halflings-regular.eot b/www/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 00000000..e69de29b diff --git a/www/fonts/glyphicons-halflings-regular.svg b/www/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 00000000..94fb5490 --- /dev/null +++ b/www/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/fonts/glyphicons-halflings-regular.ttf b/www/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 00000000..e69de29b diff --git a/www/fonts/glyphicons-halflings-regular.woff b/www/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 00000000..e69de29b diff --git a/www/fonts/glyphicons-halflings-regular.woff2 b/www/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 00000000..e69de29b diff --git a/www/img/icn-router-vpp.png b/www/img/icn-router-vpp.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/icn-router.png b/www/img/icn-router.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/ip-router-vpp.png b/www/img/ip-router-vpp.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/ip-router.png b/www/img/ip-router.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/lte.png b/www/img/lte.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/pc-video-client.png b/www/img/pc-video-client.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/server.png b/www/img/server.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/smartphone.png b/www/img/smartphone.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/tablet.png b/www/img/tablet.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/undefined.png b/www/img/undefined.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/user.png b/www/img/user.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/vicn-logo.png b/www/img/vicn-logo.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/video-server.png b/www/img/video-server.png new file mode 100644 index 00000000..e69de29b diff --git a/www/img/wifi.png b/www/img/wifi.png new file mode 100644 index 00000000..e69de29b diff --git a/www/index.html b/www/index.html new file mode 100644 index 00000000..0060b0a4 --- /dev/null +++ b/www/index.html @@ -0,0 +1,94 @@ + + + + + + vICN | Network graph + + + + + + + + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/js/class.js b/www/js/class.js new file mode 100644 index 00000000..abdecbe2 --- /dev/null +++ b/www/js/class.js @@ -0,0 +1,64 @@ +/* Simple JavaScript Inheritance + * By John Resig http://ejohn.org/ + * MIT Licensed. + */ +// Inspired by base2 and Prototype +(function(){ + var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; + + // The base Class implementation (does nothing) + this.Class = function(){}; + + // Create a new Class that inherits from this class + Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + for (var name in prop) { + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && + typeof _super[name] == "function" && fnTest.test(prop[name]) ? + (function(name, fn){ + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if ( !initializing && this.init ) + this.init.apply(this, arguments); + } + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.prototype.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); diff --git a/www/js/contrib/bootstrap-table.min.js b/www/js/contrib/bootstrap-table.min.js new file mode 100644 index 00000000..aa8e6854 --- /dev/null +++ b/www/js/contrib/bootstrap-table.min.js @@ -0,0 +1,8 @@ +/* +* bootstrap-table - v1.11.0 - 2016-07-02 +* https://github.com/wenzhixin/bootstrap-table +* Copyright (c) 2016 zhixin wen +* Licensed MIT License +*/ +!function(a){"use strict";var b=null,c=function(a){var b=arguments,c=!0,d=1;return a=a.replace(/%s/g,function(){var a=b[d++];return"undefined"==typeof a?(c=!1,""):a}),c?a:""},d=function(b,c,d,e){var f="";return a.each(b,function(a,b){return b[c]===e?(f=b[d],!1):!0}),f},e=function(b,c){var d=-1;return a.each(b,function(a,b){return b.field===c?(d=a,!1):!0}),d},f=function(b){var c,d,e,f=0,g=[];for(c=0;cd;d++)g[c][d]=!1;for(c=0;ce;e++)g[c+e][k]=!0;for(e=0;j>e;e++)g[c][k+e]=!0}},g=function(){if(null===b){var c,d,e=a("

").addClass("fixed-table-scroll-inner"),f=a("

").addClass("fixed-table-scroll-outer");f.append(e),a("body").append(f),c=e[0].offsetWidth,f.css("overflow","scroll"),d=e[0].offsetWidth,c===d&&(d=f[0].clientWidth),f.remove(),b=c-d}return b},h=function(b,d,e,f){var g=d;if("string"==typeof d){var h=d.split(".");h.length>1?(g=window,a.each(h,function(a,b){g=g[b]})):g=window[d]}return"object"==typeof g?g:"function"==typeof g?g.apply(b,e):!g&&"string"==typeof d&&c.apply(this,[d].concat(e))?c.apply(this,[d].concat(e)):f},i=function(b,c,d){var e=Object.getOwnPropertyNames(b),f=Object.getOwnPropertyNames(c),g="";if(d&&e.length!==f.length)return!1;for(var h=0;h-1&&b[g]!==c[g])return!1;return!0},j=function(a){return"string"==typeof a?a.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'").replace(/`/g,"`"):a},k=function(b){var c=0;return b.children().each(function(){c0||navigator.userAgent.match(/Trident.*rv\:11\./))},o=function(){Object.keys||(Object.keys=function(){var a=Object.prototype.hasOwnProperty,b=!{toString:null}.propertyIsEnumerable("toString"),c=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],d=c.length;return function(e){if("object"!=typeof e&&("function"!=typeof e||null===e))throw new TypeError("Object.keys called on non-object");var f,g,h=[];for(f in e)a.call(e,f)&&h.push(f);if(b)for(g=0;d>g;g++)a.call(e,c[g])&&h.push(c[g]);return h}}())},p=function(b,c){this.options=c,this.$el=a(b),this.$el_=this.$el.clone(),this.timeoutId_=0,this.timeoutFooter_=0,this.init()};p.DEFAULTS={classes:"table table-hover",locale:void 0,height:void 0,undefinedText:"-",sortName:void 0,sortOrder:"asc",sortStable:!1,striped:!1,columns:[[]],data:[],dataField:"rows",method:"get",url:void 0,ajax:void 0,cache:!0,contentType:"application/json",dataType:"json",ajaxOptions:{},queryParams:function(a){return a},queryParamsType:"limit",responseHandler:function(a){return a},pagination:!1,onlyInfoPagination:!1,sidePagination:"client",totalRows:0,pageNumber:1,pageSize:10,pageList:[10,25,50,100],paginationHAlign:"right",paginationVAlign:"bottom",paginationDetailHAlign:"left",paginationPreText:"‹",paginationNextText:"›",search:!1,searchOnEnterKey:!1,strictSearch:!1,searchAlign:"right",selectItemName:"btSelectItem",showHeader:!0,showFooter:!1,showColumns:!1,showPaginationSwitch:!1,showRefresh:!1,showToggle:!1,buttonsAlign:"right",smartDisplay:!0,escape:!1,minimumCountColumns:1,idField:void 0,uniqueId:void 0,cardView:!1,detailView:!1,detailFormatter:function(){return""},trimOnSearch:!0,clickToSelect:!1,singleSelect:!1,toolbar:void 0,toolbarAlign:"left",checkboxHeader:!0,sortable:!0,silentSort:!0,maintainSelected:!1,searchTimeOut:500,searchText:"",iconSize:void 0,buttonsClass:"default",iconsPrefix:"glyphicon",icons:{paginationSwitchDown:"glyphicon-collapse-down icon-chevron-down",paginationSwitchUp:"glyphicon-collapse-up icon-chevron-up",refresh:"glyphicon-refresh icon-refresh",toggle:"glyphicon-list-alt icon-list-alt",columns:"glyphicon-th icon-th",detailOpen:"glyphicon-plus icon-plus",detailClose:"glyphicon-minus icon-minus"},customSearch:a.noop,customSort:a.noop,rowStyle:function(){return{}},rowAttributes:function(){return{}},footerStyle:function(){return{}},onAll:function(){return!1},onClickCell:function(){return!1},onDblClickCell:function(){return!1},onClickRow:function(){return!1},onDblClickRow:function(){return!1},onSort:function(){return!1},onCheck:function(){return!1},onUncheck:function(){return!1},onCheckAll:function(){return!1},onUncheckAll:function(){return!1},onCheckSome:function(){return!1},onUncheckSome:function(){return!1},onLoadSuccess:function(){return!1},onLoadError:function(){return!1},onColumnSwitch:function(){return!1},onPageChange:function(){return!1},onSearch:function(){return!1},onToggle:function(){return!1},onPreBody:function(){return!1},onPostBody:function(){return!1},onPostHeader:function(){return!1},onExpandRow:function(){return!1},onCollapseRow:function(){return!1},onRefreshOptions:function(){return!1},onRefresh:function(){return!1},onResetView:function(){return!1}},p.LOCALES={},p.LOCALES["en-US"]=p.LOCALES.en={formatLoadingMessage:function(){return"Loading, please wait..."},formatRecordsPerPage:function(a){return c("%s rows per page",a)},formatShowingRows:function(a,b,d){return c("Showing %s to %s of %s rows",a,b,d)},formatDetailPagination:function(a){return c("Showing %s rows",a)},formatSearch:function(){return"Search"},formatNoMatches:function(){return"No matching records found"},formatPaginationSwitch:function(){return"Hide/Show pagination"},formatRefresh:function(){return"Refresh"},formatToggle:function(){return"Toggle"},formatColumns:function(){return"Columns"},formatAllRows:function(){return"All"}},a.extend(p.DEFAULTS,p.LOCALES["en-US"]),p.COLUMN_DEFAULTS={radio:!1,checkbox:!1,checkboxEnabled:!0,field:void 0,title:void 0,titleTooltip:void 0,"class":void 0,align:void 0,halign:void 0,falign:void 0,valign:void 0,width:void 0,sortable:!1,order:"asc",visible:!0,switchable:!0,clickToSelect:!0,formatter:void 0,footerFormatter:void 0,events:void 0,sorter:void 0,sortName:void 0,cellStyle:void 0,searchable:!0,searchFormatter:!0,cardVisible:!0},p.EVENTS={"all.bs.table":"onAll","click-cell.bs.table":"onClickCell","dbl-click-cell.bs.table":"onDblClickCell","click-row.bs.table":"onClickRow","dbl-click-row.bs.table":"onDblClickRow","sort.bs.table":"onSort","check.bs.table":"onCheck","uncheck.bs.table":"onUncheck","check-all.bs.table":"onCheckAll","uncheck-all.bs.table":"onUncheckAll","check-some.bs.table":"onCheckSome","uncheck-some.bs.table":"onUncheckSome","load-success.bs.table":"onLoadSuccess","load-error.bs.table":"onLoadError","column-switch.bs.table":"onColumnSwitch","page-change.bs.table":"onPageChange","search.bs.table":"onSearch","toggle.bs.table":"onToggle","pre-body.bs.table":"onPreBody","post-body.bs.table":"onPostBody","post-header.bs.table":"onPostHeader","expand-row.bs.table":"onExpandRow","collapse-row.bs.table":"onCollapseRow","refresh-options.bs.table":"onRefreshOptions","reset-view.bs.table":"onResetView","refresh.bs.table":"onRefresh"},p.prototype.init=function(){this.initLocale(),this.initContainer(),this.initTable(),this.initHeader(),this.initData(),this.initFooter(),this.initToolbar(),this.initPagination(),this.initBody(),this.initSearchText(),this.initServer()},p.prototype.initLocale=function(){if(this.options.locale){var b=this.options.locale.split(/-|_/);b[0].toLowerCase(),b[1]&&b[1].toUpperCase(),a.fn.bootstrapTable.locales[this.options.locale]?a.extend(this.options,a.fn.bootstrapTable.locales[this.options.locale]):a.fn.bootstrapTable.locales[b.join("-")]?a.extend(this.options,a.fn.bootstrapTable.locales[b.join("-")]):a.fn.bootstrapTable.locales[b[0]]&&a.extend(this.options,a.fn.bootstrapTable.locales[b[0]])}},p.prototype.initContainer=function(){this.$container=a(['
','
',"top"===this.options.paginationVAlign||"both"===this.options.paginationVAlign?'
':"",'
','
','
','
',this.options.formatLoadingMessage(),"
","
",'',"bottom"===this.options.paginationVAlign||"both"===this.options.paginationVAlign?'
':"","
","
"].join("")),this.$container.insertAfter(this.$el),this.$tableContainer=this.$container.find(".fixed-table-container"),this.$tableHeader=this.$container.find(".fixed-table-header"),this.$tableBody=this.$container.find(".fixed-table-body"),this.$tableLoading=this.$container.find(".fixed-table-loading"),this.$tableFooter=this.$container.find(".fixed-table-footer"),this.$toolbar=this.$container.find(".fixed-table-toolbar"),this.$pagination=this.$container.find(".fixed-table-pagination"),this.$tableBody.append(this.$el),this.$container.after('
'),this.$el.addClass(this.options.classes),this.options.striped&&this.$el.addClass("table-striped"),-1!==a.inArray("table-no-bordered",this.options.classes.split(" "))&&this.$tableContainer.addClass("table-no-bordered")},p.prototype.initTable=function(){var b=this,c=[],d=[];if(this.$header=this.$el.find(">thead"),this.$header.length||(this.$header=a("").appendTo(this.$el)),this.$header.find("tr").each(function(){var b=[];a(this).find("th").each(function(){"undefined"!=typeof a(this).data("field")&&a(this).data("field",a(this).data("field")+""),b.push(a.extend({},{title:a(this).html(),"class":a(this).attr("class"),titleTooltip:a(this).attr("title"),rowspan:a(this).attr("rowspan")?+a(this).attr("rowspan"):void 0,colspan:a(this).attr("colspan")?+a(this).attr("colspan"):void 0},a(this).data()))}),c.push(b)}),a.isArray(this.options.columns[0])||(this.options.columns=[this.options.columns]),this.options.columns=a.extend(!0,[],c,this.options.columns),this.columns=[],f(this.options.columns),a.each(this.options.columns,function(c,d){a.each(d,function(d,e){e=a.extend({},p.COLUMN_DEFAULTS,e),"undefined"!=typeof e.fieldIndex&&(b.columns[e.fieldIndex]=e),b.options.columns[c][d]=e})}),!this.options.data.length){var e=[];this.$el.find(">tbody>tr").each(function(c){var f={};f._id=a(this).attr("id"),f._class=a(this).attr("class"),f._data=l(a(this).data()),a(this).find(">td").each(function(d){for(var g,h,i=a(this),j=+i.attr("colspan")||1,k=+i.attr("rowspan")||1;e[c]&&e[c][d];d++);for(g=d;d+j>g;g++)for(h=c;c+k>h;h++)e[h]||(e[h]=[]),e[h][g]=!0;var m=b.columns[d].field;f[m]=a(this).html(),f["_"+m+"_id"]=a(this).attr("id"),f["_"+m+"_class"]=a(this).attr("class"),f["_"+m+"_rowspan"]=a(this).attr("rowspan"),f["_"+m+"_colspan"]=a(this).attr("colspan"),f["_"+m+"_title"]=a(this).attr("title"),f["_"+m+"_data"]=l(a(this).data())}),d.push(f)}),this.options.data=d,d.length&&(this.fromHtml=!0)}},p.prototype.initHeader=function(){var b=this,d={},e=[];this.header={fields:[],styles:[],classes:[],formatters:[],events:[],sorters:[],sortNames:[],cellStyles:[],searchables:[]},a.each(this.options.columns,function(f,g){e.push(""),0===f&&!b.options.cardView&&b.options.detailView&&e.push(c('
',b.options.columns.length)),a.each(g,function(a,f){var g="",h="",i="",j="",k=c(' class="%s"',f["class"]),l=(b.options.sortOrder||f.order,"px"),m=f.width;if(void 0===f.width||b.options.cardView||"string"==typeof f.width&&-1!==f.width.indexOf("%")&&(l="%"),f.width&&"string"==typeof f.width&&(m=f.width.replace("%","").replace("px","")),h=c("text-align: %s; ",f.halign?f.halign:f.align),i=c("text-align: %s; ",f.align),j=c("vertical-align: %s; ",f.valign),j+=c("width: %s; ",!f.checkbox&&!f.radio||m?m?m+l:void 0:"36px"),"undefined"!=typeof f.fieldIndex){if(b.header.fields[f.fieldIndex]=f.field,b.header.styles[f.fieldIndex]=i+j,b.header.classes[f.fieldIndex]=k,b.header.formatters[f.fieldIndex]=f.formatter,b.header.events[f.fieldIndex]=f.events,b.header.sorters[f.fieldIndex]=f.sorter,b.header.sortNames[f.fieldIndex]=f.sortName,b.header.cellStyles[f.fieldIndex]=f.cellStyle,b.header.searchables[f.fieldIndex]=f.searchable,!f.visible)return;if(b.options.cardView&&!f.cardVisible)return;d[f.field]=f}e.push(""),e.push(c('
',b.options.sortable&&f.sortable?"sortable both":"")),g=f.title,f.checkbox&&(!b.options.singleSelect&&b.options.checkboxHeader&&(g=''),b.header.stateField=f.field),f.radio&&(g="",b.header.stateField=f.field,b.options.singleSelect=!0),e.push(g),e.push("
"),e.push('
'),e.push("
"),e.push("")}),e.push("")}),this.$header.html(e.join("")),this.$header.find("th[data-field]").each(function(){a(this).data(d[a(this).data("field")])}),this.$container.off("click",".th-inner").on("click",".th-inner",function(c){var d=a(this);return b.options.detailView&&d.closest(".bootstrap-table")[0]!==b.$container[0]?!1:void(b.options.sortable&&d.parent().data().sortable&&b.onSort(c))}),this.$header.children().children().off("keypress").on("keypress",function(c){if(b.options.sortable&&a(this).data().sortable){var d=c.keyCode||c.which;13==d&&b.onSort(c)}}),a(window).off("resize.bootstrap-table"),!this.options.showHeader||this.options.cardView?(this.$header.hide(),this.$tableHeader.hide(),this.$tableLoading.css("top",0)):(this.$header.show(),this.$tableHeader.show(),this.$tableLoading.css("top",this.$header.outerHeight()+1),this.getCaret(),a(window).on("resize.bootstrap-table",a.proxy(this.resetWidth,this))),this.$selectAll=this.$header.find('[name="btSelectAll"]'),this.$selectAll.off("click").on("click",function(){var c=a(this).prop("checked");b[c?"checkAll":"uncheckAll"](),b.updateSelected()})},p.prototype.initFooter=function(){!this.options.showFooter||this.options.cardView?this.$tableFooter.hide():this.$tableFooter.show()},p.prototype.initData=function(a,b){this.data="append"===b?this.data.concat(a):"prepend"===b?[].concat(a).concat(this.data):a||this.options.data,this.options.data="append"===b?this.options.data.concat(a):"prepend"===b?[].concat(a).concat(this.options.data):this.data,"server"!==this.options.sidePagination&&this.initSort()},p.prototype.initSort=function(){var b=this,c=this.options.sortName,d="desc"===this.options.sortOrder?-1:1,e=a.inArray(this.options.sortName,this.header.fields);return this.options.customSort!==a.noop?void this.options.customSort.apply(this,[this.options.sortName,this.options.sortOrder]):void(-1!==e&&(this.options.sortStable&&a.each(this.data,function(a,b){b.hasOwnProperty("_position")||(b._position=a)}),this.data.sort(function(f,g){b.header.sortNames[e]&&(c=b.header.sortNames[e]);var i=m(f,c,b.options.escape),j=m(g,c,b.options.escape),k=h(b.header,b.header.sorters[e],[i,j]);return void 0!==k?d*k:((void 0===i||null===i)&&(i=""),(void 0===j||null===j)&&(j=""),b.options.sortStable&&i===j&&(i=f._position,j=g._position),a.isNumeric(i)&&a.isNumeric(j)?(i=parseFloat(i),j=parseFloat(j),j>i?-1*d:d):i===j?0:("string"!=typeof i&&(i=i.toString()),-1===i.localeCompare(j)?-1*d:d))})))},p.prototype.onSort=function(b){var c="keypress"===b.type?a(b.currentTarget):a(b.currentTarget).parent(),d=this.$header.find("th").eq(c.index());return this.$header.add(this.$header_).find("span.order").remove(),this.options.sortName===c.data("field")?this.options.sortOrder="asc"===this.options.sortOrder?"desc":"asc":(this.options.sortName=c.data("field"),this.options.sortOrder="asc"===c.data("order")?"desc":"asc"),this.trigger("sort",this.options.sortName,this.options.sortOrder),c.add(d).data("order",this.options.sortOrder),this.getCaret(),"server"===this.options.sidePagination?void this.initServer(this.options.silentSort):(this.initSort(),void this.initBody())},p.prototype.initToolbar=function(){var b,d,e=this,f=[],g=0,i=0;this.$toolbar.find(".bs-bars").children().length&&a("body").append(a(this.options.toolbar)),this.$toolbar.html(""),("string"==typeof this.options.toolbar||"object"==typeof this.options.toolbar)&&a(c('
',this.options.toolbarAlign)).appendTo(this.$toolbar).append(a(this.options.toolbar)),f=[c('
',this.options.buttonsAlign,this.options.buttonsAlign)],"string"==typeof this.options.icons&&(this.options.icons=h(null,this.options.icons)),this.options.showPaginationSwitch&&f.push(c('"),this.options.showRefresh&&f.push(c('"),this.options.showToggle&&f.push(c('"),this.options.showColumns&&(f.push(c('
',this.options.formatColumns()),'",'","
")),f.push("
"),(this.showToolbar||f.length>2)&&this.$toolbar.append(f.join("")),this.options.showPaginationSwitch&&this.$toolbar.find('button[name="paginationSwitch"]').off("click").on("click",a.proxy(this.togglePagination,this)),this.options.showRefresh&&this.$toolbar.find('button[name="refresh"]').off("click").on("click",a.proxy(this.refresh,this)),this.options.showToggle&&this.$toolbar.find('button[name="toggle"]').off("click").on("click",function(){e.toggleView()}),this.options.showColumns&&(b=this.$toolbar.find(".keep-open"),i<=this.options.minimumCountColumns&&b.find("input").prop("disabled",!0),b.find("li").off("click").on("click",function(a){a.stopImmediatePropagation()}),b.find("input").off("click").on("click",function(){var b=a(this);e.toggleColumn(a(this).val(),b.prop("checked"),!1),e.trigger("column-switch",a(this).data("field"),b.prop("checked"))})),this.options.search&&(f=[],f.push('"),this.$toolbar.append(f.join("")),d=this.$toolbar.find(".search input"),d.off("keyup drop").on("keyup drop",function(b){e.options.searchOnEnterKey&&13!==b.keyCode||a.inArray(b.keyCode,[37,38,39,40])>-1||(clearTimeout(g),g=setTimeout(function(){e.onSearch(b)},e.options.searchTimeOut))}),n()&&d.off("mouseup").on("mouseup",function(a){clearTimeout(g),g=setTimeout(function(){e.onSearch(a)},e.options.searchTimeOut)}))},p.prototype.onSearch=function(b){var c=a.trim(a(b.currentTarget).val());this.options.trimOnSearch&&a(b.currentTarget).val()!==c&&a(b.currentTarget).val(c),c!==this.searchText&&(this.searchText=c,this.options.searchText=c,this.options.pageNumber=1,this.initSearch(),this.updatePagination(),this.trigger("search",c))},p.prototype.initSearch=function(){var b=this;if("server"!==this.options.sidePagination){if(this.options.customSearch!==a.noop)return void this.options.customSearch.apply(this,[this.searchText]);var c=this.searchText&&(this.options.escape?j(this.searchText):this.searchText).toLowerCase(),d=a.isEmptyObject(this.filterColumns)?null:this.filterColumns;this.data=d?a.grep(this.options.data,function(b){for(var c in d)if(a.isArray(d[c])&&-1===a.inArray(b[c],d[c])||b[c]!==d[c])return!1;return!0}):this.options.data,this.data=c?a.grep(this.data,function(d,f){for(var g=0;g-1&&(n=!0)}this.totalPages=~~((this.options.totalRows-1)/this.options.pageSize)+1,this.options.totalPages=this.totalPages}if(this.totalPages>0&&this.options.pageNumber>this.totalPages&&(this.options.pageNumber=this.totalPages),this.pageFrom=(this.options.pageNumber-1)*this.options.pageSize+1,this.pageTo=this.options.pageNumber*this.options.pageSize,this.pageTo>this.options.totalRows&&(this.pageTo=this.options.totalRows),m.push('
','',this.options.onlyInfoPagination?this.options.formatDetailPagination(this.options.totalRows):this.options.formatShowingRows(this.pageFrom,this.pageTo,this.options.totalRows),""),!this.options.onlyInfoPagination){m.push('');var r=[c('',"top"===this.options.paginationVAlign||"both"===this.options.paginationVAlign?"dropdown":"dropup"),'",'"),m.push(this.options.formatRecordsPerPage(r.join(""))),m.push(""),m.push("
",'")}this.$pagination.html(m.join("")),this.options.onlyInfoPagination||(f=this.$pagination.find(".page-list a"),g=this.$pagination.find(".page-first"),h=this.$pagination.find(".page-pre"),i=this.$pagination.find(".page-next"),j=this.$pagination.find(".page-last"),k=this.$pagination.find(".page-number"),this.options.smartDisplay&&(this.totalPages<=1&&this.$pagination.find("div.pagination").hide(),(p.length<2||this.options.totalRows<=p[0])&&this.$pagination.find("span.page-list").hide(),this.$pagination[this.getData().length?"show":"hide"]()),n&&(this.options.pageSize=this.options.formatAllRows()),f.off("click").on("click",a.proxy(this.onPageListChange,this)),g.off("click").on("click",a.proxy(this.onPageFirst,this)),h.off("click").on("click",a.proxy(this.onPagePre,this)),i.off("click").on("click",a.proxy(this.onPageNext,this)),j.off("click").on("click",a.proxy(this.onPageLast,this)),k.off("click").on("click",a.proxy(this.onPageNumber,this)))},p.prototype.updatePagination=function(b){b&&a(b.currentTarget).hasClass("disabled")||(this.options.maintainSelected||this.resetRows(),this.initPagination(),"server"===this.options.sidePagination?this.initServer():this.initBody(),this.trigger("page-change",this.options.pageNumber,this.options.pageSize))},p.prototype.onPageListChange=function(b){var c=a(b.currentTarget);c.parent().addClass("active").siblings().removeClass("active"),this.options.pageSize=c.text().toUpperCase()===this.options.formatAllRows().toUpperCase()?this.options.formatAllRows():+c.text(),this.$toolbar.find(".page-size").text(this.options.pageSize),this.updatePagination(b)},p.prototype.onPageFirst=function(a){this.options.pageNumber=1,this.updatePagination(a)},p.prototype.onPagePre=function(a){this.options.pageNumber-1===0?this.options.pageNumber=this.options.totalPages:this.options.pageNumber--,this.updatePagination(a)},p.prototype.onPageNext=function(a){this.options.pageNumber+1>this.options.totalPages?this.options.pageNumber=1:this.options.pageNumber++,this.updatePagination(a)},p.prototype.onPageLast=function(a){this.options.pageNumber=this.totalPages,this.updatePagination(a)},p.prototype.onPageNumber=function(b){this.options.pageNumber!==+a(b.currentTarget).text()&&(this.options.pageNumber=+a(b.currentTarget).text(),this.updatePagination(b))},p.prototype.initBody=function(b){var f=this,g=[],i=this.getData();this.trigger("pre-body",i),this.$body=this.$el.find(">tbody"),this.$body.length||(this.$body=a("").appendTo(this.$el)),this.options.pagination&&"server"!==this.options.sidePagination||(this.pageFrom=1,this.pageTo=i.length);for(var k=this.pageFrom-1;k"),this.options.cardView&&g.push(c('
',this.header.fields.length)),!this.options.cardView&&this.options.detailView&&g.push("",'',c('',this.options.iconsPrefix,this.options.icons.detailOpen),"",""),a.each(this.header.fields,function(b,e){var i="",j=m(n,e,f.options.escape),l="",q={},r="",s=f.header.classes[b],t="",u="",v="",w="",x=f.columns[b];if(!(f.fromHtml&&"undefined"==typeof j||!x.visible||f.options.cardView&&!x.cardVisible)){if(o=c('style="%s"',p.concat(f.header.styles[b]).join("; ")),n["_"+e+"_id"]&&(r=c(' id="%s"',n["_"+e+"_id"])),n["_"+e+"_class"]&&(s=c(' class="%s"',n["_"+e+"_class"])),n["_"+e+"_rowspan"]&&(u=c(' rowspan="%s"',n["_"+e+"_rowspan"])),n["_"+e+"_colspan"]&&(v=c(' colspan="%s"',n["_"+e+"_colspan"])),n["_"+e+"_title"]&&(w=c(' title="%s"',n["_"+e+"_title"])),q=h(f.header,f.header.cellStyles[b],[j,n,k,e],q),q.classes&&(s=c(' class="%s"',q.classes)),q.css){var y=[];for(var z in q.css)y.push(z+": "+q.css[z]);o=c('style="%s"',y.concat(f.header.styles[b]).join("; "))}j=h(x,f.header.formatters[b],[j,n,k],j),n["_"+e+"_data"]&&!a.isEmptyObject(n["_"+e+"_data"])&&a.each(n["_"+e+"_data"],function(a,b){"index"!==a&&(t+=c(' data-%s="%s"',a,b))}),x.checkbox||x.radio?(l=x.checkbox?"checkbox":l,l=x.radio?"radio":l,i=[c(f.options.cardView?'
':'',x["class"]||""),"",f.header.formatters[b]&&"string"==typeof j?j:"",f.options.cardView?"
":""].join(""),n[f.header.stateField]=j===!0||j&&j.checked):(j="undefined"==typeof j||null===j?f.options.undefinedText:j,i=f.options.cardView?['
',f.options.showHeader?c('%s',o,d(f.columns,"field","title",e)):"",c('%s',j),"
"].join(""):[c("",r,s,o,t,u,v,w),j,""].join(""),f.options.cardView&&f.options.smartDisplay&&""===j&&(i='
')),g.push(i)}}),this.options.cardView&&g.push("
"),g.push("")}g.length||g.push('',c('%s',this.$header.find("th").length,this.options.formatNoMatches()),""),this.$body.html(g.join("")),b||this.scrollTo(0),this.$body.find("> tr[data-index] > td").off("click dblclick").on("click dblclick",function(b){var d=a(this),g=d.parent(),h=f.data[g.data("index")],i=d[0].cellIndex,j=f.getVisibleFields(),k=j[f.options.detailView&&!f.options.cardView?i-1:i],l=f.columns[e(f.columns,k)],n=m(h,k,f.options.escape);if(!d.find(".detail-icon").length&&(f.trigger("click"===b.type?"click-cell":"dbl-click-cell",k,n,h,d),f.trigger("click"===b.type?"click-row":"dbl-click-row",h,g,k), +"click"===b.type&&f.options.clickToSelect&&l.clickToSelect)){var o=g.find(c('[name="%s"]',f.options.selectItemName));o.length&&o[0].click()}}),this.$body.find("> tr[data-index] > td > .detail-icon").off("click").on("click",function(){var b=a(this),d=b.parent().parent(),e=d.data("index"),g=i[e];if(d.next().is("tr.detail-view"))b.find("i").attr("class",c("%s %s",f.options.iconsPrefix,f.options.icons.detailOpen)),d.next().remove(),f.trigger("collapse-row",e,g);else{b.find("i").attr("class",c("%s %s",f.options.iconsPrefix,f.options.icons.detailClose)),d.after(c('',d.find("td").length));var j=d.next().find("td"),k=h(f.options,f.options.detailFormatter,[e,g,j],"");1===j.length&&j.append(k),f.trigger("expand-row",e,g,j)}f.resetView()}),this.$selectItem=this.$body.find(c('[name="%s"]',this.options.selectItemName)),this.$selectItem.off("click").on("click",function(b){b.stopImmediatePropagation();var c=a(this),d=c.prop("checked"),e=f.data[c.data("index")];f.options.maintainSelected&&a(this).is(":radio")&&a.each(f.options.data,function(a,b){b[f.header.stateField]=!1}),e[f.header.stateField]=d,f.options.singleSelect&&(f.$selectItem.not(this).each(function(){f.data[a(this).data("index")][f.header.stateField]=!1}),f.$selectItem.filter(":checked").not(this).prop("checked",!1)),f.updateSelected(),f.trigger(d?"check":"uncheck",e,c)}),a.each(this.header.events,function(b,c){if(c){"string"==typeof c&&(c=h(null,c));var d=f.header.fields[b],e=a.inArray(d,f.getVisibleFields());f.options.detailView&&!f.options.cardView&&(e+=1);for(var g in c)f.$body.find(">tr:not(.no-records-found)").each(function(){var b=a(this),h=b.find(f.options.cardView?".card-view":"td").eq(e),i=g.indexOf(" "),j=g.substring(0,i),k=g.substring(i+1),l=c[g];h.find(k).off(j).on(j,function(a){var c=b.data("index"),e=f.data[c],g=e[d];l.apply(this,[a,g,e,c])})})}}),this.updateSelected(),this.resetView(),this.trigger("post-body",i)},p.prototype.initServer=function(b,c,d){var e,f=this,g={},i={searchText:this.searchText,sortName:this.options.sortName,sortOrder:this.options.sortOrder};this.options.pagination&&(i.pageSize=this.options.pageSize===this.options.formatAllRows()?this.options.totalRows:this.options.pageSize,i.pageNumber=this.options.pageNumber),(d||this.options.url||this.options.ajax)&&("limit"===this.options.queryParamsType&&(i={search:i.searchText,sort:i.sortName,order:i.sortOrder},this.options.pagination&&(i.offset=this.options.pageSize===this.options.formatAllRows()?0:this.options.pageSize*(this.options.pageNumber-1),i.limit=this.options.pageSize===this.options.formatAllRows()?this.options.totalRows:this.options.pageSize)),a.isEmptyObject(this.filterColumnsPartial)||(i.filter=JSON.stringify(this.filterColumnsPartial,null)),g=h(this.options,this.options.queryParams,[i],g),a.extend(g,c||{}),g!==!1&&(b||this.$tableLoading.show(),e=a.extend({},h(null,this.options.ajaxOptions),{type:this.options.method,url:d||this.options.url,data:"application/json"===this.options.contentType&&"post"===this.options.method?JSON.stringify(g):g,cache:this.options.cache,contentType:this.options.contentType,dataType:this.options.dataType,success:function(a){a=h(f.options,f.options.responseHandler,[a],a),f.load(a),f.trigger("load-success",a),b||f.$tableLoading.hide()},error:function(a){f.trigger("load-error",a.status,a),b||f.$tableLoading.hide()}}),this.options.ajax?h(this,this.options.ajax,[e],null):(this._xhr&&4!==this._xhr.readyState&&this._xhr.abort(),this._xhr=a.ajax(e))))},p.prototype.initSearchText=function(){if(this.options.search&&""!==this.options.searchText){var a=this.$toolbar.find(".search input");a.val(this.options.searchText),this.onSearch({currentTarget:a})}},p.prototype.getCaret=function(){var b=this;a.each(this.$header.find("th"),function(c,d){a(d).find(".sortable").removeClass("desc asc").addClass(a(d).data("field")===b.options.sortName?b.options.sortOrder:"both")})},p.prototype.updateSelected=function(){var b=this.$selectItem.filter(":enabled").length&&this.$selectItem.filter(":enabled").length===this.$selectItem.filter(":enabled").filter(":checked").length;this.$selectAll.add(this.$selectAll_).prop("checked",b),this.$selectItem.each(function(){a(this).closest("tr")[a(this).prop("checked")?"addClass":"removeClass"]("selected")})},p.prototype.updateRows=function(){var b=this;this.$selectItem.each(function(){b.data[a(this).data("index")][b.header.stateField]=a(this).prop("checked")})},p.prototype.resetRows=function(){var b=this;a.each(this.data,function(a,c){b.$selectAll.prop("checked",!1),b.$selectItem.prop("checked",!1),b.header.stateField&&(c[b.header.stateField]=!1)})},p.prototype.trigger=function(b){var c=Array.prototype.slice.call(arguments,1);b+=".bs.table",this.options[p.EVENTS[b]].apply(this.options,c),this.$el.trigger(a.Event(b),c),this.options.onAll(b,c),this.$el.trigger(a.Event("all.bs.table"),[b,c])},p.prototype.resetHeader=function(){clearTimeout(this.timeoutId_),this.timeoutId_=setTimeout(a.proxy(this.fitHeader,this),this.$el.is(":hidden")?100:0)},p.prototype.fitHeader=function(){var b,d,e,f,h=this;if(h.$el.is(":hidden"))return void(h.timeoutId_=setTimeout(a.proxy(h.fitHeader,h),100));if(b=this.$tableBody.get(0),d=b.scrollWidth>b.clientWidth&&b.scrollHeight>b.clientHeight+this.$header.outerHeight()?g():0,this.$el.css("margin-top",-this.$header.outerHeight()),e=a(":focus"),e.length>0){var i=e.parents("th");if(i.length>0){var j=i.attr("data-field");if(void 0!==j){var k=this.$header.find("[data-field='"+j+"']");k.length>0&&k.find(":input").addClass("focus-temp")}}}this.$header_=this.$header.clone(!0,!0),this.$selectAll_=this.$header_.find('[name="btSelectAll"]'),this.$tableHeader.css({"margin-right":d}).find("table").css("width",this.$el.outerWidth()).html("").attr("class",this.$el.attr("class")).append(this.$header_),f=a(".focus-temp:visible:eq(0)"),f.length>0&&(f.focus(),this.$header.find(".focus-temp").removeClass("focus-temp")),this.$header.find("th[data-field]").each(function(){h.$header_.find(c('th[data-field="%s"]',a(this).data("field"))).data(a(this).data())});var l=this.getVisibleFields(),m=this.$header_.find("th");this.$body.find(">tr:first-child:not(.no-records-found) > *").each(function(b){var d=a(this),e=b;h.options.detailView&&!h.options.cardView&&(0===b&&h.$header_.find("th.detail").find(".fht-cell").width(d.innerWidth()),e=b-1);var f=h.$header_.find(c('th[data-field="%s"]',l[e]));f.length>1&&(f=a(m[d[0].cellIndex])),f.find(".fht-cell").width(d.innerWidth())}),this.$tableBody.off("scroll").on("scroll",function(){h.$tableHeader.scrollLeft(a(this).scrollLeft()),h.options.showFooter&&!h.options.cardView&&h.$tableFooter.scrollLeft(a(this).scrollLeft())}),h.trigger("post-header")},p.prototype.resetFooter=function(){var b=this,d=b.getData(),e=[];this.options.showFooter&&!this.options.cardView&&(!this.options.cardView&&this.options.detailView&&e.push('
 
'),a.each(this.columns,function(a,f){var g,i="",j="",k=[],l={},m=c(' class="%s"',f["class"]);if(f.visible&&(!b.options.cardView||f.cardVisible)){if(i=c("text-align: %s; ",f.falign?f.falign:f.align),j=c("vertical-align: %s; ",f.valign),l=h(null,b.options.footerStyle),l&&l.css)for(g in l.css)k.push(g+": "+l.css[g]);e.push(""),e.push('
'),e.push(h(f,f.footerFormatter,[d]," ")||" "),e.push("
"),e.push('
'),e.push(""),e.push("")}}),this.$tableFooter.find("tr").html(e.join("")),this.$tableFooter.show(),clearTimeout(this.timeoutFooter_),this.timeoutFooter_=setTimeout(a.proxy(this.fitFooter,this),this.$el.is(":hidden")?100:0))},p.prototype.fitFooter=function(){var b,c,d;return clearTimeout(this.timeoutFooter_),this.$el.is(":hidden")?void(this.timeoutFooter_=setTimeout(a.proxy(this.fitFooter,this),100)):(c=this.$el.css("width"),d=c>this.$tableBody.width()?g():0,this.$tableFooter.css({"margin-right":d}).find("table").css("width",c).attr("class",this.$el.attr("class")),b=this.$tableFooter.find("td"),void this.$body.find(">tr:first-child:not(.no-records-found) > *").each(function(c){var d=a(this);b.eq(c).find(".fht-cell").width(d.innerWidth())}))},p.prototype.toggleColumn=function(a,b,d){if(-1!==a&&(this.columns[a].visible=b,this.initHeader(),this.initSearch(),this.initPagination(),this.initBody(),this.options.showColumns)){var e=this.$toolbar.find(".keep-open input").prop("disabled",!1);d&&e.filter(c('[value="%s"]',a)).prop("checked",b),e.filter(":checked").length<=this.options.minimumCountColumns&&e.filter(":checked").prop("disabled",!0)}},p.prototype.toggleRow=function(a,b,d){-1!==a&&this.$body.find("undefined"!=typeof a?c('tr[data-index="%s"]',a):c('tr[data-uniqueid="%s"]',b))[d?"show":"hide"]()},p.prototype.getVisibleFields=function(){var b=this,c=[];return a.each(this.header.fields,function(a,d){var f=b.columns[e(b.columns,d)];f.visible&&c.push(d)}),c},p.prototype.resetView=function(a){var b=0;if(a&&a.height&&(this.options.height=a.height),this.$selectAll.prop("checked",this.$selectItem.length>0&&this.$selectItem.length===this.$selectItem.filter(":checked").length),this.options.height){var c=k(this.$toolbar),d=k(this.$pagination),e=this.options.height-c-d;this.$tableContainer.css("height",e+"px")}return this.options.cardView?(this.$el.css("margin-top","0"),this.$tableContainer.css("padding-bottom","0"),void this.$tableFooter.hide()):(this.options.showHeader&&this.options.height?(this.$tableHeader.show(),this.resetHeader(),b+=this.$header.outerHeight()):(this.$tableHeader.hide(),this.trigger("post-header")),this.options.showFooter&&(this.resetFooter(),this.options.height&&(b+=this.$tableFooter.outerHeight()+1)),this.getCaret(),this.$tableContainer.css("padding-bottom",b+"px"),void this.trigger("reset-view"))},p.prototype.getData=function(b){return!this.searchText&&a.isEmptyObject(this.filterColumns)&&a.isEmptyObject(this.filterColumnsPartial)?b?this.options.data.slice(this.pageFrom-1,this.pageTo):this.options.data:b?this.data.slice(this.pageFrom-1,this.pageTo):this.data},p.prototype.load=function(b){var c=!1;"server"===this.options.sidePagination?(this.options.totalRows=b.total,c=b.fixedScroll,b=b[this.options.dataField]):a.isArray(b)||(c=b.fixedScroll,b=b.data),this.initData(b),this.initSearch(),this.initPagination(),this.initBody(c)},p.prototype.append=function(a){this.initData(a,"append"),this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0)},p.prototype.prepend=function(a){this.initData(a,"prepend"),this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0)},p.prototype.remove=function(b){var c,d,e=this.options.data.length;if(b.hasOwnProperty("field")&&b.hasOwnProperty("values")){for(c=e-1;c>=0;c--)d=this.options.data[c],d.hasOwnProperty(b.field)&&-1!==a.inArray(d[b.field],b.values)&&this.options.data.splice(c,1);e!==this.options.data.length&&(this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0))}},p.prototype.removeAll=function(){this.options.data.length>0&&(this.options.data.splice(0,this.options.data.length),this.initSearch(),this.initPagination(),this.initBody(!0))},p.prototype.getRowByUniqueId=function(a){var b,c,d,e=this.options.uniqueId,f=this.options.data.length,g=null;for(b=f-1;b>=0;b--){if(c=this.options.data[b],c.hasOwnProperty(e))d=c[e];else{if(!c._data.hasOwnProperty(e))continue;d=c._data[e]}if("string"==typeof d?a=a.toString():"number"==typeof d&&(Number(d)===d&&d%1===0?a=parseInt(a):d===Number(d)&&0!==d&&(a=parseFloat(a))),d===a){g=c;break}}return g},p.prototype.removeByUniqueId=function(a){var b=this.options.data.length,c=this.getRowByUniqueId(a);c&&this.options.data.splice(this.options.data.indexOf(c),1),b!==this.options.data.length&&(this.initSearch(),this.initPagination(),this.initBody(!0))},p.prototype.updateByUniqueId=function(b){var c=this,d=a.isArray(b)?b:[b];a.each(d,function(b,d){var e;d.hasOwnProperty("id")&&d.hasOwnProperty("row")&&(e=a.inArray(c.getRowByUniqueId(d.id),c.options.data),-1!==e&&a.extend(c.options.data[e],d.row))}),this.initSearch(),this.initSort(),this.initBody(!0)},p.prototype.insertRow=function(a){a.hasOwnProperty("index")&&a.hasOwnProperty("row")&&(this.data.splice(a.index,0,a.row),this.initSearch(),this.initPagination(),this.initSort(),this.initBody(!0))},p.prototype.updateRow=function(b){var c=this,d=a.isArray(b)?b:[b];a.each(d,function(b,d){d.hasOwnProperty("index")&&d.hasOwnProperty("row")&&a.extend(c.options.data[d.index],d.row)}),this.initSearch(),this.initSort(),this.initBody(!0)},p.prototype.showRow=function(a){(a.hasOwnProperty("index")||a.hasOwnProperty("uniqueId"))&&this.toggleRow(a.index,a.uniqueId,!0)},p.prototype.hideRow=function(a){(a.hasOwnProperty("index")||a.hasOwnProperty("uniqueId"))&&this.toggleRow(a.index,a.uniqueId,!1)},p.prototype.getRowsHidden=function(b){var c=a(this.$body[0]).children().filter(":hidden"),d=0;if(b)for(;dtr");if(this.options.detailView&&!this.options.cardView&&(g+=1),e=j.eq(f).find(">td").eq(g),!(0>f||0>g||f>=this.data.length)){for(c=f;f+h>c;c++)for(d=g;g+i>d;d++)j.eq(c).find(">td").eq(d).hide();e.attr("rowspan",h).attr("colspan",i).show()}},p.prototype.updateCell=function(a){a.hasOwnProperty("index")&&a.hasOwnProperty("field")&&a.hasOwnProperty("value")&&(this.data[a.index][a.field]=a.value,a.reinit!==!1&&(this.initSort(),this.initBody(!0)))},p.prototype.getOptions=function(){return this.options},p.prototype.getSelections=function(){var b=this;return a.grep(this.options.data,function(a){return a[b.header.stateField]})},p.prototype.getAllSelections=function(){var b=this;return a.grep(this.options.data,function(a){return a[b.header.stateField]})},p.prototype.checkAll=function(){this.checkAll_(!0)},p.prototype.uncheckAll=function(){this.checkAll_(!1)},p.prototype.checkInvert=function(){var b=this,c=b.$selectItem.filter(":enabled"),d=c.filter(":checked");c.each(function(){a(this).prop("checked",!a(this).prop("checked"))}),b.updateRows(),b.updateSelected(),b.trigger("uncheck-some",d),d=b.getSelections(),b.trigger("check-some",d)},p.prototype.checkAll_=function(a){var b;a||(b=this.getSelections()),this.$selectAll.add(this.$selectAll_).prop("checked",a),this.$selectItem.filter(":enabled").prop("checked",a),this.updateRows(),a&&(b=this.getSelections()),this.trigger(a?"check-all":"uncheck-all",b)},p.prototype.check=function(a){this.check_(!0,a)},p.prototype.uncheck=function(a){this.check_(!1,a)},p.prototype.check_=function(a,b){var d=this.$selectItem.filter(c('[data-index="%s"]',b)).prop("checked",a);this.data[b][this.header.stateField]=a,this.updateSelected(),this.trigger(a?"check":"uncheck",this.data[b],d)},p.prototype.checkBy=function(a){this.checkBy_(!0,a)},p.prototype.uncheckBy=function(a){this.checkBy_(!1,a)},p.prototype.checkBy_=function(b,d){if(d.hasOwnProperty("field")&&d.hasOwnProperty("values")){var e=this,f=[];a.each(this.options.data,function(g,h){if(!h.hasOwnProperty(d.field))return!1;if(-1!==a.inArray(h[d.field],d.values)){var i=e.$selectItem.filter(":enabled").filter(c('[data-index="%s"]',g)).prop("checked",b);h[e.header.stateField]=b,f.push(h),e.trigger(b?"check":"uncheck",h,i)}}),this.updateSelected(),this.trigger(b?"check-some":"uncheck-some",f)}},p.prototype.destroy=function(){this.$el.insertBefore(this.$container),a(this.options.toolbar).insertBefore(this.$el),this.$container.next().remove(),this.$container.remove(),this.$el.html(this.$el_.html()).css("margin-top","0").attr("class",this.$el_.attr("class")||"")},p.prototype.showLoading=function(){this.$tableLoading.show()},p.prototype.hideLoading=function(){this.$tableLoading.hide()},p.prototype.togglePagination=function(){this.options.pagination=!this.options.pagination;var a=this.$toolbar.find('button[name="paginationSwitch"] i');this.options.pagination?a.attr("class",this.options.iconsPrefix+" "+this.options.icons.paginationSwitchDown):a.attr("class",this.options.iconsPrefix+" "+this.options.icons.paginationSwitchUp),this.updatePagination()},p.prototype.refresh=function(a){a&&a.url&&(this.options.pageNumber=1),this.initServer(a&&a.silent,a&&a.query,a&&a.url),this.trigger("refresh",a)},p.prototype.resetWidth=function(){this.options.showHeader&&this.options.height&&this.fitHeader(),this.options.showFooter&&this.fitFooter()},p.prototype.showColumn=function(a){this.toggleColumn(e(this.columns,a),!0,!0)},p.prototype.hideColumn=function(a){this.toggleColumn(e(this.columns,a),!1,!0)},p.prototype.getHiddenColumns=function(){return a.grep(this.columns,function(a){return!a.visible})},p.prototype.getVisibleColumns=function(){return a.grep(this.columns,function(a){return a.visible})},p.prototype.toggleAllColumns=function(b){if(a.each(this.columns,function(a){this.columns[a].visible=b}),this.initHeader(),this.initSearch(),this.initPagination(),this.initBody(),this.options.showColumns){var c=this.$toolbar.find(".keep-open input").prop("disabled",!1);c.filter(":checked").length<=this.options.minimumCountColumns&&c.filter(":checked").prop("disabled",!0)}},p.prototype.showAllColumns=function(){this.toggleAllColumns(!0)},p.prototype.hideAllColumns=function(){this.toggleAllColumns(!1)},p.prototype.filterBy=function(b){this.filterColumns=a.isEmptyObject(b)?{}:b,this.options.pageNumber=1,this.initSearch(),this.updatePagination()},p.prototype.scrollTo=function(a){return"string"==typeof a&&(a="bottom"===a?this.$tableBody[0].scrollHeight:0),"number"==typeof a&&this.$tableBody.scrollTop(a),"undefined"==typeof a?this.$tableBody.scrollTop():void 0},p.prototype.getScrollPosition=function(){return this.scrollTo()},p.prototype.selectPage=function(a){a>0&&a<=this.options.totalPages&&(this.options.pageNumber=a,this.updatePagination())},p.prototype.prevPage=function(){this.options.pageNumber>1&&(this.options.pageNumber--,this.updatePagination())},p.prototype.nextPage=function(){this.options.pageNumber tr[data-index="%s"]',b));d.next().is("tr.detail-view")===(a?!1:!0)&&d.find("> td > .detail-icon").click()},p.prototype.expandRow=function(a){this.expandRow_(!0,a)},p.prototype.collapseRow=function(a){this.expandRow_(!1,a)},p.prototype.expandAllRows=function(b){if(b){var d=this.$body.find(c('> tr[data-index="%s"]',0)),e=this,f=null,g=!1,h=-1;if(d.next().is("tr.detail-view")?d.next().next().is("tr.detail-view")||(d.next().find(".detail-icon").click(),g=!0):(d.find("> td > .detail-icon").click(),g=!0),g)try{h=setInterval(function(){f=e.$body.find("tr.detail-view").last().find(".detail-icon"),f.length>0?f.click():clearInterval(h)},1)}catch(i){clearInterval(h)}}else for(var j=this.$body.children(),k=0;kthis.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('