Merge in upstream to avoid merge conflict.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable-msg=C0103
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
@@ -17,20 +16,17 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Teller API daemon.
|
||||
Glance API Server
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# If ../parallax/__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, 'teller', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
sys.path.append(ROOT_DIR)
|
||||
|
||||
from glance.common import flags
|
||||
from glance.common import utils
|
||||
@@ -38,15 +34,20 @@ from glance.common import server
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_integer('teller_port', 9292, 'Teller port')
|
||||
flags.DEFINE_string('api_host', '0.0.0.0', 'API server lives at this address')
|
||||
flags.DEFINE_integer('api_port', 9292, 'API server listens on this port')
|
||||
|
||||
|
||||
def main(_args):
|
||||
# NOTE(sirp): importing in main so that eventlet is imported AFTER daemonization
|
||||
# see https://bugs.launchpad.net/bugs/687661
|
||||
# NOTE(sirp): importing in main so that eventlet is imported AFTER
|
||||
# daemonization. See https://bugs.launchpad.net/bugs/687661
|
||||
from glance.common import wsgi
|
||||
from glance.teller import controllers
|
||||
wsgi.run_server(controllers.API(), FLAGS.teller_port)
|
||||
from glance.server import API
|
||||
server = wsgi.Server()
|
||||
server.start(API(), FLAGS.api_port, host=FLAGS.api_host)
|
||||
server.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
server.serve('teller-server', main)
|
||||
server.serve('glance-api', main)
|
||||
|
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# pylint: disable-msg=C0103
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
@@ -18,19 +17,15 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""
|
||||
Parallax API daemon.
|
||||
Reference implementation server for Glance Registry
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# If ../parallax/__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, 'parallax', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
sys.path.append(ROOT_DIR)
|
||||
|
||||
from glance.common import flags
|
||||
from glance.common import utils
|
||||
@@ -38,15 +33,22 @@ from glance.common import server
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_integer('parallax_port', 9191, 'Parallax port')
|
||||
flags.DEFINE_string('registry_host', '0.0.0.0',
|
||||
'Registry server lives at this address')
|
||||
flags.DEFINE_integer('registry_port', 9191,
|
||||
'Registry server listens on this port')
|
||||
|
||||
|
||||
def main(_args):
|
||||
# NOTE(sirp): importing in main so that eventlet is imported AFTER daemonization
|
||||
# see https://bugs.launchpad.net/bugs/687661
|
||||
# NOTE(sirp): importing in main so that eventlet is imported AFTER
|
||||
# daemonization. See https://bugs.launchpad.net/bugs/687661
|
||||
from glance.common import wsgi
|
||||
from glance.parallax import controllers
|
||||
wsgi.run_server(controllers.API(), FLAGS.parallax_port)
|
||||
from glance.registry.server import API
|
||||
server = wsgi.Server()
|
||||
server.start(API(), FLAGS.registry_port, host=FLAGS.registry_host)
|
||||
server.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
server.serve('parallax-server', main)
|
||||
server.serve('glance-registry', main)
|
||||
|
97
doc/Makefile
Normal file
97
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."
|
154
doc/source/_static/jquery.tweet.js
Normal file
154
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
doc/source/_static/tweaks.css
Normal file
65
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
doc/source/_templates/.placeholder
Normal file
0
doc/source/_templates/.placeholder
Normal file
86
doc/source/_theme/layout.html
Normal file
86
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
doc/source/_theme/theme.conf
Normal file
5
doc/source/_theme/theme.conf
Normal file
@@ -0,0 +1,5 @@
|
||||
[theme]
|
||||
inherit = sphinxdoc
|
||||
stylesheet = sphinxdoc.css
|
||||
pygments_style = friendly
|
||||
|
231
doc/source/client.rst
Normal file
231
doc/source/client.rst
Normal file
@@ -0,0 +1,231 @@
|
||||
..
|
||||
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.
|
||||
|
||||
Using Glance Programmatically with Glance's Client
|
||||
==================================================
|
||||
|
||||
While it is perfectly acceptable to issue HTTP requests directly to Glance
|
||||
via its RESTful API, sometimes it is better to be able to access and modify
|
||||
image resources via a client class that removes some of the complexity and
|
||||
tedium of dealing with raw HTTP requests.
|
||||
|
||||
Glance includes a client class for just this purpose. You can retrieve
|
||||
metadata about an image, change metadata about an image, remove images, and
|
||||
of course retrieve an image itself via this client class.
|
||||
|
||||
Below are some examples of using Glance's Client class. We assume that
|
||||
there is a Glance server running at the address `glance.example.com`
|
||||
on port `9292`.
|
||||
|
||||
Requesting a List of Public VM Images
|
||||
-------------------------------------
|
||||
|
||||
We want to see a list of available virtual machine images that the Glance
|
||||
server knows about.
|
||||
|
||||
Using Glance's Client, we can do this using the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.example.com", 9292)
|
||||
|
||||
print c.get_images()
|
||||
|
||||
|
||||
Requesting Detailed Metadata on Public VM Images
|
||||
------------------------------------------------
|
||||
|
||||
We want to see more detailed information on available virtual machine images
|
||||
that the Glance server knows about.
|
||||
|
||||
Using Glance's Client, we can do this using the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.example.com", 9292)
|
||||
|
||||
print c.get_images_detailed()
|
||||
|
||||
|
||||
Requesting Detailed Metadata on a Specific Image
|
||||
------------------------------------------------
|
||||
|
||||
We want to see detailed information for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get metadata about the
|
||||
first public image returned, we can use the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.example.com", 9292)
|
||||
|
||||
print c.get_image_meta("http://glance.example.com/images/1")
|
||||
|
||||
|
||||
Retrieving a Virtual Machine Image
|
||||
----------------------------------
|
||||
|
||||
We want to retrieve that actual raw data for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get both the metadata about the
|
||||
first public image returned and its image data, we can use the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.example.com", 9292)
|
||||
|
||||
meta, image_file = c.get_image("http://glance.example.com/images/1")
|
||||
|
||||
print meta
|
||||
|
||||
f = open('some_local_file', 'wb')
|
||||
for chunk in image_file:
|
||||
f.write(chunk)
|
||||
f.close()
|
||||
|
||||
Note that the return from Client.get_image() is a tuple of (`metadata`, `file`)
|
||||
where `metadata` is a mapping of metadata about the image and `file` is a
|
||||
generator that yields chunks of image data.
|
||||
|
||||
Adding a New Virtual Machine Image
|
||||
----------------------------------
|
||||
|
||||
We have created a new virtual machine image in some way (created a
|
||||
"golden image" or snapshotted/backed up an existing image) and we
|
||||
wish to do two things:
|
||||
|
||||
* Store the disk image data in Glance
|
||||
* Store metadata about this image in Glance
|
||||
|
||||
We can do the above two activities in a single call to the Glance client.
|
||||
Assuming, like in the examples above, that a Glance API server is running
|
||||
at `glance.example.com`, we issue a call to `glance.client.Client.add_image`.
|
||||
|
||||
The method signature is as follows::
|
||||
|
||||
glance.client.Client.add_image(image_meta, image_data=None)
|
||||
|
||||
The `image_meta` argument is a mapping containing various image metadata. The
|
||||
`image_data` argument is the disk image data.
|
||||
|
||||
The list of metadata that `image_meta` can contain are listed below.
|
||||
|
||||
* `name`
|
||||
|
||||
This key/value is required. Its value should be the name of the image.
|
||||
|
||||
Note that the name of an image *is not unique to a Glance node*. It
|
||||
would be an unrealistic expectation of users to know all the unique
|
||||
names of all other user's images.
|
||||
|
||||
* `id`
|
||||
|
||||
This key/value is optional.
|
||||
|
||||
When present, Glance will use the supplied identifier for the image.
|
||||
If the identifier already exists in that Glance node, then a
|
||||
`glance.common.exception.Duplicate` will be raised.
|
||||
|
||||
When this key/value is *not* present, Glance will generate an identifier
|
||||
for the image and return this identifier in the response (see below)
|
||||
|
||||
* `store`
|
||||
|
||||
This key/value is optional. Valid values are one of `file` or `swift`
|
||||
|
||||
When present, Glance will attempt to store the disk image data in the
|
||||
backing store indicated by the value. If the Glance node does not support
|
||||
the backing store, Glance will raise a `glance.common.exception.BadRequest`
|
||||
|
||||
When not present, Glance will store the disk image data in the backing
|
||||
store that is marked default. See the configuration option `default_store`
|
||||
for more information.
|
||||
|
||||
* `type`
|
||||
|
||||
This key/values is required. Valid values are one of `kernel`, `machine`,
|
||||
`raw`, or `ramdisk`.
|
||||
|
||||
* `size`
|
||||
|
||||
This key/value is optional.
|
||||
|
||||
When present, Glance assumes that the expected size of the request body
|
||||
will be the value. If the length in bytes of the request body *does not
|
||||
match* the value, Glance will raise a `glance.common.exception.BadRequest`
|
||||
|
||||
When not present, Glance will calculate the image's size based on the size
|
||||
of the request body.
|
||||
|
||||
* `is_public`
|
||||
|
||||
This key/value is optional.
|
||||
|
||||
When present, Glance converts the value to a boolean value, so "on, 1, true"
|
||||
are all true values. When true, the image is marked as a public image,
|
||||
meaning that any user may view its metadata and may read the disk image from
|
||||
Glance.
|
||||
|
||||
When not present, the image is assumed to be *not public* and specific to
|
||||
a user.
|
||||
|
||||
* `properties`
|
||||
|
||||
This key/value is optional.
|
||||
|
||||
When present, the value is assumed to be a mapping of free-form key/value
|
||||
attributes to store with the image.
|
||||
|
||||
For example, if the following is the value of the `properties` key in the
|
||||
`image_meta` argument::
|
||||
|
||||
{'distro': 'Ubuntu 10.10'}
|
||||
|
||||
Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the
|
||||
image in Glance.
|
||||
|
||||
There is no limit on the number of free-form key/value attributes that can
|
||||
be attached to the image with `properties`. However, keep in mind that there
|
||||
is a 8K limit on the size of all HTTP headers sent in a request and this
|
||||
number will effectively limit the number of image properties.
|
||||
|
||||
As a complete example, the following code would add a new machine image to
|
||||
Glance::
|
||||
|
||||
from glance.client import Client
|
||||
|
||||
c = Client("glance.example.com", 9292)
|
||||
|
||||
meta = {'name': 'Ubuntu 10.10 5G',
|
||||
'type': 'machine',
|
||||
'is_public': True,
|
||||
'properties': {'distro': 'Ubuntu 10.10'}}
|
||||
|
||||
new_meta = c.add_image(meta, open('/path/to/image.tar.gz'))
|
||||
|
||||
print 'Stored image. Got identifier: %s' % new_meta['id']
|
83
doc/source/community.rst
Normal file
83
doc/source/community.rst
Normal file
@@ -0,0 +1,83 @@
|
||||
..
|
||||
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.
|
||||
|
||||
Getting Involved
|
||||
================
|
||||
|
||||
The Glance community is a very friendly group and there are places online to join in with the
|
||||
community. Feel free to ask questions. This document points you to some of the places where you can
|
||||
communicate with people.
|
||||
|
||||
How to Join the OpenStack Community
|
||||
-----------------------------------
|
||||
|
||||
Our community welcomes all people interested in open source cloud computing, and there are no formal
|
||||
membership requirements. The best way to join the community is to talk with others online or at a meetup
|
||||
and offer contributions through Launchpad, the wiki, or blogs. We welcome all types of contributions,
|
||||
from blueprint designs to documentation to testing to deployment scripts.
|
||||
|
||||
Contributing Code
|
||||
-----------------
|
||||
|
||||
To contribute code, sign up for a Launchpad account and sign a contributor license agreement,
|
||||
available on the `<http://wiki.openstack.org/CLA>`_. Once the CLA is signed you
|
||||
can contribute code through the Bazaar version control system which is related to your Launchpad account.
|
||||
|
||||
#openstack on Freenode IRC Network
|
||||
----------------------------------
|
||||
|
||||
There is a very active chat channel at `<irc://freenode.net/#openstack>`_. This
|
||||
is usually the best place to ask questions and find your way around. IRC stands for Internet Relay
|
||||
Chat and it is a way to chat online in real time. You can also ask a question and come back to the
|
||||
log files to read the answer later. Logs for the #openstack IRC channel are stored at
|
||||
`<http://eavesdrop.openstack.org/irclogs/>`_.
|
||||
|
||||
OpenStack Wiki
|
||||
--------------
|
||||
|
||||
The wiki is a living source of knowledge. It is edited by the community, and
|
||||
has collections of links and other sources of information. Typically the pages are a good place
|
||||
to write drafts for specs or documentation, describe a blueprint, or collaborate with others.
|
||||
|
||||
`OpenStack Wiki <http://wiki.openstack.org/>`_
|
||||
|
||||
Glance on Launchpad
|
||||
-------------------
|
||||
|
||||
Launchpad is a code hosting service that hosts the Glance source code. From
|
||||
Launchpad you can report bugs, ask questions, and register blueprints (feature requests).
|
||||
|
||||
* `Learn about how to use bzr with launchpad <http://wiki.openstack.org/LifeWithBzrAndLaunchpad>`_
|
||||
* `Launchpad Glance Page <http://launchpad.net/glance>`_
|
||||
|
||||
OpenStack Blog
|
||||
--------------
|
||||
|
||||
The OpenStack blog includes a weekly newsletter that aggregates OpenStack news
|
||||
from around the internet, as well as providing inside information on upcoming
|
||||
events and posts from OpenStack contributors.
|
||||
|
||||
`OpenStack Blog <http://openstack.org/blog>`_
|
||||
|
||||
See also: `Planet OpenStack <http://planet.openstack.org/>`_, aggregating blogs
|
||||
about OpenStack from around the internet into a single feed. If you'd like to contribute to this blog
|
||||
aggregation with your blog posts, there are instructions for `adding your blog <http://wiki.openstack.org/AddingYourBlog>`_.
|
||||
|
||||
Twitter
|
||||
-------
|
||||
|
||||
Because all the cool kids do it: `@openstack <http://twitter.com/openstack>`_. Also follow the
|
||||
`#openstack <http://search.twitter.com/search?q=%23openstack>`_ tag for relevant tweets.
|
@@ -31,8 +31,7 @@ import sys, os
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.append([os.path.abspath('../teller'),
|
||||
os.path.abspath('../parallax'),
|
||||
sys.path.append([os.path.abspath('../glance'),
|
||||
os.path.abspath('..'),
|
||||
os.path.abspath('../bin')
|
||||
])
|
||||
@@ -45,7 +44,11 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo',
|
||||
todo_include_todos = True
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = []
|
||||
if os.getenv('HUDSON_PUBLISH_DOCS'):
|
||||
templates_path = ['_ga', '_templates']
|
||||
else:
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
@@ -104,14 +107,15 @@ show_authors = True
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
modindex_common_prefix = ['parallax.','teller.']
|
||||
modindex_common_prefix = ['glance.']
|
||||
|
||||
|
||||
# -- 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 = 'default'
|
||||
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
|
||||
@@ -119,7 +123,7 @@ html_theme = 'default'
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
#html_theme_path = ['_theme']
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
|
294
doc/source/glanceapi.rst
Normal file
294
doc/source/glanceapi.rst
Normal file
@@ -0,0 +1,294 @@
|
||||
..
|
||||
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.
|
||||
|
||||
The Glance REST API
|
||||
===================
|
||||
|
||||
Glance has a RESTful API that exposes both metadata about registered virtual
|
||||
machine images and the image data itself.
|
||||
|
||||
A host that runs the `bin/glance-api` service is said to be a *Glance API
|
||||
Server*.
|
||||
|
||||
Assume there is a Glance API server running at the URL
|
||||
http://glance.example.com.
|
||||
|
||||
Let's walk through how a user might request information from this server.
|
||||
|
||||
Requesting a List of Public VM Images
|
||||
-------------------------------------
|
||||
|
||||
We want to see a list of available virtual machine images that the Glance
|
||||
server knows about.
|
||||
|
||||
We issue a `GET` request to http://glance.example.com/images/ to retrieve
|
||||
this list of available *public* images. The data is returned as a JSON-encoded
|
||||
mapping in the following format::
|
||||
|
||||
{'images': [
|
||||
{'uri': 'http://glance.example.com/images/1',
|
||||
'name': 'Ubuntu 10.04 Plain',
|
||||
'type': 'kernel',
|
||||
'size': '5368709120'}
|
||||
...]}
|
||||
|
||||
Notes:
|
||||
|
||||
* All images returned from the above `GET` request are *public* images
|
||||
|
||||
|
||||
Requesting Detailed Metadata on Public VM Images
|
||||
------------------------------------------------
|
||||
|
||||
We want to see more detailed information on available virtual machine images
|
||||
that the Glance server knows about.
|
||||
|
||||
We issue a `GET` request to http://glance.example.com/images/detail to
|
||||
retrieve this list of available *public* images. The data is returned as a
|
||||
JSON-encoded mapping in the following format::
|
||||
|
||||
{'images': [
|
||||
{'uri': 'http://glance.example.com/images/1',
|
||||
'name': 'Ubuntu 10.04 Plain 5GB',
|
||||
'type': 'kernel',
|
||||
'size': '5368709120',
|
||||
'store': 'swift',
|
||||
'created_at': '2010-02-03 09:34:01',
|
||||
'updated_at': '2010-02-03 09:34:01',
|
||||
'deleted_at': '',
|
||||
'status': 'available',
|
||||
'is_public': True,
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}},
|
||||
...]}
|
||||
|
||||
Notes:
|
||||
|
||||
* All images returned from the above `GET` request are *public* images
|
||||
* All timestamps returned are in UTC
|
||||
* The `updated_at` timestamp is the timestamp when an image's metadata
|
||||
was last updated, not its image data, as all image data is immutable
|
||||
once stored in Glance
|
||||
* The `properties` field is a mapping of free-form key/value pairs that
|
||||
have been saved with the image metadata
|
||||
|
||||
|
||||
Requesting Detailed Metadata on a Specific Image
|
||||
------------------------------------------------
|
||||
|
||||
We want to see detailed information for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get metadata about the
|
||||
first public image returned, we can issue a `HEAD` request to the Glance
|
||||
server for the image's URI.
|
||||
|
||||
We issue a `HEAD` request to http://glance.example.com/images/1 to
|
||||
retrieve complete metadata for that image. The metadata is returned as a
|
||||
set of HTTP headers that begin with the prefix `x-image-meta-`. The
|
||||
following shows an example of the HTTP headers returned from the above
|
||||
`HEAD` request::
|
||||
|
||||
x-image-meta-uri http://glance.example.com/images/1
|
||||
x-image-meta-name Ubuntu 10.04 Plain 5GB
|
||||
x-image-meta-type kernel
|
||||
x-image-meta-size 5368709120
|
||||
x-image-meta-store swift
|
||||
x-image-meta-created_at 2010-02-03 09:34:01
|
||||
x-image-meta-updated_at 2010-02-03 09:34:01
|
||||
x-image-meta-deleted_at
|
||||
x-image-meta-status available
|
||||
x-image-meta-is_public True
|
||||
x-image-meta-property-distro Ubuntu 10.04 LTS
|
||||
|
||||
Notes:
|
||||
|
||||
* All timestamps returned are in UTC
|
||||
* The `x-image-meta-updated_at` timestamp is the timestamp when an
|
||||
image's metadata was last updated, not its image data, as all
|
||||
image data is immutable once stored in Glance
|
||||
* There may be multiple headers that begin with the prefix
|
||||
`x-image-meta-property-`. These headers are free-form key/value pairs
|
||||
that have been saved with the image metadata. The key is the string
|
||||
after `x-image-meta-property-` and the value is the value of the header
|
||||
|
||||
|
||||
Retrieving a Virtual Machine Image
|
||||
----------------------------------
|
||||
|
||||
We want to retrieve that actual raw data for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get metadata about the
|
||||
first public image returned, we can issue a `HEAD` request to the Glance
|
||||
server for the image's URI.
|
||||
|
||||
We issue a `GET` request to http://glance.example.com/images/1 to
|
||||
retrieve metadata for that image as well as the image itself encoded
|
||||
into the response body.
|
||||
|
||||
The metadata is returned as a set of HTTP headers that begin with the
|
||||
prefix `x-image-meta-`. The following shows an example of the HTTP headers
|
||||
returned from the above `GET` request::
|
||||
|
||||
x-image-meta-uri http://glance.example.com/images/1
|
||||
x-image-meta-name Ubuntu 10.04 Plain 5GB
|
||||
x-image-meta-type kernel
|
||||
x-image-meta-size 5368709120
|
||||
x-image-meta-store swift
|
||||
x-image-meta-created_at 2010-02-03 09:34:01
|
||||
x-image-meta-updated_at 2010-02-03 09:34:01
|
||||
x-image-meta-deleted_at
|
||||
x-image-meta-status available
|
||||
x-image-meta-is_public True
|
||||
x-image-meta-property-distro Ubuntu 10.04 LTS
|
||||
|
||||
Notes:
|
||||
|
||||
* All timestamps returned are in UTC
|
||||
* The `x-image-meta-updated_at` timestamp is the timestamp when an
|
||||
image's metadata was last updated, not its image data, as all
|
||||
image data is immutable once stored in Glance
|
||||
* There may be multiple headers that begin with the prefix
|
||||
`x-image-meta-property-`. These headers are free-form key/value pairs
|
||||
that have been saved with the image metadata. The key is the string
|
||||
after `x-image-meta-property-` and the value is the value of the header
|
||||
* The response's `Content-Length` header shall be equal to the value of
|
||||
the `x-image-meta-size` header
|
||||
* The image data itself will be the body of the HTTP response returned
|
||||
from the request, which will have content-type of
|
||||
`application/octet-stream`.
|
||||
|
||||
|
||||
Adding a New Virtual Machine Image
|
||||
----------------------------------
|
||||
|
||||
We have created a new virtual machine image in some way (created a
|
||||
"golden image" or snapshotted/backed up an existing image) and we
|
||||
wish to do two things:
|
||||
|
||||
* Store the disk image data in Glance
|
||||
* Store metadata about this image in Glance
|
||||
|
||||
We can do the above two activities in a single call to the Glance API.
|
||||
Assuming, like in the examples above, that a Glance API server is running
|
||||
at `glance.example.com`, we issue a `POST` request to add an image to
|
||||
Glance::
|
||||
|
||||
POST http://glance.example.com/images/
|
||||
|
||||
The metadata about the image is sent to Glance in HTTP headers. The body
|
||||
of the HTTP request to the Glance API will be the MIME-encoded disk
|
||||
image data.
|
||||
|
||||
|
||||
Adding Image Metadata in HTTP Headers
|
||||
*************************************
|
||||
|
||||
Glance will view as image metadata any HTTP header that it receives in a
|
||||
`POST` request where the header key is prefixed with the strings
|
||||
`x-image-meta-` and `x-image-meta-property-`.
|
||||
|
||||
The list of metadata headers that Glance accepts are listed below.
|
||||
|
||||
* `x-image-meta-name`
|
||||
|
||||
This header is required. Its value should be the name of the image.
|
||||
|
||||
Note that the name of an image *is not unique to a Glance node*. It
|
||||
would be an unrealistic expectation of users to know all the unique
|
||||
names of all other user's images.
|
||||
|
||||
* `x-image-meta-id`
|
||||
|
||||
This header is optional.
|
||||
|
||||
When present, Glance will use the supplied identifier for the image.
|
||||
If the identifier already exists in that Glance node, then a
|
||||
`409 Conflict` will be returned by Glance.
|
||||
|
||||
When this header is *not* present, Glance will generate an identifier
|
||||
for the image and return this identifier in the response (see below)
|
||||
|
||||
* `x-image-meta-store`
|
||||
|
||||
This header is optional. Valid values are one of `file` or `swift`
|
||||
|
||||
When present, Glance will attempt to store the disk image data in the
|
||||
backing store indicated by the value of the header. If the Glance node
|
||||
does not support the backing store, Glance will return a `400 Bad Request`.
|
||||
|
||||
When not present, Glance will store the disk image data in the backing
|
||||
store that is marked default. See the configuration option `default_store`
|
||||
for more information.
|
||||
|
||||
* `x-image-meta-type`
|
||||
|
||||
This header is required. Valid values are one of `kernel`, `machine`, `raw`,
|
||||
or `ramdisk`.
|
||||
|
||||
* `x-image-meta-size`
|
||||
|
||||
This header is optional.
|
||||
|
||||
When present, Glance assumes that the expected size of the request body
|
||||
will be the value of this header. If the length in bytes of the request
|
||||
body *does not match* the value of this header, Glance will return a
|
||||
`400 Bad Request`.
|
||||
|
||||
When not present, Glance will calculate the image's size based on the size
|
||||
of the request body.
|
||||
|
||||
* `x-image-meta-is_public`
|
||||
|
||||
This header is optional.
|
||||
|
||||
When present, Glance converts the value of the header to a boolean value,
|
||||
so "on, 1, true" are all true values. When true, the image is marked as
|
||||
a public image, meaning that any user may view its metadata and may read
|
||||
the disk image from Glance.
|
||||
|
||||
When not present, the image is assumed to be *not public* and specific to
|
||||
a user.
|
||||
|
||||
* `x-image-meta-property-*`
|
||||
|
||||
When Glance receives any HTTP header whose key begins with the string prefix
|
||||
`x-image-meta-property-`, Glance adds the key and value to a set of custom,
|
||||
free-form image properties stored with the image. The key is the
|
||||
lower-cased string following the prefix `x-image-meta-property-` with dashes
|
||||
and punctuation replaced with underscores.
|
||||
|
||||
For example, if the following HTTP header were sent::
|
||||
|
||||
x-image-meta-property-distro Ubuntu 10.10
|
||||
|
||||
Then a key/value pair of "distro"/"Ubuntu 10.10" will be stored with the
|
||||
image in Glance.
|
||||
|
||||
There is no limit on the number of free-form key/value attributes that can
|
||||
be attached to the image. However, keep in mind that the 8K limit on the
|
||||
size of all HTTP headers sent in a request will effectively limit the number
|
||||
of image properties.
|
27
doc/source/identifiers.rst
Normal file
27
doc/source/identifiers.rst
Normal file
@@ -0,0 +1,27 @@
|
||||
..
|
||||
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.
|
||||
|
||||
Image Identifiers
|
||||
=================
|
||||
|
||||
Images are uniquely identified by way of a URI that
|
||||
matches the following signature::
|
||||
|
||||
<Glance Server Location>/images/<ID>
|
||||
|
||||
where `<Glance Server Location>` is the resource location of the Glance service
|
||||
that knows about an image, and `<ID>` is the image's identifier that is
|
||||
*unique to that Glance server*.
|
@@ -1,7 +1,18 @@
|
||||
.. Glance documentation master file, created by
|
||||
sphinx-quickstart on Tue May 18 13:50:15 2010.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
..
|
||||
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.
|
||||
|
||||
Welcome to Glance's documentation!
|
||||
==================================
|
||||
@@ -14,316 +25,52 @@ VM images made available through Glance can be stored in a variety of
|
||||
locations from simple filesystems to object-storage systems like the
|
||||
OpenStack Swift project.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
Glance, as with all OpenStack projects, is written with the following design
|
||||
guidelines in mind:
|
||||
|
||||
Overview
|
||||
* **Component based architecture**: Quickly add new behaviors
|
||||
* **Highly available**: Scale to very serious workloads
|
||||
* **Fault tolerant**: Isolated processes avoid cascading failures
|
||||
* **Recoverable**: Failures should be easy to diagnose, debug, and rectify
|
||||
* **Open standards**: Be a reference implementation for a community-driven api
|
||||
|
||||
This documentation is generated by the Sphinx toolkit and lives in the source
|
||||
tree. Additional documentation on Glance and other components of OpenStack can
|
||||
be found on the `OpenStack wiki`_. Also see the :doc:`community` page for
|
||||
other ways to interact with the community.
|
||||
|
||||
.. _`OpenStack wiki`: http://wiki.openstack.org
|
||||
|
||||
Concepts
|
||||
========
|
||||
|
||||
The Glance project provides services for discovering, registering, and
|
||||
retrieving virtual machine images. Glance has a RESTful API that allows
|
||||
querying of VM image metadata as well as retrieval of the actual image.
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
identifiers
|
||||
registries
|
||||
|
||||
Using Glance
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 1
|
||||
|
||||
The Glance API
|
||||
glanceapi
|
||||
client
|
||||
|
||||
Developer Docs
|
||||
==============
|
||||
|
||||
Glance has a RESTful API that exposes both metadata about registered virtual
|
||||
machine images and the image data itself.
|
||||
|
||||
A host that runs the `bin/glance-api` service is said to be a *Glance API
|
||||
Server*.
|
||||
|
||||
Assume there is a Glance API server running at the URL
|
||||
http://glance.openstack.org.
|
||||
|
||||
Let's walk through how a user might request information from this server.
|
||||
|
||||
Requesting a List of Public VM Images
|
||||
-------------------------------------
|
||||
|
||||
We want to see a list of available virtual machine images that the Glance
|
||||
server knows about.
|
||||
|
||||
We issue a `GET` request to http://glance.openstack.org/images/ to retrieve
|
||||
this list of available *public* images. The data is returned as a JSON-encoded
|
||||
mapping in the following format::
|
||||
|
||||
{'images': [
|
||||
{'uri': 'http://glance.openstack.org/images/1',
|
||||
'name': 'Ubuntu 10.04 Plain',
|
||||
'type': 'kernel',
|
||||
'size': '5368709120'}
|
||||
...]}
|
||||
|
||||
Notes:
|
||||
|
||||
* All images returned from the above `GET` request are *public* images
|
||||
|
||||
|
||||
Requesting Detailed Metadata on Public VM Images
|
||||
------------------------------------------------
|
||||
|
||||
We want to see more detailed information on available virtual machine images
|
||||
that the Glance server knows about.
|
||||
|
||||
We issue a `GET` request to http://glance.openstack.org/images/detail to
|
||||
retrieve this list of available *public* images. The data is returned as a
|
||||
JSON-encoded mapping in the following format::
|
||||
|
||||
{'images': [
|
||||
{'uri': 'http://glance.openstack.org/images/1',
|
||||
'name': 'Ubuntu 10.04 Plain 5GB',
|
||||
'type': 'kernel',
|
||||
'size': '5368709120',
|
||||
'store': 'swift',
|
||||
'created_at': '2010-02-03 09:34:01',
|
||||
'updated_at': '2010-02-03 09:34:01',
|
||||
'deleted_at': '',
|
||||
'status': 'available',
|
||||
'is_public': True,
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}},
|
||||
...]}
|
||||
|
||||
Notes:
|
||||
|
||||
* All images returned from the above `GET` request are *public* images
|
||||
* All timestamps returned are in UTC
|
||||
* The `updated_at` timestamp is the timestamp when an image's metadata
|
||||
was last updated, not it's image data, as all image data is immutable
|
||||
once stored in Glance
|
||||
* The `properties` field is a mapping of free-form key/value pairs that
|
||||
have been saved with the image metadata
|
||||
|
||||
|
||||
Requesting Detailed Metadata on a Specific Image
|
||||
------------------------------------------------
|
||||
|
||||
We want to see detailed information for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get metadata about the
|
||||
first public image returned, we can issue a `HEAD` request to the Glance
|
||||
server for the image's URI.
|
||||
|
||||
We issue a `HEAD` request to http://glance.openstack.org/images/1 to
|
||||
retrieve complete metadata for that image. The metadata is returned as a
|
||||
set of HTTP headers that begin with the prefix `x-image-meta-`. The
|
||||
following shows an example of the HTTP headers returned from the above
|
||||
`HEAD` request::
|
||||
|
||||
x-image-meta-uri http://glance.openstack.org/images/1
|
||||
x-image-meta-name Ubuntu 10.04 Plain 5GB
|
||||
x-image-meta-type kernel
|
||||
x-image-meta-size 5368709120
|
||||
x-image-meta-store swift
|
||||
x-image-meta-created_at 2010-02-03 09:34:01
|
||||
x-image-meta-updated_at 2010-02-03 09:34:01
|
||||
x-image-meta-deleted_at
|
||||
x-image-meta-status available
|
||||
x-image-meta-is_public True
|
||||
x-image-meta-property-distro Ubuntu 10.04 LTS
|
||||
|
||||
Notes:
|
||||
|
||||
* All timestamps returned are in UTC
|
||||
* The `x-image-meta-updated_at` timestamp is the timestamp when an
|
||||
image's metadata was last updated, not it's image data, as all
|
||||
image data is immutable once stored in Glance
|
||||
* There may be multiple headers that begin with the prefix
|
||||
`x-image-meta-property-`. These headers are free-form key/value pairs
|
||||
that have been saved with the image metadata. The key is the string
|
||||
after `x-image-meta-property-` and the value is the value of the header
|
||||
|
||||
|
||||
Retrieving a Virtual Machine Image
|
||||
----------------------------------
|
||||
|
||||
We want to retrieve that actual raw data for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get metadata about the
|
||||
first public image returned, we can issue a `HEAD` request to the Glance
|
||||
server for the image's URI.
|
||||
|
||||
We issue a `GET` request to http://glance.openstack.org/images/1 to
|
||||
retrieve metadata for that image as well as the image itself encoded
|
||||
into the response body.
|
||||
|
||||
The metadata is returned as a set of HTTP headers that begin with the
|
||||
prefix `x-image-meta-`. The following shows an example of the HTTP headers
|
||||
returned from the above `GET` request::
|
||||
|
||||
x-image-meta-uri http://glance.openstack.org/images/1
|
||||
x-image-meta-name Ubuntu 10.04 Plain 5GB
|
||||
x-image-meta-type kernel
|
||||
x-image-meta-size 5368709120
|
||||
x-image-meta-store swift
|
||||
x-image-meta-created_at 2010-02-03 09:34:01
|
||||
x-image-meta-updated_at 2010-02-03 09:34:01
|
||||
x-image-meta-deleted_at
|
||||
x-image-meta-status available
|
||||
x-image-meta-is_public True
|
||||
x-image-meta-property-distro Ubuntu 10.04 LTS
|
||||
|
||||
Notes:
|
||||
|
||||
* All timestamps returned are in UTC
|
||||
* The `x-image-meta-updated_at` timestamp is the timestamp when an
|
||||
image's metadata was last updated, not it's image data, as all
|
||||
image data is immutable once stored in Glance
|
||||
* There may be multiple headers that begin with the prefix
|
||||
`x-image-meta-property-`. These headers are free-form key/value pairs
|
||||
that have been saved with the image metadata. The key is the string
|
||||
after `x-image-meta-property-` and the value is the value of the header
|
||||
* The response's `Content-Length` header shall be equal to the value of
|
||||
the `x-image-meta-size` header
|
||||
* The image data itself will be the body of the HTTP response returned
|
||||
from the request, which will have content-type of
|
||||
`application/octet-stream`.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:maxdepth: 1
|
||||
|
||||
Image Identifiers
|
||||
=================
|
||||
community
|
||||
|
||||
Images are uniquely identified by way of a URI that
|
||||
matches the following signature::
|
||||
Outstanding Documentation Tasks
|
||||
===============================
|
||||
|
||||
<Glance Server Location>/images/<ID>
|
||||
|
||||
where `<Glance Server Location>` is the resource location of the Glance service
|
||||
that knows about an image, and `<ID>` is the image's identifier that is
|
||||
*unique to that Glance server*.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
Image Registration
|
||||
==================
|
||||
|
||||
Image metadata made available through Glance can be stored in image
|
||||
*registries*. Image registries are any web service that adheres to the
|
||||
Glance RESTful API for image metadata.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
Using Glance Programmatically with Glance's Client
|
||||
==================================================
|
||||
|
||||
While it is perfectly acceptable to issue HTTP requests directly to Glance
|
||||
via its RESTful API, sometimes it is better to be able to access and modify
|
||||
image resources via a client class that removes some of the complexity and
|
||||
tedium of dealing with raw HTTP requests.
|
||||
|
||||
Glance includes a client class for just this purpose. You can retrieve
|
||||
metadata about an image, change metadata about an image, remove images, and
|
||||
of course retrieve an image itself via this client class.
|
||||
|
||||
Below are some examples of using Glance's Client class. We assume that
|
||||
there is a Glance server running at the address `glance.openstack.org`
|
||||
on port `9292`.
|
||||
|
||||
Requesting a List of Public VM Images
|
||||
-------------------------------------
|
||||
|
||||
We want to see a list of available virtual machine images that the Glance
|
||||
server knows about.
|
||||
|
||||
Using Glance's Client, we can do this using the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.openstack.org", 9292)
|
||||
|
||||
print c.get_images()
|
||||
|
||||
|
||||
Requesting Detailed Metadata on Public VM Images
|
||||
------------------------------------------------
|
||||
|
||||
We want to see more detailed information on available virtual machine images
|
||||
that the Glance server knows about.
|
||||
|
||||
Using Glance's Client, we can do this using the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.openstack.org", 9292)
|
||||
|
||||
print c.get_images_detailed()
|
||||
|
||||
|
||||
Requesting Detailed Metadata on a Specific Image
|
||||
------------------------------------------------
|
||||
|
||||
We want to see detailed information for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get metadata about the
|
||||
first public image returned, we can use the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.openstack.org", 9292)
|
||||
|
||||
print c.get_image_meta("http://glance.openstack.org/images/1")
|
||||
|
||||
|
||||
Retrieving a Virtual Machine Image
|
||||
----------------------------------
|
||||
|
||||
We want to retrieve that actual raw data for a specific virtual machine image
|
||||
that the Glance server knows about.
|
||||
|
||||
We have queried the Glance server for a list of public images and the
|
||||
data returned includes the `uri` field for each available image. This
|
||||
`uri` field value contains the exact location needed to get the metadata
|
||||
for a specific image.
|
||||
|
||||
Continuing the example from above, in order to get both the metadata about the
|
||||
first public image returned and its image data, we can use the following code::
|
||||
|
||||
from glance import client
|
||||
|
||||
c = client.Client("glance.openstack.org", 9292)
|
||||
|
||||
meta, image_file = c.get_image("http://glance.openstack.org/images/1")
|
||||
|
||||
print meta
|
||||
|
||||
f = open('some_local_file', 'wb')
|
||||
for chunk in image_file:
|
||||
f.write(chunk)
|
||||
f.close()
|
||||
|
||||
Note that the return from Client.get_image() is a tuple of (`metadata`, `file`)
|
||||
where `metadata` is a mapping of metadata about the image and `file` is a
|
||||
generator that yields chunks of image data.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
.. todolist::
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
22
doc/source/registries.rst
Normal file
22
doc/source/registries.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
..
|
||||
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.
|
||||
|
||||
Image Registries
|
||||
================
|
||||
|
||||
Image metadata made available through Glance can be stored in image
|
||||
*registries*. Image registries are any web service that adheres to the
|
||||
Glance RESTful API for image metadata.
|
103
glance/client.py
103
glance/client.py
@@ -30,15 +30,6 @@ from glance import util
|
||||
from glance.common import exception
|
||||
|
||||
#TODO(jaypipes) Allow a logger param for client classes
|
||||
#TODO(jaypipes) Raise proper errors or OpenStack API faults
|
||||
|
||||
|
||||
class UnsupportedProtocolError(Exception):
|
||||
"""
|
||||
Error resulting from a client connecting to a server
|
||||
on an unsupported protocol
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ClientConnectionError(Exception):
|
||||
@@ -46,11 +37,6 @@ class ClientConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BadInputError(Exception):
|
||||
"""Error resulting from a client sending bad input to a server"""
|
||||
pass
|
||||
|
||||
|
||||
class ImageBodyIterator(object):
|
||||
|
||||
"""
|
||||
@@ -84,38 +70,27 @@ class BaseClient(object):
|
||||
|
||||
"""A base client class"""
|
||||
|
||||
DEFAULT_ADDRESS = 'http://127.0.0.1'
|
||||
DEFAULT_PORT = 9090
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, host, port, use_ssl):
|
||||
"""
|
||||
Creates a new client to some service. All args are keyword
|
||||
arguments.
|
||||
Creates a new client to some service.
|
||||
|
||||
:param address: The address where service resides (defaults to
|
||||
http://127.0.0.1)
|
||||
:param port: The port where service resides (defaults to 9090)
|
||||
:param host: The host where service resides
|
||||
:param port: The port where service resides
|
||||
:param use_ssl: Should we use HTTPS?
|
||||
"""
|
||||
self.address = kwargs.get('address', self.DEFAULT_ADDRESS)
|
||||
self.port = kwargs.get('port', self.DEFAULT_PORT)
|
||||
url = urlparse.urlparse(self.address)
|
||||
self.netloc = url.netloc
|
||||
self.protocol = url.scheme
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.use_ssl = use_ssl
|
||||
self.connection = None
|
||||
|
||||
def get_connection_type(self):
|
||||
"""
|
||||
Returns the proper connection type
|
||||
"""
|
||||
try:
|
||||
connection_type = {'http': httplib.HTTPConnection,
|
||||
'https': httplib.HTTPSConnection}\
|
||||
[self.protocol]
|
||||
return connection_type
|
||||
except KeyError:
|
||||
raise UnsupportedProtocolError("Unsupported protocol %s. Unable "
|
||||
" to connect to server."
|
||||
% self.protocol)
|
||||
if self.use_ssl:
|
||||
return httplib.HTTPSConnection
|
||||
else:
|
||||
return httplib.HTTPConnection
|
||||
|
||||
def do_request(self, method, action, body=None, headers=None):
|
||||
"""
|
||||
@@ -131,12 +106,9 @@ class BaseClient(object):
|
||||
"""
|
||||
try:
|
||||
connection_type = self.get_connection_type()
|
||||
c = connection_type(self.netloc, self.port)
|
||||
c.request(method, action, body)
|
||||
if headers:
|
||||
for k, v in headers.iteritems():
|
||||
c.putheader(k, v)
|
||||
|
||||
headers = headers or {}
|
||||
c = connection_type(self.host, self.port)
|
||||
c.request(method, action, body, headers)
|
||||
res = c.getresponse()
|
||||
status_code = self.get_status_code(res)
|
||||
if status_code == httplib.OK:
|
||||
@@ -150,7 +122,7 @@ class BaseClient(object):
|
||||
elif status_code == httplib.CONFLICT:
|
||||
raise exception.Duplicate
|
||||
elif status_code == httplib.BAD_REQUEST:
|
||||
raise BadInputError
|
||||
raise exception.BadInputError
|
||||
else:
|
||||
raise Exception("Unknown error occurred! %d" % status_code)
|
||||
|
||||
@@ -173,19 +145,19 @@ class Client(BaseClient):
|
||||
|
||||
"""Main client class for accessing Glance resources"""
|
||||
|
||||
DEFAULT_ADDRESS = 'http://127.0.0.1'
|
||||
DEFAULT_PORT = 9292
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, host, port=None, use_ssl=False):
|
||||
"""
|
||||
Creates a new client to a Glance service. All args are keyword
|
||||
arguments.
|
||||
Creates a new client to a Glance API service.
|
||||
|
||||
:param address: The address where Glance resides (defaults to
|
||||
http://127.0.0.1)
|
||||
:param host: The host where Glance resides
|
||||
:param port: The port where Glance resides (defaults to 9292)
|
||||
:param use_ssl: Should we use HTTPS? (defaults to False)
|
||||
"""
|
||||
super(Client, self).__init__(**kwargs)
|
||||
|
||||
port = port or self.DEFAULT_PORT
|
||||
super(Client, self).__init__(host, port, use_ssl)
|
||||
|
||||
def get_images(self):
|
||||
"""
|
||||
@@ -235,17 +207,40 @@ class Client(BaseClient):
|
||||
"""
|
||||
Tells Glance about an image's metadata as well
|
||||
as optionally the image_data itself
|
||||
|
||||
:param image_meta: Mapping of information about the
|
||||
image
|
||||
:param image_data: Optional string of raw image data
|
||||
or file-like object that can be
|
||||
used to read the image data
|
||||
|
||||
:retval The newly-stored image's metadata.
|
||||
"""
|
||||
if not image_data and 'location' not in image_meta.keys():
|
||||
raise exception.Invalid("You must either specify a location "
|
||||
"for the image or supply the actual "
|
||||
"image data when adding an image to "
|
||||
"Glance")
|
||||
body = image_data
|
||||
if image_data:
|
||||
if hasattr(image_data, 'read'):
|
||||
# TODO(jaypipes): This is far from efficient. Implement
|
||||
# chunked transfer encoding if size is not in image_meta
|
||||
body = image_data.read()
|
||||
else:
|
||||
body = image_data
|
||||
else:
|
||||
body = None
|
||||
|
||||
if not 'size' in image_meta.keys():
|
||||
if body:
|
||||
image_meta['size'] = len(body)
|
||||
|
||||
headers = util.image_meta_to_http_headers(image_meta)
|
||||
|
||||
|
||||
if image_data:
|
||||
headers['content-type'] = 'application/octet-stream'
|
||||
|
||||
res = self.do_request("POST", "/images", body, headers)
|
||||
# Registry returns a JSONified dict(image=image_info)
|
||||
data = json.loads(res.read())
|
||||
return data['image']['id']
|
||||
|
||||
|
@@ -19,4 +19,3 @@
|
||||
"""
|
||||
DB abstraction for Nova and Glance
|
||||
"""
|
||||
|
||||
|
@@ -47,7 +47,7 @@ class ApiError(Error):
|
||||
def __init__(self, message='Unknown', code='Unknown'):
|
||||
self.message = message
|
||||
self.code = code
|
||||
super(ApiError, self).__init__('%s: %s'% (code, message))
|
||||
super(ApiError, self).__init__('%s: %s' % (code, message))
|
||||
|
||||
|
||||
class NotFound(Error):
|
||||
@@ -70,6 +70,11 @@ class Invalid(Error):
|
||||
pass
|
||||
|
||||
|
||||
class BadInputError(Exception):
|
||||
"""Error resulting from a client sending bad input to a server"""
|
||||
pass
|
||||
|
||||
|
||||
def wrap_exception(f):
|
||||
def _wrap(*args, **kw):
|
||||
try:
|
||||
|
@@ -173,3 +173,5 @@ DEFINE_string('sql_connection',
|
||||
'sqlite:///%s/glance.sqlite' % os.path.abspath("./"),
|
||||
'connection string for sql database')
|
||||
DEFINE_bool('verbose', False, 'show debug output')
|
||||
DEFINE_string('default_store', 'file',
|
||||
'Default storage backend. Default: "file"')
|
||||
|
@@ -42,6 +42,8 @@ flags.DEFINE_bool('daemonize', False, 'daemonize this process')
|
||||
# clutter.
|
||||
flags.DEFINE_bool('use_syslog', True, 'output to syslog when daemonizing')
|
||||
flags.DEFINE_string('logfile', None, 'log file to output to')
|
||||
flags.DEFINE_string('logdir', None, 'directory to keep log files in '
|
||||
'(will be prepended to $logfile)')
|
||||
flags.DEFINE_string('pidfile', None, 'pid file to output to')
|
||||
flags.DEFINE_string('working_directory', './', 'working directory...')
|
||||
flags.DEFINE_integer('uid', os.getuid(), 'uid under which to run')
|
||||
@@ -54,11 +56,11 @@ def stop(pidfile):
|
||||
"""
|
||||
# Get the pid from the pidfile
|
||||
try:
|
||||
pid = int(open(pidfile,'r').read().strip())
|
||||
pid = int(open(pidfile, 'r').read().strip())
|
||||
except IOError:
|
||||
message = "pidfile %s does not exist. Daemon not running?\n"
|
||||
sys.stderr.write(message % pidfile)
|
||||
return # not an error in a restart
|
||||
return
|
||||
|
||||
# Try killing the daemon process
|
||||
try:
|
||||
@@ -106,6 +108,7 @@ def serve(name, main):
|
||||
def daemonize(args, name, main):
|
||||
"""Does the work of daemonizing the process"""
|
||||
logging.getLogger('amqplib').setLevel(logging.WARN)
|
||||
files_to_keep = []
|
||||
if FLAGS.daemonize:
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter(
|
||||
@@ -114,12 +117,16 @@ def daemonize(args, name, main):
|
||||
syslog = logging.handlers.SysLogHandler(address='/dev/log')
|
||||
syslog.setFormatter(formatter)
|
||||
logger.addHandler(syslog)
|
||||
files_to_keep.append(syslog.socket)
|
||||
else:
|
||||
if not FLAGS.logfile:
|
||||
FLAGS.logfile = '%s.log' % name
|
||||
if FLAGS.logdir:
|
||||
FLAGS.logfile = os.path.join(FLAGS.logdir, FLAGS.logfile)
|
||||
logfile = logging.FileHandler(FLAGS.logfile)
|
||||
logfile.setFormatter(formatter)
|
||||
logger.addHandler(logfile)
|
||||
files_to_keep.append(logfile.stream)
|
||||
stdin, stdout, stderr = None, None, None
|
||||
else:
|
||||
stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr
|
||||
@@ -139,6 +146,6 @@ def daemonize(args, name, main):
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
uid=FLAGS.uid,
|
||||
gid=FLAGS.gid
|
||||
):
|
||||
gid=FLAGS.gid,
|
||||
files_preserve=files_to_keep):
|
||||
main(args)
|
||||
|
@@ -39,6 +39,7 @@ from glance.common.exception import ProcessExecutionError
|
||||
FLAGS = flags.FLAGS
|
||||
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class"""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
@@ -48,6 +49,7 @@ def import_class(import_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:
|
||||
@@ -57,6 +59,7 @@ def import_object(import_str):
|
||||
cls = import_class(import_str)
|
||||
return cls()
|
||||
|
||||
|
||||
def fetchfile(url, target):
|
||||
logging.debug("Fetching %s" % url)
|
||||
# c = pycurl.Curl()
|
||||
@@ -68,6 +71,7 @@ def fetchfile(url, target):
|
||||
# fp.close()
|
||||
execute("curl --fail %s -o %s" % (url, target))
|
||||
|
||||
|
||||
def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
|
||||
logging.debug("Running cmd: %s", cmd)
|
||||
env = os.environ.copy()
|
||||
@@ -83,7 +87,7 @@ def execute(cmd, process_input=None, addl_env=None, check_exit_code=True):
|
||||
obj.stdin.close()
|
||||
if obj.returncode:
|
||||
logging.debug("Result was %s" % (obj.returncode))
|
||||
if check_exit_code and obj.returncode <> 0:
|
||||
if check_exit_code and obj.returncode != 0:
|
||||
(stdout, stderr) = result
|
||||
raise ProcessExecutionError(exit_code=obj.returncode,
|
||||
stdout=stdout,
|
||||
@@ -109,7 +113,8 @@ def default_flagfile(filename='glance.conf'):
|
||||
script_dir = os.path.dirname(inspect.stack()[-1][1])
|
||||
filename = os.path.abspath(os.path.join(script_dir, filename))
|
||||
if os.path.exists(filename):
|
||||
sys.argv = sys.argv[:1] + ['--flagfile=%s' % filename] + sys.argv[1:]
|
||||
sys.argv = \
|
||||
sys.argv[:1] + ['--flagfile=%s' % filename] + sys.argv[1:]
|
||||
|
||||
|
||||
def debug(arg):
|
||||
@@ -117,11 +122,11 @@ def debug(arg):
|
||||
return arg
|
||||
|
||||
|
||||
def runthis(prompt, cmd, check_exit_code = True):
|
||||
def runthis(prompt, cmd, check_exit_code=True):
|
||||
logging.debug("Running %s" % (cmd))
|
||||
exit_code = subprocess.call(cmd.split(" "))
|
||||
logging.debug(prompt % (exit_code))
|
||||
if check_exit_code and exit_code <> 0:
|
||||
if check_exit_code and exit_code != 0:
|
||||
raise ProcessExecutionError(exit_code=exit_code,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
@@ -129,7 +134,9 @@ def runthis(prompt, cmd, check_exit_code = True):
|
||||
|
||||
|
||||
def generate_uid(topic, size=8):
|
||||
return '%s-%s' % (topic, ''.join([random.choice('01234567890abcdefghijklmnopqrstuvwxyz') for x in xrange(size)]))
|
||||
return '%s-%s' % (topic, ''.join(
|
||||
[random.choice('01234567890abcdefghijklmnopqrstuvwxyz')
|
||||
for x in xrange(size)]))
|
||||
|
||||
|
||||
def generate_mac():
|
||||
@@ -198,6 +205,7 @@ class LazyPluggable(object):
|
||||
backend = self.__get_backend()
|
||||
return getattr(backend, key)
|
||||
|
||||
|
||||
def deferredToThread(f):
|
||||
def g(*args, **kwargs):
|
||||
return deferToThread(f, *args, **kwargs)
|
||||
|
@@ -21,6 +21,7 @@
|
||||
Utility methods for working with WSGI servers
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import datetime
|
||||
@@ -43,6 +44,29 @@ def run_server(application, 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."""
|
||||
eventlet.wsgi.server(socket, application, custom_pool=self.pool)
|
||||
|
||||
|
||||
class Application(object):
|
||||
# TODO(gundlach): I think we should toss this class, now that it has no
|
||||
# purpose.
|
||||
@@ -92,11 +116,11 @@ class Middleware(Application):
|
||||
behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, application): # pylint: disable-msg=W0231
|
||||
def __init__(self, application): # pylint: disable-msg=W0231
|
||||
self.application = application
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req): # pylint: disable-msg=W0221
|
||||
def __call__(self, req): # pylint: disable-msg=W0221
|
||||
"""Override to implement middleware behavior."""
|
||||
return self.application
|
||||
|
||||
@@ -214,7 +238,7 @@ class Controller(object):
|
||||
arg_dict['req'] = req
|
||||
result = method(**arg_dict)
|
||||
if type(result) is dict:
|
||||
return self._serialize(result, req)
|
||||
return self._serialize(result, req)
|
||||
else:
|
||||
return result
|
||||
|
||||
@@ -259,7 +283,6 @@ class Serializer(object):
|
||||
return self._methods.get(mimetype, repr)(data)
|
||||
|
||||
def _to_json(self, data):
|
||||
import json
|
||||
def sanitizer(obj):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return obj.isoformat()
|
||||
@@ -297,7 +320,7 @@ class Serializer(object):
|
||||
else:
|
||||
node = self._to_xml_node(doc, metadata, k, v)
|
||||
result.appendChild(node)
|
||||
else: # atom
|
||||
else: # atom
|
||||
node = doc.createTextNode(str(data))
|
||||
result.appendChild(node)
|
||||
return result
|
||||
|
@@ -17,37 +17,37 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Registry API
|
||||
Registry API
|
||||
"""
|
||||
|
||||
from glance.registry import client
|
||||
|
||||
|
||||
def get_images_list():
|
||||
c = client.RegistryClient()
|
||||
c = client.RegistryClient("0.0.0.0")
|
||||
return c.get_images()
|
||||
|
||||
|
||||
def get_images_detail():
|
||||
c = client.RegistryClient()
|
||||
c = client.RegistryClient("0.0.0.0")
|
||||
return c.get_images_detailed()
|
||||
|
||||
|
||||
def get_image_metadata(image_id):
|
||||
c = client.RegistryClient()
|
||||
c = client.RegistryClient("0.0.0.0")
|
||||
return c.get_image(image_id)
|
||||
|
||||
|
||||
def add_image_metadata(image_data):
|
||||
c = client.RegistryClient()
|
||||
c = client.RegistryClient("0.0.0.0")
|
||||
return c.add_image(image_data)
|
||||
|
||||
|
||||
def update_image_metadata(image_id, image_data):
|
||||
c = client.RegistryClient()
|
||||
c = client.RegistryClient("0.0.0.0")
|
||||
return c.update_image(image_id, image_data)
|
||||
|
||||
|
||||
def delete_image_metadata(image_id):
|
||||
c = client.RegistryClient()
|
||||
c = client.RegistryClient("0.0.0.0")
|
||||
return c.delete_image(image_id)
|
||||
|
@@ -35,19 +35,19 @@ class RegistryClient(BaseClient):
|
||||
|
||||
"""A client for the Registry image metadata service"""
|
||||
|
||||
DEFAULT_ADDRESS = 'http://127.0.0.1'
|
||||
DEFAULT_PORT = 9191
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, host, port=None, use_ssl=False):
|
||||
"""
|
||||
Creates a new client to a Registry service. All args are keyword
|
||||
arguments.
|
||||
Creates a new client to a Glance Registry service.
|
||||
|
||||
:param address: The address where Registry resides (defaults to
|
||||
http://127.0.0.1)
|
||||
:param port: The port where Registry resides (defaults to 9191)
|
||||
:param host: The host where Glance resides
|
||||
:param port: The port where Glance resides (defaults to 9191)
|
||||
:param use_ssl: Should we use HTTPS? (defaults to False)
|
||||
"""
|
||||
super(RegistryClient, self).__init__(**kwargs)
|
||||
|
||||
port = port or self.DEFAULT_PORT
|
||||
super(RegistryClient, self).__init__(host, port, use_ssl)
|
||||
|
||||
def get_images(self):
|
||||
"""
|
||||
|
@@ -52,10 +52,7 @@ def _deleted(context):
|
||||
|
||||
|
||||
def image_create(_context, values):
|
||||
image_ref = models.Image()
|
||||
image_ref.update(values)
|
||||
image_ref.save()
|
||||
return image_ref
|
||||
return _image_update(_context, values, None)
|
||||
|
||||
|
||||
def image_destroy(_context, image_id):
|
||||
@@ -69,7 +66,6 @@ def image_get(context, image_id):
|
||||
session = get_session()
|
||||
try:
|
||||
return session.query(models.Image
|
||||
).options(joinedload(models.Image.files)
|
||||
).options(joinedload(models.Image.properties)
|
||||
).filter_by(deleted=_deleted(context)
|
||||
).filter_by(id=image_id
|
||||
@@ -82,7 +78,6 @@ def image_get(context, image_id):
|
||||
def image_get_all(context):
|
||||
session = get_session()
|
||||
return session.query(models.Image
|
||||
).options(joinedload(models.Image.files)
|
||||
).options(joinedload(models.Image.properties)
|
||||
).filter_by(deleted=_deleted(context)
|
||||
).all()
|
||||
@@ -91,7 +86,6 @@ def image_get_all(context):
|
||||
def image_get_all_public(context, public):
|
||||
session = get_session()
|
||||
return session.query(models.Image
|
||||
).options(joinedload(models.Image.files)
|
||||
).options(joinedload(models.Image.properties)
|
||||
).filter_by(deleted=_deleted(context)
|
||||
).filter_by(is_public=public
|
||||
@@ -103,30 +97,54 @@ def image_get_by_str(context, str_id):
|
||||
|
||||
|
||||
def image_update(_context, image_id, values):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
image_ref = models.Image.find(image_id, session=session)
|
||||
image_ref.update(values)
|
||||
image_ref.save(session=session)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def image_file_create(_context, values):
|
||||
image_file_ref = models.ImageFile()
|
||||
for (key, value) in values.iteritems():
|
||||
image_file_ref[key] = value
|
||||
image_file_ref.save()
|
||||
return image_file_ref
|
||||
return _image_update(_context, values, image_id)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
def image_property_create(_context, values):
|
||||
image_properties_ref = models.ImageProperty()
|
||||
for (key, value) in values.iteritems():
|
||||
image_properties_ref[key] = value
|
||||
image_properties_ref.save()
|
||||
return image_properties_ref
|
||||
_drop_protected_attrs(models.Image, values)
|
||||
image_property_ref = models.ImageProperty()
|
||||
image_property_ref.update(values)
|
||||
image_property_ref.save()
|
||||
return image_property_ref
|
||||
|
||||
|
||||
def _drop_protected_attrs(model_class, values):
|
||||
"""Removed protected attributes from values dictionary using the models
|
||||
__protected_attributes__ field.
|
||||
"""
|
||||
for attr in model_class.__protected_attributes__:
|
||||
if attr in values:
|
||||
del values[attr]
|
||||
|
||||
|
||||
def _image_update(_context, values, image_id):
|
||||
"""Used internally by image_create and image_update
|
||||
|
||||
:param image_id: If None, create the image, otherwise, find and update it
|
||||
"""
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
_drop_protected_attrs(models.Image, values)
|
||||
|
||||
values['size'] = int(values['size'])
|
||||
values['is_public'] = bool(values.get('is_public', False))
|
||||
properties = values.pop('properties', {})
|
||||
|
||||
if image_id:
|
||||
image_ref = models.Image.find(image_id, session=session)
|
||||
else:
|
||||
image_ref = models.Image()
|
||||
|
||||
image_ref.update(values)
|
||||
image_ref.save(session=session)
|
||||
|
||||
for key, value in properties.iteritems():
|
||||
prop_values = {'image_id': image_ref.id,
|
||||
'key': key,
|
||||
'value': value}
|
||||
image_property_create(_context, prop_values)
|
||||
|
||||
return image_get(_context, image_ref.id)
|
||||
|
@@ -45,6 +45,9 @@ class ModelBase(object):
|
||||
__table_args__ = {'mysql_engine': 'InnoDB'}
|
||||
__table_initialized__ = False
|
||||
__prefix__ = 'none'
|
||||
__protected_attributes__ = set([
|
||||
"created_at", "updated_at", "deleted_at", "deleted"])
|
||||
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = Column(DateTime, onupdate=datetime.datetime.utcnow)
|
||||
deleted_at = Column(DateTime)
|
||||
@@ -134,11 +137,12 @@ class ModelBase(object):
|
||||
def items(self):
|
||||
return self.__dict__.items()
|
||||
|
||||
|
||||
class Image(BASE, ModelBase):
|
||||
"""Represents an image in the datastore"""
|
||||
__tablename__ = 'images'
|
||||
__prefix__ = 'img'
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255))
|
||||
type = Column(String(30))
|
||||
@@ -153,29 +157,12 @@ class Image(BASE, ModelBase):
|
||||
raise exception.Invalid(
|
||||
"Invalid image type '%s' for image." % type)
|
||||
return type
|
||||
|
||||
|
||||
@validates('status')
|
||||
def validate_status(self, key, status):
|
||||
if not status in ('available', 'pending', 'disabled'):
|
||||
raise exception.Invalid("Invalid status '%s' for image." % status)
|
||||
return status
|
||||
|
||||
# TODO(sirp): should these be stored as properties?
|
||||
#user_id = Column(String(255))
|
||||
#project_id = Column(String(255))
|
||||
#arch = Column(String(255))
|
||||
#default_kernel_id = Column(String(255))
|
||||
#default_ramdisk_id = Column(String(255))
|
||||
#
|
||||
#@validates('default_kernel_id')
|
||||
#def validate_kernel_id(self, key, val):
|
||||
# if val != 'machine':
|
||||
# assert(val is None)
|
||||
#
|
||||
#@validates('default_ramdisk_id')
|
||||
#def validate_ramdisk_id(self, key, val):
|
||||
# if val != 'machine':
|
||||
# assert(val is None)
|
||||
|
||||
|
||||
class ImageProperty(BASE, ModelBase):
|
||||
@@ -187,7 +174,7 @@ class ImageProperty(BASE, ModelBase):
|
||||
id = Column(Integer, primary_key=True)
|
||||
image_id = Column(Integer, ForeignKey('images.id'), nullable=False)
|
||||
image = relationship(Image, backref=backref('properties'))
|
||||
|
||||
|
||||
key = Column(String(255), index=True)
|
||||
value = Column(Text)
|
||||
|
||||
|
@@ -28,12 +28,11 @@ from glance.registry import db
|
||||
|
||||
|
||||
class ImageController(wsgi.Controller):
|
||||
|
||||
"""Image Controller """
|
||||
|
||||
|
||||
def index(self, req):
|
||||
"""Return basic information for all public, non-deleted images
|
||||
|
||||
|
||||
:param req: the Request object coming from the wsgi layer
|
||||
:retval a mapping of the following form::
|
||||
|
||||
@@ -42,15 +41,18 @@ class ImageController(wsgi.Controller):
|
||||
Where image_list is a sequence of mappings::
|
||||
|
||||
{'id': image_id, 'name': image_name}
|
||||
|
||||
|
||||
"""
|
||||
images = db.image_get_all_public(None)
|
||||
image_dicts = [dict(id=i['id'], name=i['name']) for i in images]
|
||||
image_dicts = [dict(id=i['id'],
|
||||
name=i['name'],
|
||||
type=i['type'],
|
||||
size=i['size']) for i in images]
|
||||
return dict(images=image_dicts)
|
||||
|
||||
def detail(self, req):
|
||||
"""Return detailed information for all public, non-deleted images
|
||||
|
||||
|
||||
:param req: the Request object coming from the wsgi layer
|
||||
:retval a mapping of the following form::
|
||||
|
||||
@@ -58,7 +60,7 @@ class ImageController(wsgi.Controller):
|
||||
|
||||
Where image_list is a sequence of mappings containing
|
||||
all image model fields.
|
||||
|
||||
|
||||
"""
|
||||
images = db.image_get_all_public(None)
|
||||
image_dicts = [make_image_dict(i) for i in images]
|
||||
@@ -70,7 +72,7 @@ class ImageController(wsgi.Controller):
|
||||
image = db.image_get(None, id)
|
||||
except exception.NotFound:
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
|
||||
return dict(image=make_image_dict(image))
|
||||
|
||||
def delete(self, req, id):
|
||||
@@ -153,7 +155,7 @@ def make_image_dict(image):
|
||||
Create a dict representation of an image which we can use to
|
||||
serialize the image.
|
||||
"""
|
||||
|
||||
|
||||
def _fetch_attrs(d, attrs):
|
||||
return dict([(a, d[a]) for a in attrs
|
||||
if a in d.keys()])
|
@@ -23,6 +23,10 @@ Glance API Server
|
||||
Configuration Options
|
||||
---------------------
|
||||
|
||||
`default_store`: When no x-image-meta-store header is sent for a
|
||||
`POST /images` request, this store will be used
|
||||
for storing the image data. Default: 'file'
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -39,7 +43,9 @@ from glance.common import flags
|
||||
from glance.common import wsgi
|
||||
from glance.store import (get_from_backend,
|
||||
delete_from_backend,
|
||||
get_store_from_location)
|
||||
get_store_from_location,
|
||||
get_backend_class,
|
||||
UnsupportedBackend)
|
||||
from glance import registry
|
||||
from glance import util
|
||||
|
||||
@@ -51,10 +57,10 @@ class Controller(wsgi.Controller):
|
||||
|
||||
"""
|
||||
Main WSGI application controller for Glance.
|
||||
|
||||
|
||||
The Glance API is a RESTful web service for image data. The API
|
||||
is as follows::
|
||||
|
||||
|
||||
GET /images -- Returns a set of brief metadata about images
|
||||
GET /images/detail -- Returns a set of detailed metadata about
|
||||
images
|
||||
@@ -66,7 +72,7 @@ class Controller(wsgi.Controller):
|
||||
image data is immutable once stored)
|
||||
DELETE /images/<ID> -- Delete the image with id <ID>
|
||||
"""
|
||||
|
||||
|
||||
def index(self, req):
|
||||
"""
|
||||
Returns the following information for all public, available images:
|
||||
@@ -75,7 +81,7 @@ class Controller(wsgi.Controller):
|
||||
* name -- The name of the image
|
||||
* size -- Size of image data in bytes
|
||||
* type -- One of 'kernel', 'ramdisk', 'raw', or 'machine'
|
||||
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
:retval The response body is a mapping of the following form::
|
||||
|
||||
@@ -92,7 +98,7 @@ class Controller(wsgi.Controller):
|
||||
def detail(self, req):
|
||||
"""
|
||||
Returns detailed information for all public, available images
|
||||
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
:retval The response body is a mapping of the following form::
|
||||
|
||||
@@ -174,9 +180,6 @@ class Controller(wsgi.Controller):
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
|
||||
:see The `id_type` configuration option (default: uuid) determines
|
||||
the type of identifier that Glance generates for an image
|
||||
|
||||
:raises HTTPBadRequest if no x-image-meta-location is missing
|
||||
and the request body is not application/octet-stream
|
||||
image data.
|
||||
@@ -195,7 +198,7 @@ class Controller(wsgi.Controller):
|
||||
"mime-encoded as application/"
|
||||
"octet-stream.", request=req)
|
||||
else:
|
||||
if 'x-image-meta-store' in headers_keys:
|
||||
if 'x-image-meta-store' in header_keys:
|
||||
image_store = req.headers['x-image-meta-store']
|
||||
image_status = 'pending' # set to available when stored...
|
||||
image_in_body = True
|
||||
@@ -204,23 +207,37 @@ class Controller(wsgi.Controller):
|
||||
image_store = get_store_from_location(image_location)
|
||||
image_status = 'available'
|
||||
|
||||
# If image is the request body, validate that the requested
|
||||
# or default store is capable of storing the image data...
|
||||
if not image_store:
|
||||
image_store = FLAGS.default_store
|
||||
if image_in_body:
|
||||
store = self.get_store_or_400(req, image_store)
|
||||
|
||||
image_meta = util.get_image_meta_from_headers(req)
|
||||
|
||||
image_meta['status'] = image_status
|
||||
image_meta['store'] = image_store
|
||||
|
||||
try:
|
||||
image_meta = registry.add_image_metadata(image_meta)
|
||||
|
||||
if image_in_body:
|
||||
#store = stores.get_store()
|
||||
#store.add_image(req.body)
|
||||
try:
|
||||
location = store.add(image_meta['id'], req.body)
|
||||
except exception.Duplicate, e:
|
||||
logging.error("Error adding image to store: %s", str(e))
|
||||
return HTTPConflict(str(e), request=req)
|
||||
image_meta['status'] = 'available'
|
||||
registries.update_image(image_meta)
|
||||
image_meta['location'] = location
|
||||
registry.update_image_metadata(image_meta['id'], image_meta)
|
||||
|
||||
return dict(image=image_meta)
|
||||
|
||||
except exception.Duplicate:
|
||||
return HTTPConflict()
|
||||
msg = "An image with identifier %s already exists"\
|
||||
% image_meta['id']
|
||||
logging.error(msg)
|
||||
return HTTPConflict(msg, request=req)
|
||||
except exception.Invalid:
|
||||
return HTTPBadRequest()
|
||||
|
||||
@@ -275,6 +292,25 @@ class Controller(wsgi.Controller):
|
||||
request=request,
|
||||
content_type='text/plain')
|
||||
|
||||
def get_store_or_400(self, request, store_name):
|
||||
"""
|
||||
Grabs the storage backend for the supplied store name
|
||||
or raises an HTTPBadRequest (400) response
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
:param id: The opaque image identifier
|
||||
|
||||
:raises HTTPNotFound if image does not exist
|
||||
"""
|
||||
try:
|
||||
return get_backend_class(store_name)
|
||||
except UnsupportedBackend:
|
||||
raise HTTPBadRequest(body='Requested store %s not available '
|
||||
'for storage on this Glance node'
|
||||
% store_name,
|
||||
request=request,
|
||||
content_type='text/plain')
|
||||
|
||||
|
||||
class API(wsgi.Router):
|
||||
|
||||
|
@@ -73,7 +73,7 @@ class FilesystemBackend(Backend):
|
||||
except OSError:
|
||||
raise exception.NotAuthorized("You cannot delete file %s" % fn)
|
||||
else:
|
||||
raise exception.NotFound("File %s does not exist" % fn)
|
||||
raise exception.NotFound("File %s does not exist" % fn)
|
||||
|
||||
|
||||
def get_backend_class(backend):
|
||||
|
@@ -26,6 +26,11 @@ from glance.common import exception
|
||||
from glance.common import flags
|
||||
import glance.store
|
||||
|
||||
|
||||
flags.DEFINE_string('filesystem_store_datadir', '/var/lib/glance/images/',
|
||||
'Location to write image data. '
|
||||
'Default: /var/lib/glance/images/')
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
@@ -61,10 +66,10 @@ class ChunkedFile(object):
|
||||
self.fp = None
|
||||
|
||||
|
||||
|
||||
class FilesystemBackend(glance.store.Backend):
|
||||
@classmethod
|
||||
def get(cls, parsed_uri, opener=lambda p: open(p, "rb"), expected_size=None):
|
||||
def get(cls, parsed_uri, opener=lambda p: open(p, "rb"),
|
||||
expected_size=None):
|
||||
""" Filesystem-based backend
|
||||
|
||||
file:///path/to/file.tar.gz.0
|
||||
@@ -94,4 +99,35 @@ class FilesystemBackend(glance.store.Backend):
|
||||
except OSError:
|
||||
raise exception.NotAuthorized("You cannot delete file %s" % fn)
|
||||
else:
|
||||
raise exception.NotFound("Image file %s does not exist" % fn)
|
||||
raise exception.NotFound("Image file %s does not exist" % fn)
|
||||
|
||||
@classmethod
|
||||
def add(cls, id, data):
|
||||
"""
|
||||
Stores image data to disk and returns a location that the image was
|
||||
written to. By default, the backend writes the image data to a file
|
||||
`/<DATADIR>/<ID>`, where <DATADIR> is the value of
|
||||
FLAGS.filesystem_store_datadir and <ID> is the supplied image ID.
|
||||
|
||||
:param id: The opaque image identifier
|
||||
:param data: The image data to write
|
||||
|
||||
:retval The location that was written, with file:// scheme prepended
|
||||
"""
|
||||
|
||||
datadir = FLAGS.filesystem_store_datadir
|
||||
|
||||
if not os.path.exists(datadir):
|
||||
os.makedirs(datadir)
|
||||
|
||||
filepath = os.path.join(datadir, str(id))
|
||||
|
||||
if os.path.exists(filepath):
|
||||
raise exception.Duplicate("Image file %s already exists!"
|
||||
% filepath)
|
||||
|
||||
f = open(filepath, 'wb')
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
return 'file://%s' % filepath
|
||||
|
@@ -19,6 +19,7 @@ import httplib
|
||||
|
||||
import glance.store
|
||||
|
||||
|
||||
class HTTPBackend(glance.store.Backend):
|
||||
""" An implementation of the HTTP Backend Adapter """
|
||||
|
||||
@@ -29,17 +30,18 @@ class HTTPBackend(glance.store.Backend):
|
||||
"""
|
||||
|
||||
if conn_class:
|
||||
pass # use the conn_class passed in
|
||||
pass # use the conn_class passed in
|
||||
elif parsed_uri.scheme == "http":
|
||||
conn_class = httplib.HTTPConnection
|
||||
elif parsed_uri.scheme == "https":
|
||||
conn_class = httplib.HTTPSConnection
|
||||
else:
|
||||
raise glance.store.BackendException("scheme '%s' not supported for HTTPBackend")
|
||||
|
||||
raise glance.store.BackendException(
|
||||
"scheme '%s' not supported for HTTPBackend")
|
||||
|
||||
conn = conn_class(parsed_uri.netloc)
|
||||
conn.request("GET", parsed_uri.path, "", {})
|
||||
|
||||
|
||||
try:
|
||||
return glance.store._file_iter(conn.getresponse(), cls.CHUNKSIZE)
|
||||
finally:
|
||||
|
@@ -31,14 +31,14 @@ class SwiftBackend(glance.store.Backend):
|
||||
@classmethod
|
||||
def get(cls, parsed_uri, expected_size, conn_class=None):
|
||||
"""
|
||||
Takes a parsed_uri in the format of:
|
||||
swift://user:password@auth_url/container/file.gz.0, connects to the
|
||||
swift instance at auth_url and downloads the file. Returns the generator
|
||||
resp_body provided by get_object.
|
||||
Takes a parsed_uri in the format of:
|
||||
swift://user:password@auth_url/container/file.gz.0, connects to the
|
||||
swift instance at auth_url and downloads the file. Returns the
|
||||
generator resp_body provided by get_object.
|
||||
"""
|
||||
(user, key, authurl, container, obj) = \
|
||||
cls._parse_swift_tokens(parsed_uri)
|
||||
|
||||
|
||||
# TODO(sirp): snet=False for now, however, if the instance of
|
||||
# swift we're talking to is within our same region, we should set
|
||||
# snet=True
|
||||
@@ -52,9 +52,10 @@ class SwiftBackend(glance.store.Backend):
|
||||
|
||||
obj_size = int(resp_headers['content-length'])
|
||||
if obj_size != expected_size:
|
||||
raise glance.store.BackendException("Expected %s byte file, Swift has %s bytes"
|
||||
% (expected_size, obj_size))
|
||||
|
||||
raise glance.store.BackendException(
|
||||
"Expected %s byte file, Swift has %s bytes" %
|
||||
(expected_size, obj_size))
|
||||
|
||||
return resp_body
|
||||
|
||||
@classmethod
|
||||
@@ -64,7 +65,7 @@ class SwiftBackend(glance.store.Backend):
|
||||
"""
|
||||
(user, key, authurl, container, obj) = \
|
||||
cls._parse_swift_tokens(parsed_uri)
|
||||
|
||||
|
||||
# TODO(sirp): snet=False for now, however, if the instance of
|
||||
# swift we're talking to is within our same region, we should set
|
||||
# snet=True
|
||||
@@ -78,7 +79,7 @@ class SwiftBackend(glance.store.Backend):
|
||||
|
||||
# TODO(jaypipes): What to return here? After reading the docs
|
||||
# at swift.common.client, I'm not sure what to check for...
|
||||
|
||||
|
||||
@classmethod
|
||||
def _parse_swift_tokens(cls, parsed_uri):
|
||||
"""
|
||||
|
@@ -29,9 +29,9 @@ def image_meta_to_http_headers(image_meta):
|
||||
:param image_meta: Mapping of image metadata
|
||||
"""
|
||||
headers = {}
|
||||
for k, v in image_meta.iteritems():
|
||||
for k, v in image_meta.items():
|
||||
if k == 'properties':
|
||||
for pk, pv in v.iteritems():
|
||||
for pk, pv in v.items():
|
||||
headers["x-image-meta-property-%s"
|
||||
% pk.lower()] = pv
|
||||
|
||||
@@ -53,7 +53,7 @@ def inject_image_meta_into_headers(response, image_meta):
|
||||
"""
|
||||
headers = image_meta_to_http_headers(image_meta)
|
||||
|
||||
for k, v in headers.iteritems():
|
||||
for k, v in headers.items():
|
||||
response.headers.add(k, v)
|
||||
|
||||
|
||||
@@ -67,7 +67,13 @@ def get_image_meta_from_headers(response):
|
||||
"""
|
||||
result = {}
|
||||
properties = {}
|
||||
for key, value in response.headers.iteritems():
|
||||
|
||||
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-'):
|
||||
properties[key[len('x-image-meta-property-'):]] = value
|
||||
|
4
setup.py
4
setup.py
@@ -57,5 +57,5 @@ setup(
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
],
|
||||
install_requires=[], # removed for better compat
|
||||
scripts=['bin/parallax-server',
|
||||
'bin/teller-server'])
|
||||
scripts=['bin/glance-api',
|
||||
'bin/glance-registry'])
|
||||
|
@@ -28,7 +28,7 @@ import stubout
|
||||
import webob
|
||||
|
||||
from glance.common import exception
|
||||
from glance.registry import controllers as registry_controllers
|
||||
from glance.registry import server as rserver
|
||||
from glance import server
|
||||
import glance.store
|
||||
import glance.store.filesystem
|
||||
@@ -37,7 +37,7 @@ import glance.store.swift
|
||||
import glance.registry.db.sqlalchemy.api
|
||||
|
||||
|
||||
FAKE_FILESYSTEM_ROOTDIR = os.path.join('//tmp', 'glance-tests')
|
||||
FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests')
|
||||
|
||||
|
||||
def stub_out_http_backend(stubs):
|
||||
@@ -230,14 +230,16 @@ def stub_out_registry_and_store_server(stubs):
|
||||
def close(self):
|
||||
return True
|
||||
|
||||
def request(self, method, url, body=None):
|
||||
def request(self, method, url, body=None, headers={}):
|
||||
self.req = webob.Request.blank("/" + url.lstrip("/"))
|
||||
self.req.method = method
|
||||
if headers:
|
||||
self.req.headers = headers
|
||||
if body:
|
||||
self.req.body = body
|
||||
|
||||
def getresponse(self):
|
||||
res = self.req.get_response(registry_controllers.API())
|
||||
res = self.req.get_response(rserver.API())
|
||||
|
||||
# httplib.Response has a read() method...fake it out
|
||||
def fake_reader():
|
||||
@@ -256,13 +258,12 @@ def stub_out_registry_and_store_server(stubs):
|
||||
|
||||
def close(self):
|
||||
return True
|
||||
|
||||
def putheader(self, k, v):
|
||||
self.req.headers[k] = v
|
||||
|
||||
def request(self, method, url, body=None):
|
||||
def request(self, method, url, body=None, headers={}):
|
||||
self.req = webob.Request.blank("/" + url.lstrip("/"))
|
||||
self.req.method = method
|
||||
if headers:
|
||||
self.req.headers = headers
|
||||
if body:
|
||||
self.req.body = body
|
||||
|
||||
@@ -280,25 +281,16 @@ def stub_out_registry_and_store_server(stubs):
|
||||
"""
|
||||
Returns the proper connection type
|
||||
"""
|
||||
DEFAULT_PARALLAX_PORT = 9191
|
||||
DEFAULT_TELLER_PORT = 9292
|
||||
DEFAULT_REGISTRY_PORT = 9191
|
||||
DEFAULT_API_PORT = 9292
|
||||
|
||||
if (client.port == DEFAULT_TELLER_PORT and
|
||||
client.netloc == '127.0.0.1'):
|
||||
if (client.port == DEFAULT_API_PORT and
|
||||
client.host == '0.0.0.0'):
|
||||
return FakeGlanceConnection
|
||||
elif (client.port == DEFAULT_PARALLAX_PORT and
|
||||
client.netloc == '127.0.0.1'):
|
||||
elif (client.port == DEFAULT_REGISTRY_PORT and
|
||||
client.host == '0.0.0.0'):
|
||||
return FakeRegistryConnection
|
||||
else:
|
||||
try:
|
||||
connection_type = {'http': httplib.HTTPConnection,
|
||||
'https': httplib.HTTPSConnection}\
|
||||
[client.protocol]
|
||||
return connection_type
|
||||
except KeyError:
|
||||
raise UnsupportedProtocolError("Unsupported protocol %s. Unable "
|
||||
" to connect to server."
|
||||
% self.protocol)
|
||||
|
||||
def fake_image_iter(self):
|
||||
for i in self.response.app_iter:
|
||||
yield i
|
||||
@@ -394,6 +386,22 @@ def stub_out_registry_db_image_api(stubs):
|
||||
return values
|
||||
|
||||
def image_update(self, _context, image_id, values):
|
||||
|
||||
props = []
|
||||
|
||||
if 'properties' in values.keys():
|
||||
for k,v in values['properties'].iteritems():
|
||||
p = {}
|
||||
p['key'] = k
|
||||
p['value'] = v
|
||||
p['deleted'] = False
|
||||
p['created_at'] = datetime.datetime.utcnow()
|
||||
p['updated_at'] = datetime.datetime.utcnow()
|
||||
p['deleted_at'] = None
|
||||
props.append(p)
|
||||
|
||||
values['properties'] = props
|
||||
|
||||
image = self.image_get(_context, image_id)
|
||||
image.update(values)
|
||||
return image
|
||||
|
@@ -22,11 +22,14 @@ import stubout
|
||||
import webob
|
||||
|
||||
from glance import server
|
||||
from glance.registry import controllers
|
||||
from glance.common import flags
|
||||
from glance.registry import server as rserver
|
||||
from tests import stubs
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
class TestImageController(unittest.TestCase):
|
||||
|
||||
class TestRegistryAPI(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Establish a clean test environment"""
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
@@ -47,7 +50,7 @@ class TestImageController(unittest.TestCase):
|
||||
fixture = {'id': 2,
|
||||
'name': 'fake image #2'}
|
||||
req = webob.Request.blank('/')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -65,7 +68,7 @@ class TestImageController(unittest.TestCase):
|
||||
fixture = {'id': 2,
|
||||
'name': 'fake image #2'}
|
||||
req = webob.Request.blank('/images')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -87,7 +90,7 @@ class TestImageController(unittest.TestCase):
|
||||
'status': 'available'
|
||||
}
|
||||
req = webob.Request.blank('/images/detail')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -109,7 +112,7 @@ class TestImageController(unittest.TestCase):
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(dict(image=fixture))
|
||||
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -141,7 +144,7 @@ class TestImageController(unittest.TestCase):
|
||||
# TODO(jaypipes): Port Nova's Fault infrastructure
|
||||
# over to Glance to support exception catching into
|
||||
# standard HTTP errors.
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
||||
|
||||
def test_update_image(self):
|
||||
@@ -155,7 +158,7 @@ class TestImageController(unittest.TestCase):
|
||||
req.method = 'PUT'
|
||||
req.body = json.dumps(dict(image=fixture))
|
||||
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -182,7 +185,7 @@ class TestImageController(unittest.TestCase):
|
||||
# TODO(jaypipes): Port Nova's Fault infrastructure
|
||||
# over to Glance to support exception catching into
|
||||
# standard HTTP errors.
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
self.assertEquals(res.status_int,
|
||||
webob.exc.HTTPNotFound.code)
|
||||
|
||||
@@ -191,7 +194,7 @@ class TestImageController(unittest.TestCase):
|
||||
|
||||
# Grab the original number of images
|
||||
req = webob.Request.blank('/images')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -202,13 +205,13 @@ class TestImageController(unittest.TestCase):
|
||||
|
||||
req.method = 'DELETE'
|
||||
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
# Verify one less image
|
||||
req = webob.Request.blank('/images')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -226,11 +229,75 @@ class TestImageController(unittest.TestCase):
|
||||
# TODO(jaypipes): Port Nova's Fault infrastructure
|
||||
# over to Glance to support exception catching into
|
||||
# standard HTTP errors.
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(rserver.API())
|
||||
self.assertEquals(res.status_int,
|
||||
webob.exc.HTTPNotFound.code)
|
||||
|
||||
|
||||
class TestGlanceAPI(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Establish a clean test environment"""
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
stubs.stub_out_registry_and_store_server(self.stubs)
|
||||
stubs.stub_out_registry_db_image_api(self.stubs)
|
||||
stubs.stub_out_filesystem_backend()
|
||||
self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir
|
||||
FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir
|
||||
stubs.clean_out_fake_filesystem_backend()
|
||||
self.stubs.UnsetAll()
|
||||
|
||||
def test_add_image_no_location_no_image_as_body(self):
|
||||
"""Tests raises BadRequest for no body and no loc header"""
|
||||
fixture_headers = {'x-image-meta-store': 'file',
|
||||
'x-image-meta-name': 'fake image #3'}
|
||||
|
||||
req = webob.Request.blank("/images")
|
||||
req.method = 'POST'
|
||||
for k,v in fixture_headers.iteritems():
|
||||
req.headers[k] = v
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
||||
|
||||
def test_add_image_bad_store(self):
|
||||
"""Tests raises BadRequest for invalid store header"""
|
||||
fixture_headers = {'x-image-meta-store': 'bad',
|
||||
'x-image-meta-name': 'fake image #3'}
|
||||
|
||||
req = webob.Request.blank("/images")
|
||||
req.method = 'POST'
|
||||
for k,v in fixture_headers.iteritems():
|
||||
req.headers[k] = v
|
||||
|
||||
req.headers['Content-Type'] = 'application/octet-stream'
|
||||
req.body = "chunk00000remainder"
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
||||
|
||||
def test_add_image_basic_file_store(self):
|
||||
"""Tests raises BadRequest for invalid store header"""
|
||||
fixture_headers = {'x-image-meta-store': 'file',
|
||||
'x-image-meta-name': 'fake image #3'}
|
||||
|
||||
req = webob.Request.blank("/images")
|
||||
req.method = 'POST'
|
||||
for k,v in fixture_headers.iteritems():
|
||||
req.headers[k] = v
|
||||
|
||||
req.headers['Content-Type'] = 'application/octet-stream'
|
||||
req.body = "chunk00000remainder"
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
res_body = json.loads(res.body)['image']
|
||||
self.assertEquals(res_body['location'],
|
||||
'file:///tmp/glance-tests/3')
|
||||
|
||||
def test_image_meta(self):
|
||||
"""Test for HEAD /images/<ID>"""
|
||||
expected_headers = {'x-image-meta-id': 2,
|
||||
'x-image-meta-name': 'fake image #2'}
|
||||
req = webob.Request.blank("/images/2")
|
||||
|
@@ -16,6 +16,7 @@
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
import stubout
|
||||
import StringIO
|
||||
import unittest
|
||||
@@ -24,24 +25,20 @@ import webob
|
||||
|
||||
from glance import client
|
||||
from glance.registry import client as rclient
|
||||
from glance.common import flags
|
||||
from glance.common import exception
|
||||
from tests import stubs
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
class TestBadClients(unittest.TestCase):
|
||||
|
||||
"""Test exceptions raised for bad clients"""
|
||||
|
||||
def test_bad_protocol(self):
|
||||
"""Test unsupported protocol raised"""
|
||||
c = client.Client(address="hdsa://127.012..1./")
|
||||
self.assertRaises(client.UnsupportedProtocolError,
|
||||
c.get_image,
|
||||
1)
|
||||
|
||||
def test_bad_address(self):
|
||||
"""Test unsupported protocol raised"""
|
||||
c = client.Client(address="http://127.999.1.1/")
|
||||
"""Test ClientConnectionError raised"""
|
||||
c = client.Client("127.999.1.1")
|
||||
self.assertRaises(client.ClientConnectionError,
|
||||
c.get_image,
|
||||
1)
|
||||
@@ -59,7 +56,7 @@ class TestRegistryClient(unittest.TestCase):
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
stubs.stub_out_registry_db_image_api(self.stubs)
|
||||
stubs.stub_out_registry_and_store_server(self.stubs)
|
||||
self.client = rclient.RegistryClient()
|
||||
self.client = rclient.RegistryClient("0.0.0.0")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
@@ -212,7 +209,7 @@ class TestRegistryClient(unittest.TestCase):
|
||||
'location': "file:///tmp/glance-tests/2",
|
||||
}
|
||||
|
||||
self.assertRaises(client.BadInputError,
|
||||
self.assertRaises(exception.BadInputError,
|
||||
self.client.add_image,
|
||||
fixture)
|
||||
|
||||
@@ -279,10 +276,13 @@ class TestClient(unittest.TestCase):
|
||||
stubs.stub_out_registry_db_image_api(self.stubs)
|
||||
stubs.stub_out_registry_and_store_server(self.stubs)
|
||||
stubs.stub_out_filesystem_backend()
|
||||
self.client = client.Client()
|
||||
self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir
|
||||
FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR
|
||||
self.client = client.Client("0.0.0.0")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir
|
||||
stubs.clean_out_fake_filesystem_backend()
|
||||
self.stubs.UnsetAll()
|
||||
|
||||
@@ -480,6 +480,85 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEquals(data['status'], 'available')
|
||||
|
||||
def test_add_image_with_image_data_as_string(self):
|
||||
"""Tests can add image by passing image data as string"""
|
||||
fixture = {'name': 'fake public image',
|
||||
'is_public': True,
|
||||
'type': 'kernel',
|
||||
'size': 19,
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}
|
||||
}
|
||||
|
||||
image_data_fixture = r"chunk0000remainder"
|
||||
|
||||
new_id = self.client.add_image(fixture, image_data_fixture)
|
||||
|
||||
self.assertEquals(3, new_id)
|
||||
|
||||
new_meta, new_image_chunks = self.client.get_image(3)
|
||||
|
||||
new_image_data = ""
|
||||
for image_chunk in new_image_chunks:
|
||||
new_image_data += image_chunk
|
||||
|
||||
self.assertEquals(image_data_fixture, new_image_data)
|
||||
for k,v in fixture.iteritems():
|
||||
self.assertEquals(v, new_meta[k])
|
||||
|
||||
def test_add_image_with_image_data_as_file(self):
|
||||
"""Tests can add image by passing image data as file"""
|
||||
fixture = {'name': 'fake public image',
|
||||
'is_public': True,
|
||||
'type': 'kernel',
|
||||
'size': 19,
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}
|
||||
}
|
||||
|
||||
image_data_fixture = r"chunk0000remainder"
|
||||
|
||||
tmp_image_filepath = '/tmp/rubbish-image'
|
||||
|
||||
if os.path.exists(tmp_image_filepath):
|
||||
os.unlink(tmp_image_filepath)
|
||||
|
||||
tmp_file = open(tmp_image_filepath, 'wb')
|
||||
tmp_file.write(image_data_fixture)
|
||||
tmp_file.close()
|
||||
|
||||
new_id = self.client.add_image(fixture, open(tmp_image_filepath))
|
||||
|
||||
self.assertEquals(3, new_id)
|
||||
|
||||
if os.path.exists(tmp_image_filepath):
|
||||
os.unlink(tmp_image_filepath)
|
||||
|
||||
new_meta, new_image_chunks = self.client.get_image(3)
|
||||
|
||||
new_image_data = ""
|
||||
for image_chunk in new_image_chunks:
|
||||
new_image_data += image_chunk
|
||||
|
||||
self.assertEquals(image_data_fixture, new_image_data)
|
||||
for k,v in fixture.iteritems():
|
||||
self.assertEquals(v, new_meta[k])
|
||||
|
||||
def test_add_image_with_bad_store(self):
|
||||
"""Tests BadRequest raised when supplying bad store name in meta"""
|
||||
fixture = {'name': 'fake public image',
|
||||
'is_public': True,
|
||||
'type': 'kernel',
|
||||
'size': 19,
|
||||
'store': 'bad',
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}
|
||||
}
|
||||
|
||||
image_data_fixture = r"chunk0000remainder"
|
||||
|
||||
self.assertRaises(exception.BadInputError,
|
||||
self.client.add_image,
|
||||
fixture,
|
||||
image_data_fixture)
|
||||
|
||||
def test_update_image(self):
|
||||
"""Tests that the /images PUT registry API updates the image"""
|
||||
fixture = {'name': 'fake public image #2',
|
||||
|
@@ -21,7 +21,7 @@ import unittest
|
||||
import webob
|
||||
|
||||
from glance.common import exception
|
||||
from glance.registry import controllers
|
||||
from glance.registry import server
|
||||
from tests import stubs
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class TestImageController(unittest.TestCase):
|
||||
fixture = {'id': 2,
|
||||
'name': 'fake image #2'}
|
||||
req = webob.Request.blank('/')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -61,7 +61,7 @@ class TestImageController(unittest.TestCase):
|
||||
fixture = {'id': 2,
|
||||
'name': 'fake image #2'}
|
||||
req = webob.Request.blank('/images')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -83,7 +83,7 @@ class TestImageController(unittest.TestCase):
|
||||
'status': 'available'
|
||||
}
|
||||
req = webob.Request.blank('/images/detail')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -105,7 +105,7 @@ class TestImageController(unittest.TestCase):
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(dict(image=fixture))
|
||||
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestImageController(unittest.TestCase):
|
||||
# TODO(jaypipes): Port Nova's Fault infrastructure
|
||||
# over to Glance to support exception catching into
|
||||
# standard HTTP errors.
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
||||
|
||||
def test_update_image(self):
|
||||
@@ -151,7 +151,7 @@ class TestImageController(unittest.TestCase):
|
||||
req.method = 'PUT'
|
||||
req.body = json.dumps(dict(image=fixture))
|
||||
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -178,7 +178,7 @@ class TestImageController(unittest.TestCase):
|
||||
# TODO(jaypipes): Port Nova's Fault infrastructure
|
||||
# over to Glance to support exception catching into
|
||||
# standard HTTP errors.
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int,
|
||||
webob.exc.HTTPNotFound.code)
|
||||
|
||||
@@ -187,7 +187,7 @@ class TestImageController(unittest.TestCase):
|
||||
|
||||
# Grab the original number of images
|
||||
req = webob.Request.blank('/images')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -198,13 +198,13 @@ class TestImageController(unittest.TestCase):
|
||||
|
||||
req.method = 'DELETE'
|
||||
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
# Verify one less image
|
||||
req = webob.Request.blank('/images')
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
@@ -222,6 +222,6 @@ class TestImageController(unittest.TestCase):
|
||||
# TODO(jaypipes): Port Nova's Fault infrastructure
|
||||
# over to Glance to support exception catching into
|
||||
# standard HTTP errors.
|
||||
res = req.get_response(controllers.API())
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int,
|
||||
webob.exc.HTTPNotFound.code)
|
||||
|
@@ -4,7 +4,7 @@ pep8==0.5.0
|
||||
pylint==0.19
|
||||
anyjson
|
||||
eventlet>=0.9.12
|
||||
lockfile
|
||||
lockfile==0.8
|
||||
python-daemon==1.5.5
|
||||
python-gflags>=1.3
|
||||
routes
|
||||
|
Reference in New Issue
Block a user