Merge branch 'master' of ssh://gerrit:29418/keero/keero
This commit is contained in:
commit
3149f0caef
2
windc/.gitignore
vendored
Normal file
2
windc/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.swp
|
||||||
|
*.pyc
|
8
windc/README
Normal file
8
windc/README
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
This is the Windows DataCenter project. It serves two main purposes:
|
||||||
|
|
||||||
|
* Proof the proposed architecture for windows dc service
|
||||||
|
* Provide a Demo for the Windows Environment Management features
|
||||||
|
|
||||||
|
This is not a final project. It is a POC for the Demo and architecture verification purposes.
|
||||||
|
|
||||||
|
|
92
windc/bin/windc-api
Executable file
92
windc/bin/windc-api
Executable file
@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Windows DataCenter API Server
|
||||||
|
"""
|
||||||
|
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import gettext
|
||||||
|
# If ../windc/__init__.py exists, add ../ to Python search path, so that
|
||||||
|
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||||
|
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||||
|
os.pardir,
|
||||||
|
os.pardir))
|
||||||
|
if os.path.exists(os.path.join(possible_topdir, 'windc', '__init__.py')):
|
||||||
|
sys.path.insert(0, possible_topdir)
|
||||||
|
sys.path.insert(0, '.')
|
||||||
|
|
||||||
|
|
||||||
|
from windc.common import cfg
|
||||||
|
from windc.common import config
|
||||||
|
from windc.common import wsgi
|
||||||
|
from windc.db import session
|
||||||
|
|
||||||
|
gettext.install('balancer', unicode=1)
|
||||||
|
|
||||||
|
dbsync_opt = cfg.BoolOpt('dbsync', default=False,
|
||||||
|
help='Perform database schema synchronization')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
conf = config.WindcConfigOpts()
|
||||||
|
conf.register_cli_opt(dbsync_opt)
|
||||||
|
conf()
|
||||||
|
|
||||||
|
if conf.dbsync:
|
||||||
|
config.setup_logging(conf)
|
||||||
|
session.sync(conf)
|
||||||
|
else:
|
||||||
|
app = config.load_paste_app(conf)
|
||||||
|
server = wsgi.Server()
|
||||||
|
server.start(app, conf, default_port=8181)
|
||||||
|
server.wait()
|
||||||
|
except RuntimeError, e:
|
||||||
|
sys.exit("ERROR: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# def create_options(parser):
|
||||||
|
# """
|
||||||
|
# Sets up the CLI and config-file options that may be
|
||||||
|
# parsed and program commands.
|
||||||
|
#
|
||||||
|
# :param parser: The option parser
|
||||||
|
# """
|
||||||
|
# config.add_common_options(parser)
|
||||||
|
# config.add_log_options(parser)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# if __name__ == '__main__':
|
||||||
|
# oparser = optparse.OptionParser(version='%%prog %s'
|
||||||
|
# % version.version_string())
|
||||||
|
# create_options(oparser)
|
||||||
|
# (options, args) = config.parse_options(oparser)
|
||||||
|
#
|
||||||
|
# # try:
|
||||||
|
# conf, app = config.load_paste_app('windc', options, args)
|
||||||
|
#
|
||||||
|
# server = wsgi.Server()
|
||||||
|
# server.start(app, int(conf['bind_port']), conf['bind_host'])
|
||||||
|
# server.wait()
|
||||||
|
# # except RuntimeError, e:
|
||||||
|
# # sys.exit("ERROR: %s" % e)
|
97
windc/doc/Makefile
Normal file
97
windc/doc/Makefile
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SPHINXSOURCE = source
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE)
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
|
||||||
|
|
||||||
|
.DEFAULT_GOAL = html
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@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 " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
-rm -rf nova.sqlite
|
||||||
|
if [ -f .autogenerated ] ; then \
|
||||||
|
cat .autogenerated | xargs rm ; \
|
||||||
|
rm .autogenerated ; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/nova.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/nova.qhc"
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
|
||||||
|
"run these through (pdf)latex."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
416
windc/doc/source/_static/basic.css
Normal file
416
windc/doc/source/_static/basic.css
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
/**
|
||||||
|
* Sphinx stylesheet -- basic theme
|
||||||
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* -- main layout ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
div.clearer {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- relbar ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
div.related {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related h3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related li {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related li.right {
|
||||||
|
float: right;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- sidebar --------------------------------------------------------------- */
|
||||||
|
|
||||||
|
div.sphinxsidebarwrapper {
|
||||||
|
padding: 10px 5px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar {
|
||||||
|
float: left;
|
||||||
|
width: 230px;
|
||||||
|
margin-left: -100%;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar ul ul,
|
||||||
|
div.sphinxsidebar ul.want-points {
|
||||||
|
margin-left: 20px;
|
||||||
|
list-style: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar ul ul {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar form {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar input {
|
||||||
|
border: 1px solid #98dbcc;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- search page ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
ul.search {
|
||||||
|
margin: 10px 0 0 20px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.search li {
|
||||||
|
padding: 5px 0 5px 20px;
|
||||||
|
background-image: url(file.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.search li a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.search li div.context {
|
||||||
|
color: #888;
|
||||||
|
margin: 2px 0 0 30px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.keywordmatches li.goodmatch a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- index page ------------------------------------------------------------ */
|
||||||
|
|
||||||
|
table.contentstable {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.contentstable p.biglink {
|
||||||
|
line-height: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.biglink {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.linkdescr {
|
||||||
|
font-style: italic;
|
||||||
|
padding-top: 5px;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- general index --------------------------------------------------------- */
|
||||||
|
|
||||||
|
table.indextable td {
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.indextable dl, table.indextable dd {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.indextable tr.pcap {
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.indextable tr.cap {
|
||||||
|
margin-top: 10px;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.toggler {
|
||||||
|
margin-right: 3px;
|
||||||
|
margin-top: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- general body styles --------------------------------------------------- */
|
||||||
|
|
||||||
|
a.headerlink {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:hover > a.headerlink,
|
||||||
|
h2:hover > a.headerlink,
|
||||||
|
h3:hover > a.headerlink,
|
||||||
|
h4:hover > a.headerlink,
|
||||||
|
h5:hover > a.headerlink,
|
||||||
|
h6:hover > a.headerlink,
|
||||||
|
dt:hover > a.headerlink {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body p.caption {
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body td {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-list ul {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first {
|
||||||
|
}
|
||||||
|
|
||||||
|
p.rubric {
|
||||||
|
margin-top: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- sidebars -------------------------------------------------------------- */
|
||||||
|
|
||||||
|
div.sidebar {
|
||||||
|
margin: 0 0 0.5em 1em;
|
||||||
|
border: 1px solid #ddb;
|
||||||
|
padding: 7px 7px 0 7px;
|
||||||
|
background-color: #ffe;
|
||||||
|
width: 40%;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sidebar-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- topics ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
div.topic {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 7px 0 7px;
|
||||||
|
margin: 10px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.topic-title {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- admonitions ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
div.admonition {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition dl {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.admonition-title {
|
||||||
|
margin: 0px 10px 5px 0px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body p.centered {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- tables ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
table.docutils {
|
||||||
|
border: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.docutils td, table.docutils th {
|
||||||
|
padding: 1px 8px 1px 0;
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.field-list td, table.field-list th {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.footnote td, table.footnote th {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- other body styles ----------------------------------------------------- */
|
||||||
|
|
||||||
|
dl {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd p {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd ul, dd table {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt:target, .highlight {
|
||||||
|
background-color: #fbe54e;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.glossary dt {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-list ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-list p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refcount {
|
||||||
|
color: #060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optional {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versionmodified {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-message {
|
||||||
|
background-color: #fda;
|
||||||
|
padding: 5px;
|
||||||
|
border: 3px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footnote:target {
|
||||||
|
background-color: #ffa
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-block {
|
||||||
|
display: block;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-block .line-block {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- code displays --------------------------------------------------------- */
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.linenos pre {
|
||||||
|
padding: 5px 0px;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.highlighttable {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.highlighttable td {
|
||||||
|
padding: 0 0.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.descname {
|
||||||
|
background-color: transparent;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.descclassname {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt.xref, a tt {
|
||||||
|
background-color: transparent;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- math display ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
img.math {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body div.math p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.eqno {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- printout stylesheet --------------------------------------------------- */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
div.document,
|
||||||
|
div.documentwrapper,
|
||||||
|
div.bodywrapper {
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar,
|
||||||
|
div.related,
|
||||||
|
div.footer,
|
||||||
|
#top-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
230
windc/doc/source/_static/default.css
Normal file
230
windc/doc/source/_static/default.css
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* Sphinx stylesheet -- default theme
|
||||||
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import url("basic.css");
|
||||||
|
|
||||||
|
/* -- page layout ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 100%;
|
||||||
|
background-color: #11303d;
|
||||||
|
color: #000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.document {
|
||||||
|
background-color: #1c4e63;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.documentwrapper {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.bodywrapper {
|
||||||
|
margin: 0 0 0 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
padding: 0 20px 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer {
|
||||||
|
color: #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px 0 9px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.footer a {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related {
|
||||||
|
background-color: #133f52;
|
||||||
|
line-height: 30px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related a {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar {
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar h3 {
|
||||||
|
font-family: 'Trebuchet MS', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar h3 a {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar h4 {
|
||||||
|
font-family: 'Trebuchet MS', sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar p {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar p.topless {
|
||||||
|
margin: 5px 10px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar ul {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar a {
|
||||||
|
color: #98dbcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar input {
|
||||||
|
border: 1px solid #98dbcc;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- body styles ----------------------------------------------------------- */
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #355f7c;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body p, div.body dd, div.body li {
|
||||||
|
text-align: left;
|
||||||
|
line-height: 130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body h1,
|
||||||
|
div.body h2,
|
||||||
|
div.body h3,
|
||||||
|
div.body h4,
|
||||||
|
div.body h5,
|
||||||
|
div.body h6 {
|
||||||
|
font-family: 'Trebuchet MS', sans-serif;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #20435c;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
margin: 20px -20px 10px -20px;
|
||||||
|
padding: 3px 0 3px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body h1 { margin-top: 0; font-size: 200%; }
|
||||||
|
div.body h2 { font-size: 160%; }
|
||||||
|
div.body h3 { font-size: 140%; }
|
||||||
|
div.body h4 { font-size: 120%; }
|
||||||
|
div.body h5 { font-size: 110%; }
|
||||||
|
div.body h6 { font-size: 100%; }
|
||||||
|
|
||||||
|
a.headerlink {
|
||||||
|
color: #c60f0f;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 0 4px 0 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.headerlink:hover {
|
||||||
|
background-color: #c60f0f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body p, div.body dd, div.body li {
|
||||||
|
text-align: left;
|
||||||
|
line-height: 130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition p.admonition-title + p {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition p {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition pre {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition ul, div.admonition ol {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.note {
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.seealso {
|
||||||
|
background-color: #ffc;
|
||||||
|
border: 1px solid #ff6;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.topic {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.warning {
|
||||||
|
background-color: #ffe4e4;
|
||||||
|
border: 1px solid #f66;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.admonition-title {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.admonition-title:after {
|
||||||
|
content: ":";
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #eeffcc;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 120%;
|
||||||
|
border: 1px solid #ac9;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tt {
|
||||||
|
background-color: #ecf0f3;
|
||||||
|
padding: 0 1px 0 1px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning tt {
|
||||||
|
background: #efc2c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note tt {
|
||||||
|
background: #d6d6d6;
|
||||||
|
}
|
154
windc/doc/source/_static/jquery.tweet.js
Normal file
154
windc/doc/source/_static/jquery.tweet.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
(function($) {
|
||||||
|
|
||||||
|
$.fn.tweet = function(o){
|
||||||
|
var s = {
|
||||||
|
username: ["seaofclouds"], // [string] required, unless you want to display our tweets. :) it can be an array, just do ["username1","username2","etc"]
|
||||||
|
list: null, //[string] optional name of list belonging to username
|
||||||
|
avatar_size: null, // [integer] height and width of avatar if displayed (48px max)
|
||||||
|
count: 3, // [integer] how many tweets to display?
|
||||||
|
intro_text: null, // [string] do you want text BEFORE your your tweets?
|
||||||
|
outro_text: null, // [string] do you want text AFTER your tweets?
|
||||||
|
join_text: null, // [string] optional text in between date and tweet, try setting to "auto"
|
||||||
|
auto_join_text_default: "i said,", // [string] auto text for non verb: "i said" bullocks
|
||||||
|
auto_join_text_ed: "i", // [string] auto text for past tense: "i" surfed
|
||||||
|
auto_join_text_ing: "i am", // [string] auto tense for present tense: "i was" surfing
|
||||||
|
auto_join_text_reply: "i replied to", // [string] auto tense for replies: "i replied to" @someone "with"
|
||||||
|
auto_join_text_url: "i was looking at", // [string] auto tense for urls: "i was looking at" http:...
|
||||||
|
loading_text: null, // [string] optional loading text, displayed while tweets load
|
||||||
|
query: null // [string] optional search query
|
||||||
|
};
|
||||||
|
|
||||||
|
if(o) $.extend(s, o);
|
||||||
|
|
||||||
|
$.fn.extend({
|
||||||
|
linkUrl: function() {
|
||||||
|
var returning = [];
|
||||||
|
var regexp = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
|
||||||
|
this.each(function() {
|
||||||
|
returning.push(this.replace(regexp,"<a href=\"$1\">$1</a>"));
|
||||||
|
});
|
||||||
|
return $(returning);
|
||||||
|
},
|
||||||
|
linkUser: function() {
|
||||||
|
var returning = [];
|
||||||
|
var regexp = /[\@]+([A-Za-z0-9-_]+)/gi;
|
||||||
|
this.each(function() {
|
||||||
|
returning.push(this.replace(regexp,"<a href=\"http://twitter.com/$1\">@$1</a>"));
|
||||||
|
});
|
||||||
|
return $(returning);
|
||||||
|
},
|
||||||
|
linkHash: function() {
|
||||||
|
var returning = [];
|
||||||
|
var regexp = / [\#]+([A-Za-z0-9-_]+)/gi;
|
||||||
|
this.each(function() {
|
||||||
|
returning.push(this.replace(regexp, ' <a href="http://search.twitter.com/search?q=&tag=$1&lang=all&from='+s.username.join("%2BOR%2B")+'">#$1</a>'));
|
||||||
|
});
|
||||||
|
return $(returning);
|
||||||
|
},
|
||||||
|
capAwesome: function() {
|
||||||
|
var returning = [];
|
||||||
|
this.each(function() {
|
||||||
|
returning.push(this.replace(/\b(awesome)\b/gi, '<span class="awesome">$1</span>'));
|
||||||
|
});
|
||||||
|
return $(returning);
|
||||||
|
},
|
||||||
|
capEpic: function() {
|
||||||
|
var returning = [];
|
||||||
|
this.each(function() {
|
||||||
|
returning.push(this.replace(/\b(epic)\b/gi, '<span class="epic">$1</span>'));
|
||||||
|
});
|
||||||
|
return $(returning);
|
||||||
|
},
|
||||||
|
makeHeart: function() {
|
||||||
|
var returning = [];
|
||||||
|
this.each(function() {
|
||||||
|
returning.push(this.replace(/(<)+[3]/gi, "<tt class='heart'>♥</tt>"));
|
||||||
|
});
|
||||||
|
return $(returning);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function relative_time(time_value) {
|
||||||
|
var parsed_date = Date.parse(time_value);
|
||||||
|
var relative_to = (arguments.length > 1) ? arguments[1] : new Date();
|
||||||
|
var delta = parseInt((relative_to.getTime() - parsed_date) / 1000);
|
||||||
|
var pluralize = function (singular, n) {
|
||||||
|
return '' + n + ' ' + singular + (n == 1 ? '' : 's');
|
||||||
|
};
|
||||||
|
if(delta < 60) {
|
||||||
|
return 'less than a minute ago';
|
||||||
|
} else if(delta < (45*60)) {
|
||||||
|
return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago';
|
||||||
|
} else if(delta < (24*60*60)) {
|
||||||
|
return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago';
|
||||||
|
} else {
|
||||||
|
return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_url() {
|
||||||
|
var proto = ('https:' == document.location.protocol ? 'https:' : 'http:');
|
||||||
|
if (s.list) {
|
||||||
|
return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?";
|
||||||
|
} else if (s.query == null && s.username.length == 1) {
|
||||||
|
return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?';
|
||||||
|
} else {
|
||||||
|
var query = (s.query || 'from:'+s.username.join('%20OR%20from:'));
|
||||||
|
return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.each(function(){
|
||||||
|
var list = $('<ul class="tweet_list">').appendTo(this);
|
||||||
|
var intro = '<p class="tweet_intro">'+s.intro_text+'</p>';
|
||||||
|
var outro = '<p class="tweet_outro">'+s.outro_text+'</p>';
|
||||||
|
var loading = $('<p class="loading">'+s.loading_text+'</p>');
|
||||||
|
|
||||||
|
if(typeof(s.username) == "string"){
|
||||||
|
s.username = [s.username];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.loading_text) $(this).append(loading);
|
||||||
|
$.getJSON(build_url(), function(data){
|
||||||
|
if (s.loading_text) loading.remove();
|
||||||
|
if (s.intro_text) list.before(intro);
|
||||||
|
$.each((data.results || data), function(i,item){
|
||||||
|
// auto join text based on verb tense and content
|
||||||
|
if (s.join_text == "auto") {
|
||||||
|
if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) {
|
||||||
|
var join_text = s.auto_join_text_reply;
|
||||||
|
} else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) {
|
||||||
|
var join_text = s.auto_join_text_url;
|
||||||
|
} else if (item.text.match(/^((\w+ed)|just) .*/im)) {
|
||||||
|
var join_text = s.auto_join_text_ed;
|
||||||
|
} else if (item.text.match(/^(\w*ing) .*/i)) {
|
||||||
|
var join_text = s.auto_join_text_ing;
|
||||||
|
} else {
|
||||||
|
var join_text = s.auto_join_text_default;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var join_text = s.join_text;
|
||||||
|
};
|
||||||
|
|
||||||
|
var from_user = item.from_user || item.user.screen_name;
|
||||||
|
var profile_image_url = item.profile_image_url || item.user.profile_image_url;
|
||||||
|
var join_template = '<span class="tweet_join"> '+join_text+' </span>';
|
||||||
|
var join = ((s.join_text) ? join_template : ' ');
|
||||||
|
var avatar_template = '<a class="tweet_avatar" href="http://twitter.com/'+from_user+'"><img src="'+profile_image_url+'" height="'+s.avatar_size+'" width="'+s.avatar_size+'" alt="'+from_user+'\'s avatar" title="'+from_user+'\'s avatar" border="0"/></a>';
|
||||||
|
var avatar = (s.avatar_size ? avatar_template : '');
|
||||||
|
var date = '<a href="http://twitter.com/'+from_user+'/statuses/'+item.id+'" title="view tweet on twitter">'+relative_time(item.created_at)+'</a>';
|
||||||
|
var text = '<span class="tweet_text">' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ '</span>';
|
||||||
|
|
||||||
|
// until we create a template option, arrange the items below to alter a tweet's display.
|
||||||
|
list.append('<li>' + avatar + date + join + text + '</li>');
|
||||||
|
|
||||||
|
list.children('li:first').addClass('tweet_first');
|
||||||
|
list.children('li:odd').addClass('tweet_even');
|
||||||
|
list.children('li:even').addClass('tweet_odd');
|
||||||
|
});
|
||||||
|
if (s.outro_text) list.after(outro);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})(jQuery);
|
65
windc/doc/source/_static/tweaks.css
Normal file
65
windc/doc/source/_static/tweaks.css
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
ul.todo_list {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.todo_list li {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 7px 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.todo_list li p {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.todo_list li p.link {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.todo_list li p.details {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.todo_list li {
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition {
|
||||||
|
border: 1px solid #8F1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition p.admonition-title {
|
||||||
|
background-color: #8F1000;
|
||||||
|
border-bottom: 1px solid #8E8E8E;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #CF2F19;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.related ul li a {
|
||||||
|
color: #CF2F19;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar h4 {
|
||||||
|
background-color:#8E8E8E;
|
||||||
|
border:1px solid #255E6E;
|
||||||
|
color:white;
|
||||||
|
font-size:1em;
|
||||||
|
margin:1em 0 0.5em;
|
||||||
|
padding:0.1em 0 0.1em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.docutils {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a tt {
|
||||||
|
color:#CF2F19;
|
||||||
|
}
|
0
windc/doc/source/_templates/.placeholder
Normal file
0
windc/doc/source/_templates/.placeholder
Normal file
86
windc/doc/source/_theme/layout.html
Normal file
86
windc/doc/source/_theme/layout.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{% extends "sphinxdoc/layout.html" %}
|
||||||
|
{% set css_files = css_files + ['_static/tweaks.css'] %}
|
||||||
|
{% set script_files = script_files + ['_static/jquery.tweet.js'] %}
|
||||||
|
{% block extrahead %}
|
||||||
|
<script type='text/javascript'>
|
||||||
|
$(document).ready(function(){
|
||||||
|
$("#twitter_feed").tweet({
|
||||||
|
username: "openstack",
|
||||||
|
query: "from:openstack",
|
||||||
|
avatar_size: 32,
|
||||||
|
count: 10,
|
||||||
|
loading_text: "loading tweets..."
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{%- macro sidebar() %}
|
||||||
|
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
|
||||||
|
<div class="sphinxsidebar">
|
||||||
|
<div class="sphinxsidebarwrapper">
|
||||||
|
{%- block sidebarlogo %}
|
||||||
|
{%- if logo %}
|
||||||
|
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||||
|
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
||||||
|
</a></p>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- block sidebartoc %}
|
||||||
|
{%- if display_toc %}
|
||||||
|
<h3><a href="{{ pathto(master_doc) }}">{{ _('Table Of Contents') }}</a></h3>
|
||||||
|
{{ toc }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- block sidebarrel %}
|
||||||
|
{%- if prev %}
|
||||||
|
<h4>{{ _('Previous topic') }}</h4>
|
||||||
|
<p class="topless"><a href="{{ prev.link|e }}"
|
||||||
|
title="{{ _('previous chapter') }}">{{ prev.title }}</a></p>
|
||||||
|
{%- endif %}
|
||||||
|
{%- if next %}
|
||||||
|
<h4>{{ _('Next topic') }}</h4>
|
||||||
|
<p class="topless"><a href="{{ next.link|e }}"
|
||||||
|
title="{{ _('next chapter') }}">{{ next.title }}</a></p>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- block sidebarsourcelink %}
|
||||||
|
{%- if show_source and has_source and sourcename %}
|
||||||
|
<h3>{{ _('This Page') }}</h3>
|
||||||
|
<ul class="this-page-menu">
|
||||||
|
<li><a href="{{ pathto('_sources/' + sourcename, true)|e }}"
|
||||||
|
rel="nofollow">{{ _('Show Source') }}</a></li>
|
||||||
|
</ul>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endblock %}
|
||||||
|
{%- if customsidebar %}
|
||||||
|
{% include customsidebar %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- block sidebarsearch %}
|
||||||
|
{%- if pagename != "search" %}
|
||||||
|
<div id="searchbox" style="display: none">
|
||||||
|
<h3>{{ _('Quick search') }}</h3>
|
||||||
|
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||||
|
<input type="text" name="q" size="18" />
|
||||||
|
<input type="submit" value="{{ _('Go') }}" />
|
||||||
|
<input type="hidden" name="check_keywords" value="yes" />
|
||||||
|
<input type="hidden" name="area" value="default" />
|
||||||
|
</form>
|
||||||
|
<p class="searchtip" style="font-size: 90%">
|
||||||
|
{{ _('Enter search terms or a module, class or function name.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if pagename == "index" %}
|
||||||
|
<h3>{{ _('Twitter Feed') }}</h3>
|
||||||
|
<div id="twitter_feed" class='twitter_feed'></div>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{%- endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endif %}{% endif %}
|
||||||
|
{%- endmacro %}
|
5
windc/doc/source/_theme/theme.conf
Normal file
5
windc/doc/source/_theme/theme.conf
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[theme]
|
||||||
|
inherit = sphinxdoc
|
||||||
|
stylesheet = sphinxdoc.css
|
||||||
|
pygments_style = friendly
|
||||||
|
|
252
windc/doc/source/conf.py
Normal file
252
windc/doc/source/conf.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2011 OpenStack, LLC.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
#
|
||||||
|
# Skeleton documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Tue May 18 13:50:15 2010.
|
||||||
|
#
|
||||||
|
# This file is execfile()'d with the current directory set to it's 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 os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 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.append([os.path.abspath('../skeleton'),
|
||||||
|
os.path.abspath('..'),
|
||||||
|
os.path.abspath('../bin')
|
||||||
|
])
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# 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.coverage',
|
||||||
|
'sphinx.ext.ifconfig',
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
|
'sphinx.ext.pngmath',
|
||||||
|
'sphinx.ext.graphviz',
|
||||||
|
'sphinx.ext.todo']
|
||||||
|
|
||||||
|
todo_include_todos = True
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = []
|
||||||
|
if os.getenv('HUDSON_PUBLISH_DOCS'):
|
||||||
|
templates_path = ['_ga', '_templates']
|
||||||
|
else:
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'Skeleton'
|
||||||
|
copyright = u'2011-present, OpenStack, LLC.'
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
from skeleton import version as skeleton_version
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = skeleton_version.version_string()
|
||||||
|
# The short X.Y version.
|
||||||
|
version = skeleton_version.canonical_version_string()
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of documents that shouldn't be included in the build.
|
||||||
|
#unused_docs = []
|
||||||
|
|
||||||
|
# List of directories, relative to source directory, that shouldn't be searched
|
||||||
|
# for source files.
|
||||||
|
exclude_trees = []
|
||||||
|
|
||||||
|
# The reST default role (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 = True
|
||||||
|
|
||||||
|
# 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 = ['skeleton.']
|
||||||
|
|
||||||
|
# -- Options for man page output --------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree for man pages.
|
||||||
|
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
|
||||||
|
|
||||||
|
man_pages = [
|
||||||
|
('man/skeletonapi', 'skeleton-api', u'Skeleton API Server',
|
||||||
|
[u'OpenStack'], 1),
|
||||||
|
('man/skeletonregistry', 'skeleton-registry', u'Skeleton Registry Server',
|
||||||
|
[u'OpenStack'], 1),
|
||||||
|
('man/skeletonmanage', 'skeleton-manage', u'Skeleton Management Utility',
|
||||||
|
[u'OpenStack'], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||||
|
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||||
|
html_theme_path = ["."]
|
||||||
|
html_theme = '_theme'
|
||||||
|
|
||||||
|
# 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 = ['_theme']
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#html_logo = None
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#html_favicon = None
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#html_sidebars = {}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_use_modindex = 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, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#html_use_opensearch = ''
|
||||||
|
|
||||||
|
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = ''
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'skeletondoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
|
|
||||||
|
# The paper size ('letter' or 'a4').
|
||||||
|
#latex_paper_size = 'letter'
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#latex_font_size = '10pt'
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# documentclass [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
('index', 'Skeleton.tex', u'Skeleton Documentation',
|
||||||
|
u'Skeleton Team', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#latex_preamble = ''
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_use_modindex = True
|
||||||
|
|
||||||
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
|
intersphinx_mapping = {'python': ('http://docs.python.org/', None),
|
||||||
|
'dashboard': ('http://dashboard.openstack.org', None),
|
||||||
|
'glance': ('http://glance.openstack.org', None),
|
||||||
|
'keystone': ('http://keystone.openstack.org', None),
|
||||||
|
'nova': ('http://nova.openstack.org', None),
|
||||||
|
'swift': ('http://swift.openstack.org', None)}
|
53
windc/doc/source/index.rst
Normal file
53
windc/doc/source/index.rst
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
..
|
||||||
|
Copyright 2011 OpenStack, LLC.
|
||||||
|
All Rights Reserved.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
not use this file except in compliance with the License. You may obtain
|
||||||
|
a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
Welcome to Skeleton's documentation!
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Description of Skeleton project
|
||||||
|
|
||||||
|
Concepts
|
||||||
|
========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
Using Skeleton
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
gettingstarted
|
||||||
|
installing
|
||||||
|
|
||||||
|
Developer Docs
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
Outstanding Documentation Tasks
|
||||||
|
===============================
|
||||||
|
|
||||||
|
.. todolist::
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
53
windc/etc/windc-api-paste.ini
Normal file
53
windc/etc/windc-api-paste.ini
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
# Show more verbose log output (sets INFO log level output)
|
||||||
|
verbose = True
|
||||||
|
# Show debugging output in logs (sets DEBUG log level output)
|
||||||
|
debug = True
|
||||||
|
# Address to bind the server to
|
||||||
|
bind_host = 0.0.0.0
|
||||||
|
# Port the bind the server to
|
||||||
|
bind_port = 8082
|
||||||
|
# Log to this file. Make sure the user running skeleton-api has
|
||||||
|
# permissions to write to this file!
|
||||||
|
log_file = /tmp/api.log
|
||||||
|
# Orchestration Adapter Section
|
||||||
|
#
|
||||||
|
#provider - Cloud provider to use (openstack, amazon, dummy)
|
||||||
|
provider = openstack
|
||||||
|
|
||||||
|
# Heat specific parameters
|
||||||
|
#heat_url - url for the heat service
|
||||||
|
# [auto] - find in the keystone
|
||||||
|
heat_url = auto
|
||||||
|
|
||||||
|
#heat_api_version - version of the API to use
|
||||||
|
#
|
||||||
|
heat_api_version = 1
|
||||||
|
|
||||||
|
|
||||||
|
[pipeline:windc-api]
|
||||||
|
pipeline = apiv1app
|
||||||
|
# NOTE: use the following pipeline for keystone
|
||||||
|
#pipeline = authtoken context apiv1app
|
||||||
|
|
||||||
|
[app:apiv1app]
|
||||||
|
paste.app_factory = windc.common.wsgi:app_factory
|
||||||
|
windc.app_factory = windc.api.v1.router:API
|
||||||
|
|
||||||
|
[filter:context]
|
||||||
|
paste.filter_factory = windc.common.wsgi:filter_factory
|
||||||
|
windc.filter_factory = windc.common.context:ContextMiddleware
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
||||||
|
auth_host = 172.18.67.57
|
||||||
|
auth_port = 35357
|
||||||
|
auth_protocol = http
|
||||||
|
auth_uri = http://172.18.67.57:5000/v2.0/
|
||||||
|
admin_tenant_name = service
|
||||||
|
admin_user = windc
|
||||||
|
admin_password = 000
|
||||||
|
|
||||||
|
[filter:auth-context]
|
||||||
|
paste.filter_factory = windc.common.wsgi:filter_factory
|
||||||
|
windc.filter_factory = keystone.middleware.balancer_auth_token:KeystoneContextMiddleware
|
34
windc/etc/windc-api.conf
Normal file
34
windc/etc/windc-api.conf
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
# Show more verbose log output (sets INFO log level output)
|
||||||
|
verbose = True
|
||||||
|
|
||||||
|
# Show debugging output in logs (sets DEBUG log level output)
|
||||||
|
debug = True
|
||||||
|
|
||||||
|
# Address to bind the server to
|
||||||
|
bind_host = 0.0.0.0
|
||||||
|
|
||||||
|
# Port the bind the server to
|
||||||
|
bind_port = 8082
|
||||||
|
|
||||||
|
# Log to this file. Make sure the user running skeleton-api has
|
||||||
|
# permissions to write to this file!
|
||||||
|
log_file = /tmp/api.log
|
||||||
|
|
||||||
|
[pipeline:windc-api]
|
||||||
|
pipeline = versionnegotiation context apiv1app
|
||||||
|
|
||||||
|
[pipeline:versions]
|
||||||
|
pipeline = versionsapp
|
||||||
|
|
||||||
|
[app:versionsapp]
|
||||||
|
paste.app_factory = windc.api.versions:app_factory
|
||||||
|
|
||||||
|
[app:apiv1app]
|
||||||
|
paste.app_factory = windc.api.v1:app_factory
|
||||||
|
|
||||||
|
[filter:versionnegotiation]
|
||||||
|
paste.filter_factory = windc.api.middleware.version_negotiation:filter_factory
|
||||||
|
|
||||||
|
[filter:context]
|
||||||
|
paste.filter_factory = openstack.common.middleware.context:filter_factory
|
24
windc/openstack/__init__.py
Normal file
24
windc/openstack/__init__.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
# This ensures the openstack namespace is defined
|
||||||
|
try:
|
||||||
|
import pkg_resources
|
||||||
|
pkg_resources.declare_namespace(__name__)
|
||||||
|
except ImportError:
|
||||||
|
import pkgutil
|
||||||
|
__path__ = pkgutil.extend_path(__path__, __name__)
|
19
windc/openstack/common/__init__.py
Normal file
19
windc/openstack/common/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
# TODO(jaypipes) Code in this module is intended to be ported to the eventual
|
||||||
|
# openstack-common library
|
334
windc/openstack/common/config.py
Normal file
334
windc/openstack/common/config.py
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Routines for configuring Openstack Projects
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ConfigParser
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import logging.handlers
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from paste import deploy
|
||||||
|
|
||||||
|
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
|
||||||
|
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_options(parser, cli_args=None):
|
||||||
|
"""
|
||||||
|
Returns the parsed CLI options, command to run and its arguments, merged
|
||||||
|
with any same-named options found in a configuration file.
|
||||||
|
|
||||||
|
The function returns a tuple of (options, args), where options is a
|
||||||
|
mapping of option key/str(value) pairs, and args is the set of arguments
|
||||||
|
(not options) supplied on the command-line.
|
||||||
|
|
||||||
|
The reason that the option values are returned as strings only is that
|
||||||
|
ConfigParser and paste.deploy only accept string values...
|
||||||
|
|
||||||
|
:param parser: The option parser
|
||||||
|
:param cli_args: (Optional) Set of arguments to process. If not present,
|
||||||
|
sys.argv[1:] is used.
|
||||||
|
:retval tuple of (options, args)
|
||||||
|
"""
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args(cli_args)
|
||||||
|
|
||||||
|
return (vars(options), args)
|
||||||
|
|
||||||
|
|
||||||
|
def add_common_options(parser):
|
||||||
|
"""
|
||||||
|
Given a supplied optparse.OptionParser, adds an OptionGroup that
|
||||||
|
represents all common configuration options.
|
||||||
|
|
||||||
|
:param parser: optparse.OptionParser
|
||||||
|
"""
|
||||||
|
help_text = "The following configuration options are common to "\
|
||||||
|
"this app's programs."
|
||||||
|
|
||||||
|
group = optparse.OptionGroup(parser, "Common Options", help_text)
|
||||||
|
group.add_option('-v', '--verbose', default=False, dest="verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Print more verbose output")
|
||||||
|
group.add_option('-d', '--debug', default=False, dest="debug",
|
||||||
|
action="store_true",
|
||||||
|
help="Print debugging output")
|
||||||
|
group.add_option('--config-file', default=None, metavar="PATH",
|
||||||
|
help="Path to the config file to use. When not specified "
|
||||||
|
"(the default), we generally look at the first "
|
||||||
|
"argument specified to be a config file, and if "
|
||||||
|
"that is also missing, we search standard "
|
||||||
|
"directories for a config file.")
|
||||||
|
parser.add_option_group(group)
|
||||||
|
|
||||||
|
|
||||||
|
def add_log_options(parser):
|
||||||
|
"""
|
||||||
|
Given a supplied optparse.OptionParser, adds an OptionGroup that
|
||||||
|
represents all the configuration options around logging.
|
||||||
|
|
||||||
|
:param parser: optparse.OptionParser
|
||||||
|
"""
|
||||||
|
help_text = "The following configuration options are specific to logging "\
|
||||||
|
"functionality for this program."
|
||||||
|
|
||||||
|
group = optparse.OptionGroup(parser, "Logging Options", help_text)
|
||||||
|
group.add_option('--log-config', default=None, metavar="PATH",
|
||||||
|
help="If this option is specified, the logging "
|
||||||
|
"configuration file specified is used and overrides "
|
||||||
|
"any other logging options specified. Please see "
|
||||||
|
"the Python logging module documentation for "
|
||||||
|
"details on logging configuration files.")
|
||||||
|
group.add_option('--log-date-format', metavar="FORMAT",
|
||||||
|
default=DEFAULT_LOG_DATE_FORMAT,
|
||||||
|
help="Format string for %(asctime)s in log records. "
|
||||||
|
"Default: %default")
|
||||||
|
group.add_option('--log-file', default=None, metavar="PATH",
|
||||||
|
help="(Optional) Name of log file to output to. "
|
||||||
|
"If not set, logging will go to stdout.")
|
||||||
|
group.add_option("--log-dir", default=None,
|
||||||
|
help="(Optional) The directory to keep log files in "
|
||||||
|
"(will be prepended to --logfile)")
|
||||||
|
group.add_option('--use-syslog', default=False, dest="use_syslog",
|
||||||
|
action="store_true",
|
||||||
|
help="Use syslog for logging.")
|
||||||
|
parser.add_option_group(group)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(options, conf):
|
||||||
|
"""
|
||||||
|
Sets up the logging options for a log with supplied name
|
||||||
|
|
||||||
|
:param options: Mapping of typed option key/values
|
||||||
|
:param conf: Mapping of untyped key/values from config file
|
||||||
|
"""
|
||||||
|
|
||||||
|
if options.get('log_config', None):
|
||||||
|
# Use a logging configuration file for all settings...
|
||||||
|
if os.path.exists(options['log_config']):
|
||||||
|
logging.config.fileConfig(options['log_config'])
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unable to locate specified logging "
|
||||||
|
"config file: %s" % options['log_config'])
|
||||||
|
|
||||||
|
# If either the CLI option or the conf value
|
||||||
|
# is True, we set to True
|
||||||
|
debug = options.get('debug') or \
|
||||||
|
get_option(conf, 'debug', type='bool', default=False)
|
||||||
|
verbose = options.get('verbose') or \
|
||||||
|
get_option(conf, 'verbose', type='bool', default=False)
|
||||||
|
root_logger = logging.root
|
||||||
|
if debug:
|
||||||
|
root_logger.setLevel(logging.DEBUG)
|
||||||
|
elif verbose:
|
||||||
|
root_logger.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
root_logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Set log configuration from options...
|
||||||
|
# Note that we use a hard-coded log format in the options
|
||||||
|
# because of Paste.Deploy bug #379
|
||||||
|
# http://trac.pythonpaste.org/pythonpaste/ticket/379
|
||||||
|
log_format = options.get('log_format', DEFAULT_LOG_FORMAT)
|
||||||
|
log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT)
|
||||||
|
formatter = logging.Formatter(log_format, log_date_format)
|
||||||
|
|
||||||
|
logfile = options.get('log_file')
|
||||||
|
if not logfile:
|
||||||
|
logfile = conf.get('log_file')
|
||||||
|
|
||||||
|
use_syslog = options.get('use_syslog') or \
|
||||||
|
get_option(conf, 'use_syslog', type='bool', default=False)
|
||||||
|
|
||||||
|
if use_syslog:
|
||||||
|
handler = logging.handlers.SysLogHandler(address='/dev/log')
|
||||||
|
elif logfile:
|
||||||
|
logdir = options.get('log_dir')
|
||||||
|
if not logdir:
|
||||||
|
logdir = conf.get('log_dir')
|
||||||
|
if logdir:
|
||||||
|
logfile = os.path.join(logdir, logfile)
|
||||||
|
handler = logging.FileHandler(logfile)
|
||||||
|
else:
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def find_config_file(app_name, options, args, config_dir=None):
|
||||||
|
"""
|
||||||
|
Return the first config file found for an application.
|
||||||
|
|
||||||
|
We search for the paste config file in the following order:
|
||||||
|
* If --config-file option is used, use that
|
||||||
|
* If args[0] is a file, use that
|
||||||
|
* Search for $app.conf in standard directories:
|
||||||
|
* .
|
||||||
|
* ~.config_dir/
|
||||||
|
* ~
|
||||||
|
* /etc/config_dir
|
||||||
|
* /etc
|
||||||
|
|
||||||
|
:retval Full path to config file, or None if no config file found
|
||||||
|
"""
|
||||||
|
config_dir = config_dir or app_name
|
||||||
|
|
||||||
|
fix_path = lambda p: os.path.abspath(os.path.expanduser(p))
|
||||||
|
if options.get('config_file'):
|
||||||
|
if os.path.exists(options['config_file']):
|
||||||
|
return fix_path(options['config_file'])
|
||||||
|
elif args:
|
||||||
|
if os.path.exists(args[0]):
|
||||||
|
return fix_path(args[0])
|
||||||
|
|
||||||
|
# Handle standard directory search for $app_name.conf
|
||||||
|
config_file_dirs = [fix_path(os.getcwd()),
|
||||||
|
fix_path(os.path.join('~', '.' + config_dir)),
|
||||||
|
fix_path('~'),
|
||||||
|
os.path.join('/etc', config_dir),
|
||||||
|
'/etc']
|
||||||
|
|
||||||
|
for cfg_dir in config_file_dirs:
|
||||||
|
cfg_file = os.path.join(cfg_dir, '%s.conf' % app_name)
|
||||||
|
if os.path.exists(cfg_file):
|
||||||
|
return cfg_file
|
||||||
|
|
||||||
|
|
||||||
|
def load_paste_config(app_name, options, args, config_dir=None):
|
||||||
|
"""
|
||||||
|
Looks for a config file to use for an app and returns the
|
||||||
|
config file path and a configuration mapping from a paste config file.
|
||||||
|
|
||||||
|
We search for the paste config file in the following order:
|
||||||
|
* If --config-file option is used, use that
|
||||||
|
* If args[0] is a file, use that
|
||||||
|
* Search for $app_name.conf in standard directories:
|
||||||
|
* .
|
||||||
|
* ~.config_dir/
|
||||||
|
* ~
|
||||||
|
* /etc/config_dir
|
||||||
|
* /etc
|
||||||
|
|
||||||
|
:param app_name: Name of the application to load config for, or None.
|
||||||
|
None signifies to only load the [DEFAULT] section of
|
||||||
|
the config file.
|
||||||
|
:param options: Set of typed options returned from parse_options()
|
||||||
|
:param args: Command line arguments from argv[1:]
|
||||||
|
:retval Tuple of (conf_file, conf)
|
||||||
|
|
||||||
|
:raises RuntimeError when config file cannot be located or there was a
|
||||||
|
problem loading the configuration file.
|
||||||
|
"""
|
||||||
|
conf_file = find_config_file(app_name, options, args, config_dir)
|
||||||
|
if not conf_file:
|
||||||
|
raise RuntimeError("Unable to locate any configuration file. "
|
||||||
|
"Cannot load application %s" % app_name)
|
||||||
|
try:
|
||||||
|
app = wsgi.paste_deploy_app(conf_file, app_name, conf)
|
||||||
|
conf = deploy.appconfig("config:%s" % conf_file, name=app_name)
|
||||||
|
return conf_file, conf
|
||||||
|
except Exception, e:
|
||||||
|
raise RuntimeError("Error trying to load config %s: %s"
|
||||||
|
% (conf_file, e))
|
||||||
|
|
||||||
|
|
||||||
|
def load_paste_app(app_name, options, args, config_dir=None):
|
||||||
|
"""
|
||||||
|
Builds and returns a WSGI app from a paste config file.
|
||||||
|
|
||||||
|
We search for the paste config file in the following order:
|
||||||
|
* If --config-file option is used, use that
|
||||||
|
* If args[0] is a file, use that
|
||||||
|
* Search for $app_name.conf in standard directories:
|
||||||
|
* .
|
||||||
|
* ~.config_dir/
|
||||||
|
* ~
|
||||||
|
* /etc/config_dir
|
||||||
|
* /etc
|
||||||
|
|
||||||
|
:param app_name: Name of the application to load
|
||||||
|
:param options: Set of typed options returned from parse_options()
|
||||||
|
:param args: Command line arguments from argv[1:]
|
||||||
|
|
||||||
|
:raises RuntimeError when config file cannot be located or application
|
||||||
|
cannot be loaded from config file
|
||||||
|
"""
|
||||||
|
conf_file, conf = load_paste_config(app_name, options,
|
||||||
|
args, config_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Setup logging early, supplying both the CLI options and the
|
||||||
|
# configuration mapping from the config file
|
||||||
|
setup_logging(options, conf)
|
||||||
|
|
||||||
|
# We only update the conf dict for the verbose and debug
|
||||||
|
# flags. Everything else must be set up in the conf file...
|
||||||
|
debug = options.get('debug') or \
|
||||||
|
get_option(conf, 'debug', type='bool', default=False)
|
||||||
|
verbose = options.get('verbose') or \
|
||||||
|
get_option(conf, 'verbose', type='bool', default=False)
|
||||||
|
conf['debug'] = debug
|
||||||
|
conf['verbose'] = verbose
|
||||||
|
|
||||||
|
# Log the options used when starting if we're in debug mode...
|
||||||
|
if debug:
|
||||||
|
logger = logging.getLogger(app_name)
|
||||||
|
logger.debug("*" * 80)
|
||||||
|
logger.debug("Configuration options gathered from config file:")
|
||||||
|
logger.debug(conf_file)
|
||||||
|
logger.debug("================================================")
|
||||||
|
items = dict([(k, v) for k, v in conf.items()
|
||||||
|
if k not in ('__file__', 'here')])
|
||||||
|
for key, value in sorted(items.items()):
|
||||||
|
logger.debug("%(key)-30s %(value)s" % locals())
|
||||||
|
logger.debug("*" * 80)
|
||||||
|
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
|
||||||
|
except (LookupError, ImportError), e:
|
||||||
|
raise RuntimeError("Unable to load %(app_name)s from "
|
||||||
|
"configuration file %(conf_file)s."
|
||||||
|
"\nGot: %(e)r" % locals())
|
||||||
|
return conf, app
|
||||||
|
|
||||||
|
|
||||||
|
def get_option(options, option, **kwargs):
|
||||||
|
if option in options:
|
||||||
|
value = options[option]
|
||||||
|
type_ = kwargs.get('type', 'str')
|
||||||
|
if type_ == 'bool':
|
||||||
|
if hasattr(value, 'lower'):
|
||||||
|
return value.lower() == 'true'
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
elif type_ == 'int':
|
||||||
|
return int(value)
|
||||||
|
elif type_ == 'float':
|
||||||
|
return float(value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
elif 'default' in kwargs:
|
||||||
|
return kwargs['default']
|
||||||
|
else:
|
||||||
|
raise KeyError("option '%s' not found" % option)
|
40
windc/openstack/common/context.py
Normal file
40
windc/openstack/common/context.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Simple class that stores security context information in the web request.
|
||||||
|
|
||||||
|
Projects should subclass this class if they wish to enhance the request
|
||||||
|
context or provide additional information in their specific WSGI pipeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContext(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Stores information about the security context under which the user
|
||||||
|
accesses the system, as well as additional request information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False,
|
||||||
|
read_only=False, show_deleted=False):
|
||||||
|
self.auth_tok = auth_tok
|
||||||
|
self.user = user
|
||||||
|
self.tenant = tenant
|
||||||
|
self.is_admin = is_admin
|
||||||
|
self.read_only = read_only
|
||||||
|
self.show_deleted = show_deleted
|
143
windc/openstack/common/exception.py
Normal file
143
windc/openstack/common/exception.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Exceptions common to OpenStack projects
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessExecutionError(IOError):
|
||||||
|
def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
|
||||||
|
description=None):
|
||||||
|
if description is None:
|
||||||
|
description = "Unexpected error while running command."
|
||||||
|
if exit_code is None:
|
||||||
|
exit_code = '-'
|
||||||
|
message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (
|
||||||
|
description, cmd, exit_code, stdout, stderr)
|
||||||
|
IOError.__init__(self, message)
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
def __init__(self, message=None):
|
||||||
|
super(Error, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(Error):
|
||||||
|
def __init__(self, message='Unknown', code='Unknown'):
|
||||||
|
self.message = message
|
||||||
|
self.code = code
|
||||||
|
super(ApiError, self).__init__('%s: %s' % (code, message))
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownScheme(Error):
|
||||||
|
|
||||||
|
msg = "Unknown scheme '%s' found in URI"
|
||||||
|
|
||||||
|
def __init__(self, scheme):
|
||||||
|
msg = self.__class__.msg % scheme
|
||||||
|
super(UnknownScheme, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class BadStoreUri(Error):
|
||||||
|
|
||||||
|
msg = "The Store URI %s was malformed. Reason: %s"
|
||||||
|
|
||||||
|
def __init__(self, uri, reason):
|
||||||
|
msg = self.__class__.msg % (uri, reason)
|
||||||
|
super(BadStoreUri, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class Duplicate(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorized(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotEmpty(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Invalid(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadInputError(Exception):
|
||||||
|
"""Error resulting from a client sending bad input to a server"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingArgumentError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseMigrationError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ClientConnectionError(Exception):
|
||||||
|
"""Error resulting from a client connecting to a server"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exception(f):
|
||||||
|
def _wrap(*args, **kw):
|
||||||
|
try:
|
||||||
|
return f(*args, **kw)
|
||||||
|
except Exception, e:
|
||||||
|
if not isinstance(e, Error):
|
||||||
|
#exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||||
|
logging.exception('Uncaught exception')
|
||||||
|
#logging.error(traceback.extract_stack(exc_traceback))
|
||||||
|
raise Error(str(e))
|
||||||
|
raise
|
||||||
|
_wrap.func_name = f.func_name
|
||||||
|
return _wrap
|
||||||
|
|
||||||
|
|
||||||
|
class OpenstackException(Exception):
|
||||||
|
"""
|
||||||
|
Base Exception
|
||||||
|
|
||||||
|
To correctly use this class, inherit from it and define
|
||||||
|
a 'message' property. That message will get printf'd
|
||||||
|
with the keyword arguments provided to the constructor.
|
||||||
|
"""
|
||||||
|
message = "An unknown exception occurred"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
try:
|
||||||
|
self._error_string = self.message % kwargs
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# at least get the core message out if something happened
|
||||||
|
self._error_string = self.message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self._error_string
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidContentType(OpenstackException):
|
||||||
|
message = "Invalid content type %(content_type)s"
|
0
windc/openstack/common/middleware/__init__.py
Normal file
0
windc/openstack/common/middleware/__init__.py
Normal file
64
windc/openstack/common/middleware/context.py
Normal file
64
windc/openstack/common/middleware/context.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Middleware that attaches a context to the WSGI request
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openstack.common import utils
|
||||||
|
from openstack.common import wsgi
|
||||||
|
from openstack.common import context
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMiddleware(wsgi.Middleware):
|
||||||
|
def __init__(self, app, options):
|
||||||
|
self.options = options
|
||||||
|
super(ContextMiddleware, self).__init__(app)
|
||||||
|
|
||||||
|
def make_context(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a context with the given arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Determine the context class to use
|
||||||
|
ctxcls = context.RequestContext
|
||||||
|
if 'context_class' in self.options:
|
||||||
|
ctxcls = utils.import_class(self.options['context_class'])
|
||||||
|
|
||||||
|
return ctxcls(*args, **kwargs)
|
||||||
|
|
||||||
|
def process_request(self, req):
|
||||||
|
"""
|
||||||
|
Extract any authentication information in the request and
|
||||||
|
construct an appropriate context from it.
|
||||||
|
"""
|
||||||
|
# Use the default empty context, with admin turned on for
|
||||||
|
# backwards compatibility
|
||||||
|
req.context = self.make_context(is_admin=True)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_factory(global_conf, **local_conf):
|
||||||
|
"""
|
||||||
|
Factory method for paste.deploy
|
||||||
|
"""
|
||||||
|
conf = global_conf.copy()
|
||||||
|
conf.update(local_conf)
|
||||||
|
|
||||||
|
def filter(app):
|
||||||
|
return ContextMiddleware(app, conf)
|
||||||
|
|
||||||
|
return filter
|
89
windc/openstack/common/utils.py
Normal file
89
windc/openstack/common/utils.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
System-level utilities and helper functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from openstack.common import exception
|
||||||
|
|
||||||
|
|
||||||
|
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
|
||||||
|
|
||||||
|
def int_from_bool_as_string(subject):
|
||||||
|
"""
|
||||||
|
Interpret a string as a boolean and return either 1 or 0.
|
||||||
|
|
||||||
|
Any string value in:
|
||||||
|
('True', 'true', 'On', 'on', '1')
|
||||||
|
is interpreted as a boolean True.
|
||||||
|
|
||||||
|
Useful for JSON-decoded stuff and config file parsing
|
||||||
|
"""
|
||||||
|
return bool_from_string(subject) and 1 or 0
|
||||||
|
|
||||||
|
|
||||||
|
def bool_from_string(subject):
|
||||||
|
"""
|
||||||
|
Interpret a string as a boolean.
|
||||||
|
|
||||||
|
Any string value in:
|
||||||
|
('True', 'true', 'On', 'on', '1')
|
||||||
|
is interpreted as a boolean True.
|
||||||
|
|
||||||
|
Useful for JSON-decoded stuff and config file parsing
|
||||||
|
"""
|
||||||
|
if type(subject) == type(bool):
|
||||||
|
return subject
|
||||||
|
if hasattr(subject, 'startswith'): # str or unicode...
|
||||||
|
if subject.strip().lower() in ('true', 'on', '1'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def import_class(import_str):
|
||||||
|
"""Returns a class from a string including module and class"""
|
||||||
|
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||||
|
try:
|
||||||
|
__import__(mod_str)
|
||||||
|
return getattr(sys.modules[mod_str], class_str)
|
||||||
|
except (ImportError, ValueError, AttributeError):
|
||||||
|
raise exception.NotFound('Class %s cannot be found' % class_str)
|
||||||
|
|
||||||
|
|
||||||
|
def import_object(import_str):
|
||||||
|
"""Returns an object including a module or module and class"""
|
||||||
|
try:
|
||||||
|
__import__(import_str)
|
||||||
|
return sys.modules[import_str]
|
||||||
|
except ImportError:
|
||||||
|
cls = import_class(import_str)
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
def isotime(at=None):
|
||||||
|
if not at:
|
||||||
|
at = datetime.datetime.utcnow()
|
||||||
|
return at.strftime(TIME_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_isotime(timestr):
|
||||||
|
return datetime.datetime.strptime(timestr, TIME_FORMAT)
|
395
windc/openstack/common/wsgi.py
Normal file
395
windc/openstack/common/wsgi.py
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility methods for working with WSGI servers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import datetime
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
import eventlet.wsgi
|
||||||
|
eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||||
|
import routes
|
||||||
|
import routes.middleware
|
||||||
|
import webob.dec
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
|
from openstack.common import exception
|
||||||
|
|
||||||
|
logger = logging.getLogger('openstack.common.wsgi')
|
||||||
|
|
||||||
|
|
||||||
|
class WritableLogger(object):
|
||||||
|
"""A thin wrapper that responds to `write` and logs."""
|
||||||
|
|
||||||
|
def __init__(self, logger, level=logging.DEBUG):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
|
||||||
|
def write(self, msg):
|
||||||
|
self.logger.log(self.level, msg.strip("\n"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_server(application, port):
|
||||||
|
"""Run a WSGI server with the given application."""
|
||||||
|
sock = eventlet.listen(('0.0.0.0', port))
|
||||||
|
eventlet.wsgi.server(sock, application)
|
||||||
|
|
||||||
|
|
||||||
|
class Server(object):
|
||||||
|
"""Server class to manage multiple WSGI sockets and applications."""
|
||||||
|
|
||||||
|
def __init__(self, threads=1000):
|
||||||
|
self.pool = eventlet.GreenPool(threads)
|
||||||
|
|
||||||
|
def start(self, application, port, host='0.0.0.0', backlog=128):
|
||||||
|
"""Run a WSGI server with the given application."""
|
||||||
|
socket = eventlet.listen((host, port), backlog=backlog)
|
||||||
|
self.pool.spawn_n(self._run, application, socket)
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait until all servers have completed running."""
|
||||||
|
try:
|
||||||
|
self.pool.waitall()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _run(self, application, socket):
|
||||||
|
"""Start a WSGI server in a new green thread."""
|
||||||
|
logger = logging.getLogger('eventlet.wsgi.server')
|
||||||
|
eventlet.wsgi.server(socket, application, custom_pool=self.pool,
|
||||||
|
log=WritableLogger(logger))
|
||||||
|
|
||||||
|
|
||||||
|
class Middleware(object):
|
||||||
|
"""
|
||||||
|
Base WSGI middleware wrapper. These classes require an application to be
|
||||||
|
initialized that will be called next. By default the middleware will
|
||||||
|
simply call its wrapped app, or you can override __call__ to customize its
|
||||||
|
behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
self.application = application
|
||||||
|
|
||||||
|
def process_request(self, req):
|
||||||
|
"""
|
||||||
|
Called on each request.
|
||||||
|
|
||||||
|
If this returns None, the next application down the stack will be
|
||||||
|
executed. If it returns a response then that response will be returned
|
||||||
|
and execution will stop here.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_response(self, response):
|
||||||
|
"""Do whatever you'd like to the response."""
|
||||||
|
return response
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
response = self.process_request(req)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
response = req.get_response(self.application)
|
||||||
|
return self.process_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
class Debug(Middleware):
|
||||||
|
"""
|
||||||
|
Helper class that can be inserted into any WSGI application chain
|
||||||
|
to get information about the request and response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
print ("*" * 40) + " REQUEST ENVIRON"
|
||||||
|
for key, value in req.environ.items():
|
||||||
|
print key, "=", value
|
||||||
|
print
|
||||||
|
resp = req.get_response(self.application)
|
||||||
|
|
||||||
|
print ("*" * 40) + " RESPONSE HEADERS"
|
||||||
|
for (key, value) in resp.headers.iteritems():
|
||||||
|
print key, "=", value
|
||||||
|
print
|
||||||
|
|
||||||
|
resp.app_iter = self.print_generator(resp.app_iter)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_generator(app_iter):
|
||||||
|
"""
|
||||||
|
Iterator that prints the contents of a wrapper string iterator
|
||||||
|
when iterated.
|
||||||
|
"""
|
||||||
|
print ("*" * 40) + " BODY"
|
||||||
|
for part in app_iter:
|
||||||
|
sys.stdout.write(part)
|
||||||
|
sys.stdout.flush()
|
||||||
|
yield part
|
||||||
|
print
|
||||||
|
|
||||||
|
|
||||||
|
class Router(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
WSGI middleware that maps incoming requests to WSGI apps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mapper):
|
||||||
|
"""
|
||||||
|
Create a router for the given routes.Mapper.
|
||||||
|
|
||||||
|
Each route in `mapper` must specify a 'controller', which is a
|
||||||
|
WSGI app to call. You'll probably want to specify an 'action' as
|
||||||
|
well and have your controller be a wsgi.Controller, who will route
|
||||||
|
the request to the action method.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
mapper = routes.Mapper()
|
||||||
|
sc = ServerController()
|
||||||
|
|
||||||
|
# Explicit mapping of one route to a controller+action
|
||||||
|
mapper.connect(None, "/svrlist", controller=sc, action="list")
|
||||||
|
|
||||||
|
# Actions are all implicitly defined
|
||||||
|
mapper.resource("server", "servers", controller=sc)
|
||||||
|
|
||||||
|
# Pointing to an arbitrary WSGI app. You can specify the
|
||||||
|
# {path_info:.*} parameter so the target app can be handed just that
|
||||||
|
# section of the URL.
|
||||||
|
mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
|
||||||
|
"""
|
||||||
|
self.map = mapper
|
||||||
|
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||||
|
self.map)
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
"""
|
||||||
|
Route the incoming request to a controller based on self.map.
|
||||||
|
If no match, return a 404.
|
||||||
|
"""
|
||||||
|
return self._router
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def _dispatch(req):
|
||||||
|
"""
|
||||||
|
Called by self._router after matching the incoming request to a route
|
||||||
|
and putting the information into req.environ. Either returns 404
|
||||||
|
or the routed WSGI app's response.
|
||||||
|
"""
|
||||||
|
match = req.environ['wsgiorg.routing_args'][1]
|
||||||
|
if not match:
|
||||||
|
return webob.exc.HTTPNotFound()
|
||||||
|
app = match['controller']
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
class Request(webob.Request):
|
||||||
|
|
||||||
|
"""Add some Openstack API-specific logic to the base webob.Request."""
|
||||||
|
|
||||||
|
def best_match_content_type(self):
|
||||||
|
"""Determine the requested response content-type."""
|
||||||
|
supported = ('application/json',)
|
||||||
|
bm = self.accept.best_match(supported)
|
||||||
|
return bm or 'application/json'
|
||||||
|
|
||||||
|
def get_content_type(self, allowed_content_types):
|
||||||
|
"""Determine content type of the request body."""
|
||||||
|
if not "Content-Type" in self.headers:
|
||||||
|
raise exception.InvalidContentType(content_type=None)
|
||||||
|
|
||||||
|
content_type = self.content_type
|
||||||
|
|
||||||
|
if content_type not in allowed_content_types:
|
||||||
|
raise exception.InvalidContentType(content_type=content_type)
|
||||||
|
else:
|
||||||
|
return content_type
|
||||||
|
|
||||||
|
|
||||||
|
class JSONRequestDeserializer(object):
|
||||||
|
def has_body(self, request):
|
||||||
|
"""
|
||||||
|
Returns whether a Webob.Request object will possess an entity body.
|
||||||
|
|
||||||
|
:param request: Webob.Request object
|
||||||
|
"""
|
||||||
|
if 'transfer-encoding' in request.headers:
|
||||||
|
return True
|
||||||
|
elif request.content_length > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def from_json(self, datastring):
|
||||||
|
return json.loads(datastring)
|
||||||
|
|
||||||
|
def default(self, request):
|
||||||
|
msg = "Request deserialization: %s" % request
|
||||||
|
logger.debug(msg)
|
||||||
|
if self.has_body(request):
|
||||||
|
logger.debug("Deserialization: request has body")
|
||||||
|
if request.headers['Content-Type'] == 'application/x-www-form-urlencoded':
|
||||||
|
body = urllib2.unquote(request.body)
|
||||||
|
else:
|
||||||
|
body = request.body
|
||||||
|
msg = "Request body: %s" % body
|
||||||
|
logger.debug(msg)
|
||||||
|
return {'body': self.from_json(body)}
|
||||||
|
else:
|
||||||
|
logger.debug("Deserialization: request has NOT body")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class JSONResponseSerializer(object):
|
||||||
|
|
||||||
|
def to_json(self, data):
|
||||||
|
def sanitizer(obj):
|
||||||
|
if isinstance(obj, datetime.datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
return json.dumps(data, default=sanitizer)
|
||||||
|
|
||||||
|
def default(self, response, result):
|
||||||
|
logger.debug("JSONSerializer default method called.")
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
response.body = self.to_json(result)
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""
|
||||||
|
WSGI app that handles (de)serialization and controller dispatch.
|
||||||
|
|
||||||
|
Reads routing information supplied by RoutesMiddleware and calls
|
||||||
|
the requested action method upon its deserializer, controller,
|
||||||
|
and serializer. Those three objects may implement any of the basic
|
||||||
|
controller action methods (create, update, show, index, delete)
|
||||||
|
along with any that may be specified in the api router. A 'default'
|
||||||
|
method may also be implemented to be used in place of any
|
||||||
|
non-implemented actions. Deserializer methods must accept a request
|
||||||
|
argument and return a dictionary. Controller methods must accept a
|
||||||
|
request argument. Additionally, they must also accept keyword
|
||||||
|
arguments that represent the keys returned by the Deserializer. They
|
||||||
|
may raise a webob.exc exception or return a dict, which will be
|
||||||
|
serialized by requested content type.
|
||||||
|
"""
|
||||||
|
def __init__(self, controller, deserializer, serializer):
|
||||||
|
"""
|
||||||
|
:param controller: object that implement methods created by routes lib
|
||||||
|
:param deserializer: object that supports webob request deserialization
|
||||||
|
through controller-like actions
|
||||||
|
:param serializer: object that supports webob response serialization
|
||||||
|
through controller-like actions
|
||||||
|
"""
|
||||||
|
self.controller = controller
|
||||||
|
self.serializer = serializer
|
||||||
|
self.deserializer = deserializer
|
||||||
|
|
||||||
|
# NOTE(yorik-sar): ugly fix for Routes misbehaviour
|
||||||
|
def __add__(self, other):
|
||||||
|
return other
|
||||||
|
|
||||||
|
@webob.dec.wsgify(RequestClass=Request)
|
||||||
|
def __call__(self, request):
|
||||||
|
"""WSGI method that controls (de)serialization and method dispatch."""
|
||||||
|
logger.debug("Resource __call__ is invoked")
|
||||||
|
action_args = self.get_action_args(request.environ)
|
||||||
|
action = action_args.pop('action', None)
|
||||||
|
|
||||||
|
deserialized_params = self.deserialize_request(action, request)
|
||||||
|
action_args.update(deserialized_params)
|
||||||
|
action_result = self.execute_action(action, request, **action_args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.serialize_response(action, action_result, request)
|
||||||
|
|
||||||
|
# return unserializable result (typically a webob exc)
|
||||||
|
except Exception:
|
||||||
|
return action_result
|
||||||
|
|
||||||
|
def deserialize_request(self, action, request):
|
||||||
|
return self.dispatch(self.deserializer, action, request)
|
||||||
|
|
||||||
|
def serialize_response(self, action, action_result, request):
|
||||||
|
msg = "Called serialize response Action:%s Result:%s Request:%s" % (action, action_result, request)
|
||||||
|
logger.debug(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.controller:
|
||||||
|
meth = getattr(self, action)
|
||||||
|
else:
|
||||||
|
meth = getattr(self.controller, action)
|
||||||
|
except AttributeError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
code = 200
|
||||||
|
if hasattr(meth, 'wsgi_code'):
|
||||||
|
code = meth.wsgi_code
|
||||||
|
|
||||||
|
response = webob.Response()
|
||||||
|
response.status = code
|
||||||
|
logger.debug("serializer: dispatching call")
|
||||||
|
#TODO check why it fails with original openstack code
|
||||||
|
#self.dispatch(self.serializer, action, response,
|
||||||
|
# action_result, request)
|
||||||
|
if action_result is not None:
|
||||||
|
self.serializer.default(response, action_result)
|
||||||
|
msg = "Response: %s" % response
|
||||||
|
logger.debug(msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def execute_action(self, action, request, **action_args):
|
||||||
|
return self.dispatch(self.controller, action, request, **action_args)
|
||||||
|
|
||||||
|
def dispatch(self, obj, action, *args, **kwargs):
|
||||||
|
"""Find action-specific method on self and call it."""
|
||||||
|
try:
|
||||||
|
method = getattr(obj, action)
|
||||||
|
except AttributeError:
|
||||||
|
method = getattr(obj, 'default')
|
||||||
|
|
||||||
|
return method(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_action_args(self, request_environment):
|
||||||
|
"""Parse dictionary created by routes library."""
|
||||||
|
try:
|
||||||
|
args = request_environment['wsgiorg.routing_args'][1].copy()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['controller']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['format']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return args
|
19
windc/openstack/oldcommon/__init__.py
Normal file
19
windc/openstack/oldcommon/__init__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
# TODO(jaypipes) Code in this module is intended to be ported to the eventual
|
||||||
|
# openstack-common library
|
337
windc/openstack/oldcommon/config.py
Normal file
337
windc/openstack/oldcommon/config.py
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Routines for configuring Openstack Projects
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import logging.handlers
|
||||||
|
import optparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from paste import deploy
|
||||||
|
|
||||||
|
DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
|
||||||
|
DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_options(parser, cli_args=None):
|
||||||
|
"""
|
||||||
|
Returns the parsed CLI options, command to run and its arguments, merged
|
||||||
|
with any same-named options found in a configuration file.
|
||||||
|
|
||||||
|
The function returns a tuple of (options, args), where options is a
|
||||||
|
mapping of option key/str(value) pairs, and args is the set of arguments
|
||||||
|
(not options) supplied on the command-line.
|
||||||
|
|
||||||
|
The reason that the option values are returned as strings only is that
|
||||||
|
ConfigParser and paste.deploy only accept string values...
|
||||||
|
|
||||||
|
:param parser: The option parser
|
||||||
|
:param cli_args: (Optional) Set of arguments to process. If not present,
|
||||||
|
sys.argv[1:] is used.
|
||||||
|
:retval tuple of (options, args)
|
||||||
|
"""
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args(cli_args)
|
||||||
|
|
||||||
|
return (vars(options), args)
|
||||||
|
|
||||||
|
|
||||||
|
def add_common_options(parser):
|
||||||
|
"""
|
||||||
|
Given a supplied optparse.OptionParser, adds an OptionGroup that
|
||||||
|
represents all common configuration options.
|
||||||
|
|
||||||
|
:param parser: optparse.OptionParser
|
||||||
|
"""
|
||||||
|
help_text = "The following configuration options are common to "\
|
||||||
|
"this app's programs."
|
||||||
|
|
||||||
|
group = optparse.OptionGroup(parser, "Common Options", help_text)
|
||||||
|
group.add_option('-v', '--verbose', default=False, dest="verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Print more verbose output")
|
||||||
|
group.add_option('-d', '--debug', default=False, dest="debug",
|
||||||
|
action="store_true",
|
||||||
|
help="Print debugging output")
|
||||||
|
group.add_option('--config-file', default=None, metavar="PATH",
|
||||||
|
help="Path to the config file to use. When not specified "
|
||||||
|
"(the default), we generally look at the first "
|
||||||
|
"argument specified to be a config file, and if "
|
||||||
|
"that is also missing, we search standard "
|
||||||
|
"directories for a config file.")
|
||||||
|
parser.add_option_group(group)
|
||||||
|
|
||||||
|
|
||||||
|
def add_log_options(parser):
|
||||||
|
"""
|
||||||
|
Given a supplied optparse.OptionParser, adds an OptionGroup that
|
||||||
|
represents all the configuration options around logging.
|
||||||
|
|
||||||
|
:param parser: optparse.OptionParser
|
||||||
|
"""
|
||||||
|
help_text = "The following configuration options are specific to logging "\
|
||||||
|
"functionality for this program."
|
||||||
|
|
||||||
|
group = optparse.OptionGroup(parser, "Logging Options", help_text)
|
||||||
|
group.add_option('--log-config', default=None, metavar="PATH",
|
||||||
|
help="If this option is specified, the logging "
|
||||||
|
"configuration file specified is used and overrides "
|
||||||
|
"any other logging options specified. Please see "
|
||||||
|
"the Python logging module documentation for "
|
||||||
|
"details on logging configuration files.")
|
||||||
|
group.add_option('--log-date-format', metavar="FORMAT",
|
||||||
|
default=DEFAULT_LOG_DATE_FORMAT,
|
||||||
|
help="Format string for %(asctime)s in log records. "
|
||||||
|
"Default: %default")
|
||||||
|
group.add_option('--log-file', default=None, metavar="PATH",
|
||||||
|
help="(Optional) Name of log file to output to. "
|
||||||
|
"If not set, logging will go to stdout.")
|
||||||
|
group.add_option("--log-dir", default=None,
|
||||||
|
help="(Optional) The directory to keep log files in "
|
||||||
|
"(will be prepended to --logfile)")
|
||||||
|
group.add_option('--use-syslog', default=False, dest="use_syslog",
|
||||||
|
action="store_true",
|
||||||
|
help="Use syslog for logging.")
|
||||||
|
parser.add_option_group(group)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(options, conf):
|
||||||
|
"""
|
||||||
|
Sets up the logging options for a log with supplied name
|
||||||
|
|
||||||
|
:param options: Mapping of typed option key/values
|
||||||
|
:param conf: Mapping of untyped key/values from config file
|
||||||
|
"""
|
||||||
|
|
||||||
|
if options.get('log_config', None):
|
||||||
|
# Use a logging configuration file for all settings...
|
||||||
|
if os.path.exists(options['log_config']):
|
||||||
|
logging.config.fileConfig(options['log_config'])
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unable to locate specified logging "
|
||||||
|
"config file: %s" % options['log_config'])
|
||||||
|
|
||||||
|
# If either the CLI option or the conf value
|
||||||
|
# is True, we set to True
|
||||||
|
debug = options.get('debug') or \
|
||||||
|
get_option(conf, 'debug', type='bool', default=False)
|
||||||
|
verbose = options.get('verbose') or \
|
||||||
|
get_option(conf, 'verbose', type='bool', default=False)
|
||||||
|
root_logger = logging.root
|
||||||
|
if debug:
|
||||||
|
root_logger.setLevel(logging.DEBUG)
|
||||||
|
elif verbose:
|
||||||
|
root_logger.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
root_logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Set log configuration from options...
|
||||||
|
# Note that we use a hard-coded log format in the options
|
||||||
|
# because of Paste.Deploy bug #379
|
||||||
|
# http://trac.pythonpaste.org/pythonpaste/ticket/379
|
||||||
|
log_format = options.get('log_format', DEFAULT_LOG_FORMAT)
|
||||||
|
log_date_format = options.get('log_date_format', DEFAULT_LOG_DATE_FORMAT)
|
||||||
|
formatter = logging.Formatter(log_format, log_date_format)
|
||||||
|
|
||||||
|
logfile = options.get('log_file')
|
||||||
|
if not logfile:
|
||||||
|
logfile = conf.get('log_file')
|
||||||
|
|
||||||
|
use_syslog = options.get('use_syslog') or \
|
||||||
|
get_option(conf, 'use_syslog', type='bool', default=False)
|
||||||
|
|
||||||
|
if use_syslog:
|
||||||
|
handler = logging.handlers.SysLogHandler(address='/dev/log')
|
||||||
|
elif logfile:
|
||||||
|
logdir = options.get('log_dir')
|
||||||
|
if not logdir:
|
||||||
|
logdir = conf.get('log_dir')
|
||||||
|
if logdir:
|
||||||
|
logfile = os.path.join(logdir, logfile)
|
||||||
|
handler = logging.FileHandler(logfile)
|
||||||
|
else:
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_path(path):
|
||||||
|
"""
|
||||||
|
Return the full absolute path
|
||||||
|
"""
|
||||||
|
return os.path.abspath(os.path.expanduser(path))
|
||||||
|
|
||||||
|
|
||||||
|
def find_config_file(app_name, options, args, config_dir=None):
|
||||||
|
"""
|
||||||
|
Return the first config file found for an application.
|
||||||
|
|
||||||
|
We search for the paste config file in the following order:
|
||||||
|
* If --config-file option is used, use that
|
||||||
|
* If args[0] is a file, use that
|
||||||
|
* Search for $app.conf in standard directories:
|
||||||
|
* .
|
||||||
|
* ~.config_dir/
|
||||||
|
* ~
|
||||||
|
* /etc/config_dir
|
||||||
|
* /etc
|
||||||
|
|
||||||
|
:retval Full path to config file, or None if no config file found
|
||||||
|
"""
|
||||||
|
config_dir = config_dir or app_name
|
||||||
|
|
||||||
|
if options.get('config_file'):
|
||||||
|
if os.path.exists(options['config_file']):
|
||||||
|
return fix_path(options['config_file'])
|
||||||
|
elif args:
|
||||||
|
if os.path.exists(args[0]):
|
||||||
|
return fix_path(args[0])
|
||||||
|
|
||||||
|
# Handle standard directory search for $app_name.conf
|
||||||
|
config_file_dirs = [fix_path(os.getcwd()),
|
||||||
|
fix_path(os.path.join('~', '.' + config_dir)),
|
||||||
|
fix_path('~'),
|
||||||
|
os.path.join('/etc', config_dir),
|
||||||
|
'/etc']
|
||||||
|
|
||||||
|
for cfg_dir in config_file_dirs:
|
||||||
|
cfg_file = os.path.join(cfg_dir, '%s.conf' % app_name)
|
||||||
|
if os.path.exists(cfg_file):
|
||||||
|
return cfg_file
|
||||||
|
|
||||||
|
|
||||||
|
def load_paste_config(app_name, options, args, config_dir=None):
|
||||||
|
"""
|
||||||
|
Looks for a config file to use for an app and returns the
|
||||||
|
config file path and a configuration mapping from a paste config file.
|
||||||
|
|
||||||
|
We search for the paste config file in the following order:
|
||||||
|
* If --config-file option is used, use that
|
||||||
|
* If args[0] is a file, use that
|
||||||
|
* Search for $app_name.conf in standard directories:
|
||||||
|
* .
|
||||||
|
* ~.config_dir/
|
||||||
|
* ~
|
||||||
|
* /etc/config_dir
|
||||||
|
* /etc
|
||||||
|
|
||||||
|
:param app_name: Name of the application to load config for, or None.
|
||||||
|
None signifies to only load the [DEFAULT] section of
|
||||||
|
the config file.
|
||||||
|
:param options: Set of typed options returned from parse_options()
|
||||||
|
:param args: Command line arguments from argv[1:]
|
||||||
|
:retval Tuple of (conf_file, conf)
|
||||||
|
|
||||||
|
:raises RuntimeError when config file cannot be located or there was a
|
||||||
|
problem loading the configuration file.
|
||||||
|
"""
|
||||||
|
conf_file = find_config_file(app_name, options, args, config_dir)
|
||||||
|
if not conf_file:
|
||||||
|
raise RuntimeError("Unable to locate any configuration file. "
|
||||||
|
"Cannot load application %s" % app_name)
|
||||||
|
try:
|
||||||
|
conf = deploy.appconfig("config:%s" % conf_file, name=app_name)
|
||||||
|
return conf_file, conf
|
||||||
|
except Exception, e:
|
||||||
|
raise RuntimeError("Error trying to load config %s: %s"
|
||||||
|
% (conf_file, e))
|
||||||
|
|
||||||
|
|
||||||
|
def load_paste_app(app_name, options, args, config_dir=None):
|
||||||
|
"""
|
||||||
|
Builds and returns a WSGI app from a paste config file.
|
||||||
|
|
||||||
|
We search for the paste config file in the following order:
|
||||||
|
* If --config-file option is used, use that
|
||||||
|
* If args[0] is a file, use that
|
||||||
|
* Search for $app_name.conf in standard directories:
|
||||||
|
* .
|
||||||
|
* ~.config_dir/
|
||||||
|
* ~
|
||||||
|
* /etc/config_dir
|
||||||
|
* /etc
|
||||||
|
|
||||||
|
:param app_name: Name of the application to load
|
||||||
|
:param options: Set of typed options returned from parse_options()
|
||||||
|
:param args: Command line arguments from argv[1:]
|
||||||
|
|
||||||
|
:raises RuntimeError when config file cannot be located or application
|
||||||
|
cannot be loaded from config file
|
||||||
|
"""
|
||||||
|
conf_file, conf = load_paste_config(app_name, options,
|
||||||
|
args, config_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Setup logging early, supplying both the CLI options and the
|
||||||
|
# configuration mapping from the config file
|
||||||
|
setup_logging(options, conf)
|
||||||
|
|
||||||
|
# We only update the conf dict for the verbose and debug
|
||||||
|
# flags. Everything else must be set up in the conf file...
|
||||||
|
debug = options.get('debug') or \
|
||||||
|
get_option(conf, 'debug', type='bool', default=False)
|
||||||
|
verbose = options.get('verbose') or \
|
||||||
|
get_option(conf, 'verbose', type='bool', default=False)
|
||||||
|
conf['debug'] = debug
|
||||||
|
conf['verbose'] = verbose
|
||||||
|
|
||||||
|
# Log the options used when starting if we're in debug mode...
|
||||||
|
if debug:
|
||||||
|
logger = logging.getLogger(app_name)
|
||||||
|
logger.debug("*" * 80)
|
||||||
|
logger.debug("Configuration options gathered from config file:")
|
||||||
|
logger.debug(conf_file)
|
||||||
|
logger.debug("================================================")
|
||||||
|
items = dict([(k, v) for k, v in conf.items()
|
||||||
|
if k not in ('__file__', 'here')])
|
||||||
|
for key, value in sorted(items.items()):
|
||||||
|
logger.debug("%(key)-30s %(value)s" % locals())
|
||||||
|
logger.debug("*" * 80)
|
||||||
|
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
|
||||||
|
except (LookupError, ImportError), e:
|
||||||
|
raise RuntimeError("Unable to load %(app_name)s from "
|
||||||
|
"configuration file %(conf_file)s."
|
||||||
|
"\nGot: %(e)r" % locals())
|
||||||
|
return conf, app
|
||||||
|
|
||||||
|
|
||||||
|
def get_option(options, option, **kwargs):
|
||||||
|
if option in options:
|
||||||
|
value = options[option]
|
||||||
|
type_ = kwargs.get('type', 'str')
|
||||||
|
if type_ == 'bool':
|
||||||
|
if hasattr(value, 'lower'):
|
||||||
|
return value.lower() == 'true'
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
elif type_ == 'int':
|
||||||
|
return int(value)
|
||||||
|
elif type_ == 'float':
|
||||||
|
return float(value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
elif 'default' in kwargs:
|
||||||
|
return kwargs['default']
|
||||||
|
else:
|
||||||
|
raise KeyError("option '%s' not found" % option)
|
40
windc/openstack/oldcommon/context.py
Normal file
40
windc/openstack/oldcommon/context.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Simple class that stores security context information in the web request.
|
||||||
|
|
||||||
|
Projects should subclass this class if they wish to enhance the request
|
||||||
|
context or provide additional information in their specific WSGI pipeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContext(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Stores information about the security context under which the user
|
||||||
|
accesses the system, as well as additional request information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, auth_tok=None, user=None, tenant=None, is_admin=False,
|
||||||
|
read_only=False, show_deleted=False):
|
||||||
|
self.auth_tok = auth_tok
|
||||||
|
self.user = user
|
||||||
|
self.tenant = tenant
|
||||||
|
self.is_admin = is_admin
|
||||||
|
self.read_only = read_only
|
||||||
|
self.show_deleted = show_deleted
|
147
windc/openstack/oldcommon/exception.py
Normal file
147
windc/openstack/oldcommon/exception.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Exceptions common to OpenStack projects
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessExecutionError(IOError):
|
||||||
|
def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
|
||||||
|
description=None):
|
||||||
|
if description is None:
|
||||||
|
description = "Unexpected error while running command."
|
||||||
|
if exit_code is None:
|
||||||
|
exit_code = '-'
|
||||||
|
message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (
|
||||||
|
description, cmd, exit_code, stdout, stderr)
|
||||||
|
IOError.__init__(self, message)
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
def __init__(self, message=None):
|
||||||
|
super(Error, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(Error):
|
||||||
|
def __init__(self, message='Unknown', code='Unknown'):
|
||||||
|
self.message = message
|
||||||
|
self.code = code
|
||||||
|
super(ApiError, self).__init__('%s: %s' % (code, message))
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownScheme(Error):
|
||||||
|
|
||||||
|
msg = "Unknown scheme '%s' found in URI"
|
||||||
|
|
||||||
|
def __init__(self, scheme):
|
||||||
|
msg = self.__class__.msg % scheme
|
||||||
|
super(UnknownScheme, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class BadStoreUri(Error):
|
||||||
|
|
||||||
|
msg = "The Store URI %s was malformed. Reason: %s"
|
||||||
|
|
||||||
|
def __init__(self, uri, reason):
|
||||||
|
msg = self.__class__.msg % (uri, reason)
|
||||||
|
super(BadStoreUri, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class Duplicate(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorized(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotEmpty(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Invalid(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadInputError(Exception):
|
||||||
|
"""Error resulting from a client sending bad input to a server"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingArgumentError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseMigrationError(Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ClientConnectionError(Exception):
|
||||||
|
"""Error resulting from a client connecting to a server"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_exception(f):
|
||||||
|
def _wrap(*args, **kw):
|
||||||
|
try:
|
||||||
|
return f(*args, **kw)
|
||||||
|
except Exception, e:
|
||||||
|
if not isinstance(e, Error):
|
||||||
|
#exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||||
|
logging.exception('Uncaught exception')
|
||||||
|
#logging.error(traceback.extract_stack(exc_traceback))
|
||||||
|
raise Error(str(e))
|
||||||
|
raise
|
||||||
|
_wrap.func_name = f.func_name
|
||||||
|
return _wrap
|
||||||
|
|
||||||
|
|
||||||
|
class OpenstackException(Exception):
|
||||||
|
"""
|
||||||
|
Base Exception
|
||||||
|
|
||||||
|
To correctly use this class, inherit from it and define
|
||||||
|
a 'message' property. That message will get printf'd
|
||||||
|
with the keyword arguments provided to the constructor.
|
||||||
|
"""
|
||||||
|
message = "An unknown exception occurred"
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
try:
|
||||||
|
self._error_string = self.message % kwargs
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# at least get the core message out if something happened
|
||||||
|
self._error_string = self.message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self._error_string
|
||||||
|
|
||||||
|
|
||||||
|
class MalformedRequestBody(OpenstackException):
|
||||||
|
message = "Malformed message body: %(reason)s"
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidContentType(OpenstackException):
|
||||||
|
message = "Invalid content type %(content_type)s"
|
538
windc/openstack/oldcommon/extensions.py
Normal file
538
windc/openstack/oldcommon/extensions.py
Normal file
@ -0,0 +1,538 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011 Justin Santa Barbara
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import imp
|
||||||
|
import os
|
||||||
|
import routes
|
||||||
|
import webob.dec
|
||||||
|
import webob.exc
|
||||||
|
import logging
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from openstack.common import exception
|
||||||
|
from openstack.common import wsgi
|
||||||
|
|
||||||
|
LOG = logging.getLogger('extensions')
|
||||||
|
DEFAULT_XMLNS = "http://docs.openstack.org/"
|
||||||
|
XMLNS_ATOM = "http://www.w3.org/2005/Atom"
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionDescriptor(object):
|
||||||
|
"""Base class that defines the contract for extensions.
|
||||||
|
|
||||||
|
Note that you don't have to derive from this class to have a valid
|
||||||
|
extension; it is purely a convenience.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
"""The name of the extension.
|
||||||
|
|
||||||
|
e.g. 'Fox In Socks'
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_alias(self):
|
||||||
|
"""The alias for the extension.
|
||||||
|
|
||||||
|
e.g. 'FOXNSOX'
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_description(self):
|
||||||
|
"""Friendly description for the extension.
|
||||||
|
|
||||||
|
e.g. 'The Fox In Socks Extension'
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_namespace(self):
|
||||||
|
"""The XML namespace for the extension.
|
||||||
|
|
||||||
|
e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_updated(self):
|
||||||
|
"""The timestamp when the extension was last updated.
|
||||||
|
|
||||||
|
e.g. '2011-01-22T13:25:27-06:00'
|
||||||
|
|
||||||
|
"""
|
||||||
|
# NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_resources(self):
|
||||||
|
"""List of extensions.ResourceExtension extension objects.
|
||||||
|
|
||||||
|
Resources define new nouns, and are accessible through URLs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
resources = []
|
||||||
|
return resources
|
||||||
|
|
||||||
|
def get_actions(self):
|
||||||
|
"""List of extensions.ActionExtension extension objects.
|
||||||
|
|
||||||
|
Actions are verbs callable from the API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
actions = []
|
||||||
|
return actions
|
||||||
|
|
||||||
|
def get_request_extensions(self):
|
||||||
|
"""List of extensions.RequestException extension objects.
|
||||||
|
|
||||||
|
Request extensions are used to handle custom request data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
request_exts = []
|
||||||
|
return request_exts
|
||||||
|
|
||||||
|
|
||||||
|
class ActionExtensionController(object):
|
||||||
|
def __init__(self, application):
|
||||||
|
self.application = application
|
||||||
|
self.action_handlers = {}
|
||||||
|
|
||||||
|
def add_action(self, action_name, handler):
|
||||||
|
self.action_handlers[action_name] = handler
|
||||||
|
|
||||||
|
def action(self, req, id, body):
|
||||||
|
for action_name, handler in self.action_handlers.iteritems():
|
||||||
|
if action_name in body:
|
||||||
|
return handler(body, req, id)
|
||||||
|
# no action handler found (bump to downstream application)
|
||||||
|
res = self.application
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class ActionExtensionResource(wsgi.Resource):
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
controller = ActionExtensionController(application)
|
||||||
|
wsgi.Resource.__init__(self, controller)
|
||||||
|
|
||||||
|
def add_action(self, action_name, handler):
|
||||||
|
self.controller.add_action(action_name, handler)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestExtensionController(object):
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
self.application = application
|
||||||
|
self.handlers = []
|
||||||
|
|
||||||
|
def add_handler(self, handler):
|
||||||
|
self.handlers.append(handler)
|
||||||
|
|
||||||
|
def process(self, req, *args, **kwargs):
|
||||||
|
res = req.get_response(self.application)
|
||||||
|
# currently request handlers are un-ordered
|
||||||
|
for handler in self.handlers:
|
||||||
|
res = handler(req, res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class RequestExtensionResource(wsgi.Resource):
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
controller = RequestExtensionController(application)
|
||||||
|
wsgi.Resource.__init__(self, controller)
|
||||||
|
|
||||||
|
def add_handler(self, handler):
|
||||||
|
self.controller.add_handler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionsResource(wsgi.Resource):
|
||||||
|
|
||||||
|
def __init__(self, extension_manager):
|
||||||
|
self.extension_manager = extension_manager
|
||||||
|
body_serializers = {'application/xml': ExtensionsXMLSerializer()}
|
||||||
|
serializer = wsgi.ResponseSerializer(body_serializers=body_serializers)
|
||||||
|
super(ExtensionsResource, self).__init__(self, None, serializer)
|
||||||
|
|
||||||
|
def _translate(self, ext):
|
||||||
|
ext_data = {}
|
||||||
|
ext_data['name'] = ext.get_name()
|
||||||
|
ext_data['alias'] = ext.get_alias()
|
||||||
|
ext_data['description'] = ext.get_description()
|
||||||
|
ext_data['namespace'] = ext.get_namespace()
|
||||||
|
ext_data['updated'] = ext.get_updated()
|
||||||
|
ext_data['links'] = [] # TODO(dprince): implement extension links
|
||||||
|
return ext_data
|
||||||
|
|
||||||
|
def index(self, req):
|
||||||
|
extensions = []
|
||||||
|
for _alias, ext in self.extension_manager.extensions.iteritems():
|
||||||
|
extensions.append(self._translate(ext))
|
||||||
|
return dict(extensions=extensions)
|
||||||
|
|
||||||
|
def show(self, req, id):
|
||||||
|
# NOTE(dprince): the extensions alias is used as the 'id' for show
|
||||||
|
ext = self.extension_manager.extensions.get(id, None)
|
||||||
|
if not ext:
|
||||||
|
raise webob.exc.HTTPNotFound(
|
||||||
|
_("Extension with alias %s does not exist") % id)
|
||||||
|
|
||||||
|
return dict(extension=self._translate(ext))
|
||||||
|
|
||||||
|
def delete(self, req, id):
|
||||||
|
raise webob.exc.HTTPNotFound()
|
||||||
|
|
||||||
|
def create(self, req):
|
||||||
|
raise webob.exc.HTTPNotFound()
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionMiddleware(wsgi.Middleware):
|
||||||
|
"""Extensions middleware for WSGI."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def factory(cls, global_config, **local_config):
|
||||||
|
"""Paste factory."""
|
||||||
|
def _factory(app):
|
||||||
|
return cls(app, global_config, **local_config)
|
||||||
|
return _factory
|
||||||
|
|
||||||
|
def _action_ext_resources(self, application, ext_mgr, mapper):
|
||||||
|
"""Return a dict of ActionExtensionResource-s by collection."""
|
||||||
|
action_resources = {}
|
||||||
|
for action in ext_mgr.get_actions():
|
||||||
|
if not action.collection in action_resources.keys():
|
||||||
|
resource = ActionExtensionResource(application)
|
||||||
|
mapper.connect("/%s/:(id)/action.:(format)" %
|
||||||
|
action.collection,
|
||||||
|
action='action',
|
||||||
|
controller=resource,
|
||||||
|
conditions=dict(method=['POST']))
|
||||||
|
mapper.connect("/%s/:(id)/action" %
|
||||||
|
action.collection,
|
||||||
|
action='action',
|
||||||
|
controller=resource,
|
||||||
|
conditions=dict(method=['POST']))
|
||||||
|
action_resources[action.collection] = resource
|
||||||
|
|
||||||
|
return action_resources
|
||||||
|
|
||||||
|
def _request_ext_resources(self, application, ext_mgr, mapper):
|
||||||
|
"""Returns a dict of RequestExtensionResource-s by collection."""
|
||||||
|
request_ext_resources = {}
|
||||||
|
for req_ext in ext_mgr.get_request_extensions():
|
||||||
|
if not req_ext.key in request_ext_resources.keys():
|
||||||
|
resource = RequestExtensionResource(application)
|
||||||
|
mapper.connect(req_ext.url_route + '.:(format)',
|
||||||
|
action='process',
|
||||||
|
controller=resource,
|
||||||
|
conditions=req_ext.conditions)
|
||||||
|
|
||||||
|
mapper.connect(req_ext.url_route,
|
||||||
|
action='process',
|
||||||
|
controller=resource,
|
||||||
|
conditions=req_ext.conditions)
|
||||||
|
request_ext_resources[req_ext.key] = resource
|
||||||
|
|
||||||
|
return request_ext_resources
|
||||||
|
|
||||||
|
def __init__(self, application, config, ext_mgr=None):
|
||||||
|
ext_mgr = ext_mgr or ExtensionManager(
|
||||||
|
config['api_extensions_path'])
|
||||||
|
mapper = routes.Mapper()
|
||||||
|
|
||||||
|
# extended resources
|
||||||
|
for resource_ext in ext_mgr.get_resources():
|
||||||
|
LOG.debug(_('Extended resource: %s'), resource_ext.collection)
|
||||||
|
controller_resource = wsgi.Resource(resource_ext.controller,
|
||||||
|
resource_ext.deserializer,
|
||||||
|
resource_ext.serializer)
|
||||||
|
self._map_custom_collection_actions(resource_ext, mapper,
|
||||||
|
controller_resource)
|
||||||
|
kargs = dict(controller=controller_resource,
|
||||||
|
collection=resource_ext.collection_actions,
|
||||||
|
member=resource_ext.member_actions)
|
||||||
|
if resource_ext.parent:
|
||||||
|
kargs['parent_resource'] = resource_ext.parent
|
||||||
|
mapper.resource(resource_ext.collection,
|
||||||
|
resource_ext.collection, **kargs)
|
||||||
|
|
||||||
|
# extended actions
|
||||||
|
action_resources = self._action_ext_resources(application, ext_mgr,
|
||||||
|
mapper)
|
||||||
|
for action in ext_mgr.get_actions():
|
||||||
|
LOG.debug(_('Extended action: %s'), action.action_name)
|
||||||
|
resource = action_resources[action.collection]
|
||||||
|
resource.add_action(action.action_name, action.handler)
|
||||||
|
|
||||||
|
# extended requests
|
||||||
|
req_controllers = self._request_ext_resources(application, ext_mgr,
|
||||||
|
mapper)
|
||||||
|
for request_ext in ext_mgr.get_request_extensions():
|
||||||
|
LOG.debug(_('Extended request: %s'), request_ext.key)
|
||||||
|
controller = req_controllers[request_ext.key]
|
||||||
|
controller.add_handler(request_ext.handler)
|
||||||
|
|
||||||
|
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||||
|
mapper)
|
||||||
|
|
||||||
|
super(ExtensionMiddleware, self).__init__(application)
|
||||||
|
|
||||||
|
def _map_custom_collection_actions(self, resource_ext, mapper,
|
||||||
|
controller_resource):
|
||||||
|
for action, method in resource_ext.collection_actions.iteritems():
|
||||||
|
parent = resource_ext.parent
|
||||||
|
conditions = dict(method=[method])
|
||||||
|
path = "/%s/%s" % (resource_ext.collection, action)
|
||||||
|
|
||||||
|
path_prefix = ""
|
||||||
|
if parent:
|
||||||
|
path_prefix = "/%s/{%s_id}" % (parent["collection_name"],
|
||||||
|
parent["member_name"])
|
||||||
|
|
||||||
|
with mapper.submapper(controller=controller_resource,
|
||||||
|
action=action,
|
||||||
|
path_prefix=path_prefix,
|
||||||
|
conditions=conditions) as submap:
|
||||||
|
submap.connect(path)
|
||||||
|
submap.connect("%s.:(format)" % path)
|
||||||
|
|
||||||
|
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||||
|
def __call__(self, req):
|
||||||
|
"""Route the incoming request with router."""
|
||||||
|
req.environ['extended.app'] = self.application
|
||||||
|
return self._router
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||||
|
def _dispatch(req):
|
||||||
|
"""Dispatch the request.
|
||||||
|
|
||||||
|
Returns the routed WSGI app's response or defers to the extended
|
||||||
|
application.
|
||||||
|
|
||||||
|
"""
|
||||||
|
match = req.environ['wsgiorg.routing_args'][1]
|
||||||
|
if not match:
|
||||||
|
return req.environ['extended.app']
|
||||||
|
app = match['controller']
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionManager(object):
|
||||||
|
"""Load extensions from the configured extension path.
|
||||||
|
|
||||||
|
See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an
|
||||||
|
example extension implementation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
LOG.debug(_('Initializing extension manager.'))
|
||||||
|
|
||||||
|
self.path = path
|
||||||
|
self.extensions = {}
|
||||||
|
self._load_all_extensions()
|
||||||
|
|
||||||
|
def get_resources(self):
|
||||||
|
"""Returns a list of ResourceExtension objects."""
|
||||||
|
resources = []
|
||||||
|
extension_resource = ExtensionsResource(self)
|
||||||
|
res_ext = ResourceExtension('extensions',
|
||||||
|
extension_resource,
|
||||||
|
serializer=extension_resource.serializer)
|
||||||
|
resources.append(res_ext)
|
||||||
|
for alias, ext in self.extensions.iteritems():
|
||||||
|
try:
|
||||||
|
resources.extend(ext.get_resources())
|
||||||
|
except AttributeError:
|
||||||
|
# NOTE(dprince): Extension aren't required to have resource
|
||||||
|
# extensions
|
||||||
|
pass
|
||||||
|
return resources
|
||||||
|
|
||||||
|
def get_actions(self):
|
||||||
|
"""Returns a list of ActionExtension objects."""
|
||||||
|
actions = []
|
||||||
|
for alias, ext in self.extensions.iteritems():
|
||||||
|
try:
|
||||||
|
actions.extend(ext.get_actions())
|
||||||
|
except AttributeError:
|
||||||
|
# NOTE(dprince): Extension aren't required to have action
|
||||||
|
# extensions
|
||||||
|
pass
|
||||||
|
return actions
|
||||||
|
|
||||||
|
def get_request_extensions(self):
|
||||||
|
"""Returns a list of RequestExtension objects."""
|
||||||
|
request_exts = []
|
||||||
|
for alias, ext in self.extensions.iteritems():
|
||||||
|
try:
|
||||||
|
request_exts.extend(ext.get_request_extensions())
|
||||||
|
except AttributeError:
|
||||||
|
# NOTE(dprince): Extension aren't required to have request
|
||||||
|
# extensions
|
||||||
|
pass
|
||||||
|
return request_exts
|
||||||
|
|
||||||
|
def _check_extension(self, extension):
|
||||||
|
"""Checks for required methods in extension objects."""
|
||||||
|
try:
|
||||||
|
LOG.debug(_('Ext name: %s'), extension.get_name())
|
||||||
|
LOG.debug(_('Ext alias: %s'), extension.get_alias())
|
||||||
|
LOG.debug(_('Ext description: %s'), extension.get_description())
|
||||||
|
LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
|
||||||
|
LOG.debug(_('Ext updated: %s'), extension.get_updated())
|
||||||
|
except AttributeError as ex:
|
||||||
|
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _load_all_extensions(self):
|
||||||
|
"""Load extensions from the configured path.
|
||||||
|
|
||||||
|
Load extensions from the configured path. The extension name is
|
||||||
|
constructed from the module_name. If your extension module was named
|
||||||
|
widgets.py the extension class within that module should be
|
||||||
|
'Widgets'.
|
||||||
|
|
||||||
|
In addition, extensions are loaded from the 'contrib' directory.
|
||||||
|
|
||||||
|
See nova/tests/api/openstack/extensions/foxinsocks.py for an example
|
||||||
|
extension implementation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if os.path.exists(self.path):
|
||||||
|
self._load_all_extensions_from_path(self.path)
|
||||||
|
|
||||||
|
contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
|
||||||
|
if os.path.exists(contrib_path):
|
||||||
|
self._load_all_extensions_from_path(contrib_path)
|
||||||
|
|
||||||
|
def _load_all_extensions_from_path(self, path):
|
||||||
|
for f in os.listdir(path):
|
||||||
|
LOG.debug(_('Loading extension file: %s'), f)
|
||||||
|
mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
|
||||||
|
ext_path = os.path.join(path, f)
|
||||||
|
if file_ext.lower() == '.py' and not mod_name.startswith('_'):
|
||||||
|
mod = imp.load_source(mod_name, ext_path)
|
||||||
|
ext_name = mod_name[0].upper() + mod_name[1:]
|
||||||
|
new_ext_class = getattr(mod, ext_name, None)
|
||||||
|
if not new_ext_class:
|
||||||
|
LOG.warn(_('Did not find expected name '
|
||||||
|
'"%(ext_name)s" in %(file)s'),
|
||||||
|
{'ext_name': ext_name,
|
||||||
|
'file': ext_path})
|
||||||
|
continue
|
||||||
|
new_ext = new_ext_class()
|
||||||
|
self.add_extension(new_ext)
|
||||||
|
|
||||||
|
def add_extension(self, ext):
|
||||||
|
# Do nothing if the extension doesn't check out
|
||||||
|
if not self._check_extension(ext):
|
||||||
|
return
|
||||||
|
|
||||||
|
alias = ext.get_alias()
|
||||||
|
LOG.debug(_('Loaded extension: %s'), alias)
|
||||||
|
|
||||||
|
if alias in self.extensions:
|
||||||
|
raise exception.Error("Found duplicate extension: %s" % alias)
|
||||||
|
self.extensions[alias] = ext
|
||||||
|
|
||||||
|
|
||||||
|
class RequestExtension(object):
|
||||||
|
"""Extend requests and responses of core nova OpenStack API resources.
|
||||||
|
|
||||||
|
Provide a way to add data to responses and handle custom request data
|
||||||
|
that is sent to core nova OpenStack API controllers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, method, url_route, handler):
|
||||||
|
self.url_route = url_route
|
||||||
|
self.handler = handler
|
||||||
|
self.conditions = dict(method=[method])
|
||||||
|
self.key = "%s-%s" % (method, url_route)
|
||||||
|
|
||||||
|
|
||||||
|
class ActionExtension(object):
|
||||||
|
"""Add custom actions to core nova OpenStack API resources."""
|
||||||
|
|
||||||
|
def __init__(self, collection, action_name, handler):
|
||||||
|
self.collection = collection
|
||||||
|
self.action_name = action_name
|
||||||
|
self.handler = handler
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceExtension(object):
|
||||||
|
"""Add top level resources to the OpenStack API in nova."""
|
||||||
|
|
||||||
|
def __init__(self, collection, controller, parent=None,
|
||||||
|
collection_actions=None, member_actions=None,
|
||||||
|
deserializer=None, serializer=None):
|
||||||
|
if not collection_actions:
|
||||||
|
collection_actions = {}
|
||||||
|
if not member_actions:
|
||||||
|
member_actions = {}
|
||||||
|
self.collection = collection
|
||||||
|
self.controller = controller
|
||||||
|
self.parent = parent
|
||||||
|
self.collection_actions = collection_actions
|
||||||
|
self.member_actions = member_actions
|
||||||
|
self.deserializer = deserializer
|
||||||
|
self.serializer = serializer
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionsXMLSerializer(wsgi.XMLDictSerializer):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.nsmap = {None: DEFAULT_XMLNS, 'atom': XMLNS_ATOM}
|
||||||
|
|
||||||
|
def show(self, ext_dict):
|
||||||
|
ext = etree.Element('extension', nsmap=self.nsmap)
|
||||||
|
self._populate_ext(ext, ext_dict['extension'])
|
||||||
|
return self._to_xml(ext)
|
||||||
|
|
||||||
|
def index(self, exts_dict):
|
||||||
|
exts = etree.Element('extensions', nsmap=self.nsmap)
|
||||||
|
for ext_dict in exts_dict['extensions']:
|
||||||
|
ext = etree.SubElement(exts, 'extension')
|
||||||
|
self._populate_ext(ext, ext_dict)
|
||||||
|
return self._to_xml(exts)
|
||||||
|
|
||||||
|
def _populate_ext(self, ext_elem, ext_dict):
|
||||||
|
"""Populate an extension xml element from a dict."""
|
||||||
|
|
||||||
|
ext_elem.set('name', ext_dict['name'])
|
||||||
|
ext_elem.set('namespace', ext_dict['namespace'])
|
||||||
|
ext_elem.set('alias', ext_dict['alias'])
|
||||||
|
ext_elem.set('updated', ext_dict['updated'])
|
||||||
|
desc = etree.Element('description')
|
||||||
|
desc.text = ext_dict['description']
|
||||||
|
ext_elem.append(desc)
|
||||||
|
for link in ext_dict.get('links', []):
|
||||||
|
elem = etree.SubElement(ext_elem, '{%s}link' % XMLNS_ATOM)
|
||||||
|
elem.set('rel', link['rel'])
|
||||||
|
elem.set('href', link['href'])
|
||||||
|
elem.set('type', link['type'])
|
||||||
|
return ext_elem
|
||||||
|
|
||||||
|
def _to_xml(self, root):
|
||||||
|
"""Convert the xml object to an xml string."""
|
||||||
|
|
||||||
|
return etree.tostring(root, encoding='UTF-8')
|
0
windc/openstack/oldcommon/middleware/__init__.py
Normal file
0
windc/openstack/oldcommon/middleware/__init__.py
Normal file
64
windc/openstack/oldcommon/middleware/context.py
Normal file
64
windc/openstack/oldcommon/middleware/context.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Middleware that attaches a context to the WSGI request
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openstack.common import utils
|
||||||
|
from openstack.common import wsgi
|
||||||
|
from openstack.common import context
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMiddleware(wsgi.Middleware):
|
||||||
|
def __init__(self, app, options):
|
||||||
|
self.options = options
|
||||||
|
super(ContextMiddleware, self).__init__(app)
|
||||||
|
|
||||||
|
def make_context(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a context with the given arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Determine the context class to use
|
||||||
|
ctxcls = context.RequestContext
|
||||||
|
if 'context_class' in self.options:
|
||||||
|
ctxcls = utils.import_class(self.options['context_class'])
|
||||||
|
|
||||||
|
return ctxcls(*args, **kwargs)
|
||||||
|
|
||||||
|
def process_request(self, req):
|
||||||
|
"""
|
||||||
|
Extract any authentication information in the request and
|
||||||
|
construct an appropriate context from it.
|
||||||
|
"""
|
||||||
|
# Use the default empty context, with admin turned on for
|
||||||
|
# backwards compatibility
|
||||||
|
req.context = self.make_context(is_admin=True)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_factory(global_conf, **local_conf):
|
||||||
|
"""
|
||||||
|
Factory method for paste.deploy
|
||||||
|
"""
|
||||||
|
conf = global_conf.copy()
|
||||||
|
conf.update(local_conf)
|
||||||
|
|
||||||
|
def filter(app):
|
||||||
|
return ContextMiddleware(app, conf)
|
||||||
|
|
||||||
|
return filter
|
210
windc/openstack/oldcommon/utils.py
Normal file
210
windc/openstack/oldcommon/utils.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
System-level utilities and helper functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
from eventlet import greenthread
|
||||||
|
from eventlet.green import subprocess
|
||||||
|
|
||||||
|
from openstack.common import exception
|
||||||
|
|
||||||
|
|
||||||
|
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def int_from_bool_as_string(subject):
|
||||||
|
"""
|
||||||
|
Interpret a string as a boolean and return either 1 or 0.
|
||||||
|
|
||||||
|
Any string value in:
|
||||||
|
('True', 'true', 'On', 'on', '1')
|
||||||
|
is interpreted as a boolean True.
|
||||||
|
|
||||||
|
Useful for JSON-decoded stuff and config file parsing
|
||||||
|
"""
|
||||||
|
return bool_from_string(subject) and 1 or 0
|
||||||
|
|
||||||
|
|
||||||
|
def bool_from_string(subject):
|
||||||
|
"""
|
||||||
|
Interpret a string as a boolean.
|
||||||
|
|
||||||
|
Any string value in:
|
||||||
|
('True', 'true', 'On', 'on', '1')
|
||||||
|
is interpreted as a boolean True.
|
||||||
|
|
||||||
|
Useful for JSON-decoded stuff and config file parsing
|
||||||
|
"""
|
||||||
|
if isinstance(subject, types.BooleanType):
|
||||||
|
return subject
|
||||||
|
if isinstance(subject, types.StringTypes):
|
||||||
|
if subject.strip().lower() in ('true', 'on', '1'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def execute(*cmd, **kwargs):
|
||||||
|
"""
|
||||||
|
Helper method to execute command with optional retry.
|
||||||
|
|
||||||
|
:cmd Passed to subprocess.Popen.
|
||||||
|
:process_input Send to opened process.
|
||||||
|
:check_exit_code Defaults to 0. Raise exception.ProcessExecutionError
|
||||||
|
unless program exits with this code.
|
||||||
|
:delay_on_retry True | False. Defaults to True. If set to True, wait a
|
||||||
|
short amount of time before retrying.
|
||||||
|
:attempts How many times to retry cmd.
|
||||||
|
:run_as_root True | False. Defaults to False. If set to True,
|
||||||
|
the command is prefixed by the command specified
|
||||||
|
in the root_helper kwarg.
|
||||||
|
:root_helper command to prefix all cmd's with
|
||||||
|
|
||||||
|
:raises exception.Error on receiving unknown arguments
|
||||||
|
:raises exception.ProcessExecutionError
|
||||||
|
"""
|
||||||
|
|
||||||
|
process_input = kwargs.pop('process_input', None)
|
||||||
|
check_exit_code = kwargs.pop('check_exit_code', 0)
|
||||||
|
delay_on_retry = kwargs.pop('delay_on_retry', True)
|
||||||
|
attempts = kwargs.pop('attempts', 1)
|
||||||
|
run_as_root = kwargs.pop('run_as_root', False)
|
||||||
|
root_helper = kwargs.pop('root_helper', '')
|
||||||
|
if len(kwargs):
|
||||||
|
raise exception.Error(_('Got unknown keyword args '
|
||||||
|
'to utils.execute: %r') % kwargs)
|
||||||
|
if run_as_root:
|
||||||
|
cmd = shlex.split(root_helper) + list(cmd)
|
||||||
|
cmd = map(str, cmd)
|
||||||
|
|
||||||
|
while attempts > 0:
|
||||||
|
attempts -= 1
|
||||||
|
try:
|
||||||
|
LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd))
|
||||||
|
_PIPE = subprocess.PIPE # pylint: disable=E1101
|
||||||
|
obj = subprocess.Popen(cmd,
|
||||||
|
stdin=_PIPE,
|
||||||
|
stdout=_PIPE,
|
||||||
|
stderr=_PIPE,
|
||||||
|
close_fds=True)
|
||||||
|
result = None
|
||||||
|
if process_input is not None:
|
||||||
|
result = obj.communicate(process_input)
|
||||||
|
else:
|
||||||
|
result = obj.communicate()
|
||||||
|
obj.stdin.close() # pylint: disable=E1101
|
||||||
|
_returncode = obj.returncode # pylint: disable=E1101
|
||||||
|
if _returncode:
|
||||||
|
LOG.debug(_('Result was %s') % _returncode)
|
||||||
|
if type(check_exit_code) == types.IntType \
|
||||||
|
and _returncode != check_exit_code:
|
||||||
|
(stdout, stderr) = result
|
||||||
|
raise exception.ProcessExecutionError(
|
||||||
|
exit_code=_returncode,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
cmd=' '.join(cmd))
|
||||||
|
return result
|
||||||
|
except exception.ProcessExecutionError:
|
||||||
|
if not attempts:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
LOG.debug(_('%r failed. Retrying.'), cmd)
|
||||||
|
if delay_on_retry:
|
||||||
|
greenthread.sleep(random.randint(20, 200) / 100.0)
|
||||||
|
finally:
|
||||||
|
# NOTE(termie): this appears to be necessary to let the subprocess
|
||||||
|
# call clean something up in between calls, without
|
||||||
|
# it two execute calls in a row hangs the second one
|
||||||
|
greenthread.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
def import_class(import_str):
|
||||||
|
"""Returns a class from a string including module and class"""
|
||||||
|
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||||
|
try:
|
||||||
|
__import__(mod_str)
|
||||||
|
return getattr(sys.modules[mod_str], class_str)
|
||||||
|
except (ImportError, ValueError, AttributeError):
|
||||||
|
raise exception.NotFound('Class %s cannot be found' % class_str)
|
||||||
|
|
||||||
|
|
||||||
|
def import_object(import_str):
|
||||||
|
"""Returns an object including a module or module and class"""
|
||||||
|
try:
|
||||||
|
__import__(import_str)
|
||||||
|
return sys.modules[import_str]
|
||||||
|
except ImportError:
|
||||||
|
return import_class(import_str)
|
||||||
|
|
||||||
|
|
||||||
|
def isotime(at=None):
|
||||||
|
if not at:
|
||||||
|
at = datetime.datetime.utcnow()
|
||||||
|
return at.strftime(TIME_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_isotime(timestr):
|
||||||
|
return datetime.datetime.strptime(timestr, TIME_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mailmap(mailmap='.mailmap'):
|
||||||
|
mapping = {}
|
||||||
|
if os.path.exists(mailmap):
|
||||||
|
fp = open(mailmap, 'r')
|
||||||
|
for l in fp:
|
||||||
|
l = l.strip()
|
||||||
|
if not l.startswith('#') and ' ' in l:
|
||||||
|
canonical_email, alias = l.split(' ')
|
||||||
|
mapping[alias] = canonical_email
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def str_dict_replace(s, mapping):
|
||||||
|
for s1, s2 in mapping.iteritems():
|
||||||
|
s = s.replace(s1, s2)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def utcnow():
|
||||||
|
"""Overridable version of utils.utcnow."""
|
||||||
|
if utcnow.override_time:
|
||||||
|
return utcnow.override_time
|
||||||
|
return datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
|
||||||
|
utcnow.override_time = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_time_override(override_time=datetime.datetime.utcnow()):
|
||||||
|
"""Override utils.utcnow to return a constant time."""
|
||||||
|
utcnow.override_time = override_time
|
||||||
|
|
||||||
|
|
||||||
|
def clear_time_override():
|
||||||
|
"""Remove the overridden time."""
|
||||||
|
utcnow.override_time = None
|
717
windc/openstack/oldcommon/wsgi.py
Normal file
717
windc/openstack/oldcommon/wsgi.py
Normal file
@ -0,0 +1,717 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Utility methods for working with WSGI servers."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import eventlet
|
||||||
|
import eventlet.wsgi
|
||||||
|
|
||||||
|
eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import routes
|
||||||
|
import routes.middleware
|
||||||
|
import webob.dec
|
||||||
|
import webob.exc
|
||||||
|
from xml.dom import minidom
|
||||||
|
from xml.parsers import expat
|
||||||
|
|
||||||
|
from openstack.common import exception
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger('wsgi')
|
||||||
|
|
||||||
|
|
||||||
|
class WritableLogger(object):
|
||||||
|
"""A thin wrapper that responds to `write` and logs."""
|
||||||
|
|
||||||
|
def __init__(self, logger, level=logging.DEBUG):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
|
||||||
|
def write(self, msg):
|
||||||
|
self.logger.log(self.level, msg.strip("\n"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_server(application, port):
|
||||||
|
"""Run a WSGI server with the given application."""
|
||||||
|
sock = eventlet.listen(('0.0.0.0', port))
|
||||||
|
eventlet.wsgi.server(sock, application)
|
||||||
|
|
||||||
|
|
||||||
|
class Server(object):
|
||||||
|
"""Server class to manage multiple WSGI sockets and applications."""
|
||||||
|
|
||||||
|
def __init__(self, threads=1000):
|
||||||
|
self.pool = eventlet.GreenPool(threads)
|
||||||
|
|
||||||
|
def start(self, application, port, host='0.0.0.0', backlog=128):
|
||||||
|
"""Run a WSGI server with the given application."""
|
||||||
|
socket = eventlet.listen((host, port), backlog=backlog)
|
||||||
|
self.pool.spawn_n(self._run, application, socket)
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait until all servers have completed running."""
|
||||||
|
try:
|
||||||
|
self.pool.waitall()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _run(self, application, socket):
|
||||||
|
"""Start a WSGI server in a new green thread."""
|
||||||
|
logger = logging.getLogger('eventlet.wsgi.server')
|
||||||
|
eventlet.wsgi.server(socket, application, custom_pool=self.pool,
|
||||||
|
log=WritableLogger(logger))
|
||||||
|
|
||||||
|
|
||||||
|
class Middleware(object):
|
||||||
|
"""
|
||||||
|
Base WSGI middleware wrapper. These classes require an application to be
|
||||||
|
initialized that will be called next. By default the middleware will
|
||||||
|
simply call its wrapped app, or you can override __call__ to customize its
|
||||||
|
behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
self.application = application
|
||||||
|
|
||||||
|
def process_request(self, req):
|
||||||
|
"""
|
||||||
|
Called on each request.
|
||||||
|
|
||||||
|
If this returns None, the next application down the stack will be
|
||||||
|
executed. If it returns a response then that response will be returned
|
||||||
|
and execution will stop here.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_response(self, response):
|
||||||
|
"""Do whatever you'd like to the response."""
|
||||||
|
return response
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
response = self.process_request(req)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
response = req.get_response(self.application)
|
||||||
|
return self.process_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
class Debug(Middleware):
|
||||||
|
"""
|
||||||
|
Helper class that can be inserted into any WSGI application chain
|
||||||
|
to get information about the request and response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
print ("*" * 40) + " REQUEST ENVIRON"
|
||||||
|
for key, value in req.environ.items():
|
||||||
|
print key, "=", value
|
||||||
|
print
|
||||||
|
resp = req.get_response(self.application)
|
||||||
|
|
||||||
|
print ("*" * 40) + " RESPONSE HEADERS"
|
||||||
|
for (key, value) in resp.headers.iteritems():
|
||||||
|
print key, "=", value
|
||||||
|
print
|
||||||
|
|
||||||
|
resp.app_iter = self.print_generator(resp.app_iter)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_generator(app_iter):
|
||||||
|
"""
|
||||||
|
Iterator that prints the contents of a wrapper string iterator
|
||||||
|
when iterated.
|
||||||
|
"""
|
||||||
|
print ("*" * 40) + " BODY"
|
||||||
|
for part in app_iter:
|
||||||
|
sys.stdout.write(part)
|
||||||
|
sys.stdout.flush()
|
||||||
|
yield part
|
||||||
|
print
|
||||||
|
|
||||||
|
|
||||||
|
class Router(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
WSGI middleware that maps incoming requests to WSGI apps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mapper):
|
||||||
|
"""
|
||||||
|
Create a router for the given routes.Mapper.
|
||||||
|
|
||||||
|
Each route in `mapper` must specify a 'controller', which is a
|
||||||
|
WSGI app to call. You'll probably want to specify an 'action' as
|
||||||
|
well and have your controller be a wsgi.Controller, who will route
|
||||||
|
the request to the action method.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
mapper = routes.Mapper()
|
||||||
|
sc = ServerController()
|
||||||
|
|
||||||
|
# Explicit mapping of one route to a controller+action
|
||||||
|
mapper.connect(None, "/svrlist", controller=sc, action="list")
|
||||||
|
|
||||||
|
# Actions are all implicitly defined
|
||||||
|
mapper.resource("server", "servers", controller=sc)
|
||||||
|
|
||||||
|
# Pointing to an arbitrary WSGI app. You can specify the
|
||||||
|
# {path_info:.*} parameter so the target app can be handed just that
|
||||||
|
# section of the URL.
|
||||||
|
mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
|
||||||
|
"""
|
||||||
|
self.map = mapper
|
||||||
|
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||||
|
self.map)
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
"""
|
||||||
|
Route the incoming request to a controller based on self.map.
|
||||||
|
If no match, return a 404.
|
||||||
|
"""
|
||||||
|
return self._router
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def _dispatch(req):
|
||||||
|
"""
|
||||||
|
Called by self._router after matching the incoming request to a route
|
||||||
|
and putting the information into req.environ. Either returns 404
|
||||||
|
or the routed WSGI app's response.
|
||||||
|
"""
|
||||||
|
match = req.environ['wsgiorg.routing_args'][1]
|
||||||
|
if not match:
|
||||||
|
return webob.exc.HTTPNotFound()
|
||||||
|
app = match['controller']
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
class Request(webob.Request):
|
||||||
|
"""Add some Openstack API-specific logic to the base webob.Request."""
|
||||||
|
|
||||||
|
default_request_content_types = ('application/json', 'application/xml')
|
||||||
|
default_accept_types = ('application/json', 'application/xml')
|
||||||
|
default_accept_type = 'application/json'
|
||||||
|
|
||||||
|
def best_match_content_type(self, supported_content_types=None):
|
||||||
|
"""Determine the requested response content-type.
|
||||||
|
|
||||||
|
Based on the query extension then the Accept header.
|
||||||
|
Defaults to default_accept_type if we don't find a preference
|
||||||
|
|
||||||
|
"""
|
||||||
|
supported_content_types = (supported_content_types or
|
||||||
|
self.default_accept_types)
|
||||||
|
|
||||||
|
parts = self.path.rsplit('.', 1)
|
||||||
|
if len(parts) > 1:
|
||||||
|
ctype = 'application/{0}'.format(parts[1])
|
||||||
|
if ctype in supported_content_types:
|
||||||
|
return ctype
|
||||||
|
|
||||||
|
bm = self.accept.best_match(supported_content_types)
|
||||||
|
return bm or self.default_accept_type
|
||||||
|
|
||||||
|
def get_content_type(self, allowed_content_types=None):
|
||||||
|
"""Determine content type of the request body.
|
||||||
|
|
||||||
|
Does not do any body introspection, only checks header
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not "Content-Type" in self.headers:
|
||||||
|
return None
|
||||||
|
|
||||||
|
content_type = self.content_type
|
||||||
|
allowed_content_types = (allowed_content_types or
|
||||||
|
self.default_request_content_types)
|
||||||
|
|
||||||
|
if content_type not in allowed_content_types:
|
||||||
|
raise exception.InvalidContentType(content_type=content_type)
|
||||||
|
return content_type
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""
|
||||||
|
WSGI app that handles (de)serialization and controller dispatch.
|
||||||
|
|
||||||
|
Reads routing information supplied by RoutesMiddleware and calls
|
||||||
|
the requested action method upon its deserializer, controller,
|
||||||
|
and serializer. Those three objects may implement any of the basic
|
||||||
|
controller action methods (create, update, show, index, delete)
|
||||||
|
along with any that may be specified in the api router. A 'default'
|
||||||
|
method may also be implemented to be used in place of any
|
||||||
|
non-implemented actions. Deserializer methods must accept a request
|
||||||
|
argument and return a dictionary. Controller methods must accept a
|
||||||
|
request argument. Additionally, they must also accept keyword
|
||||||
|
arguments that represent the keys returned by the Deserializer. They
|
||||||
|
may raise a webob.exc exception or return a dict, which will be
|
||||||
|
serialized by requested content type.
|
||||||
|
"""
|
||||||
|
def __init__(self, controller, deserializer=None, serializer=None):
|
||||||
|
"""
|
||||||
|
:param controller: object that implement methods created by routes lib
|
||||||
|
:param deserializer: object that supports webob request deserialization
|
||||||
|
through controller-like actions
|
||||||
|
:param serializer: object that supports webob response serialization
|
||||||
|
through controller-like actions
|
||||||
|
"""
|
||||||
|
self.controller = controller
|
||||||
|
self.serializer = serializer or ResponseSerializer()
|
||||||
|
self.deserializer = deserializer or RequestDeserializer()
|
||||||
|
|
||||||
|
@webob.dec.wsgify(RequestClass=Request)
|
||||||
|
def __call__(self, request):
|
||||||
|
"""WSGI method that controls (de)serialization and method dispatch."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
action, action_args, accept = self.deserialize_request(request)
|
||||||
|
except exception.InvalidContentType:
|
||||||
|
msg = _("Unsupported Content-Type")
|
||||||
|
return webob.exc.HTTPUnsupportedMediaType(explanation=msg)
|
||||||
|
except exception.MalformedRequestBody:
|
||||||
|
msg = _("Malformed request body")
|
||||||
|
return webob.exc.HTTPBadRequest(explanation=msg)
|
||||||
|
|
||||||
|
action_result = self.execute_action(action, request, **action_args)
|
||||||
|
try:
|
||||||
|
return self.serialize_response(action, action_result, accept)
|
||||||
|
# return unserializable result (typically a webob exc)
|
||||||
|
except Exception:
|
||||||
|
return action_result
|
||||||
|
|
||||||
|
def deserialize_request(self, request):
|
||||||
|
return self.deserializer.deserialize(request)
|
||||||
|
|
||||||
|
def serialize_response(self, action, action_result, accept):
|
||||||
|
return self.serializer.serialize(action_result, accept, action)
|
||||||
|
|
||||||
|
def execute_action(self, action, request, **action_args):
|
||||||
|
return self.dispatch(self.controller, action, request, **action_args)
|
||||||
|
|
||||||
|
def dispatch(self, obj, action, *args, **kwargs):
|
||||||
|
"""Find action-specific method on self and call it."""
|
||||||
|
try:
|
||||||
|
method = getattr(obj, action)
|
||||||
|
except AttributeError:
|
||||||
|
method = getattr(obj, 'default')
|
||||||
|
|
||||||
|
return method(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_action_args(self, request_environment):
|
||||||
|
"""Parse dictionary created by routes library."""
|
||||||
|
try:
|
||||||
|
args = request_environment['wsgiorg.routing_args'][1].copy()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['controller']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['format']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class ActionDispatcher(object):
|
||||||
|
"""Maps method name to local methods through action name."""
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
"""Find and call local method."""
|
||||||
|
action = kwargs.pop('action', 'default')
|
||||||
|
action_method = getattr(self, str(action), self.default)
|
||||||
|
return action_method(*args, **kwargs)
|
||||||
|
|
||||||
|
def default(self, data):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class DictSerializer(ActionDispatcher):
|
||||||
|
"""Default request body serialization"""
|
||||||
|
|
||||||
|
def serialize(self, data, action='default'):
|
||||||
|
return self.dispatch(data, action=action)
|
||||||
|
|
||||||
|
def default(self, data):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class JSONDictSerializer(DictSerializer):
|
||||||
|
"""Default JSON request body serialization"""
|
||||||
|
|
||||||
|
def default(self, data):
|
||||||
|
def sanitizer(obj):
|
||||||
|
if isinstance(obj, datetime.datetime):
|
||||||
|
_dtime = obj - datetime.timedelta(microseconds=obj.microsecond)
|
||||||
|
return _dtime.isoformat()
|
||||||
|
return obj
|
||||||
|
return json.dumps(data, default=sanitizer)
|
||||||
|
|
||||||
|
|
||||||
|
class XMLDictSerializer(DictSerializer):
|
||||||
|
|
||||||
|
def __init__(self, metadata=None, xmlns=None):
|
||||||
|
"""
|
||||||
|
:param metadata: information needed to deserialize xml into
|
||||||
|
a dictionary.
|
||||||
|
:param xmlns: XML namespace to include with serialized xml
|
||||||
|
"""
|
||||||
|
super(XMLDictSerializer, self).__init__()
|
||||||
|
self.metadata = metadata or {}
|
||||||
|
self.xmlns = xmlns
|
||||||
|
|
||||||
|
def default(self, data):
|
||||||
|
# We expect data to contain a single key which is the XML root.
|
||||||
|
root_key = data.keys()[0]
|
||||||
|
doc = minidom.Document()
|
||||||
|
node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
|
||||||
|
|
||||||
|
return self.to_xml_string(node)
|
||||||
|
|
||||||
|
def to_xml_string(self, node, has_atom=False):
|
||||||
|
self._add_xmlns(node, has_atom)
|
||||||
|
return node.toprettyxml(indent=' ', encoding='UTF-8')
|
||||||
|
|
||||||
|
#NOTE (ameade): the has_atom should be removed after all of the
|
||||||
|
# xml serializers and view builders have been updated to the current
|
||||||
|
# spec that required all responses include the xmlns:atom, the has_atom
|
||||||
|
# flag is to prevent current tests from breaking
|
||||||
|
def _add_xmlns(self, node, has_atom=False):
|
||||||
|
if self.xmlns is not None:
|
||||||
|
node.setAttribute('xmlns', self.xmlns)
|
||||||
|
if has_atom:
|
||||||
|
node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom")
|
||||||
|
|
||||||
|
def _to_xml_node(self, doc, metadata, nodename, data):
|
||||||
|
"""Recursive method to convert data members to XML nodes."""
|
||||||
|
result = doc.createElement(nodename)
|
||||||
|
|
||||||
|
# Set the xml namespace if one is specified
|
||||||
|
# TODO(justinsb): We could also use prefixes on the keys
|
||||||
|
xmlns = metadata.get('xmlns', None)
|
||||||
|
if xmlns:
|
||||||
|
result.setAttribute('xmlns', xmlns)
|
||||||
|
|
||||||
|
#TODO(bcwaldon): accomplish this without a type-check
|
||||||
|
if type(data) is list:
|
||||||
|
collections = metadata.get('list_collections', {})
|
||||||
|
if nodename in collections:
|
||||||
|
metadata = collections[nodename]
|
||||||
|
for item in data:
|
||||||
|
node = doc.createElement(metadata['item_name'])
|
||||||
|
node.setAttribute(metadata['item_key'], str(item))
|
||||||
|
result.appendChild(node)
|
||||||
|
return result
|
||||||
|
singular = metadata.get('plurals', {}).get(nodename, None)
|
||||||
|
if singular is None:
|
||||||
|
if nodename.endswith('s'):
|
||||||
|
singular = nodename[:-1]
|
||||||
|
else:
|
||||||
|
singular = 'item'
|
||||||
|
for item in data:
|
||||||
|
node = self._to_xml_node(doc, metadata, singular, item)
|
||||||
|
result.appendChild(node)
|
||||||
|
#TODO(bcwaldon): accomplish this without a type-check
|
||||||
|
elif type(data) is dict:
|
||||||
|
collections = metadata.get('dict_collections', {})
|
||||||
|
if nodename in collections:
|
||||||
|
metadata = collections[nodename]
|
||||||
|
for k, v in data.items():
|
||||||
|
node = doc.createElement(metadata['item_name'])
|
||||||
|
node.setAttribute(metadata['item_key'], str(k))
|
||||||
|
text = doc.createTextNode(str(v))
|
||||||
|
node.appendChild(text)
|
||||||
|
result.appendChild(node)
|
||||||
|
return result
|
||||||
|
attrs = metadata.get('attributes', {}).get(nodename, {})
|
||||||
|
for k, v in data.items():
|
||||||
|
if k in attrs:
|
||||||
|
result.setAttribute(k, str(v))
|
||||||
|
else:
|
||||||
|
node = self._to_xml_node(doc, metadata, k, v)
|
||||||
|
result.appendChild(node)
|
||||||
|
else:
|
||||||
|
# Type is atom
|
||||||
|
node = doc.createTextNode(str(data))
|
||||||
|
result.appendChild(node)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _create_link_nodes(self, xml_doc, links):
|
||||||
|
link_nodes = []
|
||||||
|
for link in links:
|
||||||
|
link_node = xml_doc.createElement('atom:link')
|
||||||
|
link_node.setAttribute('rel', link['rel'])
|
||||||
|
link_node.setAttribute('href', link['href'])
|
||||||
|
if 'type' in link:
|
||||||
|
link_node.setAttribute('type', link['type'])
|
||||||
|
link_nodes.append(link_node)
|
||||||
|
return link_nodes
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseHeadersSerializer(ActionDispatcher):
|
||||||
|
"""Default response headers serialization"""
|
||||||
|
|
||||||
|
def serialize(self, response, data, action):
|
||||||
|
self.dispatch(response, data, action=action)
|
||||||
|
|
||||||
|
def default(self, response, data):
|
||||||
|
response.status_int = 200
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseSerializer(object):
|
||||||
|
"""Encode the necessary pieces into a response object"""
|
||||||
|
|
||||||
|
def __init__(self, body_serializers=None, headers_serializer=None):
|
||||||
|
self.body_serializers = {
|
||||||
|
'application/xml': XMLDictSerializer(),
|
||||||
|
'application/json': JSONDictSerializer(),
|
||||||
|
}
|
||||||
|
self.body_serializers.update(body_serializers or {})
|
||||||
|
|
||||||
|
self.headers_serializer = headers_serializer or \
|
||||||
|
ResponseHeadersSerializer()
|
||||||
|
|
||||||
|
def serialize(self, response_data, content_type, action='default'):
|
||||||
|
"""Serialize a dict into a string and wrap in a wsgi.Request object.
|
||||||
|
|
||||||
|
:param response_data: dict produced by the Controller
|
||||||
|
:param content_type: expected mimetype of serialized response body
|
||||||
|
|
||||||
|
"""
|
||||||
|
response = webob.Response()
|
||||||
|
self.serialize_headers(response, response_data, action)
|
||||||
|
self.serialize_body(response, response_data, content_type, action)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def serialize_headers(self, response, data, action):
|
||||||
|
self.headers_serializer.serialize(response, data, action)
|
||||||
|
|
||||||
|
def serialize_body(self, response, data, content_type, action):
|
||||||
|
response.headers['Content-Type'] = content_type
|
||||||
|
if data is not None:
|
||||||
|
serializer = self.get_body_serializer(content_type)
|
||||||
|
response.body = serializer.serialize(data, action)
|
||||||
|
|
||||||
|
def get_body_serializer(self, content_type):
|
||||||
|
try:
|
||||||
|
return self.body_serializers[content_type]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise exception.InvalidContentType(content_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHeadersDeserializer(ActionDispatcher):
|
||||||
|
"""Default request headers deserializer"""
|
||||||
|
|
||||||
|
def deserialize(self, request, action):
|
||||||
|
return self.dispatch(request, action=action)
|
||||||
|
|
||||||
|
def default(self, request):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RequestDeserializer(object):
|
||||||
|
"""Break up a Request object into more useful pieces."""
|
||||||
|
|
||||||
|
def __init__(self, body_deserializers=None, headers_deserializer=None,
|
||||||
|
supported_content_types=None):
|
||||||
|
|
||||||
|
self.supported_content_types = supported_content_types
|
||||||
|
|
||||||
|
self.body_deserializers = {
|
||||||
|
'application/xml': XMLDeserializer(),
|
||||||
|
'application/json': JSONDeserializer(),
|
||||||
|
}
|
||||||
|
self.body_deserializers.update(body_deserializers or {})
|
||||||
|
|
||||||
|
self.headers_deserializer = headers_deserializer or \
|
||||||
|
RequestHeadersDeserializer()
|
||||||
|
|
||||||
|
def deserialize(self, request):
|
||||||
|
"""Extract necessary pieces of the request.
|
||||||
|
|
||||||
|
:param request: Request object
|
||||||
|
:returns tuple of expected controller action name, dictionary of
|
||||||
|
keyword arguments to pass to the controller, the expected
|
||||||
|
content type of the response
|
||||||
|
|
||||||
|
"""
|
||||||
|
action_args = self.get_action_args(request.environ)
|
||||||
|
action = action_args.pop('action', None)
|
||||||
|
|
||||||
|
action_args.update(self.deserialize_headers(request, action))
|
||||||
|
action_args.update(self.deserialize_body(request, action))
|
||||||
|
|
||||||
|
accept = self.get_expected_content_type(request)
|
||||||
|
|
||||||
|
return (action, action_args, accept)
|
||||||
|
|
||||||
|
def deserialize_headers(self, request, action):
|
||||||
|
return self.headers_deserializer.deserialize(request, action)
|
||||||
|
|
||||||
|
def deserialize_body(self, request, action):
|
||||||
|
if not len(request.body) > 0:
|
||||||
|
LOG.debug(_("Empty body provided in request"))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_type = request.get_content_type()
|
||||||
|
except exception.InvalidContentType:
|
||||||
|
LOG.debug(_("Unrecognized Content-Type provided in request"))
|
||||||
|
raise
|
||||||
|
|
||||||
|
if content_type is None:
|
||||||
|
LOG.debug(_("No Content-Type provided in request"))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
deserializer = self.get_body_deserializer(content_type)
|
||||||
|
except exception.InvalidContentType:
|
||||||
|
LOG.debug(_("Unable to deserialize body as provided Content-Type"))
|
||||||
|
raise
|
||||||
|
|
||||||
|
return deserializer.deserialize(request.body, action)
|
||||||
|
|
||||||
|
def get_body_deserializer(self, content_type):
|
||||||
|
try:
|
||||||
|
return self.body_deserializers[content_type]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise exception.InvalidContentType(content_type=content_type)
|
||||||
|
|
||||||
|
def get_expected_content_type(self, request):
|
||||||
|
return request.best_match_content_type(self.supported_content_types)
|
||||||
|
|
||||||
|
def get_action_args(self, request_environment):
|
||||||
|
"""Parse dictionary created by routes library."""
|
||||||
|
try:
|
||||||
|
args = request_environment['wsgiorg.routing_args'][1].copy()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['controller']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['format']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class TextDeserializer(ActionDispatcher):
|
||||||
|
"""Default request body deserialization"""
|
||||||
|
|
||||||
|
def deserialize(self, datastring, action='default'):
|
||||||
|
return self.dispatch(datastring, action=action)
|
||||||
|
|
||||||
|
def default(self, datastring):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class JSONDeserializer(TextDeserializer):
|
||||||
|
|
||||||
|
def _from_json(self, datastring):
|
||||||
|
try:
|
||||||
|
return json.loads(datastring)
|
||||||
|
except ValueError:
|
||||||
|
msg = _("cannot understand JSON")
|
||||||
|
raise exception.MalformedRequestBody(reason=msg)
|
||||||
|
|
||||||
|
def default(self, datastring):
|
||||||
|
return {'body': self._from_json(datastring)}
|
||||||
|
|
||||||
|
|
||||||
|
class XMLDeserializer(TextDeserializer):
|
||||||
|
|
||||||
|
def __init__(self, metadata=None):
|
||||||
|
"""
|
||||||
|
:param metadata: information needed to deserialize xml into
|
||||||
|
a dictionary.
|
||||||
|
"""
|
||||||
|
super(XMLDeserializer, self).__init__()
|
||||||
|
self.metadata = metadata or {}
|
||||||
|
|
||||||
|
def _from_xml(self, datastring):
|
||||||
|
plurals = set(self.metadata.get('plurals', {}))
|
||||||
|
|
||||||
|
try:
|
||||||
|
node = minidom.parseString(datastring).childNodes[0]
|
||||||
|
return {node.nodeName: self._from_xml_node(node, plurals)}
|
||||||
|
except expat.ExpatError:
|
||||||
|
msg = _("cannot understand XML")
|
||||||
|
raise exception.MalformedRequestBody(reason=msg)
|
||||||
|
|
||||||
|
def _from_xml_node(self, node, listnames):
|
||||||
|
"""Convert a minidom node to a simple Python type.
|
||||||
|
|
||||||
|
:param listnames: list of XML node names whose subnodes should
|
||||||
|
be considered list items.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
|
||||||
|
return node.childNodes[0].nodeValue
|
||||||
|
elif node.nodeName in listnames:
|
||||||
|
return [self._from_xml_node(n, listnames) for n in node.childNodes]
|
||||||
|
else:
|
||||||
|
result = dict()
|
||||||
|
for attr in node.attributes.keys():
|
||||||
|
result[attr] = node.attributes[attr].nodeValue
|
||||||
|
for child in node.childNodes:
|
||||||
|
if child.nodeType != node.TEXT_NODE:
|
||||||
|
result[child.nodeName] = self._from_xml_node(child,
|
||||||
|
listnames)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def find_first_child_named(self, parent, name):
|
||||||
|
"""Search a nodes children for the first child with a given name"""
|
||||||
|
for node in parent.childNodes:
|
||||||
|
if node.nodeName == name:
|
||||||
|
return node
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_children_named(self, parent, name):
|
||||||
|
"""Return all of a nodes children who have the given name"""
|
||||||
|
for node in parent.childNodes:
|
||||||
|
if node.nodeName == name:
|
||||||
|
yield node
|
||||||
|
|
||||||
|
def extract_text(self, node):
|
||||||
|
"""Get the text field contained by the given node"""
|
||||||
|
if len(node.childNodes) == 1:
|
||||||
|
child = node.childNodes[0]
|
||||||
|
if child.nodeType == child.TEXT_NODE:
|
||||||
|
return child.nodeValue
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def default(self, datastring):
|
||||||
|
return {'body': self._from_xml(datastring)}
|
3
windc/run_tests.log
Normal file
3
windc/run_tests.log
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']
|
||||||
|
nose.selector: INFO: /Users/gokrokve/Keero/windc/run_tests.sh is executable; skipped
|
||||||
|
2013-02-11 13:35:01,989 DEBUG Initialized with method overriding = True, and path info altering = True
|
293
windc/run_tests.py
Normal file
293
windc/run_tests.py
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 OpenStack, LLC
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# Colorizer Code is borrowed from Twisted:
|
||||||
|
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
# a copy of this software and associated documentation files (the
|
||||||
|
# "Software"), to deal in the Software without restriction, including
|
||||||
|
# without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
# permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
# the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unittest runner for balancer
|
||||||
|
|
||||||
|
To run all test::
|
||||||
|
python run_tests.py
|
||||||
|
|
||||||
|
To run a single test::
|
||||||
|
python run_tests.py test_stores:TestSwiftBackend.test_get
|
||||||
|
|
||||||
|
To run a single test module::
|
||||||
|
python run_tests.py test_stores
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
|
||||||
|
gettext.install('windc', unicode=1)
|
||||||
|
|
||||||
|
from nose import config
|
||||||
|
from nose import result
|
||||||
|
from nose import core
|
||||||
|
|
||||||
|
|
||||||
|
class _AnsiColorizer(object):
|
||||||
|
"""
|
||||||
|
A colorizer is an object that loosely wraps around a stream, allowing
|
||||||
|
callers to write text to the stream in a particular color.
|
||||||
|
|
||||||
|
Colorizer classes must implement C{supported()} and C{write(text, color)}.
|
||||||
|
"""
|
||||||
|
_colors = dict(black=30, red=31, green=32, yellow=33,
|
||||||
|
blue=34, magenta=35, cyan=36, white=37)
|
||||||
|
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def supported(cls, stream=sys.stdout):
|
||||||
|
"""
|
||||||
|
A class method that returns True if the current platform supports
|
||||||
|
coloring terminal output using this method. Returns False otherwise.
|
||||||
|
"""
|
||||||
|
if not stream.isatty():
|
||||||
|
return False # auto color only on TTYs
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
return curses.tigetnum("colors") > 2
|
||||||
|
except curses.error:
|
||||||
|
curses.setupterm()
|
||||||
|
return curses.tigetnum("colors") > 2
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
# guess false in case of error
|
||||||
|
return False
|
||||||
|
supported = classmethod(supported)
|
||||||
|
|
||||||
|
def write(self, text, color):
|
||||||
|
"""
|
||||||
|
Write the given text to the stream in the given color.
|
||||||
|
|
||||||
|
@param text: Text to be written to the stream.
|
||||||
|
|
||||||
|
@param color: A string label for a color. e.g. 'red', 'white'.
|
||||||
|
"""
|
||||||
|
color = self._colors[color]
|
||||||
|
self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
|
||||||
|
|
||||||
|
|
||||||
|
class _Win32Colorizer(object):
|
||||||
|
"""
|
||||||
|
See _AnsiColorizer docstring.
|
||||||
|
"""
|
||||||
|
def __init__(self, stream):
|
||||||
|
from win32console import GetStdHandle, STD_OUT_HANDLE, \
|
||||||
|
FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \
|
||||||
|
FOREGROUND_INTENSITY
|
||||||
|
red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
|
||||||
|
FOREGROUND_BLUE, FOREGROUND_INTENSITY)
|
||||||
|
self.stream = stream
|
||||||
|
self.screenBuffer = GetStdHandle(STD_OUT_HANDLE)
|
||||||
|
self._colors = {
|
||||||
|
'normal': red | green | blue,
|
||||||
|
'red': red | bold,
|
||||||
|
'green': green | bold,
|
||||||
|
'blue': blue | bold,
|
||||||
|
'yellow': red | green | bold,
|
||||||
|
'magenta': red | blue | bold,
|
||||||
|
'cyan': green | blue | bold,
|
||||||
|
'white': red | green | blue | bold}
|
||||||
|
|
||||||
|
def supported(cls, stream=sys.stdout):
|
||||||
|
try:
|
||||||
|
import win32console
|
||||||
|
screenBuffer = win32console.GetStdHandle(
|
||||||
|
win32console.STD_OUT_HANDLE)
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
import pywintypes
|
||||||
|
try:
|
||||||
|
screenBuffer.SetConsoleTextAttribute(
|
||||||
|
win32console.FOREGROUND_RED |
|
||||||
|
win32console.FOREGROUND_GREEN |
|
||||||
|
win32console.FOREGROUND_BLUE)
|
||||||
|
except pywintypes.error:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
supported = classmethod(supported)
|
||||||
|
|
||||||
|
def write(self, text, color):
|
||||||
|
color = self._colors[color]
|
||||||
|
self.screenBuffer.SetConsoleTextAttribute(color)
|
||||||
|
self.stream.write(text)
|
||||||
|
self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
|
||||||
|
|
||||||
|
|
||||||
|
class _NullColorizer(object):
|
||||||
|
"""
|
||||||
|
See _AnsiColorizer docstring.
|
||||||
|
"""
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
|
def supported(cls, stream=sys.stdout):
|
||||||
|
return True
|
||||||
|
supported = classmethod(supported)
|
||||||
|
|
||||||
|
def write(self, text, color):
|
||||||
|
self.stream.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
class WindcTestResult(result.TextTestResult):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
result.TextTestResult.__init__(self, *args, **kw)
|
||||||
|
self._last_case = None
|
||||||
|
self.colorizer = None
|
||||||
|
# NOTE(vish, tfukushima): reset stdout for the terminal check
|
||||||
|
stdout = sys.stdout
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
|
||||||
|
if colorizer.supported():
|
||||||
|
self.colorizer = colorizer(self.stream)
|
||||||
|
break
|
||||||
|
sys.stdout = stdout
|
||||||
|
|
||||||
|
def getDescription(self, test):
|
||||||
|
return str(test)
|
||||||
|
|
||||||
|
# NOTE(vish, tfukushima): copied from unittest with edit to add color
|
||||||
|
def addSuccess(self, test):
|
||||||
|
unittest.TestResult.addSuccess(self, test)
|
||||||
|
if self.showAll:
|
||||||
|
self.colorizer.write("OK", 'green')
|
||||||
|
self.stream.writeln()
|
||||||
|
elif self.dots:
|
||||||
|
self.stream.write('.')
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
# NOTE(vish, tfukushima): copied from unittest with edit to add color
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
unittest.TestResult.addFailure(self, test, err)
|
||||||
|
if self.showAll:
|
||||||
|
self.colorizer.write("FAIL", 'red')
|
||||||
|
self.stream.writeln()
|
||||||
|
elif self.dots:
|
||||||
|
self.stream.write('F')
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
# NOTE(vish, tfukushima): copied from unittest with edit to add color
|
||||||
|
def addError(self, test, err):
|
||||||
|
"""
|
||||||
|
Overrides normal addError to add support for errorClasses.
|
||||||
|
If the exception is a registered class, the error will be added
|
||||||
|
to the list for that class, not errors.
|
||||||
|
"""
|
||||||
|
stream = getattr(self, 'stream', None)
|
||||||
|
ec, ev, tb = err
|
||||||
|
try:
|
||||||
|
exc_info = self._exc_info_to_string(err, test)
|
||||||
|
except TypeError:
|
||||||
|
# This is for compatibility with Python 2.3.
|
||||||
|
exc_info = self._exc_info_to_string(err)
|
||||||
|
for cls, (storage, label, isfail) in self.errorClasses.items():
|
||||||
|
if result.isclass(ec) and issubclass(ec, cls):
|
||||||
|
if isfail:
|
||||||
|
test.passwd = False
|
||||||
|
storage.append((test, exc_info))
|
||||||
|
# Might get patched into a streamless result
|
||||||
|
if stream is not None:
|
||||||
|
if self.showAll:
|
||||||
|
message = [label]
|
||||||
|
detail = result._exception_detail(err[1])
|
||||||
|
if detail:
|
||||||
|
message.append(detail)
|
||||||
|
stream.writeln(": ".join(message))
|
||||||
|
elif self.dots:
|
||||||
|
stream.write(label[:1])
|
||||||
|
return
|
||||||
|
self.errors.append((test, exc_info))
|
||||||
|
test.passed = False
|
||||||
|
if stream is not None:
|
||||||
|
if self.showAll:
|
||||||
|
self.colorizer.write("ERROR", 'red')
|
||||||
|
self.stream.writeln()
|
||||||
|
elif self.dots:
|
||||||
|
stream.write('E')
|
||||||
|
|
||||||
|
def startTest(self, test):
|
||||||
|
unittest.TestResult.startTest(self, test)
|
||||||
|
current_case = test.test.__class__.__name__
|
||||||
|
|
||||||
|
if self.showAll:
|
||||||
|
if current_case != self._last_case:
|
||||||
|
self.stream.writeln(current_case)
|
||||||
|
self._last_case = current_case
|
||||||
|
|
||||||
|
self.stream.write(
|
||||||
|
' %s' % str(test.test._testMethodName).ljust(60))
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class WindcTestRunner(core.TextTestRunner):
|
||||||
|
def _makeResult(self):
|
||||||
|
return WindcTestResult(self.stream,
|
||||||
|
self.descriptions,
|
||||||
|
self.verbosity,
|
||||||
|
self.config)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger = logging.getLogger()
|
||||||
|
hdlr = logging.StreamHandler()
|
||||||
|
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
||||||
|
hdlr.setFormatter(formatter)
|
||||||
|
logger.addHandler(hdlr)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
c = config.Config(stream=sys.stdout,
|
||||||
|
env=os.environ,
|
||||||
|
verbosity=3,
|
||||||
|
plugins=core.DefaultPluginManager())
|
||||||
|
|
||||||
|
runner = WindcTestRunner(stream=c.stream,
|
||||||
|
verbosity=c.verbosity,
|
||||||
|
config=c)
|
||||||
|
sys.exit(not core.run(config=c, testRunner=runner))
|
129
windc/run_tests.sh
Executable file
129
windc/run_tests.sh
Executable file
@ -0,0 +1,129 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function usage {
|
||||||
|
echo "Usage: $0 [OPTION]..."
|
||||||
|
echo "Run Loadbalancer's test suite(s)"
|
||||||
|
echo ""
|
||||||
|
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
|
||||||
|
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
|
||||||
|
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
|
||||||
|
echo " --unittests-only Run unit tests only, exclude functional tests."
|
||||||
|
echo " -c, --coverage Generate coverage report"
|
||||||
|
echo " -p, --pep8 Just run pep8"
|
||||||
|
echo " -h, --help Print this usage message"
|
||||||
|
echo ""
|
||||||
|
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
|
||||||
|
echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
|
||||||
|
echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
function process_option {
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) usage;;
|
||||||
|
-V|--virtual-env) let always_venv=1; let never_venv=0;;
|
||||||
|
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
|
||||||
|
-p|--pep8) let just_pep8=1;;
|
||||||
|
-f|--force) let force=1;;
|
||||||
|
--unittests-only) noseopts="$noseopts --exclude-dir=windc/tests/functional";;
|
||||||
|
-c|--coverage) coverage=1;;
|
||||||
|
-*) noseopts="$noseopts $1";;
|
||||||
|
*) noseargs="$noseargs $1"
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
venv=.venv
|
||||||
|
with_venv=tools/with_venv.sh
|
||||||
|
always_venv=0
|
||||||
|
never_venv=0
|
||||||
|
force=0
|
||||||
|
noseargs=
|
||||||
|
noseopts=
|
||||||
|
wrapper=""
|
||||||
|
just_pep8=0
|
||||||
|
coverage=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
process_option $arg
|
||||||
|
done
|
||||||
|
|
||||||
|
# If enabled, tell nose to collect coverage data
|
||||||
|
if [ $coverage -eq 1 ]; then
|
||||||
|
noseopts="$noseopts --with-coverage --cover-package=windc --cover-inclusive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
function run_tests {
|
||||||
|
# Just run the test suites in current environment
|
||||||
|
${wrapper} $NOSETESTS 2> run_tests.log
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_pep8 {
|
||||||
|
echo "Running pep8 ..."
|
||||||
|
PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat"
|
||||||
|
PEP8_INCLUDE="bin/* windc tools setup.py run_tests.py"
|
||||||
|
${wrapper} pep8 $PEP8_OPTIONS $PEP8_INCLUDE
|
||||||
|
PEP_RESULT=$?
|
||||||
|
case "$TERM" in
|
||||||
|
*color* ) function out { printf "\033[3%d;1m%s\033[m\n" "$1" "$2"; } ;;
|
||||||
|
* ) function out { printf "%s\n" "$2"; } ;;
|
||||||
|
esac
|
||||||
|
if [ $PEP_RESULT -eq 0 ]; then
|
||||||
|
out 2 "PEP8 OK"
|
||||||
|
else
|
||||||
|
out 1 "PEP8 FAIL"
|
||||||
|
fi
|
||||||
|
return $PEP_RESULT
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
NOSETESTS="python run_tests.py $noseopts $noseargs"
|
||||||
|
|
||||||
|
if [ $never_venv -eq 0 ]
|
||||||
|
then
|
||||||
|
# Remove the virtual environment if --force used
|
||||||
|
if [ $force -eq 1 ]; then
|
||||||
|
echo "Cleaning virtualenv..."
|
||||||
|
rm -rf ${venv}
|
||||||
|
fi
|
||||||
|
if [ -e ${venv} ]; then
|
||||||
|
wrapper="${with_venv}"
|
||||||
|
else
|
||||||
|
if [ $always_venv -eq 1 ]; then
|
||||||
|
# Automatically install the virtualenv
|
||||||
|
python tools/install_venv.py || exit 1
|
||||||
|
wrapper="${with_venv}"
|
||||||
|
else
|
||||||
|
echo -e "No virtual environment found...create one? (Y/n) \c"
|
||||||
|
read use_ve
|
||||||
|
if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
|
||||||
|
# Install the virtualenv and run the test suite in it
|
||||||
|
python tools/install_venv.py || exit 1
|
||||||
|
wrapper=${with_venv}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete old coverage data from previous runs
|
||||||
|
if [ $coverage -eq 1 ]; then
|
||||||
|
${wrapper} coverage erase
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $just_pep8 -eq 1 ]; then
|
||||||
|
run_pep8
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_tests
|
||||||
|
TEST_RESULT=$?
|
||||||
|
|
||||||
|
if [ -z "$noseargs" ]; then
|
||||||
|
run_pep8 || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $coverage -eq 1 ]; then
|
||||||
|
echo "Generating coverage report in covhtml/"
|
||||||
|
${wrapper} coverage html -d covhtml -i --include='windc/*' --omit='windc/db/migrate_repo*,windc/common*,windc/tests*'
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $TEST_RESULT
|
9
windc/setup.cfg
Normal file
9
windc/setup.cfg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[build_sphinx]
|
||||||
|
all_files = 1
|
||||||
|
build-dir = doc/build
|
||||||
|
source-dir = doc/source
|
||||||
|
|
||||||
|
[egg_info]
|
||||||
|
tag_build =
|
||||||
|
tag_date = 0
|
||||||
|
tag_svn_revision = 0
|
90
windc/setup.py
Normal file
90
windc/setup.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
from setuptools.command.sdist import sdist
|
||||||
|
|
||||||
|
from windc import version
|
||||||
|
|
||||||
|
|
||||||
|
if os.path.isdir('.bzr'):
|
||||||
|
with open("windc/vcsversion.py", 'w') as version_file:
|
||||||
|
vcs_cmd = subprocess.Popen(["bzr", "version-info", "--python"],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
vcsversion = vcs_cmd.communicate()[0]
|
||||||
|
version_file.write(vcsversion)
|
||||||
|
|
||||||
|
|
||||||
|
class local_sdist(sdist):
|
||||||
|
"""Customized sdist hook - builds the ChangeLog file from VC first"""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if os.path.isdir('.bzr'):
|
||||||
|
# We're in a bzr branch
|
||||||
|
|
||||||
|
log_cmd = subprocess.Popen(["bzr", "log", "--gnu"],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
changelog = log_cmd.communicate()[0]
|
||||||
|
with open("ChangeLog", "w") as changelog_file:
|
||||||
|
changelog_file.write(changelog)
|
||||||
|
sdist.run(self)
|
||||||
|
|
||||||
|
cmdclass = {'sdist': local_sdist}
|
||||||
|
|
||||||
|
# If Sphinx is installed on the box running setup.py,
|
||||||
|
# enable setup.py to build the documentation, otherwise,
|
||||||
|
# just ignore it
|
||||||
|
try:
|
||||||
|
from sphinx.setup_command import BuildDoc
|
||||||
|
|
||||||
|
class local_BuildDoc(BuildDoc):
|
||||||
|
def run(self):
|
||||||
|
for builder in ['html', 'man']:
|
||||||
|
self.builder = builder
|
||||||
|
self.finalize_options()
|
||||||
|
BuildDoc.run(self)
|
||||||
|
cmdclass['build_sphinx'] = local_BuildDoc
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='windc',
|
||||||
|
version=version.canonical_version_string(),
|
||||||
|
description='The WinDC project provides a simple WSGI server for Windows Environment Management',
|
||||||
|
license='Apache License (2.0)',
|
||||||
|
author='OpenStack',
|
||||||
|
author_email='openstack@lists.launchpad.net',
|
||||||
|
url='http://windc.openstack.org/',
|
||||||
|
packages=find_packages(exclude=['tests', 'bin']),
|
||||||
|
test_suite='nose.collector',
|
||||||
|
cmdclass=cmdclass,
|
||||||
|
include_package_data=True,
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 4 - Beta',
|
||||||
|
'License :: OSI Approved :: Apache Software License',
|
||||||
|
'Operating System :: POSIX :: Linux',
|
||||||
|
'Programming Language :: Python :: 2.6',
|
||||||
|
'Environment :: No Input/Output (Daemon)',
|
||||||
|
],
|
||||||
|
scripts=['bin/windc',
|
||||||
|
'bin/windc-api'])
|
0
windc/tests/__init__.py
Normal file
0
windc/tests/__init__.py
Normal file
0
windc/tests/functional/__init__.py
Normal file
0
windc/tests/functional/__init__.py
Normal file
4
windc/tests/manual/createDataCenter.sh
Executable file
4
windc/tests/manual/createDataCenter.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
URL=http://localhost:8082/foo/datacenters
|
||||||
|
curl -v -H "Content-Type: application/json" -X POST -d@createDataCenterParameters$1 $URL
|
7
windc/tests/manual/createDataCenterParameters
Normal file
7
windc/tests/manual/createDataCenterParameters
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Test Data Center 2",
|
||||||
|
"type": "SingleZone",
|
||||||
|
"version":"1.1",
|
||||||
|
"KMS":"172.16.1.2",
|
||||||
|
"WSUS":"172.16.1.3"
|
||||||
|
}
|
4
windc/tests/manual/createService.sh
Executable file
4
windc/tests/manual/createService.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
URL=http://localhost:8082/foo/datacenters/$1/services
|
||||||
|
curl -v -H "Content-Type: application/json" -X POST -d@createServiceParameters$2 $URL
|
8
windc/tests/manual/createServiceParameters
Normal file
8
windc/tests/manual/createServiceParameters
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "active_directory_service",
|
||||||
|
"zones": ["zone1"],
|
||||||
|
"domain": "ACME.cloud",
|
||||||
|
"AdminUser": "Admin",
|
||||||
|
"AdminPassword": "StrongPassword",
|
||||||
|
"DomainControllerNames": ["APP-AD001","APP-AD002"]
|
||||||
|
}
|
1
windc/tests/manual/listDataCenter.sh
Executable file
1
windc/tests/manual/listDataCenter.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
curl -X GET http://localhost:8082/foo/datacenters
|
0
windc/tests/unit/__init__.py
Normal file
0
windc/tests/unit/__init__.py
Normal file
14
windc/tests/unit/test_base_driver.py
Normal file
14
windc/tests/unit/test_base_driver.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import unittest
|
||||||
|
import mock
|
||||||
|
|
||||||
|
|
||||||
|
from windc.api.v1.router import API
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseDriver(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestBaseDriver, self).setUp()
|
||||||
|
self.conf = mock.Mock()
|
||||||
|
|
||||||
|
def testAPI(self):
|
||||||
|
api = API(None)
|
154
windc/tools/install_venv.py
Normal file
154
windc/tools/install_venv.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Copyright 2010 OpenStack LLC.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installation script for Glance's development virtualenv
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||||
|
VENV = os.path.join(ROOT, '.venv')
|
||||||
|
PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
|
||||||
|
TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires')
|
||||||
|
|
||||||
|
|
||||||
|
def die(message, *args):
|
||||||
|
print >> sys.stderr, message % args
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, redirect_output=True, check_exit_code=True):
|
||||||
|
"""
|
||||||
|
Runs a command in an out-of-process shell, returning the
|
||||||
|
output of that command. Working directory is ROOT.
|
||||||
|
"""
|
||||||
|
if redirect_output:
|
||||||
|
stdout = subprocess.PIPE
|
||||||
|
else:
|
||||||
|
stdout = None
|
||||||
|
|
||||||
|
proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
|
||||||
|
output = proc.communicate()[0]
|
||||||
|
if check_exit_code and proc.returncode != 0:
|
||||||
|
die('Command "%s" failed.\n%s', ' '.join(cmd), output)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'],
|
||||||
|
check_exit_code=False).strip())
|
||||||
|
HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'],
|
||||||
|
check_exit_code=False).strip())
|
||||||
|
|
||||||
|
|
||||||
|
def check_dependencies():
|
||||||
|
"""Make sure virtualenv is in the path."""
|
||||||
|
|
||||||
|
if not HAS_VIRTUALENV:
|
||||||
|
print 'not found.'
|
||||||
|
# Try installing it via easy_install...
|
||||||
|
if HAS_EASY_INSTALL:
|
||||||
|
print 'Installing virtualenv via easy_install...',
|
||||||
|
if not run_command(['which', 'easy_install']):
|
||||||
|
die('ERROR: virtualenv not found.\n\n'
|
||||||
|
'Balancer development requires virtualenv, please install'
|
||||||
|
' it using your favorite package management tool')
|
||||||
|
print 'done.'
|
||||||
|
print 'done.'
|
||||||
|
|
||||||
|
|
||||||
|
def create_virtualenv(venv=VENV):
|
||||||
|
"""
|
||||||
|
Creates the virtual environment and installs PIP only into the
|
||||||
|
virtual environment
|
||||||
|
"""
|
||||||
|
print 'Creating venv...',
|
||||||
|
run_command(['virtualenv', '-q', '--no-site-packages', VENV])
|
||||||
|
print 'done.'
|
||||||
|
print 'Installing pip in virtualenv...',
|
||||||
|
if not run_command(['tools/with_venv.sh', 'easy_install',
|
||||||
|
'pip>1.0']).strip():
|
||||||
|
die("Failed to install pip.")
|
||||||
|
print 'done.'
|
||||||
|
|
||||||
|
|
||||||
|
def pip_install(*args):
|
||||||
|
run_command(['tools/with_venv.sh',
|
||||||
|
'pip', 'install', '--upgrade'] + list(args),
|
||||||
|
redirect_output=False)
|
||||||
|
|
||||||
|
|
||||||
|
def install_dependencies(venv=VENV):
|
||||||
|
print 'Installing dependencies with pip (this can take a while)...'
|
||||||
|
|
||||||
|
pip_install('pip')
|
||||||
|
|
||||||
|
pip_install('-r', PIP_REQUIRES)
|
||||||
|
pip_install('-r', TEST_REQUIRES)
|
||||||
|
|
||||||
|
# Tell the virtual env how to "import glance"
|
||||||
|
py_ver = _detect_python_version(venv)
|
||||||
|
pthfile = os.path.join(venv, "lib", py_ver,
|
||||||
|
"site-packages", "balancer.pth")
|
||||||
|
f = open(pthfile, 'w')
|
||||||
|
f.write("%s\n" % ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_python_version(venv):
|
||||||
|
lib_dir = os.path.join(venv, "lib")
|
||||||
|
for pathname in os.listdir(lib_dir):
|
||||||
|
if pathname.startswith('python'):
|
||||||
|
return pathname
|
||||||
|
raise Exception('Unable to detect Python version')
|
||||||
|
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
help = """
|
||||||
|
Glance development environment setup is complete.
|
||||||
|
|
||||||
|
Glance development uses virtualenv to track and manage Python dependencies
|
||||||
|
while in development and testing.
|
||||||
|
|
||||||
|
To activate the Glance virtualenv for the extent of your current shell session
|
||||||
|
you can run:
|
||||||
|
|
||||||
|
$ source .venv/bin/activate
|
||||||
|
|
||||||
|
Or, if you prefer, you can run commands in the virtualenv on a case by case
|
||||||
|
basis by running:
|
||||||
|
|
||||||
|
$ tools/with_venv.sh <your command>
|
||||||
|
|
||||||
|
Also, make test will automatically use the virtualenv.
|
||||||
|
"""
|
||||||
|
print help
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
check_dependencies()
|
||||||
|
create_virtualenv()
|
||||||
|
install_dependencies()
|
||||||
|
print_help()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(sys.argv)
|
22
windc/tools/pip-requires
Normal file
22
windc/tools/pip-requires
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# The greenlet package must be compiled with gcc and needs
|
||||||
|
# the Python.h headers. Make sure you install the python-dev
|
||||||
|
# package to get the right headers...
|
||||||
|
greenlet>=0.3.1
|
||||||
|
|
||||||
|
SQLAlchemy>=0.7
|
||||||
|
anyjson
|
||||||
|
eventlet>=0.9.12
|
||||||
|
PasteDeploy
|
||||||
|
Routes
|
||||||
|
webob==1.0.8
|
||||||
|
wsgiref
|
||||||
|
argparse
|
||||||
|
sqlalchemy-migrate>=0.7.2
|
||||||
|
httplib2
|
||||||
|
kombu
|
||||||
|
iso8601>=0.1.4
|
||||||
|
|
||||||
|
# For paste.util.template used in keystone.common.template
|
||||||
|
Paste
|
||||||
|
|
||||||
|
passlib
|
16
windc/tools/test-requires
Normal file
16
windc/tools/test-requires
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Packages needed for dev testing
|
||||||
|
# For translations processing
|
||||||
|
Babel
|
||||||
|
|
||||||
|
# Needed for testing
|
||||||
|
unittest2
|
||||||
|
mock==0.8.0
|
||||||
|
nose
|
||||||
|
nose-exclude
|
||||||
|
nosexcover
|
||||||
|
#openstack.nose_plugin
|
||||||
|
pep8==1.0.1
|
||||||
|
sphinx>=1.1.2
|
||||||
|
paramiko
|
||||||
|
ipaddr
|
||||||
|
pysqlite
|
4
windc/tools/with_venv.sh
Executable file
4
windc/tools/with_venv.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
TOOLS=`dirname $0`
|
||||||
|
VENV=$TOOLS/../.venv
|
||||||
|
source $VENV/bin/activate && $@
|
0
windc/windc/__init__.py
Normal file
0
windc/windc/__init__.py
Normal file
0
windc/windc/api/__init__.py
Normal file
0
windc/windc/api/__init__.py
Normal file
0
windc/windc/api/middleware/__init__.py
Normal file
0
windc/windc/api/middleware/__init__.py
Normal file
0
windc/windc/api/v1/__init__.py
Normal file
0
windc/windc/api/v1/__init__.py
Normal file
83
windc/windc/api/v1/datacenters.py
Normal file
83
windc/windc/api/v1/datacenters.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from openstack.common import wsgi
|
||||||
|
|
||||||
|
from windc import utils
|
||||||
|
from windc.core import api as core_api
|
||||||
|
from windc.db import api as db_api
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(object):
|
||||||
|
def __init__(self, conf):
|
||||||
|
LOG.debug("Creating data centers controller with config:"
|
||||||
|
"datacenters.py %s", conf)
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def findLBforVM(self, req, tenant_id, vm_id):
|
||||||
|
LOG.debug("Got index request. Request: %s", req)
|
||||||
|
result = core_api.lb_find_for_vm(self.conf, tenant_id, vm_id)
|
||||||
|
return {'loadbalancers': result}
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def index(self, req, tenant_id):
|
||||||
|
LOG.debug("Got index request. Request: %s", req)
|
||||||
|
result = core_api.dc_get_index(self.conf, tenant_id)
|
||||||
|
LOG.debug("Got list of datacenters: %s", result)
|
||||||
|
result
|
||||||
|
return {'datacenters': result}
|
||||||
|
|
||||||
|
@utils.http_success_code(202)
|
||||||
|
@utils.verify_tenant
|
||||||
|
def create(self, req, tenant_id, body):
|
||||||
|
LOG.debug("Got create request. Request: %s", req)
|
||||||
|
#here we need to decide which device should be used
|
||||||
|
params = body.copy()
|
||||||
|
LOG.debug("Headers: %s", req.headers)
|
||||||
|
# We need to create DataCenter object and return its id
|
||||||
|
params['tenant_id'] = tenant_id
|
||||||
|
dc_id = core_api.create_dc(self.conf, params)
|
||||||
|
return {'datacenter': {'id': dc_id}}
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def delete(self, req, tenant_id, datacenter_id):
|
||||||
|
LOG.debug("Got delete request. Request: %s", req)
|
||||||
|
core_api.delete_dc(self.conf, tenant_id, datacenter_id)
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def show(self, req, tenant_id, datacenter_id):
|
||||||
|
LOG.debug("Got datacenter info request. Request: %s", req)
|
||||||
|
result = core_api.dc_get_data(self.conf, tenant_id, datacenter_id)
|
||||||
|
return {'datacenter': result}
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def update(self, req, tenant_id, datacenter_id, body):
|
||||||
|
LOG.debug("Got update request. Request: %s", req)
|
||||||
|
core_api.update_dc(self.conf, tenant_id, datacenter_id, body)
|
||||||
|
return {'datacenter': {'id': dc_id}}
|
||||||
|
|
||||||
|
|
||||||
|
def create_resource(conf):
|
||||||
|
"""Datacenters resource factory method"""
|
||||||
|
deserializer = wsgi.JSONRequestDeserializer()
|
||||||
|
serializer = wsgi.JSONResponseSerializer()
|
||||||
|
return wsgi.Resource(Controller(conf), deserializer, serializer)
|
42
windc/windc/api/v1/filters.py
Normal file
42
windc/windc/api/v1/filters.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2012, Piston Cloud Computing, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
def validate(filter, value):
|
||||||
|
return FILTER_FUNCTIONS.get(filter, lambda v: True)(value)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_int_in_range(min=0, max=None):
|
||||||
|
def _validator(v):
|
||||||
|
try:
|
||||||
|
if max is None:
|
||||||
|
return min <= int(v)
|
||||||
|
return min <= int(v) <= max
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return _validator
|
||||||
|
|
||||||
|
|
||||||
|
def validate_boolean(v):
|
||||||
|
return v.lower() in ('none', 'true', 'false', '1', '0')
|
||||||
|
|
||||||
|
|
||||||
|
FILTER_FUNCTIONS = {'size_max': validate_int_in_range(), # build validator
|
||||||
|
'size_min': validate_int_in_range(), # build validator
|
||||||
|
'min_ram': validate_int_in_range(), # build validator
|
||||||
|
'protected': validate_boolean,
|
||||||
|
'is_public': validate_boolean, }
|
56
windc/windc/api/v1/router.py
Normal file
56
windc/windc/api/v1/router.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import routes
|
||||||
|
|
||||||
|
from windc.api.v1 import datacenters
|
||||||
|
from windc.api.v1 import services
|
||||||
|
|
||||||
|
#from . import tasks
|
||||||
|
|
||||||
|
|
||||||
|
from openstack.common import wsgi
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class API(wsgi.Router):
|
||||||
|
|
||||||
|
"""WSGI router for balancer v1 API requests."""
|
||||||
|
|
||||||
|
def __init__(self, conf, **local_conf):
|
||||||
|
self.conf = conf
|
||||||
|
mapper = routes.Mapper()
|
||||||
|
tenant_mapper = mapper.submapper(path_prefix="/{tenant_id}")
|
||||||
|
datacenter_resource = datacenters.create_resource(self.conf)
|
||||||
|
datacenter_collection = tenant_mapper.collection(
|
||||||
|
"datacenters", "datacenter",
|
||||||
|
controller=datacenter_resource, member_prefix="/{datacenter_id}",
|
||||||
|
formatted=False)
|
||||||
|
service_resource = services.create_resource(self.conf)
|
||||||
|
service_collection = datacenter_collection.member.collection('services', 'service',
|
||||||
|
controller=service_resource, member_prefix="/{service_id}",
|
||||||
|
formatted=False)
|
||||||
|
service_collection.member.connect("/{status}", action="changeServiceStatus",
|
||||||
|
conditions={'method': ["PUT"]})
|
||||||
|
mapper.connect("/servicetypes",
|
||||||
|
controller=datacenter_resource,
|
||||||
|
action="show_servicetypes",
|
||||||
|
conditions={'method': ["GET"]})
|
||||||
|
super(API, self).__init__(mapper)
|
87
windc/windc/api/v1/services.py
Normal file
87
windc/windc/api/v1/services.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from openstack.common import wsgi
|
||||||
|
|
||||||
|
from windc import utils
|
||||||
|
from windc.core import api as core_api
|
||||||
|
from windc.db import api as db_api
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(object):
|
||||||
|
def __init__(self, conf):
|
||||||
|
LOG.debug("Creating services controller with config:"
|
||||||
|
"services.py %s", conf)
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def findLBforVM(self, req, tenant_id, vm_id):
|
||||||
|
LOG.debug("Got index request. Request: %s", req)
|
||||||
|
result = core_api.lb_find_for_vm(self.conf, tenant_id, vm_id)
|
||||||
|
return {'loadbalancers': result}
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def index(self, req, tenant_id, datacenter_id):
|
||||||
|
LOG.debug("Got index request. Request: %s", req)
|
||||||
|
result = core_api.service_get_index(self.conf, tenant_id, datacenter_id)
|
||||||
|
return {'services': result}
|
||||||
|
|
||||||
|
@utils.http_success_code(202)
|
||||||
|
@utils.verify_tenant
|
||||||
|
def create(self, req, tenant_id, datacenter_id, body):
|
||||||
|
LOG.debug("Got create request. Request: %s", req)
|
||||||
|
#here we need to decide which device should be used
|
||||||
|
params = body.copy()
|
||||||
|
LOG.debug("Headers: %s", req.headers)
|
||||||
|
# We need to create Service object and return its id
|
||||||
|
params['tenant_id'] = tenant_id
|
||||||
|
params['datacenter_id'] = datacenter_id
|
||||||
|
service_id = core_api.create_service(self.conf, params)
|
||||||
|
return {'service': {'id': service_id}}
|
||||||
|
|
||||||
|
@utils.http_success_code(204)
|
||||||
|
@utils.verify_tenant
|
||||||
|
def delete(self, req, tenant_id, datacenter_id, service_id):
|
||||||
|
LOG.debug("Got delete request. Request: %s", req)
|
||||||
|
core_api.delete_service(self.conf, tenant_id, datacenter_id, service_id)
|
||||||
|
|
||||||
|
@utils.verify_tenant
|
||||||
|
def show(self, req, tenant_id, datacenter_id, service_id):
|
||||||
|
LOG.debug("Got loadbalancerr info request. Request: %s", req)
|
||||||
|
result = core_api.service_get_data(self.conf, tenant_id, datacenter_id, service_id)
|
||||||
|
return {'service': result}
|
||||||
|
|
||||||
|
@utils.http_success_code(202)
|
||||||
|
@utils.verify_tenant
|
||||||
|
def update(self, req, tenant_id, datacenter_id, service_id, body):
|
||||||
|
LOG.debug("Got update request. Request: %s", req)
|
||||||
|
core_api.update_service(self.conf, tenant_id, datacenter_id, service_id, body)
|
||||||
|
return {'service': {'id': service_id}}
|
||||||
|
|
||||||
|
|
||||||
|
def create_resource(conf):
|
||||||
|
"""Services resource factory method"""
|
||||||
|
deserializer = wsgi.JSONRequestDeserializer()
|
||||||
|
serializer = wsgi.JSONResponseSerializer()
|
||||||
|
return wsgi.Resource(Controller(conf), deserializer, serializer)
|
||||||
|
|
||||||
|
|
||||||
|
|
66
windc/windc/api/versions.py
Normal file
66
windc/windc/api/versions.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Controller that returns information on the Glance API versions
|
||||||
|
"""
|
||||||
|
|
||||||
|
import httplib
|
||||||
|
import json
|
||||||
|
|
||||||
|
import webob.dec
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A controller that produces information on the Glance API versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
"""Respond to a request for all OpenStack API versions."""
|
||||||
|
version_objs = [
|
||||||
|
{
|
||||||
|
"id": "v1.0",
|
||||||
|
"status": "CURRENT",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"href": self.get_href(req)}]},
|
||||||
|
{
|
||||||
|
"id": "v1.1",
|
||||||
|
"status": "SUPPORTED",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"href": self.get_href(req)}]}]
|
||||||
|
|
||||||
|
body = json.dumps(dict(versions=version_objs))
|
||||||
|
|
||||||
|
response = webob.Response(request=req,
|
||||||
|
status=httplib.MULTIPLE_CHOICES,
|
||||||
|
content_type='application/json')
|
||||||
|
response.body = body
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_href(self, req):
|
||||||
|
return "%s/v1/" % req.host_url
|
16
windc/windc/common/__init__.py
Normal file
16
windc/windc/common/__init__.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010-2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
1135
windc/windc/common/cfg.py
Normal file
1135
windc/windc/common/cfg.py
Normal file
File diff suppressed because it is too large
Load Diff
605
windc/windc/common/client.py
Normal file
605
windc/windc/common/client.py
Normal file
@ -0,0 +1,605 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010-2011 OpenStack, LLC
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
# HTTPSClientAuthConnection code comes courtesy of ActiveState website:
|
||||||
|
# http://code.activestate.com/recipes/
|
||||||
|
# 577548-https-httplib-client-connection-with-certificate-v/
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import errno
|
||||||
|
import functools
|
||||||
|
import httplib
|
||||||
|
import os
|
||||||
|
import select
|
||||||
|
import urllib
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
from eventlet.green import socket, ssl
|
||||||
|
except ImportError:
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sendfile
|
||||||
|
SENDFILE_SUPPORTED = True
|
||||||
|
except ImportError:
|
||||||
|
SENDFILE_SUPPORTED = False
|
||||||
|
|
||||||
|
#from glance.common import auth
|
||||||
|
#from glance.common import exception, utils
|
||||||
|
|
||||||
|
|
||||||
|
# common chunk size for get and put
|
||||||
|
CHUNKSIZE = 65536
|
||||||
|
|
||||||
|
|
||||||
|
def handle_unauthorized(func):
|
||||||
|
"""
|
||||||
|
Wrap a function to re-authenticate and retry.
|
||||||
|
"""
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
except exception.NotAuthorized:
|
||||||
|
self._authenticate(force_reauth=True)
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def handle_redirects(func):
|
||||||
|
"""
|
||||||
|
Wrap the _do_request function to handle HTTP redirects.
|
||||||
|
"""
|
||||||
|
MAX_REDIRECTS = 5
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(self, method, url, body, headers):
|
||||||
|
for _ in xrange(MAX_REDIRECTS):
|
||||||
|
try:
|
||||||
|
return func(self, method, url, body, headers)
|
||||||
|
except exception.RedirectException as redirect:
|
||||||
|
if redirect.url is None:
|
||||||
|
raise exception.InvalidRedirect()
|
||||||
|
url = redirect.url
|
||||||
|
raise exception.MaxRedirectsExceeded(redirects=MAX_REDIRECTS)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
class ImageBodyIterator(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A class that acts as an iterator over an image file's
|
||||||
|
chunks of data. This is returned as part of the result
|
||||||
|
tuple from `glance.client.Client.get_image`
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, source):
|
||||||
|
"""
|
||||||
|
Constructs the object from a readable image source
|
||||||
|
(such as an HTTPResponse or file-like object)
|
||||||
|
"""
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
Exposes an iterator over the chunks of data in the
|
||||||
|
image file.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
chunk = self.source.read(CHUNKSIZE)
|
||||||
|
if chunk:
|
||||||
|
yield chunk
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class SendFileIterator:
|
||||||
|
"""
|
||||||
|
Emulate iterator pattern over sendfile, in order to allow
|
||||||
|
send progress be followed by wrapping the iteration.
|
||||||
|
"""
|
||||||
|
def __init__(self, connection, body):
|
||||||
|
self.connection = connection
|
||||||
|
self.body = body
|
||||||
|
self.offset = 0
|
||||||
|
self.sending = True
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
class OfLength:
|
||||||
|
def __init__(self, len):
|
||||||
|
self.len = len
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.len
|
||||||
|
|
||||||
|
while self.sending:
|
||||||
|
try:
|
||||||
|
sent = sendfile.sendfile(self.connection.sock.fileno(),
|
||||||
|
self.body.fileno(),
|
||||||
|
self.offset,
|
||||||
|
CHUNKSIZE)
|
||||||
|
except OSError as e:
|
||||||
|
# suprisingly, sendfile may fail transiently instead of
|
||||||
|
# blocking, in which case we select on the socket in order
|
||||||
|
# to wait on its return to a writeable state before resuming
|
||||||
|
# the send loop
|
||||||
|
if e.errno in (errno.EAGAIN, errno.EBUSY):
|
||||||
|
wlist = [self.connection.sock.fileno()]
|
||||||
|
rfds, wfds, efds = select.select([], wlist, [])
|
||||||
|
if wfds:
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.sending = (sent != 0)
|
||||||
|
self.offset += sent
|
||||||
|
yield OfLength(sent)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPSClientAuthConnection(httplib.HTTPSConnection):
|
||||||
|
"""
|
||||||
|
Class to make a HTTPS connection, with support for
|
||||||
|
full client-based SSL Authentication
|
||||||
|
|
||||||
|
:see http://code.activestate.com/recipes/
|
||||||
|
577548-https-httplib-client-connection-with-certificate-v/
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, host, port, key_file, cert_file,
|
||||||
|
ca_file, timeout=None, insecure=False):
|
||||||
|
httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file,
|
||||||
|
cert_file=cert_file)
|
||||||
|
self.key_file = key_file
|
||||||
|
self.cert_file = cert_file
|
||||||
|
self.ca_file = ca_file
|
||||||
|
self.timeout = timeout
|
||||||
|
self.insecure = insecure
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""
|
||||||
|
Connect to a host on a given (SSL) port.
|
||||||
|
If ca_file is pointing somewhere, use it to check Server Certificate.
|
||||||
|
|
||||||
|
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
|
||||||
|
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
|
||||||
|
ssl.wrap_socket(), which forces SSL to check server certificate against
|
||||||
|
our client certificate.
|
||||||
|
"""
|
||||||
|
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||||
|
if self._tunnel_host:
|
||||||
|
self.sock = sock
|
||||||
|
self._tunnel()
|
||||||
|
# Check CA file unless 'insecure' is specificed
|
||||||
|
if self.insecure is True:
|
||||||
|
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
|
||||||
|
cert_reqs=ssl.CERT_NONE)
|
||||||
|
else:
|
||||||
|
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
|
||||||
|
ca_certs=self.ca_file,
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseClient(object):
|
||||||
|
|
||||||
|
"""A base client class"""
|
||||||
|
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
DEFAULT_DOC_ROOT = None
|
||||||
|
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
|
||||||
|
# Suse, FreeBSD/OpenBSD
|
||||||
|
DEFAULT_CA_FILE_PATH = '/etc/ssl/certs/ca-certificates.crt:'\
|
||||||
|
'/etc/pki/tls/certs/ca-bundle.crt:'\
|
||||||
|
'/etc/ssl/ca-bundle.pem:'\
|
||||||
|
'/etc/ssl/cert.pem'
|
||||||
|
|
||||||
|
OK_RESPONSE_CODES = (
|
||||||
|
httplib.OK,
|
||||||
|
httplib.CREATED,
|
||||||
|
httplib.ACCEPTED,
|
||||||
|
httplib.NO_CONTENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
REDIRECT_RESPONSE_CODES = (
|
||||||
|
httplib.MOVED_PERMANENTLY,
|
||||||
|
httplib.FOUND,
|
||||||
|
httplib.SEE_OTHER,
|
||||||
|
httplib.USE_PROXY,
|
||||||
|
httplib.TEMPORARY_REDIRECT,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, host, port=None, use_ssl=False, auth_tok=None,
|
||||||
|
creds=None, doc_root=None, key_file=None,
|
||||||
|
cert_file=None, ca_file=None, insecure=False,
|
||||||
|
configure_via_auth=True):
|
||||||
|
"""
|
||||||
|
Creates a new client to some service.
|
||||||
|
|
||||||
|
:param host: The host where service resides
|
||||||
|
:param port: The port where service resides
|
||||||
|
:param use_ssl: Should we use HTTPS?
|
||||||
|
:param auth_tok: The auth token to pass to the server
|
||||||
|
:param creds: The credentials to pass to the auth plugin
|
||||||
|
:param doc_root: Prefix for all URLs we request from host
|
||||||
|
:param key_file: Optional PEM-formatted file that contains the private
|
||||||
|
key.
|
||||||
|
If use_ssl is True, and this param is None (the
|
||||||
|
default), then an environ variable
|
||||||
|
GLANCE_CLIENT_KEY_FILE is looked for. If no such
|
||||||
|
environ variable is found, ClientConnectionError
|
||||||
|
will be raised.
|
||||||
|
:param cert_file: Optional PEM-formatted certificate chain file.
|
||||||
|
If use_ssl is True, and this param is None (the
|
||||||
|
default), then an environ variable
|
||||||
|
GLANCE_CLIENT_CERT_FILE is looked for. If no such
|
||||||
|
environ variable is found, ClientConnectionError
|
||||||
|
will be raised.
|
||||||
|
:param ca_file: Optional CA cert file to use in SSL connections
|
||||||
|
If use_ssl is True, and this param is None (the
|
||||||
|
default), then an environ variable
|
||||||
|
GLANCE_CLIENT_CA_FILE is looked for.
|
||||||
|
:param insecure: Optional. If set then the server's certificate
|
||||||
|
will not be verified.
|
||||||
|
"""
|
||||||
|
self.host = host
|
||||||
|
self.port = port or self.DEFAULT_PORT
|
||||||
|
self.use_ssl = use_ssl
|
||||||
|
self.auth_tok = auth_tok
|
||||||
|
self.creds = creds or {}
|
||||||
|
self.connection = None
|
||||||
|
self.configure_via_auth = configure_via_auth
|
||||||
|
# doc_root can be a nullstring, which is valid, and why we
|
||||||
|
# cannot simply do doc_root or self.DEFAULT_DOC_ROOT below.
|
||||||
|
self.doc_root = (doc_root if doc_root is not None
|
||||||
|
else self.DEFAULT_DOC_ROOT)
|
||||||
|
self.auth_plugin = self.make_auth_plugin(self.creds)
|
||||||
|
|
||||||
|
self.key_file = key_file
|
||||||
|
self.cert_file = cert_file
|
||||||
|
self.ca_file = ca_file
|
||||||
|
self.insecure = insecure
|
||||||
|
self.connect_kwargs = self.get_connect_kwargs()
|
||||||
|
|
||||||
|
def get_connect_kwargs(self):
|
||||||
|
connect_kwargs = {}
|
||||||
|
if self.use_ssl:
|
||||||
|
if self.key_file is None:
|
||||||
|
self.key_file = os.environ.get('BALANCER_CLIENT_KEY_FILE')
|
||||||
|
if self.cert_file is None:
|
||||||
|
self.cert_file = os.environ.get('BALANCER_CLIENT_CERT_FILE')
|
||||||
|
if self.ca_file is None:
|
||||||
|
self.ca_file = os.environ.get('BALANCER_CLIENT_CA_FILE')
|
||||||
|
|
||||||
|
# Check that key_file/cert_file are either both set or both unset
|
||||||
|
if self.cert_file is not None and self.key_file is None:
|
||||||
|
msg = _("You have selected to use SSL in connecting, "
|
||||||
|
"and you have supplied a cert, "
|
||||||
|
"however you have failed to supply either a "
|
||||||
|
"key_file parameter or set the "
|
||||||
|
"BALANCER_CLIENT_KEY_FILE environ variable")
|
||||||
|
raise exception.ClientConnectionError(msg)
|
||||||
|
|
||||||
|
if self.key_file is not None and self.cert_file is None:
|
||||||
|
msg = _("You have selected to use SSL in connecting, "
|
||||||
|
"and you have supplied a key, "
|
||||||
|
"however you have failed to supply either a "
|
||||||
|
"cert_file parameter or set the "
|
||||||
|
"BALANCER_CLIENT_CERT_FILE environ variable")
|
||||||
|
raise exception.ClientConnectionError(msg)
|
||||||
|
|
||||||
|
if (self.key_file is not None and
|
||||||
|
not os.path.exists(self.key_file)):
|
||||||
|
msg = _("The key file you specified %s does not "
|
||||||
|
"exist") % self.key_file
|
||||||
|
raise exception.ClientConnectionError(msg)
|
||||||
|
connect_kwargs['key_file'] = self.key_file
|
||||||
|
|
||||||
|
if (self.cert_file is not None and
|
||||||
|
not os.path.exists(self.cert_file)):
|
||||||
|
msg = _("The cert file you specified %s does not "
|
||||||
|
"exist") % self.cert_file
|
||||||
|
raise exception.ClientConnectionError(msg)
|
||||||
|
connect_kwargs['cert_file'] = self.cert_file
|
||||||
|
|
||||||
|
if (self.ca_file is not None and
|
||||||
|
not os.path.exists(self.ca_file)):
|
||||||
|
msg = _("The CA file you specified %s does not "
|
||||||
|
"exist") % self.ca_file
|
||||||
|
raise exception.ClientConnectionError(msg)
|
||||||
|
|
||||||
|
if self.ca_file is None:
|
||||||
|
for ca in self.DEFAULT_CA_FILE_PATH.split(":"):
|
||||||
|
if os.path.exists(ca):
|
||||||
|
self.ca_file = ca
|
||||||
|
break
|
||||||
|
|
||||||
|
connect_kwargs['ca_file'] = self.ca_file
|
||||||
|
connect_kwargs['insecure'] = self.insecure
|
||||||
|
|
||||||
|
return connect_kwargs
|
||||||
|
|
||||||
|
def set_auth_token(self, auth_tok):
|
||||||
|
"""
|
||||||
|
Updates the authentication token for this client connection.
|
||||||
|
"""
|
||||||
|
# FIXME(sirp): Nova image/glance.py currently calls this. Since this
|
||||||
|
# method isn't really doing anything useful[1], we should go ahead and
|
||||||
|
# rip it out, first in Nova, then here. Steps:
|
||||||
|
#
|
||||||
|
# 1. Change auth_tok in Glance to auth_token
|
||||||
|
# 2. Change image/glance.py in Nova to use client.auth_token
|
||||||
|
# 3. Remove this method
|
||||||
|
#
|
||||||
|
# [1] http://mail.python.org/pipermail/tutor/2003-October/025932.html
|
||||||
|
self.auth_tok = auth_tok
|
||||||
|
|
||||||
|
def configure_from_url(self, url):
|
||||||
|
"""
|
||||||
|
Setups the connection based on the given url.
|
||||||
|
|
||||||
|
The form is:
|
||||||
|
|
||||||
|
<http|https>://<host>:port/doc_root
|
||||||
|
"""
|
||||||
|
parsed = urlparse.urlparse(url)
|
||||||
|
self.use_ssl = parsed.scheme == 'https'
|
||||||
|
self.host = parsed.hostname
|
||||||
|
self.port = parsed.port or 80
|
||||||
|
self.doc_root = parsed.path
|
||||||
|
|
||||||
|
# ensure connection kwargs are re-evaluated after the service catalog
|
||||||
|
# publicURL is parsed for potential SSL usage
|
||||||
|
self.connect_kwargs = self.get_connect_kwargs()
|
||||||
|
|
||||||
|
def make_auth_plugin(self, creds):
|
||||||
|
"""
|
||||||
|
Returns an instantiated authentication plugin.
|
||||||
|
"""
|
||||||
|
strategy = creds.get('strategy', 'noauth')
|
||||||
|
plugin = auth.get_plugin_from_strategy(strategy, creds)
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
def get_connection_type(self):
|
||||||
|
"""
|
||||||
|
Returns the proper connection type
|
||||||
|
"""
|
||||||
|
if self.use_ssl:
|
||||||
|
return HTTPSClientAuthConnection
|
||||||
|
else:
|
||||||
|
return httplib.HTTPConnection
|
||||||
|
|
||||||
|
def _authenticate(self, force_reauth=False):
|
||||||
|
"""
|
||||||
|
Use the authentication plugin to authenticate and set the auth token.
|
||||||
|
|
||||||
|
:param force_reauth: For re-authentication to bypass cache.
|
||||||
|
"""
|
||||||
|
auth_plugin = self.auth_plugin
|
||||||
|
|
||||||
|
if not auth_plugin.is_authenticated or force_reauth:
|
||||||
|
auth_plugin.authenticate()
|
||||||
|
|
||||||
|
self.auth_tok = auth_plugin.auth_token
|
||||||
|
|
||||||
|
management_url = auth_plugin.management_url
|
||||||
|
if management_url and self.configure_via_auth:
|
||||||
|
self.configure_from_url(management_url)
|
||||||
|
|
||||||
|
@handle_unauthorized
|
||||||
|
def do_request(self, method, action, body=None, headers=None,
|
||||||
|
params=None):
|
||||||
|
"""
|
||||||
|
Make a request, returning an HTTP response object.
|
||||||
|
|
||||||
|
:param method: HTTP verb (GET, POST, PUT, etc.)
|
||||||
|
:param action: Requested path to append to self.doc_root
|
||||||
|
:param body: Data to send in the body of the request
|
||||||
|
:param headers: Headers to send with the request
|
||||||
|
:param params: Key/value pairs to use in query string
|
||||||
|
:returns: HTTP response object
|
||||||
|
"""
|
||||||
|
if not self.auth_tok:
|
||||||
|
self._authenticate()
|
||||||
|
|
||||||
|
url = self._construct_url(action, params)
|
||||||
|
return self._do_request(method=method, url=url, body=body,
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
def _construct_url(self, action, params=None):
|
||||||
|
"""
|
||||||
|
Create a URL object we can use to pass to _do_request().
|
||||||
|
"""
|
||||||
|
path = '/'.join([self.doc_root or '', action.lstrip('/')])
|
||||||
|
scheme = "https" if self.use_ssl else "http"
|
||||||
|
netloc = "%s:%d" % (self.host, self.port)
|
||||||
|
|
||||||
|
if isinstance(params, dict):
|
||||||
|
for (key, value) in params.items():
|
||||||
|
if value is None:
|
||||||
|
del params[key]
|
||||||
|
query = urllib.urlencode(params)
|
||||||
|
else:
|
||||||
|
query = None
|
||||||
|
|
||||||
|
return urlparse.ParseResult(scheme, netloc, path, '', query, '')
|
||||||
|
|
||||||
|
@handle_redirects
|
||||||
|
def _do_request(self, method, url, body, headers):
|
||||||
|
"""
|
||||||
|
Connects to the server and issues a request. Handles converting
|
||||||
|
any returned HTTP error status codes to OpenStack/Glance exceptions
|
||||||
|
and closing the server connection. Returns the result data, or
|
||||||
|
raises an appropriate exception.
|
||||||
|
|
||||||
|
:param method: HTTP method ("GET", "POST", "PUT", etc...)
|
||||||
|
:param url: urlparse.ParsedResult object with URL information
|
||||||
|
:param body: data to send (as string, filelike or iterable),
|
||||||
|
or None (default)
|
||||||
|
:param headers: mapping of key/value pairs to add as headers
|
||||||
|
|
||||||
|
:note
|
||||||
|
|
||||||
|
If the body param has a read attribute, and method is either
|
||||||
|
POST or PUT, this method will automatically conduct a chunked-transfer
|
||||||
|
encoding and use the body as a file object or iterable, transferring
|
||||||
|
chunks of data using the connection's send() method. This allows large
|
||||||
|
objects to be transferred efficiently without buffering the entire
|
||||||
|
body in memory.
|
||||||
|
"""
|
||||||
|
if url.query:
|
||||||
|
path = url.path + "?" + url.query
|
||||||
|
else:
|
||||||
|
path = url.path
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection_type = self.get_connection_type()
|
||||||
|
headers = headers or {}
|
||||||
|
|
||||||
|
if 'x-auth-token' not in headers and self.auth_tok:
|
||||||
|
headers['x-auth-token'] = self.auth_tok
|
||||||
|
|
||||||
|
c = connection_type(url.hostname, url.port, **self.connect_kwargs)
|
||||||
|
|
||||||
|
def _pushing(method):
|
||||||
|
return method.lower() in ('post', 'put')
|
||||||
|
|
||||||
|
def _simple(body):
|
||||||
|
return body is None or isinstance(body, basestring)
|
||||||
|
|
||||||
|
def _filelike(body):
|
||||||
|
return hasattr(body, 'read')
|
||||||
|
|
||||||
|
def _sendbody(connection, iter):
|
||||||
|
connection.endheaders()
|
||||||
|
for sent in iter:
|
||||||
|
# iterator has done the heavy lifting
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _chunkbody(connection, iter):
|
||||||
|
connection.putheader('Transfer-Encoding', 'chunked')
|
||||||
|
connection.endheaders()
|
||||||
|
for chunk in iter:
|
||||||
|
connection.send('%x\r\n%s\r\n' % (len(chunk), chunk))
|
||||||
|
connection.send('0\r\n\r\n')
|
||||||
|
|
||||||
|
# Do a simple request or a chunked request, depending
|
||||||
|
# on whether the body param is file-like or iterable and
|
||||||
|
# the method is PUT or POST
|
||||||
|
#
|
||||||
|
if not _pushing(method) or _simple(body):
|
||||||
|
# Simple request...
|
||||||
|
c.request(method, path, body, headers)
|
||||||
|
elif _filelike(body) or self._iterable(body):
|
||||||
|
c.putrequest(method, path)
|
||||||
|
|
||||||
|
for header, value in headers.items():
|
||||||
|
c.putheader(header, value)
|
||||||
|
|
||||||
|
iter = self.image_iterator(c, headers, body)
|
||||||
|
|
||||||
|
if self._sendable(body):
|
||||||
|
# send actual file without copying into userspace
|
||||||
|
_sendbody(c, iter)
|
||||||
|
else:
|
||||||
|
# otherwise iterate and chunk
|
||||||
|
_chunkbody(c, iter)
|
||||||
|
else:
|
||||||
|
raise TypeError('Unsupported image type: %s' % body.__class__)
|
||||||
|
|
||||||
|
res = c.getresponse()
|
||||||
|
status_code = self.get_status_code(res)
|
||||||
|
if status_code in self.OK_RESPONSE_CODES:
|
||||||
|
return res
|
||||||
|
elif status_code in self.REDIRECT_RESPONSE_CODES:
|
||||||
|
raise exception.RedirectException(res.getheader('Location'))
|
||||||
|
elif status_code == httplib.UNAUTHORIZED:
|
||||||
|
raise exception.NotAuthorized(res.read())
|
||||||
|
elif status_code == httplib.FORBIDDEN:
|
||||||
|
raise exception.NotAuthorized(res.read())
|
||||||
|
elif status_code == httplib.NOT_FOUND:
|
||||||
|
raise exception.NotFound(res.read())
|
||||||
|
elif status_code == httplib.CONFLICT:
|
||||||
|
raise exception.Duplicate(res.read())
|
||||||
|
elif status_code == httplib.BAD_REQUEST:
|
||||||
|
raise exception.Invalid(res.read())
|
||||||
|
elif status_code == httplib.MULTIPLE_CHOICES:
|
||||||
|
raise exception.MultipleChoices(body=res.read())
|
||||||
|
elif status_code == httplib.INTERNAL_SERVER_ERROR:
|
||||||
|
raise Exception("Internal Server error: %s" % res.read())
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown error occurred! %s" % res.read())
|
||||||
|
|
||||||
|
except (socket.error, IOError), e:
|
||||||
|
raise exception.ClientConnectionError(e)
|
||||||
|
|
||||||
|
def _seekable(self, body):
|
||||||
|
# pipes are not seekable, avoids sendfile() failure on e.g.
|
||||||
|
# cat /path/to/image | glance add ...
|
||||||
|
# or where add command is launched via popen
|
||||||
|
try:
|
||||||
|
os.lseek(body.fileno(), 0, os.SEEK_SET)
|
||||||
|
return True
|
||||||
|
except OSError as e:
|
||||||
|
return (e.errno != errno.ESPIPE)
|
||||||
|
|
||||||
|
def _sendable(self, body):
|
||||||
|
return (SENDFILE_SUPPORTED and hasattr(body, 'fileno') and
|
||||||
|
self._seekable(body) and not self.use_ssl)
|
||||||
|
|
||||||
|
def _iterable(self, body):
|
||||||
|
return isinstance(body, collections.Iterable)
|
||||||
|
|
||||||
|
def image_iterator(self, connection, headers, body):
|
||||||
|
if self._sendable(body):
|
||||||
|
return SendFileIterator(connection, body)
|
||||||
|
elif self._iterable(body):
|
||||||
|
return utils.chunkreadable(body)
|
||||||
|
else:
|
||||||
|
return ImageBodyIterator(body)
|
||||||
|
|
||||||
|
def get_status_code(self, response):
|
||||||
|
"""
|
||||||
|
Returns the integer status code from the response, which
|
||||||
|
can be either a Webob.Response (used in testing) or httplib.Response
|
||||||
|
"""
|
||||||
|
if hasattr(response, 'status_int'):
|
||||||
|
return response.status_int
|
||||||
|
else:
|
||||||
|
return response.status
|
||||||
|
|
||||||
|
def _extract_params(self, actual_params, allowed_params):
|
||||||
|
"""
|
||||||
|
Extract a subset of keys from a dictionary. The filters key
|
||||||
|
will also be extracted, and each of its values will be returned
|
||||||
|
as an individual param.
|
||||||
|
|
||||||
|
:param actual_params: dict of keys to filter
|
||||||
|
:param allowed_params: list of keys that 'actual_params' will be
|
||||||
|
reduced to
|
||||||
|
:retval subset of 'params' dict
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# expect 'filters' param to be a dict here
|
||||||
|
result = dict(actual_params.get('filters'))
|
||||||
|
except TypeError:
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for allowed_param in allowed_params:
|
||||||
|
if allowed_param in actual_params:
|
||||||
|
result[allowed_param] = actual_params[allowed_param]
|
||||||
|
|
||||||
|
return result
|
181
windc/windc/common/config.py
Normal file
181
windc/windc/common/config.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Routines for configuring balancer
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import logging.handlers
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from windc.common import cfg
|
||||||
|
from windc.common import wsgi
|
||||||
|
from windc import version
|
||||||
|
|
||||||
|
|
||||||
|
paste_deploy_group = cfg.OptGroup('paste_deploy')
|
||||||
|
paste_deploy_opts = [
|
||||||
|
cfg.StrOpt('flavor'),
|
||||||
|
cfg.StrOpt('config_file')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WindcConfigOpts(cfg.CommonConfigOpts):
|
||||||
|
def __init__(self, default_config_files=None, **kwargs):
|
||||||
|
super(WindcConfigOpts, self).__init__(
|
||||||
|
project='windc',
|
||||||
|
version='%%prog %s' % version.version_string(),
|
||||||
|
default_config_files=default_config_files,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class WindcCacheConfigOpts(WindcConfigOpts):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
config_files = cfg.find_config_files(project='windc',
|
||||||
|
prog='windc-cache')
|
||||||
|
super(BalancerCacheConfigOpts, self).__init__(config_files, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(conf):
|
||||||
|
"""
|
||||||
|
Sets up the logging options for a log with supplied name
|
||||||
|
|
||||||
|
:param conf: a cfg.ConfOpts object
|
||||||
|
"""
|
||||||
|
|
||||||
|
if conf.log_config:
|
||||||
|
# Use a logging configuration file for all settings...
|
||||||
|
if os.path.exists(conf.log_config):
|
||||||
|
logging.config.fileConfig(conf.log_config)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unable to locate specified logging "
|
||||||
|
"config file: %s" % conf.log_config)
|
||||||
|
|
||||||
|
root_logger = logging.root
|
||||||
|
if conf.debug:
|
||||||
|
root_logger.setLevel(logging.DEBUG)
|
||||||
|
elif conf.verbose:
|
||||||
|
root_logger.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
root_logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
formatter = logging.Formatter(conf.log_format, conf.log_date_format)
|
||||||
|
|
||||||
|
if conf.use_syslog:
|
||||||
|
try:
|
||||||
|
facility = getattr(logging.handlers.SysLogHandler,
|
||||||
|
conf.syslog_log_facility)
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError(_("Invalid syslog facility"))
|
||||||
|
|
||||||
|
handler = logging.handlers.SysLogHandler(address='/dev/log',
|
||||||
|
facility=facility)
|
||||||
|
elif conf.log_file:
|
||||||
|
logfile = conf.log_file
|
||||||
|
if conf.log_dir:
|
||||||
|
logfile = os.path.join(conf.log_dir, logfile)
|
||||||
|
handler = logging.handlers.WatchedFileHandler(logfile)
|
||||||
|
else:
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_paste_deploy_opts(conf):
|
||||||
|
"""
|
||||||
|
Idempotent registration of paste_deploy option group
|
||||||
|
|
||||||
|
:param conf: a cfg.ConfigOpts object
|
||||||
|
"""
|
||||||
|
conf.register_group(paste_deploy_group)
|
||||||
|
conf.register_opts(paste_deploy_opts, group=paste_deploy_group)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_deployment_flavor(conf):
|
||||||
|
"""
|
||||||
|
Retrieve the paste_deploy.flavor config item, formatted appropriately
|
||||||
|
for appending to the application name.
|
||||||
|
|
||||||
|
:param conf: a cfg.ConfigOpts object
|
||||||
|
"""
|
||||||
|
_register_paste_deploy_opts(conf)
|
||||||
|
flavor = conf.paste_deploy.flavor
|
||||||
|
return '' if not flavor else ('-' + flavor)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_deployment_config_file(conf):
|
||||||
|
"""
|
||||||
|
Retrieve the deployment_config_file config item, formatted as an
|
||||||
|
absolute pathname.
|
||||||
|
|
||||||
|
:param conf: a cfg.ConfigOpts object
|
||||||
|
"""
|
||||||
|
_register_paste_deploy_opts(conf)
|
||||||
|
config_file = conf.paste_deploy.config_file
|
||||||
|
if not config_file:
|
||||||
|
# Assume paste config is in a paste.ini file corresponding
|
||||||
|
# to the last config file
|
||||||
|
path = conf.config_file[-1].replace(".conf", "-paste.ini")
|
||||||
|
else:
|
||||||
|
path = config_file
|
||||||
|
return os.path.abspath(path)
|
||||||
|
|
||||||
|
|
||||||
|
def load_paste_app(conf, app_name=None):
|
||||||
|
"""
|
||||||
|
Builds and returns a WSGI app from a paste config file.
|
||||||
|
|
||||||
|
We assume the last config file specified in the supplied ConfigOpts
|
||||||
|
object is the paste config file.
|
||||||
|
|
||||||
|
:param conf: a cfg.ConfigOpts object
|
||||||
|
:param app_name: name of the application to load
|
||||||
|
|
||||||
|
:raises RuntimeError when config file cannot be located or application
|
||||||
|
cannot be loaded from config file
|
||||||
|
"""
|
||||||
|
if app_name is None:
|
||||||
|
app_name = conf.prog
|
||||||
|
|
||||||
|
# append the deployment flavor to the application name,
|
||||||
|
# in order to identify the appropriate paste pipeline
|
||||||
|
app_name += _get_deployment_flavor(conf)
|
||||||
|
|
||||||
|
conf_file = _get_deployment_config_file(conf)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Setup logging early
|
||||||
|
setup_logging(conf)
|
||||||
|
|
||||||
|
app = wsgi.paste_deploy_app(conf_file, app_name, conf)
|
||||||
|
|
||||||
|
# Log the options used when starting if we're in debug mode...
|
||||||
|
if conf.debug:
|
||||||
|
conf.log_opt_values(logging.getLogger(app_name), logging.DEBUG)
|
||||||
|
|
||||||
|
return app
|
||||||
|
except (LookupError, ImportError), e:
|
||||||
|
raise RuntimeError("Unable to load %(app_name)s from "
|
||||||
|
"configuration file %(conf_file)s."
|
||||||
|
"\nGot: %(e)r" % locals())
|
130
windc/windc/common/context.py
Normal file
130
windc/windc/common/context.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from balancer.common import cfg
|
||||||
|
from balancer.common import exception
|
||||||
|
from balancer.common import utils
|
||||||
|
from balancer.common import wsgi
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContext(object):
|
||||||
|
"""
|
||||||
|
Stores information about the security context under which the user
|
||||||
|
accesses the system, as well as additional request information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, auth_tok=None, user=None, user_id=None, tenant=None,
|
||||||
|
tenant_id=None, roles=None, is_admin=False, read_only=False,
|
||||||
|
show_deleted=False, owner_is_tenant=True):
|
||||||
|
self.auth_tok = auth_tok
|
||||||
|
self.user = user
|
||||||
|
self.user_id = user_id
|
||||||
|
self.tenant = tenant
|
||||||
|
self.tenant_id = tenant_id
|
||||||
|
self.roles = roles or []
|
||||||
|
self.is_admin = is_admin
|
||||||
|
self.read_only = read_only
|
||||||
|
self._show_deleted = show_deleted
|
||||||
|
self.owner_is_tenant = owner_is_tenant
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self):
|
||||||
|
"""Return the owner to correlate with an image."""
|
||||||
|
return self.tenant if self.owner_is_tenant else self.user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def show_deleted(self):
|
||||||
|
"""Admins can see deleted by default"""
|
||||||
|
if self._show_deleted or self.is_admin:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMiddleware(wsgi.Middleware):
|
||||||
|
|
||||||
|
opts = [
|
||||||
|
cfg.BoolOpt('owner_is_tenant', default=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, app, conf, **local_conf):
|
||||||
|
self.conf = conf
|
||||||
|
self.conf.register_opts(self.opts)
|
||||||
|
|
||||||
|
# Determine the context class to use
|
||||||
|
self.ctxcls = RequestContext
|
||||||
|
if 'context_class' in local_conf:
|
||||||
|
self.ctxcls = utils.import_class(local_conf['context_class'])
|
||||||
|
|
||||||
|
super(ContextMiddleware, self).__init__(app)
|
||||||
|
|
||||||
|
def make_context(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a context with the given arguments.
|
||||||
|
"""
|
||||||
|
kwargs.setdefault('owner_is_tenant', self.conf.owner_is_tenant)
|
||||||
|
|
||||||
|
return self.ctxcls(*args, **kwargs)
|
||||||
|
|
||||||
|
def process_request(self, req):
|
||||||
|
"""
|
||||||
|
Extract any authentication information in the request and
|
||||||
|
construct an appropriate context from it.
|
||||||
|
|
||||||
|
A few scenarios exist:
|
||||||
|
|
||||||
|
1. If X-Auth-Token is passed in, then consult TENANT and ROLE headers
|
||||||
|
to determine permissions.
|
||||||
|
|
||||||
|
2. An X-Auth-Token was passed in, but the Identity-Status is not
|
||||||
|
confirmed. For now, just raising a NotAuthorized exception.
|
||||||
|
|
||||||
|
3. X-Auth-Token is omitted. If we were using Keystone, then the
|
||||||
|
tokenauth middleware would have rejected the request, so we must be
|
||||||
|
using NoAuth. In that case, assume that is_admin=True.
|
||||||
|
"""
|
||||||
|
# TODO(sirp): should we be using the balancer_tokeauth shim from
|
||||||
|
# Keystone here? If we do, we need to make sure it handles the NoAuth
|
||||||
|
# case
|
||||||
|
auth_tok = req.headers.get('X-Auth-Token',
|
||||||
|
req.headers.get('X-Storage-Token'))
|
||||||
|
if auth_tok:
|
||||||
|
if req.headers.get('X-Identity-Status') == 'Confirmed':
|
||||||
|
# 1. Auth-token is passed, check other headers
|
||||||
|
user = req.headers.get('X-User-Name')
|
||||||
|
user_id = req.headers.get('X-User-Id')
|
||||||
|
tenant = req.headers.get('X-Tenant-Name')
|
||||||
|
tenant_id = req.headers.get('X-Tenant-Id')
|
||||||
|
roles = [r.strip()
|
||||||
|
for r in req.headers.get('X-Role', '').split(',')]
|
||||||
|
is_admin = any(role.lower() == 'admin' for role in roles)
|
||||||
|
else:
|
||||||
|
# 2. Indentity-Status not confirmed
|
||||||
|
# FIXME(sirp): not sure what the correct behavior in this case
|
||||||
|
# is; just raising NotAuthorized for now
|
||||||
|
raise exception.NotAuthorized()
|
||||||
|
else:
|
||||||
|
# 3. Auth-token is ommited, assume NoAuth
|
||||||
|
user = None
|
||||||
|
user_id = None
|
||||||
|
tenant = None
|
||||||
|
tenant_id = None
|
||||||
|
roles = []
|
||||||
|
is_admin = True
|
||||||
|
|
||||||
|
req.context = self.make_context(auth_tok=auth_tok, user=user,
|
||||||
|
user_id=user_id, tenant=tenant, tenant_id=tenant_id,
|
||||||
|
roles=roles, is_admin=is_admin)
|
184
windc/windc/common/exception.py
Normal file
184
windc/windc/common/exception.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Glance exception subclasses"""
|
||||||
|
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectException(Exception):
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = urlparse.urlparse(url)
|
||||||
|
|
||||||
|
|
||||||
|
class GlanceException(Exception):
|
||||||
|
"""
|
||||||
|
Base Glance Exception
|
||||||
|
|
||||||
|
To correctly use this class, inherit from it and define
|
||||||
|
a 'message' property. That message will get printf'd
|
||||||
|
with the keyword arguments provided to the constructor.
|
||||||
|
"""
|
||||||
|
message = "An unknown exception occurred"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self._error_string = self.message % kwargs
|
||||||
|
except Exception:
|
||||||
|
# at least get the core message out if something happened
|
||||||
|
self._error_string = self.message
|
||||||
|
if len(args) > 0:
|
||||||
|
# If there is a non-kwarg parameter, assume it's the error
|
||||||
|
# message or reason description and tack it on to the end
|
||||||
|
# of the exception message
|
||||||
|
# Convert all arguments into their string representations...
|
||||||
|
args = ["%s" % arg for arg in args]
|
||||||
|
self._error_string = (self._error_string +
|
||||||
|
"\nDetails: %s" % '\n'.join(args))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self._error_string
|
||||||
|
|
||||||
|
|
||||||
|
class MissingArgumentError(GlanceException):
|
||||||
|
message = "Missing required argument."
|
||||||
|
|
||||||
|
|
||||||
|
class MissingCredentialError(GlanceException):
|
||||||
|
message = "Missing required credential: %(required)s"
|
||||||
|
|
||||||
|
|
||||||
|
class BadAuthStrategy(GlanceException):
|
||||||
|
message = "Incorrect auth strategy, expected \"%(expected)s\" but "
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(GlanceException):
|
||||||
|
message = "An object with the specified identifier was not found."
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownScheme(GlanceException):
|
||||||
|
message = "Unknown scheme '%(scheme)s' found in URI"
|
||||||
|
|
||||||
|
|
||||||
|
class BadStoreUri(GlanceException):
|
||||||
|
message = "The Store URI %(uri)s was malformed. Reason: %(reason)s"
|
||||||
|
|
||||||
|
|
||||||
|
class Duplicate(GlanceException):
|
||||||
|
message = "An object with the same identifier already exists."
|
||||||
|
|
||||||
|
|
||||||
|
class StorageFull(GlanceException):
|
||||||
|
message = "There is not enough disk space on the image storage media."
|
||||||
|
|
||||||
|
|
||||||
|
class StorageWriteDenied(GlanceException):
|
||||||
|
message = "Permission to write image storage media denied."
|
||||||
|
|
||||||
|
|
||||||
|
class ImportFailure(GlanceException):
|
||||||
|
message = "Failed to import requested object/class: '%(import_str)s'. \
|
||||||
|
Reason: %(reason)s"
|
||||||
|
|
||||||
|
|
||||||
|
class AuthBadRequest(GlanceException):
|
||||||
|
message = "Connect error/bad request to Auth service at URL %(url)s."
|
||||||
|
|
||||||
|
|
||||||
|
class AuthUrlNotFound(GlanceException):
|
||||||
|
message = "Auth service at URL %(url)s not found."
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationFailure(GlanceException):
|
||||||
|
message = "Authorization failed."
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorized(GlanceException):
|
||||||
|
message = "You are not authorized to complete this action."
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorizedPublicImage(NotAuthorized):
|
||||||
|
message = "You are not authorized to complete this action."
|
||||||
|
|
||||||
|
|
||||||
|
class Invalid(GlanceException):
|
||||||
|
message = "Data supplied was not valid."
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationRedirect(GlanceException):
|
||||||
|
message = "Redirecting to %(uri)s for authorization."
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseMigrationError(GlanceException):
|
||||||
|
message = "There was an error migrating the database."
|
||||||
|
|
||||||
|
|
||||||
|
class ClientConnectionError(GlanceException):
|
||||||
|
message = "There was an error connecting to a server"
|
||||||
|
|
||||||
|
|
||||||
|
class ClientConfigurationError(GlanceException):
|
||||||
|
message = "There was an error configuring the client."
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleChoices(GlanceException):
|
||||||
|
message = "The request returned a 302 Multiple Choices. This generally "
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidContentType(GlanceException):
|
||||||
|
message = "Invalid content type %(content_type)s"
|
||||||
|
|
||||||
|
|
||||||
|
class BadRegistryConnectionConfiguration(GlanceException):
|
||||||
|
message = "Registry was not configured correctly on API server. "
|
||||||
|
|
||||||
|
|
||||||
|
class BadStoreConfiguration(GlanceException):
|
||||||
|
message = "Store %(store_name)s could not be configured correctly. "
|
||||||
|
|
||||||
|
|
||||||
|
class BadDriverConfiguration(GlanceException):
|
||||||
|
message = "Driver %(driver_name)s could not be configured correctly. "
|
||||||
|
|
||||||
|
|
||||||
|
class StoreDeleteNotSupported(GlanceException):
|
||||||
|
message = "Deleting images from this store is not supported."
|
||||||
|
|
||||||
|
|
||||||
|
class StoreAddDisabled(GlanceException):
|
||||||
|
message = "Configuration for store failed. Adding images to this "
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidNotifierStrategy(GlanceException):
|
||||||
|
message = "'%(strategy)s' is not an available notifier strategy."
|
||||||
|
|
||||||
|
|
||||||
|
class MaxRedirectsExceeded(GlanceException):
|
||||||
|
message = "Maximum redirects (%(redirects)s) was exceeded."
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRedirect(GlanceException):
|
||||||
|
message = "Received invalid HTTP redirect."
|
||||||
|
|
||||||
|
|
||||||
|
class NoServiceEndpoint(GlanceException):
|
||||||
|
message = "Response from Keystone does not contain a Glance endpoint."
|
||||||
|
|
||||||
|
|
||||||
|
class RegionAmbiguity(GlanceException):
|
||||||
|
message = "Multiple 'image' service matches for region %(region)s. This "
|
182
windc/windc/common/policy.py
Normal file
182
windc/windc/common/policy.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2011 OpenStack, LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Common Policy Engine Implementation"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthorized(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_BRAIN = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_brain(brain):
|
||||||
|
"""Set the brain used by enforce().
|
||||||
|
|
||||||
|
Defaults use Brain() if not set.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global _BRAIN
|
||||||
|
_BRAIN = brain
|
||||||
|
|
||||||
|
|
||||||
|
def reset():
|
||||||
|
"""Clear the brain used by enforce()."""
|
||||||
|
global _BRAIN
|
||||||
|
_BRAIN = None
|
||||||
|
|
||||||
|
|
||||||
|
def enforce(match_list, target_dict, credentials_dict):
|
||||||
|
"""Enforces authorization of some rules against credentials.
|
||||||
|
|
||||||
|
:param match_list: nested tuples of data to match against
|
||||||
|
The basic brain supports three types of match lists:
|
||||||
|
1) rules
|
||||||
|
looks like: ('rule:compute:get_instance',)
|
||||||
|
Retrieves the named rule from the rules dict and recursively
|
||||||
|
checks against the contents of the rule.
|
||||||
|
2) roles
|
||||||
|
looks like: ('role:compute:admin',)
|
||||||
|
Matches if the specified role is in credentials_dict['roles'].
|
||||||
|
3) generic
|
||||||
|
('tenant_id:%(tenant_id)s',)
|
||||||
|
Substitutes values from the target dict into the match using
|
||||||
|
the % operator and matches them against the creds dict.
|
||||||
|
|
||||||
|
Combining rules:
|
||||||
|
The brain returns True if any of the outer tuple of rules match
|
||||||
|
and also True if all of the inner tuples match. You can use this to
|
||||||
|
perform simple boolean logic. For example, the following rule would
|
||||||
|
return True if the creds contain the role 'admin' OR the if the
|
||||||
|
tenant_id matches the target dict AND the the creds contains the
|
||||||
|
role 'compute_sysadmin':
|
||||||
|
|
||||||
|
{
|
||||||
|
"rule:combined": (
|
||||||
|
'role:admin',
|
||||||
|
('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Note that rule and role are reserved words in the credentials match, so
|
||||||
|
you can't match against properties with those names. Custom brains may
|
||||||
|
also add new reserved words. For example, the HttpBrain adds http as a
|
||||||
|
reserved word.
|
||||||
|
|
||||||
|
:param target_dict: dict of object properties
|
||||||
|
Target dicts contain as much information as we can about the object being
|
||||||
|
operated on.
|
||||||
|
|
||||||
|
:param credentials_dict: dict of actor properties
|
||||||
|
Credentials dicts contain as much information as we can about the user
|
||||||
|
performing the action.
|
||||||
|
|
||||||
|
:raises NotAuthorized if the check fails
|
||||||
|
|
||||||
|
"""
|
||||||
|
global _BRAIN
|
||||||
|
if not _BRAIN:
|
||||||
|
_BRAIN = Brain()
|
||||||
|
if not _BRAIN.check(match_list, target_dict, credentials_dict):
|
||||||
|
raise NotAuthorized()
|
||||||
|
|
||||||
|
|
||||||
|
class Brain(object):
|
||||||
|
"""Implements policy checking."""
|
||||||
|
@classmethod
|
||||||
|
def load_json(cls, data, default_rule=None):
|
||||||
|
"""Init a brain using json instead of a rules dictionary."""
|
||||||
|
rules_dict = json.loads(data)
|
||||||
|
return cls(rules=rules_dict, default_rule=default_rule)
|
||||||
|
|
||||||
|
def __init__(self, rules=None, default_rule=None):
|
||||||
|
self.rules = rules or {}
|
||||||
|
self.default_rule = default_rule
|
||||||
|
|
||||||
|
def add_rule(self, key, match):
|
||||||
|
self.rules[key] = match
|
||||||
|
|
||||||
|
def _check(self, match, target_dict, cred_dict):
|
||||||
|
match_kind, match_value = match.split(':', 1)
|
||||||
|
try:
|
||||||
|
f = getattr(self, '_check_%s' % match_kind)
|
||||||
|
except AttributeError:
|
||||||
|
if not self._check_generic(match, target_dict, cred_dict):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if not f(match_value, target_dict, cred_dict):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check(self, match_list, target_dict, cred_dict):
|
||||||
|
"""Checks authorization of some rules against credentials.
|
||||||
|
|
||||||
|
Detailed description of the check with examples in policy.enforce().
|
||||||
|
|
||||||
|
:param match_list: nested tuples of data to match against
|
||||||
|
:param target_dict: dict of object properties
|
||||||
|
:param credentials_dict: dict of actor properties
|
||||||
|
|
||||||
|
:returns: True if the check passes
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not match_list:
|
||||||
|
return True
|
||||||
|
for and_list in match_list:
|
||||||
|
if isinstance(and_list, basestring):
|
||||||
|
and_list = (and_list,)
|
||||||
|
if all([self._check(item, target_dict, cred_dict)
|
||||||
|
for item in and_list]):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_rule(self, match, target_dict, cred_dict):
|
||||||
|
"""Recursively checks credentials based on the brains rules."""
|
||||||
|
try:
|
||||||
|
new_match_list = self.rules[match]
|
||||||
|
except KeyError:
|
||||||
|
if self.default_rule and match != self.default_rule:
|
||||||
|
new_match_list = ('rule:%s' % self.default_rule,)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.check(new_match_list, target_dict, cred_dict)
|
||||||
|
|
||||||
|
def _check_role(self, match, target_dict, cred_dict):
|
||||||
|
"""Check that there is a matching role in the cred dict."""
|
||||||
|
return match in cred_dict['roles']
|
||||||
|
|
||||||
|
def _check_generic(self, match, target_dict, cred_dict):
|
||||||
|
"""Check an individual match.
|
||||||
|
|
||||||
|
Matches look like:
|
||||||
|
|
||||||
|
tenant:%(tenant_id)s
|
||||||
|
role:compute:admin
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO(termie): do dict inspection via dot syntax
|
||||||
|
match = match % target_dict
|
||||||
|
key, value = match.split(':', 1)
|
||||||
|
if key in cred_dict:
|
||||||
|
return value == cred_dict[key]
|
||||||
|
return False
|
421
windc/windc/common/utils.py
Normal file
421
windc/windc/common/utils.py
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
System-level utilities and helper functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import iso8601
|
||||||
|
|
||||||
|
from windc.common import exception
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
|
class Singleton:
|
||||||
|
"""
|
||||||
|
A non-thread-safe helper class to ease implementing singletons.
|
||||||
|
This should be used as a decorator -- not a metaclass -- to the
|
||||||
|
class that should be a singleton.
|
||||||
|
|
||||||
|
The decorated class can define one `__init__` function that
|
||||||
|
takes only the `self` argument. Other than that, there are
|
||||||
|
no restrictions that apply to the decorated class.
|
||||||
|
|
||||||
|
To get the singleton instance, use the `Instance` method. Trying
|
||||||
|
to use `__call__` will result in a `TypeError` being raised.
|
||||||
|
|
||||||
|
Limitations: The decorated class cannot be inherited from and the
|
||||||
|
type of the singleton instance cannot be checked with `isinstance`..
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, decorated):
|
||||||
|
self._decorated = decorated
|
||||||
|
|
||||||
|
def Instance(self, conf):
|
||||||
|
"""
|
||||||
|
Returns the singleton instance. Upon its first call, it creates a
|
||||||
|
new instance of the decorated class and calls its `__init__` method.
|
||||||
|
On all subsequent calls, the already created instance is returned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self._instance
|
||||||
|
except AttributeError:
|
||||||
|
self._instance = self._decorated(conf)
|
||||||
|
return self._instance
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
"""
|
||||||
|
Call method that raises an exception in order to prevent creation
|
||||||
|
of multiple instances of the singleton. The `Instance` method should
|
||||||
|
be used instead.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise TypeError(
|
||||||
|
'Singletons must be accessed through the `Instance` method.')
|
||||||
|
|
||||||
|
|
||||||
|
def checkNone(obj):
|
||||||
|
if bool(obj):
|
||||||
|
if obj != 'None':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def chunkreadable(iter, chunk_size=65536):
|
||||||
|
"""
|
||||||
|
Wrap a readable iterator with a reader yielding chunks of
|
||||||
|
a preferred size, otherwise leave iterator unchanged.
|
||||||
|
|
||||||
|
:param iter: an iter which may also be readable
|
||||||
|
:param chunk_size: maximum size of chunk
|
||||||
|
"""
|
||||||
|
return chunkiter(iter, chunk_size) if hasattr(iter, 'read') else iter
|
||||||
|
|
||||||
|
|
||||||
|
def chunkiter(fp, chunk_size=65536):
|
||||||
|
"""
|
||||||
|
Return an iterator to a file-like obj which yields fixed size chunks
|
||||||
|
|
||||||
|
:param fp: a file-like object
|
||||||
|
:param chunk_size: maximum size of chunk
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
chunk = fp.read(chunk_size)
|
||||||
|
if chunk:
|
||||||
|
yield chunk
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def image_meta_to_http_headers(image_meta):
|
||||||
|
"""
|
||||||
|
Returns a set of image metadata into a dict
|
||||||
|
of HTTP headers that can be fed to either a Webob
|
||||||
|
Request object or an httplib.HTTP(S)Connection object
|
||||||
|
|
||||||
|
:param image_meta: Mapping of image metadata
|
||||||
|
"""
|
||||||
|
headers = {}
|
||||||
|
for k, v in image_meta.items():
|
||||||
|
if v is not None:
|
||||||
|
if k == 'properties':
|
||||||
|
for pk, pv in v.items():
|
||||||
|
if pv is not None:
|
||||||
|
headers["x-image-meta-property-%s"
|
||||||
|
% pk.lower()] = unicode(pv)
|
||||||
|
else:
|
||||||
|
headers["x-image-meta-%s" % k.lower()] = unicode(v)
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def add_features_to_http_headers(features, headers):
|
||||||
|
"""
|
||||||
|
Adds additional headers representing balancer features to be enabled.
|
||||||
|
|
||||||
|
:param headers: Base set of headers
|
||||||
|
:param features: Map of enabled features
|
||||||
|
"""
|
||||||
|
if features:
|
||||||
|
for k, v in features.items():
|
||||||
|
if v is not None:
|
||||||
|
headers[k.lower()] = unicode(v)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_meta_from_headers(response):
|
||||||
|
"""
|
||||||
|
Processes HTTP headers from a supplied response that
|
||||||
|
match the x-image-meta and x-image-meta-property and
|
||||||
|
returns a mapping of image metadata and properties
|
||||||
|
|
||||||
|
:param response: Response to process
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
properties = {}
|
||||||
|
|
||||||
|
if hasattr(response, 'getheaders'): # httplib.HTTPResponse
|
||||||
|
headers = response.getheaders()
|
||||||
|
else: # webob.Response
|
||||||
|
headers = response.headers.items()
|
||||||
|
|
||||||
|
for key, value in headers:
|
||||||
|
key = str(key.lower())
|
||||||
|
if key.startswith('x-image-meta-property-'):
|
||||||
|
field_name = key[len('x-image-meta-property-'):].replace('-', '_')
|
||||||
|
properties[field_name] = value or None
|
||||||
|
elif key.startswith('x-image-meta-'):
|
||||||
|
field_name = key[len('x-image-meta-'):].replace('-', '_')
|
||||||
|
result[field_name] = value or None
|
||||||
|
result['properties'] = properties
|
||||||
|
if 'size' in result:
|
||||||
|
try:
|
||||||
|
result['size'] = int(result['size'])
|
||||||
|
except ValueError:
|
||||||
|
raise exception.Invalid
|
||||||
|
for key in ('is_public', 'deleted', 'protected'):
|
||||||
|
if key in result:
|
||||||
|
result[key] = bool_from_header_value(result[key])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def bool_from_header_value(value):
|
||||||
|
"""
|
||||||
|
Returns True if value is a boolean True or the
|
||||||
|
string 'true', case-insensitive, False otherwise
|
||||||
|
"""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
elif isinstance(value, (basestring, unicode)):
|
||||||
|
if str(value).lower() == 'true':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def bool_from_string(subject):
|
||||||
|
"""
|
||||||
|
Interpret a string as a boolean.
|
||||||
|
|
||||||
|
Any string value in:
|
||||||
|
('True', 'true', 'On', 'on', '1')
|
||||||
|
is interpreted as a boolean True.
|
||||||
|
|
||||||
|
Useful for JSON-decoded stuff and config file parsing
|
||||||
|
"""
|
||||||
|
if isinstance(subject, bool):
|
||||||
|
return subject
|
||||||
|
elif isinstance(subject, int):
|
||||||
|
return subject == 1
|
||||||
|
if hasattr(subject, 'startswith'): # str or unicode...
|
||||||
|
if subject.strip().lower() in ('true', 'on', '1'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def import_class(import_str):
|
||||||
|
"""Returns a class from a string including module and class"""
|
||||||
|
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||||
|
try:
|
||||||
|
__import__(mod_str)
|
||||||
|
return getattr(sys.modules[mod_str], class_str)
|
||||||
|
except (ImportError, ValueError, AttributeError), e:
|
||||||
|
raise exception.ImportFailure(import_str=import_str,
|
||||||
|
reason=e)
|
||||||
|
|
||||||
|
|
||||||
|
def import_object(import_str):
|
||||||
|
"""Returns an object including a module or module and class"""
|
||||||
|
try:
|
||||||
|
__import__(import_str)
|
||||||
|
return sys.modules[import_str]
|
||||||
|
except ImportError:
|
||||||
|
cls = import_class(import_str)
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_uuid():
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def is_uuid_like(value):
|
||||||
|
try:
|
||||||
|
uuid.UUID(value)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def isotime(at=None):
|
||||||
|
"""Stringify time in ISO 8601 format"""
|
||||||
|
if not at:
|
||||||
|
at = datetime.datetime.utcnow()
|
||||||
|
str = at.strftime(TIME_FORMAT)
|
||||||
|
tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
|
||||||
|
str += ('Z' if tz == 'UTC' else tz)
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
def parse_isotime(timestr):
|
||||||
|
"""Parse time from ISO 8601 format"""
|
||||||
|
try:
|
||||||
|
return iso8601.parse_date(timestr)
|
||||||
|
except iso8601.ParseError as e:
|
||||||
|
raise ValueError(e.message)
|
||||||
|
except TypeError as e:
|
||||||
|
raise ValueError(e.message)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_time(timestamp):
|
||||||
|
"""Normalize time in arbitrary timezone to UTC"""
|
||||||
|
offset = timestamp.utcoffset()
|
||||||
|
return timestamp.replace(tzinfo=None) - offset if offset else timestamp
|
||||||
|
|
||||||
|
|
||||||
|
def safe_mkdirs(path):
|
||||||
|
try:
|
||||||
|
os.makedirs(path)
|
||||||
|
except OSError, e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def safe_remove(path):
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except OSError, e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class PrettyTable(object):
|
||||||
|
"""Creates an ASCII art table for use in bin/balancer
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
ID Name Size Hits
|
||||||
|
--- ----------------- ------------ -----
|
||||||
|
122 image 22 0
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.columns = []
|
||||||
|
|
||||||
|
def add_column(self, width, label="", just='l'):
|
||||||
|
"""Add a column to the table
|
||||||
|
|
||||||
|
:param width: number of characters wide the column should be
|
||||||
|
:param label: column heading
|
||||||
|
:param just: justification for the column, 'l' for left,
|
||||||
|
'r' for right
|
||||||
|
"""
|
||||||
|
self.columns.append((width, label, just))
|
||||||
|
|
||||||
|
def make_header(self):
|
||||||
|
label_parts = []
|
||||||
|
break_parts = []
|
||||||
|
for width, label, _ in self.columns:
|
||||||
|
# NOTE(sirp): headers are always left justified
|
||||||
|
label_part = self._clip_and_justify(label, width, 'l')
|
||||||
|
label_parts.append(label_part)
|
||||||
|
|
||||||
|
break_part = '-' * width
|
||||||
|
break_parts.append(break_part)
|
||||||
|
|
||||||
|
label_line = ' '.join(label_parts)
|
||||||
|
break_line = ' '.join(break_parts)
|
||||||
|
return '\n'.join([label_line, break_line])
|
||||||
|
|
||||||
|
def make_row(self, *args):
|
||||||
|
row = args
|
||||||
|
row_parts = []
|
||||||
|
for data, (width, _, just) in zip(row, self.columns):
|
||||||
|
row_part = self._clip_and_justify(data, width, just)
|
||||||
|
row_parts.append(row_part)
|
||||||
|
|
||||||
|
row_line = ' '.join(row_parts)
|
||||||
|
return row_line
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _clip_and_justify(data, width, just):
|
||||||
|
# clip field to column width
|
||||||
|
clipped_data = str(data)[:width]
|
||||||
|
|
||||||
|
if just == 'r':
|
||||||
|
# right justify
|
||||||
|
justified = clipped_data.rjust(width)
|
||||||
|
else:
|
||||||
|
# left justify
|
||||||
|
justified = clipped_data.ljust(width)
|
||||||
|
|
||||||
|
return justified
|
||||||
|
|
||||||
|
|
||||||
|
def get_terminal_size():
|
||||||
|
|
||||||
|
def _get_terminal_size_posix():
|
||||||
|
import fcntl
|
||||||
|
import struct
|
||||||
|
import termios
|
||||||
|
|
||||||
|
height_width = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
height_width = struct.unpack('hh', fcntl.ioctl(sys.stderr.fileno(),
|
||||||
|
termios.TIOCGWINSZ,
|
||||||
|
struct.pack('HH', 0, 0)))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not height_width:
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(['stty', 'size'],
|
||||||
|
shell=False,
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
return tuple(int(x) for x in p.communicate()[0].split())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return height_width
|
||||||
|
|
||||||
|
def _get_terminal_size_win32():
|
||||||
|
try:
|
||||||
|
from ctypes import windll, create_string_buffer
|
||||||
|
handle = windll.kernel32.GetStdHandle(-12)
|
||||||
|
csbi = create_string_buffer(22)
|
||||||
|
res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
if res:
|
||||||
|
import struct
|
||||||
|
unpack_tmp = struct.unpack("hhhhHhhhhhh", csbi.raw)
|
||||||
|
(bufx, bufy, curx, cury, wattr,
|
||||||
|
left, top, right, bottom, maxx, maxy) = unpack_tmp
|
||||||
|
height = bottom - top + 1
|
||||||
|
width = right - left + 1
|
||||||
|
return (height, width)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_terminal_size_unknownOS():
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
func = {'posix': _get_terminal_size_posix,
|
||||||
|
'win32': _get_terminal_size_win32}
|
||||||
|
|
||||||
|
height_width = func.get(platform.os.name, _get_terminal_size_unknownOS)()
|
||||||
|
|
||||||
|
if height_width == None:
|
||||||
|
raise exception.Invalid()
|
||||||
|
|
||||||
|
for i in height_width:
|
||||||
|
if not isinstance(i, int) or i <= 0:
|
||||||
|
raise exception.Invalid()
|
||||||
|
|
||||||
|
return height_width[0], height_width[1]
|
652
windc/windc/common/wsgi.py
Normal file
652
windc/windc/common/wsgi.py
Normal file
@ -0,0 +1,652 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2010 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility methods for working with WSGI servers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
import eventlet.greenio
|
||||||
|
from eventlet.green import socket, ssl
|
||||||
|
import eventlet.wsgi
|
||||||
|
from paste import deploy
|
||||||
|
import routes
|
||||||
|
import routes.middleware
|
||||||
|
import webob.dec
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
|
from windc.common import cfg
|
||||||
|
from windc.common import exception
|
||||||
|
from windc.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
bind_opts = [
|
||||||
|
cfg.StrOpt('bind_host', default='0.0.0.0'),
|
||||||
|
cfg.IntOpt('bind_port'),
|
||||||
|
]
|
||||||
|
|
||||||
|
socket_opts = [
|
||||||
|
cfg.IntOpt('backlog', default=4096),
|
||||||
|
cfg.StrOpt('cert_file'),
|
||||||
|
cfg.StrOpt('key_file'),
|
||||||
|
]
|
||||||
|
|
||||||
|
workers_opt = cfg.IntOpt('workers', default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class WritableLogger(object):
|
||||||
|
"""A thin wrapper that responds to `write` and logs."""
|
||||||
|
|
||||||
|
def __init__(self, logger, level=logging.DEBUG):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
|
||||||
|
def write(self, msg):
|
||||||
|
self.logger.log(self.level, msg.strip("\n"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_bind_addr(conf, default_port=None):
|
||||||
|
"""Return the host and port to bind to."""
|
||||||
|
conf.register_opts(bind_opts)
|
||||||
|
return (conf.bind_host, conf.bind_port or default_port)
|
||||||
|
|
||||||
|
|
||||||
|
def get_socket(conf, default_port):
|
||||||
|
"""
|
||||||
|
Bind socket to bind ip:port in conf
|
||||||
|
|
||||||
|
note: Mostly comes from Swift with a few small changes...
|
||||||
|
|
||||||
|
:param conf: a cfg.ConfigOpts object
|
||||||
|
:param default_port: port to bind to if none is specified in conf
|
||||||
|
|
||||||
|
:returns : a socket object as returned from socket.listen or
|
||||||
|
ssl.wrap_socket if conf specifies cert_file
|
||||||
|
"""
|
||||||
|
bind_addr = get_bind_addr(conf, default_port)
|
||||||
|
|
||||||
|
# TODO(jaypipes): eventlet's greened socket module does not actually
|
||||||
|
# support IPv6 in getaddrinfo(). We need to get around this in the
|
||||||
|
# future or monitor upstream for a fix
|
||||||
|
address_family = [addr[0] for addr in socket.getaddrinfo(bind_addr[0],
|
||||||
|
bind_addr[1], socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||||
|
if addr[0] in (socket.AF_INET, socket.AF_INET6)][0]
|
||||||
|
|
||||||
|
conf.register_opts(socket_opts)
|
||||||
|
|
||||||
|
cert_file = conf.cert_file
|
||||||
|
key_file = conf.key_file
|
||||||
|
use_ssl = cert_file or key_file
|
||||||
|
if use_ssl and (not cert_file or not key_file):
|
||||||
|
raise RuntimeError(_("When running server in SSL mode, you must "
|
||||||
|
"specify both a cert_file and key_file "
|
||||||
|
"option value in your configuration file"))
|
||||||
|
|
||||||
|
sock = None
|
||||||
|
retry_until = time.time() + 30
|
||||||
|
while not sock and time.time() < retry_until:
|
||||||
|
try:
|
||||||
|
sock = eventlet.listen(bind_addr, backlog=conf.backlog,
|
||||||
|
family=address_family)
|
||||||
|
if use_ssl:
|
||||||
|
sock = ssl.wrap_socket(sock, certfile=cert_file,
|
||||||
|
keyfile=key_file)
|
||||||
|
except socket.error, err:
|
||||||
|
if err.args[0] != errno.EADDRINUSE:
|
||||||
|
raise
|
||||||
|
eventlet.sleep(0.1)
|
||||||
|
if not sock:
|
||||||
|
raise RuntimeError(_("Could not bind to %s:%s after trying for 30 "
|
||||||
|
"seconds") % bind_addr)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
# in my experience, sockets can hang around forever without keepalive
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||||
|
|
||||||
|
# This option isn't available in the OS X version of eventlet
|
||||||
|
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||||
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 600)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
class Server(object):
|
||||||
|
"""Server class to manage multiple WSGI sockets and applications."""
|
||||||
|
|
||||||
|
def __init__(self, threads=1000):
|
||||||
|
self.threads = threads
|
||||||
|
self.children = []
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def start(self, application, conf, default_port):
|
||||||
|
"""
|
||||||
|
Run a WSGI server with the given application.
|
||||||
|
|
||||||
|
:param application: The application to run in the WSGI server
|
||||||
|
:param conf: a cfg.ConfigOpts object
|
||||||
|
:param default_port: Port to bind to if none is specified in conf
|
||||||
|
"""
|
||||||
|
def kill_children(*args):
|
||||||
|
"""Kills the entire process group."""
|
||||||
|
self.logger.error(_('SIGTERM received'))
|
||||||
|
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||||
|
self.running = False
|
||||||
|
os.killpg(0, signal.SIGTERM)
|
||||||
|
|
||||||
|
def hup(*args):
|
||||||
|
"""
|
||||||
|
Shuts down the server, but allows running requests to complete
|
||||||
|
"""
|
||||||
|
self.logger.error(_('SIGHUP received'))
|
||||||
|
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
self.application = application
|
||||||
|
self.sock = get_socket(conf, default_port)
|
||||||
|
conf.register_opt(workers_opt)
|
||||||
|
|
||||||
|
self.logger = logging.getLogger('eventlet.wsgi.server')
|
||||||
|
|
||||||
|
if conf.workers == 0:
|
||||||
|
# Useful for profiling, test, debug etc.
|
||||||
|
self.pool = eventlet.GreenPool(size=self.threads)
|
||||||
|
self.pool.spawn_n(self._single_run, application, self.sock)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(_("Starting %d workers") % conf.workers)
|
||||||
|
signal.signal(signal.SIGTERM, kill_children)
|
||||||
|
signal.signal(signal.SIGHUP, hup)
|
||||||
|
while len(self.children) < conf.workers:
|
||||||
|
self.run_child()
|
||||||
|
|
||||||
|
def wait_on_children(self):
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
pid, status = os.wait()
|
||||||
|
if os.WIFEXITED(status) or os.WIFSIGNALED(status):
|
||||||
|
self.logger.error(_('Removing dead child %s') % pid)
|
||||||
|
self.children.remove(pid)
|
||||||
|
self.run_child()
|
||||||
|
except OSError, err:
|
||||||
|
if err.errno not in (errno.EINTR, errno.ECHILD):
|
||||||
|
raise
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
||||||
|
self.logger.info(_('Caught keyboard interrupt. Exiting.'))
|
||||||
|
break
|
||||||
|
eventlet.greenio.shutdown_safe(self.sock)
|
||||||
|
self.sock.close()
|
||||||
|
self.logger.debug(_('Exited'))
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""Wait until all servers have completed running."""
|
||||||
|
try:
|
||||||
|
if self.children:
|
||||||
|
self.wait_on_children()
|
||||||
|
else:
|
||||||
|
self.pool.waitall()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run_child(self):
|
||||||
|
pid = os.fork()
|
||||||
|
if pid == 0:
|
||||||
|
signal.signal(signal.SIGHUP, signal.SIG_DFL)
|
||||||
|
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||||
|
self.run_server()
|
||||||
|
self.logger.info(_('Child %d exiting normally') % os.getpid())
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.logger.info(_('Started child %s') % pid)
|
||||||
|
self.children.append(pid)
|
||||||
|
|
||||||
|
def run_server(self):
|
||||||
|
"""Run a WSGI server."""
|
||||||
|
eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
|
||||||
|
eventlet.hubs.use_hub('poll')
|
||||||
|
eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||||
|
self.pool = eventlet.GreenPool(size=self.threads)
|
||||||
|
try:
|
||||||
|
eventlet.wsgi.server(self.sock, self.application,
|
||||||
|
log=WritableLogger(self.logger), custom_pool=self.pool)
|
||||||
|
except socket.error, err:
|
||||||
|
if err[0] != errno.EINVAL:
|
||||||
|
raise
|
||||||
|
self.pool.waitall()
|
||||||
|
|
||||||
|
def _single_run(self, application, sock):
|
||||||
|
"""Start a WSGI server in a new green thread."""
|
||||||
|
self.logger.info(_("Starting single process server"))
|
||||||
|
eventlet.wsgi.server(sock, application, custom_pool=self.pool,
|
||||||
|
log=WritableLogger(self.logger))
|
||||||
|
|
||||||
|
|
||||||
|
class Middleware(object):
|
||||||
|
"""
|
||||||
|
Base WSGI middleware wrapper. These classes require an application to be
|
||||||
|
initialized that will be called next. By default the middleware will
|
||||||
|
simply call its wrapped app, or you can override __call__ to customize its
|
||||||
|
behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
self.application = application
|
||||||
|
|
||||||
|
def process_request(self, req):
|
||||||
|
"""
|
||||||
|
Called on each request.
|
||||||
|
|
||||||
|
If this returns None, the next application down the stack will be
|
||||||
|
executed. If it returns a response then that response will be returned
|
||||||
|
and execution will stop here.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def process_response(self, response):
|
||||||
|
"""Do whatever you'd like to the response."""
|
||||||
|
return response
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
response = self.process_request(req)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
response = req.get_response(self.application)
|
||||||
|
return self.process_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
class Debug(Middleware):
|
||||||
|
"""
|
||||||
|
Helper class that can be inserted into any WSGI application chain
|
||||||
|
to get information about the request and response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
print ("*" * 40) + " REQUEST ENVIRON"
|
||||||
|
for key, value in req.environ.items():
|
||||||
|
print key, "=", value
|
||||||
|
print
|
||||||
|
resp = req.get_response(self.application)
|
||||||
|
|
||||||
|
print ("*" * 40) + " RESPONSE HEADERS"
|
||||||
|
for (key, value) in resp.headers.iteritems():
|
||||||
|
print key, "=", value
|
||||||
|
print
|
||||||
|
|
||||||
|
resp.app_iter = self.print_generator(resp.app_iter)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_generator(app_iter):
|
||||||
|
"""
|
||||||
|
Iterator that prints the contents of a wrapper string iterator
|
||||||
|
when iterated.
|
||||||
|
"""
|
||||||
|
print ("*" * 40) + " BODY"
|
||||||
|
for part in app_iter:
|
||||||
|
sys.stdout.write(part)
|
||||||
|
sys.stdout.flush()
|
||||||
|
yield part
|
||||||
|
print
|
||||||
|
|
||||||
|
|
||||||
|
class Router(object):
|
||||||
|
"""
|
||||||
|
WSGI middleware that maps incoming requests to WSGI apps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, mapper):
|
||||||
|
"""
|
||||||
|
Create a router for the given routes.Mapper.
|
||||||
|
|
||||||
|
Each route in `mapper` must specify a 'controller', which is a
|
||||||
|
WSGI app to call. You'll probably want to specify an 'action' as
|
||||||
|
well and have your controller be a wsgi.Controller, who will route
|
||||||
|
the request to the action method.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
mapper = routes.Mapper()
|
||||||
|
sc = ServerController()
|
||||||
|
|
||||||
|
# Explicit mapping of one route to a controller+action
|
||||||
|
mapper.connect(None, "/svrlist", controller=sc, action="list")
|
||||||
|
|
||||||
|
# Actions are all implicitly defined
|
||||||
|
mapper.resource("server", "servers", controller=sc)
|
||||||
|
|
||||||
|
# Pointing to an arbitrary WSGI app. You can specify the
|
||||||
|
# {path_info:.*} parameter so the target app can be handed just that
|
||||||
|
# section of the URL.
|
||||||
|
mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
|
||||||
|
"""
|
||||||
|
self.map = mapper
|
||||||
|
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||||
|
self.map)
|
||||||
|
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def __call__(self, req):
|
||||||
|
"""
|
||||||
|
Route the incoming request to a controller based on self.map.
|
||||||
|
If no match, return a 404.
|
||||||
|
"""
|
||||||
|
return self._router
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@webob.dec.wsgify
|
||||||
|
def _dispatch(req):
|
||||||
|
"""
|
||||||
|
Called by self._router after matching the incoming request to a route
|
||||||
|
and putting the information into req.environ. Either returns 404
|
||||||
|
or the routed WSGI app's response.
|
||||||
|
"""
|
||||||
|
match = req.environ['wsgiorg.routing_args'][1]
|
||||||
|
if not match:
|
||||||
|
return webob.exc.HTTPNotFound()
|
||||||
|
app = match['controller']
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
class Request(webob.Request):
|
||||||
|
"""Add some Openstack API-specific logic to the base webob.Request."""
|
||||||
|
|
||||||
|
def best_match_content_type(self):
|
||||||
|
"""Determine the requested response content-type."""
|
||||||
|
supported = ('application/json',)
|
||||||
|
bm = self.accept.best_match(supported)
|
||||||
|
return bm or 'application/json'
|
||||||
|
|
||||||
|
def get_content_type(self, allowed_content_types):
|
||||||
|
"""Determine content type of the request body."""
|
||||||
|
if not "Content-Type" in self.headers:
|
||||||
|
raise exception.InvalidContentType(content_type=None)
|
||||||
|
|
||||||
|
content_type = self.content_type
|
||||||
|
|
||||||
|
if content_type not in allowed_content_types:
|
||||||
|
raise exception.InvalidContentType(content_type=content_type)
|
||||||
|
else:
|
||||||
|
return content_type
|
||||||
|
|
||||||
|
|
||||||
|
class JSONRequestDeserializer(object):
|
||||||
|
def has_body(self, request):
|
||||||
|
"""
|
||||||
|
Returns whether a Webob.Request object will possess an entity body.
|
||||||
|
|
||||||
|
:param request: Webob.Request object
|
||||||
|
"""
|
||||||
|
if 'transfer-encoding' in request.headers:
|
||||||
|
return True
|
||||||
|
elif request.content_length > 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def from_json(self, datastring):
|
||||||
|
return json.loads(datastring)
|
||||||
|
|
||||||
|
def default(self, request):
|
||||||
|
if self.has_body(request):
|
||||||
|
return {'body': self.from_json(request.body)}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class JSONResponseSerializer(object):
|
||||||
|
|
||||||
|
def to_json(self, data):
|
||||||
|
def sanitizer(obj):
|
||||||
|
if isinstance(obj, datetime.datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
return json.dumps(data, default=sanitizer)
|
||||||
|
|
||||||
|
def default(self, response, result):
|
||||||
|
response.content_type = 'application/json'
|
||||||
|
response.body = self.to_json(result)
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""
|
||||||
|
WSGI app that handles (de)serialization and controller dispatch.
|
||||||
|
|
||||||
|
Reads routing information supplied by RoutesMiddleware and calls
|
||||||
|
the requested action method upon its deserializer, controller,
|
||||||
|
and serializer. Those three objects may implement any of the basic
|
||||||
|
controller action methods (create, update, show, index, delete)
|
||||||
|
along with any that may be specified in the api router. A 'default'
|
||||||
|
method may also be implemented to be used in place of any
|
||||||
|
non-implemented actions. Deserializer methods must accept a request
|
||||||
|
argument and return a dictionary. Controller methods must accept a
|
||||||
|
request argument. Additionally, they must also accept keyword
|
||||||
|
arguments that represent the keys returned by the Deserializer. They
|
||||||
|
may raise a webob.exc exception or return a dict, which will be
|
||||||
|
serialized by requested content type.
|
||||||
|
"""
|
||||||
|
def __init__(self, controller, deserializer, serializer):
|
||||||
|
"""
|
||||||
|
:param controller: object that implement methods created by routes lib
|
||||||
|
:param deserializer: object that supports webob request deserialization
|
||||||
|
through controller-like actions
|
||||||
|
:param serializer: object that supports webob response serialization
|
||||||
|
through controller-like actions
|
||||||
|
"""
|
||||||
|
self.controller = controller
|
||||||
|
self.serializer = serializer
|
||||||
|
self.deserializer = deserializer
|
||||||
|
|
||||||
|
@webob.dec.wsgify(RequestClass=Request)
|
||||||
|
def __call__(self, request):
|
||||||
|
"""WSGI method that controls (de)serialization and method dispatch."""
|
||||||
|
action_args = self.get_action_args(request.environ)
|
||||||
|
action = action_args.pop('action', None)
|
||||||
|
|
||||||
|
deserialized_request = self.dispatch(self.deserializer,
|
||||||
|
action, request)
|
||||||
|
action_args.update(deserialized_request)
|
||||||
|
|
||||||
|
action_result = self.dispatch(self.controller, action,
|
||||||
|
request, **action_args)
|
||||||
|
try:
|
||||||
|
response = webob.Response(request=request)
|
||||||
|
self.dispatch(self.serializer, action, response, action_result)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# return unserializable result (typically a webob exc)
|
||||||
|
except Exception:
|
||||||
|
return action_result
|
||||||
|
|
||||||
|
def dispatch(self, obj, action, *args, **kwargs):
|
||||||
|
"""Find action-specific method on self and call it."""
|
||||||
|
try:
|
||||||
|
method = getattr(obj, action)
|
||||||
|
except AttributeError:
|
||||||
|
method = getattr(obj, 'default')
|
||||||
|
|
||||||
|
return method(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_action_args(self, request_environment):
|
||||||
|
"""Parse dictionary created by routes library."""
|
||||||
|
try:
|
||||||
|
args = request_environment['wsgiorg.routing_args'][1].copy()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['controller']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
del args['format']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
class BasePasteFactory(object):
|
||||||
|
|
||||||
|
"""A base class for paste app and filter factories.
|
||||||
|
|
||||||
|
Sub-classes must override the KEY class attribute and provide
|
||||||
|
a __call__ method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
KEY = None
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
def __call__(self, global_conf, **local_conf):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _import_factory(self, local_conf):
|
||||||
|
"""Import an app/filter class.
|
||||||
|
|
||||||
|
Lookup the KEY from the PasteDeploy local conf and import the
|
||||||
|
class named there. This class can then be used as an app or
|
||||||
|
filter factory.
|
||||||
|
|
||||||
|
Note we support the <module>:<class> format.
|
||||||
|
|
||||||
|
Note also that if you do e.g.
|
||||||
|
|
||||||
|
key =
|
||||||
|
value
|
||||||
|
|
||||||
|
then ConfigParser returns a value with a leading newline, so
|
||||||
|
we strip() the value before using it.
|
||||||
|
"""
|
||||||
|
class_name = local_conf[self.KEY].replace(':', '.').strip()
|
||||||
|
return utils.import_class(class_name)
|
||||||
|
|
||||||
|
|
||||||
|
class AppFactory(BasePasteFactory):
|
||||||
|
|
||||||
|
"""A Generic paste.deploy app factory.
|
||||||
|
|
||||||
|
This requires balancer.app_factory to be set to a callable which returns a
|
||||||
|
WSGI app when invoked. The format of the name is <module>:<callable> e.g.
|
||||||
|
|
||||||
|
[app:apiv1app]
|
||||||
|
paste.app_factory = balancer.common.wsgi:app_factory
|
||||||
|
balancer.app_factory = balancer.api.v1:API
|
||||||
|
|
||||||
|
The WSGI app constructor must accept a ConfigOpts object and a local config
|
||||||
|
dict as its two arguments.
|
||||||
|
"""
|
||||||
|
print "DEBUG AppFactory start\n"
|
||||||
|
KEY = 'windc.app_factory'
|
||||||
|
|
||||||
|
def __call__(self, global_conf, **local_conf):
|
||||||
|
"""The actual paste.app_factory protocol method."""
|
||||||
|
print "DEBUG Call factory"
|
||||||
|
factory = self._import_factory(local_conf)
|
||||||
|
return factory(self.conf, **local_conf)
|
||||||
|
|
||||||
|
|
||||||
|
class FilterFactory(AppFactory):
|
||||||
|
|
||||||
|
"""A Generic paste.deploy filter factory.
|
||||||
|
|
||||||
|
This requires balancer.filter_factory to be set to a callable which returns
|
||||||
|
a WSGI filter when invoked. The format is <module>:<callable> e.g.
|
||||||
|
|
||||||
|
[filter:cache]
|
||||||
|
paste.filter_factory = balancer.common.wsgi:filter_factory
|
||||||
|
balancer.filter_factory = balancer.api.middleware.cache:CacheFilter
|
||||||
|
|
||||||
|
The WSGI filter constructor must accept a WSGI app, a ConfigOpts object and
|
||||||
|
a local config dict as its three arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
KEY = 'windc.filter_factory'
|
||||||
|
|
||||||
|
def __call__(self, global_conf, **local_conf):
|
||||||
|
"""The actual paste.filter_factory protocol method."""
|
||||||
|
factory = self._import_factory(local_conf)
|
||||||
|
|
||||||
|
def filter(app):
|
||||||
|
return factory(app, self.conf, **local_conf)
|
||||||
|
|
||||||
|
return filter
|
||||||
|
|
||||||
|
|
||||||
|
def setup_paste_factories(conf):
|
||||||
|
"""Set up the generic paste app and filter factories.
|
||||||
|
|
||||||
|
Set things up so that:
|
||||||
|
|
||||||
|
paste.app_factory = balancer.common.wsgi:app_factory
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
paste.filter_factory = balancer.common.wsgi:filter_factory
|
||||||
|
|
||||||
|
work correctly while loading PasteDeploy configuration.
|
||||||
|
|
||||||
|
The app factories are constructed at runtime to allow us to pass a
|
||||||
|
ConfigOpts object to the WSGI classes.
|
||||||
|
|
||||||
|
:param conf: a ConfigOpts object
|
||||||
|
"""
|
||||||
|
print "DEBUG Setup Factories\n"
|
||||||
|
global app_factory, filter_factory
|
||||||
|
app_factory = AppFactory(conf)
|
||||||
|
filter_factory = FilterFactory(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def teardown_paste_factories():
|
||||||
|
"""Reverse the effect of setup_paste_factories()."""
|
||||||
|
global app_factory, filter_factory
|
||||||
|
del app_factory
|
||||||
|
del filter_factory
|
||||||
|
|
||||||
|
|
||||||
|
def paste_deploy_app(paste_config_file, app_name, conf):
|
||||||
|
"""Load a WSGI app from a PasteDeploy configuration.
|
||||||
|
|
||||||
|
Use deploy.loadapp() to load the app from the PasteDeploy configuration,
|
||||||
|
ensuring that the supplied ConfigOpts object is passed to the app and
|
||||||
|
filter constructors.
|
||||||
|
|
||||||
|
:param paste_config_file: a PasteDeploy config file
|
||||||
|
:param app_name: the name of the app/pipeline to load from the file
|
||||||
|
:param conf: a ConfigOpts object to supply to the app and its filters
|
||||||
|
:returns: the WSGI app
|
||||||
|
"""
|
||||||
|
print "DEBUG Paste deploy\n"
|
||||||
|
setup_paste_factories(conf)
|
||||||
|
try:
|
||||||
|
return deploy.loadapp("config:%s" % paste_config_file, name=app_name)
|
||||||
|
finally:
|
||||||
|
teardown_paste_factories()
|
21
windc/windc/core/__init__.py
Normal file
21
windc/windc/core/__init__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import builder_set
|
||||||
|
|
||||||
|
builder_set.builders = builder_set.BuilderSet()
|
||||||
|
builder_set.builders.load()
|
96
windc/windc/core/api.py
Normal file
96
windc/windc/core/api.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from windc.db import api as db_api
|
||||||
|
from windc.core import change_events as events
|
||||||
|
|
||||||
|
def dc_get_index(conf, tenant_id):
|
||||||
|
dcs = db_api.datacenter_get_all(conf, tenant_id)
|
||||||
|
dc_list = [db_api.unpack_extra(dc) for dc in dcs]
|
||||||
|
return dc_list
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_dc(conf, params):
|
||||||
|
# We need to pack all attributes which are not defined by the model explicitly
|
||||||
|
dc_params = db_api.datacenter_pack_extra(params)
|
||||||
|
dc = db_api.datacenter_create(conf, dc_params)
|
||||||
|
event = events.Event(events.SCOPE_DATACENTER_CHANGE, events.ACTION_ADD)
|
||||||
|
events.change_event(conf, event, dc)
|
||||||
|
return dc.id
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete_dc(conf, tenant_id, datacenter_id):
|
||||||
|
dc = db_api.datacenter_get(conf, tenant_id, datacenter_id)
|
||||||
|
event = events.Event(events.SCOPE_DATACENTER_CHANGE, events.ACTION_DELETE)
|
||||||
|
events.change_event(conf, event, dc)
|
||||||
|
db_api.datacenter_destroy(conf, datacenter_id)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def dc_get_data(conf, tenant_id, datacenter_id):
|
||||||
|
dc = db_api.datacenter_get(conf, tenant_id, datacenter_id)
|
||||||
|
dc_data = db_api.unpack_extra(dc)
|
||||||
|
return dc_data
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_dc(conf, tenant_id, datacenter_id, body):
|
||||||
|
dc = db_api.datacenter_get(conf, tenant_id, datacenter_id)
|
||||||
|
old_dc = copy.deepcopy(dc)
|
||||||
|
db_api.pack_update(dc, body)
|
||||||
|
dc = db_api.datacenter_update(conf, datacenter_id, dc)
|
||||||
|
event = events.Event(events.SCOPE_DATACENTER_CHANGE, events.ACTION_MODIFY)
|
||||||
|
event.previous_state = old_dc
|
||||||
|
events.change_event(conf, event, dc)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def service_get_index(conf, tenant_id, datacenter_id):
|
||||||
|
srvcs = db_api.service_get_all_by_datacenter_id(conf, tenant_id, dtacenter_id)
|
||||||
|
srv_list = [db_api.unpack_extra(srv) for srv in srvcs]
|
||||||
|
return srv_list
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_service(conf, params):
|
||||||
|
# We need to pack all attributes which are not defined by the model explicitly
|
||||||
|
srv_params = db_api.service_pack_extra(params)
|
||||||
|
srv = db_api.service_create(conf, srv_params)
|
||||||
|
event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_ADD)
|
||||||
|
events.change_event(conf, event, srv)
|
||||||
|
return srv.id
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete_service(conf, tenant_id, datacenter_id, service_id):
|
||||||
|
srv = db_api.service_get(conf, service_id, tenant_id)
|
||||||
|
srv_data = db_api.unpack_extra(srv)
|
||||||
|
event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_DELETE)
|
||||||
|
events.change_event(conf, event, srv)
|
||||||
|
db_api.service_destroy(conf,service_id)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def service_get_data(conf, tenant_id, datacenter_id, service_id):
|
||||||
|
srv = db_api.service_get(conf,service_id, tenant_id)
|
||||||
|
srv_data = db_api.unpack_extra(srv)
|
||||||
|
return srv_data
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_service(conf, tenant_id, datacenter_id, service_id, body):
|
||||||
|
srv = db_api.service_get(conf, service_id, tenant_id)
|
||||||
|
old_srv = copy.deepcopy(srv)
|
||||||
|
db_api.pack_update(srv, body)
|
||||||
|
srv = db_api.service_update(conf, service_id, srv)
|
||||||
|
event = events.Event(events.SCOPE_SERVICE_CHANGE, events.ACTION_MODIFY)
|
||||||
|
event.previous_state = old_srv
|
||||||
|
events.change_event(conf, event, srv)
|
||||||
|
pass
|
33
windc/windc/core/builder.py
Normal file
33
windc/windc/core/builder.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
class Builder:
|
||||||
|
name = "Abstract Builder"
|
||||||
|
type = "abstract"
|
||||||
|
version = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name+' type: '+self.type+ ' version: ' + str(self.version)
|
||||||
|
|
||||||
|
def build(self, context, event, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
72
windc/windc/core/builder_set.py
Normal file
72
windc/windc/core/builder_set.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from builder import Builder
|
||||||
|
|
||||||
|
import imp
|
||||||
|
import os
|
||||||
|
import sys, glob
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
global builders
|
||||||
|
|
||||||
|
def load_from_file(filepath):
|
||||||
|
class_inst = None
|
||||||
|
|
||||||
|
mod_name,file_ext = os.path.splitext(os.path.split(filepath)[-1])
|
||||||
|
|
||||||
|
if file_ext.lower() == '.py':
|
||||||
|
py_mod = imp.load_source(mod_name, filepath)
|
||||||
|
|
||||||
|
elif file_ext.lower() == '.pyc':
|
||||||
|
py_mod = imp.load_compiled(mod_name, filepath)
|
||||||
|
|
||||||
|
if hasattr(py_mod, mod_name):
|
||||||
|
callable = getattr(__import__(mod_name),mod_name)
|
||||||
|
class_inst = callable()
|
||||||
|
|
||||||
|
return class_inst
|
||||||
|
|
||||||
|
|
||||||
|
class BuilderSet:
|
||||||
|
def __init__(self):
|
||||||
|
self.path = './windc/core/builders'
|
||||||
|
sys.path.append(self.path)
|
||||||
|
self.set = {}
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
|
||||||
|
files = glob.glob(self.path+'/*.py')
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
LOG.debug("Trying to load builder from file: %s", file)
|
||||||
|
try:
|
||||||
|
builder = load_from_file(file)
|
||||||
|
LOG.info("Buider '%s' loaded.", builder.name)
|
||||||
|
self.set[builder.type] = builder
|
||||||
|
except:
|
||||||
|
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||||
|
LOG.error('Can`t load builder from the file %s. Skip it.', file)
|
||||||
|
LOG.debug(repr(traceback.format_exception(exc_type, exc_value,
|
||||||
|
exc_traceback)))
|
||||||
|
|
||||||
|
|
||||||
|
def reload(self):
|
||||||
|
self.set = {}
|
||||||
|
self.load()
|
53
windc/windc/core/builders/ActiveDirectory.py
Normal file
53
windc/windc/core/builders/ActiveDirectory.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
from windc.core.builder import Builder
|
||||||
|
from windc.core import change_events as events
|
||||||
|
from windc.db import api as db_api
|
||||||
|
|
||||||
|
class ActiveDirectory(Builder):
|
||||||
|
def __init__(self):
|
||||||
|
self.name = "Active Directory Builder"
|
||||||
|
self.type = "active_directory_service"
|
||||||
|
self.version = 1
|
||||||
|
|
||||||
|
def build(self, context, event, data):
|
||||||
|
dc = db_api.unpack_extra(data)
|
||||||
|
if event.scope == events.SCOPE_SERVICE_CHANGE:
|
||||||
|
LOG.info ("Got service change event. Analysing..")
|
||||||
|
if self.do_analysis(context, event, dc):
|
||||||
|
self.plan_changes(context, event, dc)
|
||||||
|
else:
|
||||||
|
LOG.debug("Not in my scope. Skip event.")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_analysis(self, context, event, data):
|
||||||
|
LOG.debug("Doing analysis for data: %s", data)
|
||||||
|
zones = data['zones']
|
||||||
|
if data['type'] == self.type and len(zones) == 1:
|
||||||
|
LOG.debug("It is a service which I should build.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def plan_changes(self, context, event, data):
|
||||||
|
pass
|
||||||
|
|
37
windc/windc/core/builders/DataCenter.py
Normal file
37
windc/windc/core/builders/DataCenter.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
from windc.core.builder import Builder
|
||||||
|
from windc.core import change_events as events
|
||||||
|
|
||||||
|
class DataCenter(Builder):
|
||||||
|
def __init__(self):
|
||||||
|
self.name = "Data Center Builder"
|
||||||
|
self.type = "datacenter"
|
||||||
|
self.version = 1
|
||||||
|
|
||||||
|
def build(self, context, event, data):
|
||||||
|
if event.scope == events.SCOPE_DATACENTER_CHANGE:
|
||||||
|
LOG.info ("Got Data Center change event. Analysing...")
|
||||||
|
else:
|
||||||
|
LOG.debug("Not in my scope. Skip event.")
|
||||||
|
pass
|
||||||
|
|
53
windc/windc/core/change_events.py
Normal file
53
windc/windc/core/change_events.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
from windc.core import builder_set
|
||||||
|
#Declare events types
|
||||||
|
|
||||||
|
SCOPE_SERVICE_CHANGE = "Service"
|
||||||
|
SCOPE_DATACENTER_CHANGE = "Datacenter"
|
||||||
|
SCOPE_VM_CHANGE = "VMChange"
|
||||||
|
|
||||||
|
ACTION_ADD = "Add"
|
||||||
|
ACTION_MODIFY = "Modify"
|
||||||
|
ACTION_DELETE = "Delete"
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
scope = None
|
||||||
|
action = None
|
||||||
|
previous_state = None
|
||||||
|
def __init__(self, scope, action):
|
||||||
|
self.scope = scope
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
def change_event(conf, event, data):
|
||||||
|
LOG.info("Change event of type: %s ", event)
|
||||||
|
context = {}
|
||||||
|
context['conf'] = conf
|
||||||
|
for builder_type in builder_set.builders.set:
|
||||||
|
builder = builder_set.builders.set[builder_type]
|
||||||
|
builder.build(context, event, data)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
25
windc/windc/core/service_status.py
Normal file
25
windc/windc/core/service_status.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Possible load balancer statuses."""
|
||||||
|
|
||||||
|
BUILD = "BUILD"
|
||||||
|
ACTIVE = "ACTIVE"
|
||||||
|
PENDING_UPDATE = "PENDING_UPDATE"
|
||||||
|
ERROR = "ERROR"
|
1
windc/windc/db/__init__.py
Normal file
1
windc/windc/db/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from windc.db.api import *
|
175
windc/windc/db/api.py
Normal file
175
windc/windc/db/api.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Database storage API."""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from windc.db import models
|
||||||
|
from windc.db.session import get_session
|
||||||
|
from windc import exception
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# XXX(akscram): pack_ and unpack_ are helper methods to compatibility
|
||||||
|
def pack_extra(model, values):
|
||||||
|
obj_ref = model()
|
||||||
|
pack_update(obj_ref, values)
|
||||||
|
return obj_ref
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_extra(obj_ref):
|
||||||
|
obj_dict = dict(obj_ref.iteritems())
|
||||||
|
obj_dict.update(obj_dict.pop('extra', None) or {})
|
||||||
|
return obj_dict
|
||||||
|
|
||||||
|
|
||||||
|
def pack_update(obj_ref, values):
|
||||||
|
obj_dict = values.copy()
|
||||||
|
for k, v in values.iteritems():
|
||||||
|
if k in obj_ref.keys():
|
||||||
|
obj_ref[k] = obj_dict.pop(k)
|
||||||
|
if obj_dict:
|
||||||
|
if obj_ref['extra'] is not None:
|
||||||
|
obj_ref['extra'].update(obj_dict)
|
||||||
|
else:
|
||||||
|
obj_ref['extra'] = obj_dict.copy()
|
||||||
|
|
||||||
|
|
||||||
|
datacenter_pack_extra = functools.partial(pack_extra, models.DataCenter)
|
||||||
|
service_pack_extra = functools.partial(pack_extra, models.Service)
|
||||||
|
|
||||||
|
|
||||||
|
# Datacenter
|
||||||
|
|
||||||
|
|
||||||
|
def datacenter_get(conf, tenant_id, datacenter_id, session=None):
|
||||||
|
session = session or get_session(conf)
|
||||||
|
datacenter_ref = session.query(models.DataCenter).\
|
||||||
|
filter_by(id=datacenter_id).first()
|
||||||
|
if not datacenter_ref:
|
||||||
|
raise exception.DeviceNotFound(datacenter_id=datacenter_id)
|
||||||
|
return datacenter_ref
|
||||||
|
|
||||||
|
|
||||||
|
def datacenter_get_all(conf, tenant_id):
|
||||||
|
session = get_session(conf)
|
||||||
|
query = session.query(models.DataCenter).\
|
||||||
|
filter_by(tenant_id=tenant_id)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
def datacenter_create(conf, values):
|
||||||
|
session = get_session(conf)
|
||||||
|
with session.begin():
|
||||||
|
datacenter_ref = models.DataCenter()
|
||||||
|
datacenter_ref.update(values)
|
||||||
|
session.add(datacenter_ref)
|
||||||
|
return datacenter_ref
|
||||||
|
|
||||||
|
|
||||||
|
def datacenter_update(conf, datacenter_id, values):
|
||||||
|
session = get_session(conf)
|
||||||
|
with session.begin():
|
||||||
|
datacenter_ref = datacenter_get(conf, datacenter_id, session=session)
|
||||||
|
datacenter_ref.update(values)
|
||||||
|
return datacenter_ref
|
||||||
|
|
||||||
|
|
||||||
|
def datacenter_destroy(conf, datacenter_id):
|
||||||
|
session = get_session(conf)
|
||||||
|
with session.begin():
|
||||||
|
datacenter_ref = device_get(conf, datacenter_id, session=session)
|
||||||
|
session.delete(datacenter_ref)
|
||||||
|
|
||||||
|
# Service
|
||||||
|
|
||||||
|
|
||||||
|
def service_get(conf, service_id, tenant_id=None, session=None):
|
||||||
|
session = session or get_session(conf)
|
||||||
|
query = session.query(models.Service).filter_by(id=service_id)
|
||||||
|
if tenant_id:
|
||||||
|
query = query.filter_by(tenant_id=tenant_id)
|
||||||
|
service_ref = query.first()
|
||||||
|
if not service_ref:
|
||||||
|
raise exception.ServiceNotFound(service_ref=service_ref)
|
||||||
|
return service_ref
|
||||||
|
|
||||||
|
|
||||||
|
def service_get_all_by_project(conf, tenant_id):
|
||||||
|
session = get_session(conf)
|
||||||
|
query = session.query(models.Service).filter_by(tenant_id=tenant_id)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
def service_get_all_by_vm_id(conf, tenant_id, vm_id):
|
||||||
|
session = get_session(conf)
|
||||||
|
query = session.query(models.Service).distinct().\
|
||||||
|
filter_by(tenant_id=tenant_id).\
|
||||||
|
filter(vm_id == vm_id)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
def service_get_all_by_datacenter_id(conf, tenant_id, datacenter_id):
|
||||||
|
session = get_session(conf)
|
||||||
|
query = session.query(models.Service).filter_by(datacenter_id=datacenter_id)
|
||||||
|
service_refs = query.all()
|
||||||
|
if not service_refs:
|
||||||
|
raise exception.ServiceNotFound('No service '
|
||||||
|
'for the datacenter %s found'
|
||||||
|
% datacenter_id)
|
||||||
|
return service_refs
|
||||||
|
|
||||||
|
|
||||||
|
def service_create(conf, values):
|
||||||
|
session = get_session(conf)
|
||||||
|
with session.begin():
|
||||||
|
service_ref = models.Service()
|
||||||
|
service_ref.update(values)
|
||||||
|
session.add(service_ref)
|
||||||
|
return service_ref
|
||||||
|
|
||||||
|
|
||||||
|
def service_update(conf, service_id, values):
|
||||||
|
session = get_session(conf)
|
||||||
|
with session.begin():
|
||||||
|
service_ref = service_get(conf, service_id, session=session)
|
||||||
|
service_ref.update(values)
|
||||||
|
service_ref['updated_at'] = datetime.datetime.utcnow()
|
||||||
|
return service_ref
|
||||||
|
|
||||||
|
|
||||||
|
def service_destroy(conf, service_id):
|
||||||
|
session = get_session(conf)
|
||||||
|
with session.begin():
|
||||||
|
service_ref = service_get(conf, service_id, session=session)
|
||||||
|
session.delete(service_ref)
|
||||||
|
|
||||||
|
|
||||||
|
def service_count_active_by_datacenter(conf, datacenter_id):
|
||||||
|
session = get_session(conf)
|
||||||
|
with session.begin():
|
||||||
|
service_count = session.query(models.Service).\
|
||||||
|
filter_by(datacenter_id=datacenter_id).\
|
||||||
|
filter_by(status=service_status.ACTIVE).\
|
||||||
|
count()
|
||||||
|
return service_count
|
||||||
|
|
||||||
|
|
76
windc/windc/db/base.py
Normal file
76
windc/windc/db/base.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Base classes and custome fields for balancer models."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import object_mapper
|
||||||
|
from sqlalchemy.types import TypeDecorator
|
||||||
|
from sqlalchemy import Text
|
||||||
|
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class DictBase(object):
|
||||||
|
def to_dict(self):
|
||||||
|
return dict(self.iteritems())
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return getattr(self, key)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return getattr(self, key, default)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return (col.name for col in object_mapper(self).columns)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
def update(self, values):
|
||||||
|
for key, value in values.iteritems():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.copy()
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def iteritems(self):
|
||||||
|
items = []
|
||||||
|
for key in self:
|
||||||
|
value = getattr(self, key)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.copy()
|
||||||
|
items.append((key, value))
|
||||||
|
return iter(items)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonBlob(TypeDecorator):
|
||||||
|
|
||||||
|
impl = Text
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
return json.dumps(value)
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
return json.loads(value)
|
4
windc/windc/db/migrate_repo/README
Normal file
4
windc/windc/db/migrate_repo/README
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
This is a database migration repository.
|
||||||
|
|
||||||
|
More information at
|
||||||
|
http://code.google.com/p/sqlalchemy-migrate/
|
0
windc/windc/db/migrate_repo/__init__.py
Normal file
0
windc/windc/db/migrate_repo/__init__.py
Normal file
5
windc/windc/db/migrate_repo/manage.py
Normal file
5
windc/windc/db/migrate_repo/manage.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from migrate.versioning.shell import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(debug='False')
|
25
windc/windc/db/migrate_repo/migrate.cfg
Normal file
25
windc/windc/db/migrate_repo/migrate.cfg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[db_settings]
|
||||||
|
# Used to identify which repository this database is versioned under.
|
||||||
|
# You can use the name of your project.
|
||||||
|
repository_id=windc
|
||||||
|
|
||||||
|
# The name of the database table used to track the schema version.
|
||||||
|
# This name shouldn't already be used by your project.
|
||||||
|
# If this is changed once a database is under version control, you'll need to
|
||||||
|
# change the table name in each database too.
|
||||||
|
version_table=migrate_version
|
||||||
|
|
||||||
|
# When committing a change script, Migrate will attempt to generate the
|
||||||
|
# sql for all supported databases; normally, if one of them fails - probably
|
||||||
|
# because you don't have that database installed - it is ignored and the
|
||||||
|
# commit continues, perhaps ending successfully.
|
||||||
|
# Databases in this list MUST compile successfully during a commit, or the
|
||||||
|
# entire commit will fail. List the databases your application will actually
|
||||||
|
# be using to ensure your updates to that database work properly.
|
||||||
|
# This must be a list; example: ['postgres','sqlite']
|
||||||
|
required_dbs=[]
|
||||||
|
|
||||||
|
# When creating new change scripts, Migrate will stamp the new script with
|
||||||
|
# a version number. By default this is latest_version + 1. You can set this
|
||||||
|
# to 'true' to tell Migrate to use the UTC timestamp instead.
|
||||||
|
use_timestamp_numbering=False
|
@ -0,0 +1,41 @@
|
|||||||
|
from sqlalchemy.schema import MetaData, Table, Column, ForeignKey
|
||||||
|
from sqlalchemy.types import Integer, String, Text, DateTime
|
||||||
|
|
||||||
|
|
||||||
|
meta = MetaData()
|
||||||
|
|
||||||
|
Table('datacenter', meta,
|
||||||
|
Column('id', String(32), primary_key=True),
|
||||||
|
Column('name', String(255)),
|
||||||
|
Column('type', String(255)),
|
||||||
|
Column('version', String(255)),
|
||||||
|
Column('tenant_id',String(100)),
|
||||||
|
Column('KMS', String(80)),
|
||||||
|
Column('WSUS', String(80)),
|
||||||
|
Column('extra', Text()),
|
||||||
|
)
|
||||||
|
|
||||||
|
Table('service', meta,
|
||||||
|
Column('id', String(32), primary_key=True),
|
||||||
|
Column('datacenter_id', String(32), ForeignKey('datacenter.id')),
|
||||||
|
Column('name', String(255)),
|
||||||
|
Column('type', String(40)),
|
||||||
|
Column('status', String(255)),
|
||||||
|
Column('tenant_id', String(40)),
|
||||||
|
Column('created_at', DateTime, nullable=False),
|
||||||
|
Column('updated_at', DateTime, nullable=False),
|
||||||
|
Column('deployed', String(40)),
|
||||||
|
Column('vm_id',String(40)),
|
||||||
|
Column('extra', Text()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
meta.create_all()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(migrate_engine):
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
meta.drop_all()
|
0
windc/windc/db/migrate_repo/versions/__init__.py
Normal file
0
windc/windc/db/migrate_repo/versions/__init__.py
Normal file
81
windc/windc/db/models.py
Normal file
81
windc/windc/db/models.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
#Copyright by Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""SQLAlchemy models for balancer data."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
from sqlalchemy import (Column, ForeignKey, Integer, String, Boolean,
|
||||||
|
DateTime)
|
||||||
|
|
||||||
|
from windc.db.base import Base, DictBase, JsonBlob
|
||||||
|
|
||||||
|
|
||||||
|
def create_uuid():
|
||||||
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
class DataCenter(DictBase, Base):
|
||||||
|
"""
|
||||||
|
Represents a data center - a Windows Environment with different
|
||||||
|
services in it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = 'datacenter'
|
||||||
|
id = Column(String(32), primary_key=True, default=create_uuid)
|
||||||
|
name = Column(String(255))
|
||||||
|
type = Column(String(255))
|
||||||
|
version = Column(String(255))
|
||||||
|
tenant_id = Column(String(100))
|
||||||
|
KMS = Column(String(80))
|
||||||
|
WSUS = Column(String(80))
|
||||||
|
extra = Column(JsonBlob())
|
||||||
|
|
||||||
|
|
||||||
|
class Service(DictBase, Base):
|
||||||
|
"""
|
||||||
|
Represents an instance of service.
|
||||||
|
|
||||||
|
:var name: string
|
||||||
|
:var type: string - type of service (e.g. Active Directory)
|
||||||
|
:var tenant_id: string - OpenStack tenant ID
|
||||||
|
:var extra: dictionary - additional attributes
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = 'service'
|
||||||
|
id = Column(String(32), primary_key=True, default=create_uuid)
|
||||||
|
datacenter_id = Column(String(32), ForeignKey('datacenter.id'))
|
||||||
|
name = Column(String(255))
|
||||||
|
type = Column(String(40))
|
||||||
|
status = Column(String(40))
|
||||||
|
tenant_id = Column(String(40))
|
||||||
|
created_at = Column(DateTime, default=datetime.datetime.utcnow,
|
||||||
|
nullable=False)
|
||||||
|
updated_at = Column(DateTime, default=datetime.datetime.utcnow,
|
||||||
|
onupdate=datetime.datetime.utcnow,
|
||||||
|
nullable=False)
|
||||||
|
deployed = Column(String(40))
|
||||||
|
vm_id = Column(String(40))
|
||||||
|
extra = Column(JsonBlob())
|
||||||
|
datacenter = relationship(DataCenter,
|
||||||
|
backref=backref('service', order_by=id),
|
||||||
|
uselist=False)
|
||||||
|
|
||||||
|
def register_models(engine):
|
||||||
|
"""Create tables for models."""
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
122
windc/windc/db/session.py
Normal file
122
windc/windc/db/session.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Session management functions."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from migrate.versioning import api as versioning_api
|
||||||
|
from migrate import exceptions as versioning_exceptions
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.engine.url import make_url
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import NullPool
|
||||||
|
from sqlalchemy.exc import DisconnectionError
|
||||||
|
|
||||||
|
from windc.common import cfg
|
||||||
|
from windc.db import migrate_repo
|
||||||
|
|
||||||
|
|
||||||
|
DB_GROUP_NAME = 'sql'
|
||||||
|
DB_OPTIONS = (
|
||||||
|
cfg.IntOpt('idle_timeout', default=3600),
|
||||||
|
cfg.StrOpt('connection', default='sqlite:///windc.sqlite'),
|
||||||
|
)
|
||||||
|
|
||||||
|
MAKER = None
|
||||||
|
ENGINE = None
|
||||||
|
|
||||||
|
|
||||||
|
class MySQLPingListener(object):
|
||||||
|
"""
|
||||||
|
Ensures that MySQL connections checked out of the
|
||||||
|
pool are alive.
|
||||||
|
|
||||||
|
Borrowed from:
|
||||||
|
http://groups.google.com/group/sqlalchemy/msg/a4ce563d802c929f
|
||||||
|
|
||||||
|
Error codes caught:
|
||||||
|
* 2006 MySQL server has gone away
|
||||||
|
* 2013 Lost connection to MySQL server during query
|
||||||
|
* 2014 Commands out of sync; you can't run this command now
|
||||||
|
* 2045 Can't open shared memory; no answer from server (%lu)
|
||||||
|
* 2055 Lost connection to MySQL server at '%s', system error: %d
|
||||||
|
|
||||||
|
from http://dev.mysql.com/doc/refman/5.6/en/error-messages-client.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
def checkout(self, dbapi_con, con_record, con_proxy):
|
||||||
|
try:
|
||||||
|
dbapi_con.cursor().execute('select 1')
|
||||||
|
except dbapi_con.OperationalError, ex:
|
||||||
|
if ex.args[0] in (2006, 2013, 2014, 2045, 2055):
|
||||||
|
logging.warn('Got mysql server has gone away: %s', ex)
|
||||||
|
raise DisconnectionError("Database server went away")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_session(conf, autocommit=True, expire_on_commit=False):
|
||||||
|
"""Return a SQLAlchemy session."""
|
||||||
|
global MAKER
|
||||||
|
|
||||||
|
if MAKER is None:
|
||||||
|
MAKER = sessionmaker(autocommit=autocommit,
|
||||||
|
expire_on_commit=expire_on_commit)
|
||||||
|
engine = get_engine(conf)
|
||||||
|
MAKER.configure(bind=engine)
|
||||||
|
session = MAKER()
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine(conf):
|
||||||
|
"""Return a SQLAlchemy engine."""
|
||||||
|
global ENGINE
|
||||||
|
|
||||||
|
register_conf_opts(conf)
|
||||||
|
connection_url = make_url(conf.sql.connection)
|
||||||
|
if ENGINE is None or not ENGINE.url == connection_url:
|
||||||
|
engine_args = {'pool_recycle': conf.sql.idle_timeout,
|
||||||
|
'echo': False,
|
||||||
|
'convert_unicode': True
|
||||||
|
}
|
||||||
|
if 'sqlite' in connection_url.drivername:
|
||||||
|
engine_args['poolclass'] = NullPool
|
||||||
|
if 'mysql' in connection_url.drivername:
|
||||||
|
engine_args['listeners'] = [MySQLPingListener()]
|
||||||
|
ENGINE = create_engine(conf.sql.connection, **engine_args)
|
||||||
|
return ENGINE
|
||||||
|
|
||||||
|
|
||||||
|
def register_conf_opts(conf, options=DB_OPTIONS, group=DB_GROUP_NAME):
|
||||||
|
"""Register database options."""
|
||||||
|
|
||||||
|
conf.register_group(cfg.OptGroup(name=group))
|
||||||
|
conf.register_opts(options, group=group)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(conf):
|
||||||
|
register_conf_opts(conf)
|
||||||
|
repo_path = os.path.abspath(os.path.dirname(migrate_repo.__file__))
|
||||||
|
try:
|
||||||
|
versioning_api.upgrade(conf.sql.connection, repo_path)
|
||||||
|
except versioning_exceptions.DatabaseNotControlledError:
|
||||||
|
versioning_api.version_control(conf.sql.connection, repo_path)
|
||||||
|
versioning_api.upgrade(conf.sql.connection, repo_path)
|
0
windc/windc/drivers/__init__.py
Normal file
0
windc/windc/drivers/__init__.py
Normal file
50
windc/windc/exception.py
Normal file
50
windc/windc/exception.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""Balancer base exception handling."""
|
||||||
|
|
||||||
|
import webob.exc as exception
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(exception.HTTPNotFound):
|
||||||
|
message = 'Resource not found.'
|
||||||
|
|
||||||
|
def __init__(self, message=None, **kwargs):
|
||||||
|
super(NotFound, self).__init__(message)
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceNotFound(NotFound):
|
||||||
|
message = 'Device not found'
|
||||||
|
|
||||||
|
|
||||||
|
class NoValidDevice(NotFound):
|
||||||
|
message = 'Suitable device not found'
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceNotFound(NotFound):
|
||||||
|
message = 'LoadBalancer not found'
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceConflict(exception.HTTPConflict):
|
||||||
|
message = 'Conflict while device deleting'
|
||||||
|
|
||||||
|
def __init__(self, message=None, **kwargs):
|
||||||
|
super(DeviceConflict, self).__init__(message)
|
||||||
|
self.kwargs = kwargs
|
66
windc/windc/utils.py
Normal file
66
windc/windc/utils.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import webob.exc
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def http_success_code(code):
|
||||||
|
"""Attaches response code to a method.
|
||||||
|
|
||||||
|
This decorator associates a response code with a method. Note
|
||||||
|
that the function attributes are directly manipulated; the method
|
||||||
|
is not wrapped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
func.wsgi_code = code
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def verify_tenant(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def __inner(self, req, tenant_id, *args, **kwargs):
|
||||||
|
if hasattr(req, 'context') and tenant_id != req.context.tenant_id:
|
||||||
|
LOG.info('User is not authorized to access this tenant.')
|
||||||
|
raise webob.exc.HTTPUnauthorized
|
||||||
|
return func(self, req, tenant_id, *args, **kwargs)
|
||||||
|
return __inner
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def __inner(self, req, *args, **kwargs):
|
||||||
|
if hasattr(req, 'context') and not req.context.is_admin:
|
||||||
|
LOG.info('User has no admin priviledges.')
|
||||||
|
raise webob.exc.HTTPUnauthorized
|
||||||
|
return func(self, req, *args, **kwargs)
|
||||||
|
return __inner
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def save_and_reraise_exception():
|
||||||
|
"""Save current exception, run some code and then re-raise.
|
||||||
|
|
||||||
|
In some cases the exception context can be cleared, resulting in None
|
||||||
|
being attempted to be reraised after an exception handler is run. This
|
||||||
|
can happen when eventlet switches greenthreads or when running an
|
||||||
|
exception handler, code raises and catches an exception. In both
|
||||||
|
cases the exception context will be cleared.
|
||||||
|
|
||||||
|
To work around this, we save the exception state, run handler code, and
|
||||||
|
then re-raise the original exception. If another exception occurs, the
|
||||||
|
saved exception is logged and the new exception is reraised.
|
||||||
|
"""
|
||||||
|
type_, value, traceback = sys.exc_info()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception:
|
||||||
|
LOG.error('Original exception being dropped',
|
||||||
|
exc_info=(type_, value, traceback))
|
||||||
|
raise
|
||||||
|
raise type_, value, traceback
|
49
windc/windc/version.py
Normal file
49
windc/windc/version.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Determine version of Skeleton library"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from skeleton.vcsversion import version_info
|
||||||
|
except ImportError:
|
||||||
|
version_info = {'branch_nick': u'LOCALBRANCH',
|
||||||
|
'revision_id': 'LOCALREVISION',
|
||||||
|
'revno': 0}
|
||||||
|
|
||||||
|
SKELETON_VERSION = ['2011', '3']
|
||||||
|
YEAR, COUNT = SKELETON_VERSION
|
||||||
|
|
||||||
|
FINAL = False # This becomes true at Release Candidate time
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_version_string():
|
||||||
|
return '.'.join([YEAR, COUNT])
|
||||||
|
|
||||||
|
|
||||||
|
def version_string():
|
||||||
|
if FINAL:
|
||||||
|
return canonical_version_string()
|
||||||
|
else:
|
||||||
|
return '%s-dev' % (canonical_version_string(),)
|
||||||
|
|
||||||
|
|
||||||
|
def vcs_version_string():
|
||||||
|
return "%s:%s" % (version_info['branch_nick'], version_info['revision_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def version_string_with_vcs():
|
||||||
|
return "%s-%s" % (canonical_version_string(), vcs_version_string())
|
Loading…
x
Reference in New Issue
Block a user