Merge from trunk and merge conflict resolution
This commit is contained in:
@@ -1,37 +1,43 @@
|
||||
# Format is:
|
||||
# <preferred e-mail> <other e-mail>
|
||||
<code@term.ie> <github@anarkystic.com>
|
||||
<code@term.ie> <termie@preciousroy.local>
|
||||
<Armando.Migliaccio@eu.citrix.com> <armando.migliaccio@citrix.com>
|
||||
<matt.dietz@rackspace.com> <matthewdietz@Matthew-Dietzs-MacBook-Pro.local>
|
||||
<matt.dietz@rackspace.com> <mdietz@openstack>
|
||||
<cbehrens@codestud.com> <chris.behrens@rackspace.com>
|
||||
<devin.carlen@gmail.com> <devcamcar@illian.local>
|
||||
<ewan.mellor@citrix.com> <emellor@silver>
|
||||
<jaypipes@gmail.com> <jpipes@serialcoder>
|
||||
# <preferred e-mail> <other e-mail 1>
|
||||
# <preferred e-mail> <other e-mail 2>
|
||||
<anotherjesse@gmail.com> <jesse@dancelamb>
|
||||
<anotherjesse@gmail.com> <jesse@gigantor.local>
|
||||
<anotherjesse@gmail.com> <jesse@ubuntu>
|
||||
<jmckenty@gmail.com> <jmckenty@yyj-dhcp171.corp.flock.com>
|
||||
<ant@openstack.org> <amesserl@rackspace.com>
|
||||
<Armando.Migliaccio@eu.citrix.com> <armando.migliaccio@citrix.com>
|
||||
<brian.lamar@rackspace.com> <brian.lamar@gmail.com>
|
||||
<bschott@isi.edu> <bfschott@gmail.com>
|
||||
<cbehrens@codestud.com> <chris.behrens@rackspace.com>
|
||||
<chiradeep@cloud.com> <chiradeep@chiradeep-lt2>
|
||||
<code@term.ie> <github@anarkystic.com>
|
||||
<code@term.ie> <termie@preciousroy.local>
|
||||
<corywright@gmail.com> <cory.wright@rackspace.com>
|
||||
<devin.carlen@gmail.com> <devcamcar@illian.local>
|
||||
<ewan.mellor@citrix.com> <emellor@silver>
|
||||
<jaypipes@gmail.com> <jpipes@serialcoder>
|
||||
<jmckenty@gmail.com> <jmckenty@joshua-mckentys-macbook-pro.local>
|
||||
<jmckenty@gmail.com> <jmckenty@yyj-dhcp171.corp.flock.com>
|
||||
<jmckenty@gmail.com> <joshua.mckenty@nasa.gov>
|
||||
<justin@fathomdb.com> <justinsb@justinsb-desktop>
|
||||
<masumotok@nttdata.co.jp> <root@openstack2-api>
|
||||
<justin@fathomdb.com> <superstack@superstack.org>
|
||||
<masumotok@nttdata.co.jp> Masumoto<masumotok@nttdata.co.jp>
|
||||
<masumotok@nttdata.co.jp> <root@openstack2-api>
|
||||
<matt.dietz@rackspace.com> <matthewdietz@Matthew-Dietzs-MacBook-Pro.local>
|
||||
<matt.dietz@rackspace.com> <mdietz@openstack>
|
||||
<mordred@inaugust.com> <mordred@hudson>
|
||||
<paul@openstack.org> <pvoccio@castor.local>
|
||||
<paul@openstack.org> <paul.voccio@rackspace.com>
|
||||
<paul@openstack.org> <pvoccio@castor.local>
|
||||
<rconradharris@gmail.com> <rick.harris@rackspace.com>
|
||||
<rlane@wikimedia.org> <laner@controller>
|
||||
<sleepsonthefloor@gmail.com> <root@tonbuntu>
|
||||
<soren.hansen@rackspace.com> <soren@linux2go.dk>
|
||||
<todd@ansolabs.com> <todd@lapex>
|
||||
<todd@ansolabs.com> <todd@rubidine.com>
|
||||
<vishvananda@gmail.com> <vishvananda@yahoo.com>
|
||||
<tushar.vitthal.patil@gmail.com> <tpatil@vertex.co.in>
|
||||
<ueno.nachi@lab.ntt.co.jp> <nati.ueno@gmail.com>
|
||||
<ueno.nachi@lab.ntt.co.jp> <nova@u4>
|
||||
<ueno.nachi@lab.ntt.co.jp> <openstack@lab.ntt.co.jp>
|
||||
<vishvananda@gmail.com> <root@mirror.nasanebula.net>
|
||||
<vishvananda@gmail.com> <root@ubuntu>
|
||||
<sleepsonthefloor@gmail.com> <root@tonbuntu>
|
||||
<rlane@wikimedia.org> <laner@controller>
|
||||
<rconradharris@gmail.com> <rick.harris@rackspace.com>
|
||||
<corywright@gmail.com> <cory.wright@rackspace.com>
|
||||
<ant@openstack.org> <amesserl@rackspace.com>
|
||||
<chiradeep@cloud.com> <chiradeep@chiradeep-lt2>
|
||||
<justin@fathomdb.com> <superstack@superstack.org>
|
||||
<brian.lamar@rackspace.com> <brian.lamar@gmail.com>
|
||||
<vishvananda@gmail.com> <vishvananda@yahoo.com>
|
||||
|
||||
@@ -3,17 +3,17 @@ Anne Gentle <anne@openstack.org>
|
||||
Anthony Young <sleepsonthefloor@gmail.com>
|
||||
Antony Messerli <ant@openstack.org>
|
||||
Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
|
||||
Brian Waldon <brian.waldon@rackspace.com>
|
||||
Bilal Akhtar <bilalakhtar@ubuntu.com>
|
||||
Brian Lamar <brian.lamar@rackspace.com>
|
||||
Brian Schott <bschott@isi.edu> <bfschott@gmail.com>
|
||||
Brian Schott <bschott@isi.edu>
|
||||
Brian Waldon <brian.waldon@rackspace.com>
|
||||
Chiradeep Vittal <chiradeep@cloud.com>
|
||||
Chmouel Boudjnah <chmouel@chmouel.com>
|
||||
Chris Behrens <cbehrens@codestud.com>
|
||||
Christian Berendt <berendt@b1-systems.de>
|
||||
Cory Wright <corywright@gmail.com>
|
||||
David Pravec <David.Pravec@danix.org>
|
||||
Dan Prince <dan.prince@rackspace.com>
|
||||
David Pravec <David.Pravec@danix.org>
|
||||
Dean Troyer <dtroyer@gmail.com>
|
||||
Devin Carlen <devin.carlen@gmail.com>
|
||||
Ed Leafe <ed@leafe.com>
|
||||
@@ -44,7 +44,7 @@ Monsyne Dragon <mdragon@rackspace.com>
|
||||
Monty Taylor <mordred@inaugust.com>
|
||||
MORITA Kazutaka <morita.kazutaka@gmail.com>
|
||||
Muneyuki Noguchi <noguchimn@nttdata.co.jp>
|
||||
Nachi Ueno <ueno.nachi@lab.ntt.co.jp> <openstack@lab.ntt.co.jp> <nati.ueno@gmail.com> <nova@u4>
|
||||
Nachi Ueno <ueno.nachi@lab.ntt.co.jp>
|
||||
Naveed Massjouni <naveed.massjouni@rackspace.com>
|
||||
Paul Voccio <paul@openstack.org>
|
||||
Ricardo Carrillo Cruz <emaildericky@gmail.com>
|
||||
@@ -59,7 +59,7 @@ Soren Hansen <soren.hansen@rackspace.com>
|
||||
Thierry Carrez <thierry@openstack.org>
|
||||
Todd Willey <todd@ansolabs.com>
|
||||
Trey Morris <trey.morris@rackspace.com>
|
||||
Tushar Patil <tushar.vitthal.patil@gmail.com> <tpatil@vertex.co.in>
|
||||
Tushar Patil <tushar.vitthal.patil@gmail.com>
|
||||
Vasiliy Shlykov <vash@vasiliyshlykov.org>
|
||||
Vishvananda Ishaya <vishvananda@gmail.com>
|
||||
Youcef Laribi <Youcef.Laribi@eu.citrix.com>
|
||||
|
||||
@@ -34,6 +34,7 @@ from nova.api.openstack import flavors
|
||||
from nova.api.openstack import images
|
||||
from nova.api.openstack import servers
|
||||
from nova.api.openstack import shared_ip_groups
|
||||
from nova.api.openstack import zones
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.api.openstack')
|
||||
@@ -81,6 +82,9 @@ class APIRouter(wsgi.Router):
|
||||
server_members['resume'] = 'POST'
|
||||
server_members['reset_network'] = 'POST'
|
||||
|
||||
mapper.resource("zone", "zones", controller=zones.Controller(),
|
||||
collection={'detail': 'GET'})
|
||||
|
||||
mapper.resource("server", "servers", controller=servers.Controller(),
|
||||
collection={'detail': 'GET'},
|
||||
member=server_members)
|
||||
|
||||
@@ -19,6 +19,7 @@ import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
|
||||
import webob.exc
|
||||
import webob.dec
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
@@ -179,7 +177,8 @@ class Controller(wsgi.Controller):
|
||||
display_name=env['server']['name'],
|
||||
display_description=env['server']['name'],
|
||||
key_name=key_pair['name'],
|
||||
key_data=key_pair['public_key'])
|
||||
key_data=key_pair['public_key'],
|
||||
onset_files=env.get('onset_files', []))
|
||||
return _translate_keys(instances[0])
|
||||
|
||||
def update(self, req, id):
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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.
|
||||
|
||||
import common
|
||||
import logging
|
||||
|
||||
from nova import flags
|
||||
from nova import wsgi
|
||||
from nova import db
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
def _filter_keys(item, keys):
|
||||
"""
|
||||
Filters all model attributes except for keys
|
||||
item is a dict
|
||||
|
||||
"""
|
||||
return dict((k, v) for k, v in item.iteritems() if k in keys)
|
||||
|
||||
|
||||
def _scrub_zone(zone):
|
||||
return _filter_keys(zone, ('id', 'api_url'))
|
||||
|
||||
|
||||
class Controller(wsgi.Controller):
|
||||
|
||||
_serialization_metadata = {
|
||||
'application/xml': {
|
||||
"attributes": {
|
||||
"zone": ["id", "api_url"]}}}
|
||||
|
||||
def index(self, req):
|
||||
"""Return all zones in brief"""
|
||||
items = db.zone_get_all(req.environ['nova.context'])
|
||||
items = common.limited(items, req)
|
||||
items = [_scrub_zone(item) for item in items]
|
||||
return dict(zones=items)
|
||||
|
||||
def detail(self, req):
|
||||
"""Return all zones in detail"""
|
||||
return self.index(req)
|
||||
|
||||
def show(self, req, id):
|
||||
"""Return data about the given zone id"""
|
||||
zone_id = int(id)
|
||||
zone = db.zone_get(req.environ['nova.context'], zone_id)
|
||||
return dict(zone=_scrub_zone(zone))
|
||||
|
||||
def delete(self, req, id):
|
||||
zone_id = int(id)
|
||||
db.zone_delete(req.environ['nova.context'], zone_id)
|
||||
return {}
|
||||
|
||||
def create(self, req):
|
||||
context = req.environ['nova.context']
|
||||
env = self._deserialize(req.body, req)
|
||||
zone = db.zone_create(context, env["zone"])
|
||||
return dict(zone=_scrub_zone(zone))
|
||||
|
||||
def update(self, req, id):
|
||||
context = req.environ['nova.context']
|
||||
env = self._deserialize(req.body, req)
|
||||
zone_id = int(id)
|
||||
zone = db.zone_update(context, zone_id, env["zone"])
|
||||
return dict(zone=_scrub_zone(zone))
|
||||
@@ -10,7 +10,6 @@ export NOVA_CERT=${NOVA_KEY_DIR}/%(nova)s
|
||||
export EUCALYPTUS_CERT=${NOVA_CERT} # euca-bundle-image seems to require this set
|
||||
alias ec2-bundle-image="ec2-bundle-image --cert ${EC2_CERT} --privatekey ${EC2_PRIVATE_KEY} --user 42 --ec2cert ${NOVA_CERT}"
|
||||
alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_KEY} --url ${S3_URL} --ec2cert ${NOVA_CERT}"
|
||||
export CLOUD_SERVERS_API_KEY="%(access)s"
|
||||
export CLOUD_SERVERS_USERNAME="%(user)s"
|
||||
export CLOUD_SERVERS_URL="%(os)s"
|
||||
|
||||
export NOVA_API_KEY="%(access)s"
|
||||
export NOVA_USERNAME="%(user)s"
|
||||
export NOVA_URL="%(os)s"
|
||||
|
||||
+10
-5
@@ -85,10 +85,11 @@ class API(base.Base):
|
||||
min_count=1, max_count=1,
|
||||
display_name='', display_description='',
|
||||
key_name=None, key_data=None, security_group='default',
|
||||
availability_zone=None, user_data=None):
|
||||
availability_zone=None, user_data=None,
|
||||
onset_files=None):
|
||||
"""Create the number of instances requested if quota and
|
||||
other arguments check out ok."""
|
||||
|
||||
other arguments check out ok.
|
||||
"""
|
||||
type_data = instance_types.INSTANCE_TYPES[instance_type]
|
||||
num_instances = quota.allowed_instances(context, max_count, type_data)
|
||||
if num_instances < min_count:
|
||||
@@ -156,7 +157,6 @@ class API(base.Base):
|
||||
'key_data': key_data,
|
||||
'locked': False,
|
||||
'availability_zone': availability_zone}
|
||||
|
||||
elevated = context.elevated()
|
||||
instances = []
|
||||
LOG.debug(_("Going to run %s instances..."), num_instances)
|
||||
@@ -193,7 +193,8 @@ class API(base.Base):
|
||||
{"method": "run_instance",
|
||||
"args": {"topic": FLAGS.compute_topic,
|
||||
"instance_id": instance_id,
|
||||
"availability_zone": availability_zone}})
|
||||
"availability_zone": availability_zone,
|
||||
"onset_files": onset_files}})
|
||||
|
||||
for group_id in security_groups:
|
||||
self.trigger_security_group_members_refresh(elevated, group_id)
|
||||
@@ -477,6 +478,10 @@ class API(base.Base):
|
||||
"""Set the root/admin password for the given instance."""
|
||||
self._cast_compute_message('set_admin_password', context, instance_id)
|
||||
|
||||
def inject_file(self, context, instance_id):
|
||||
"""Write a file to the given instance."""
|
||||
self._cast_compute_message('inject_file', context, instance_id)
|
||||
|
||||
def get_ajax_console(self, context, instance_id):
|
||||
"""Get a url to an AJAX Console"""
|
||||
instance = self.get(context, instance_id)
|
||||
|
||||
+39
-22
@@ -34,6 +34,7 @@ terminating it.
|
||||
:func:`nova.utils.import_object`
|
||||
"""
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import random
|
||||
import string
|
||||
@@ -130,7 +131,7 @@ class ComputeManager(manager.Manager):
|
||||
state = power_state.FAILED
|
||||
self.db.instance_set_state(context, instance_id, state)
|
||||
|
||||
def get_console_topic(self, context, **_kwargs):
|
||||
def get_console_topic(self, context, **kwargs):
|
||||
"""Retrieves the console host for a project on this host
|
||||
Currently this is just set in the flags for each compute
|
||||
host."""
|
||||
@@ -139,7 +140,7 @@ class ComputeManager(manager.Manager):
|
||||
FLAGS.console_topic,
|
||||
FLAGS.console_host)
|
||||
|
||||
def get_network_topic(self, context, **_kwargs):
|
||||
def get_network_topic(self, context, **kwargs):
|
||||
"""Retrieves the network host for a project on this host"""
|
||||
# TODO(vish): This method should be memoized. This will make
|
||||
# the call to get_network_host cheaper, so that
|
||||
@@ -158,21 +159,22 @@ class ComputeManager(manager.Manager):
|
||||
|
||||
@exception.wrap_exception
|
||||
def refresh_security_group_rules(self, context,
|
||||
security_group_id, **_kwargs):
|
||||
security_group_id, **kwargs):
|
||||
"""This call passes straight through to the virtualization driver."""
|
||||
return self.driver.refresh_security_group_rules(security_group_id)
|
||||
|
||||
@exception.wrap_exception
|
||||
def refresh_security_group_members(self, context,
|
||||
security_group_id, **_kwargs):
|
||||
security_group_id, **kwargs):
|
||||
"""This call passes straight through to the virtualization driver."""
|
||||
return self.driver.refresh_security_group_members(security_group_id)
|
||||
|
||||
@exception.wrap_exception
|
||||
def run_instance(self, context, instance_id, **_kwargs):
|
||||
def run_instance(self, context, instance_id, **kwargs):
|
||||
"""Launch a new instance with specified options."""
|
||||
context = context.elevated()
|
||||
instance_ref = self.db.instance_get(context, instance_id)
|
||||
instance_ref.onset_files = kwargs.get('onset_files', [])
|
||||
if instance_ref['name'] in self.driver.list_instances():
|
||||
raise exception.Error(_("Instance has already been created"))
|
||||
LOG.audit(_("instance %s: starting..."), instance_id,
|
||||
@@ -323,28 +325,43 @@ class ComputeManager(manager.Manager):
|
||||
"""Set the root/admin password for an instance on this server."""
|
||||
context = context.elevated()
|
||||
instance_ref = self.db.instance_get(context, instance_id)
|
||||
if instance_ref['state'] != power_state.RUNNING:
|
||||
logging.warn('trying to reset the password on a non-running '
|
||||
'instance: %s (state: %s expected: %s)',
|
||||
instance_ref['id'],
|
||||
instance_ref['state'],
|
||||
power_state.RUNNING)
|
||||
|
||||
logging.debug('instance %s: setting admin password',
|
||||
instance_id = instance_ref['id']
|
||||
instance_state = instance_ref['state']
|
||||
expected_state = power_state.RUNNING
|
||||
if instance_state != expected_state:
|
||||
LOG.warn(_('trying to reset the password on a non-running '
|
||||
'instance: %(instance_id)s (state: %(instance_state)s '
|
||||
'expected: %(expected_state)s)') % locals())
|
||||
LOG.audit(_('instance %s: setting admin password'),
|
||||
instance_ref['name'])
|
||||
if new_pass is None:
|
||||
# Generate a random password
|
||||
new_pass = self._generate_password(FLAGS.password_length)
|
||||
|
||||
new_pass = utils.generate_password(FLAGS.password_length)
|
||||
self.driver.set_admin_password(instance_ref, new_pass)
|
||||
self._update_state(context, instance_id)
|
||||
|
||||
def _generate_password(self, length=20):
|
||||
"""Generate a random sequence of letters and digits
|
||||
to be used as a password.
|
||||
"""
|
||||
chrs = string.letters + string.digits
|
||||
return "".join([random.choice(chrs) for i in xrange(length)])
|
||||
@exception.wrap_exception
|
||||
@checks_instance_lock
|
||||
def inject_file(self, context, instance_id, path, file_contents):
|
||||
"""Write a file to the specified path on an instance on this server"""
|
||||
context = context.elevated()
|
||||
instance_ref = self.db.instance_get(context, instance_id)
|
||||
instance_id = instance_ref['id']
|
||||
instance_state = instance_ref['state']
|
||||
expected_state = power_state.RUNNING
|
||||
if instance_state != expected_state:
|
||||
LOG.warn(_('trying to inject a file into a non-running '
|
||||
'instance: %(instance_id)s (state: %(instance_state)s '
|
||||
'expected: %(expected_state)s)') % locals())
|
||||
# Files/paths *should* be base64-encoded at this point, but
|
||||
# double-check to make sure.
|
||||
b64_path = utils.ensure_b64_encoding(path)
|
||||
b64_contents = utils.ensure_b64_encoding(file_contents)
|
||||
plain_path = base64.b64decode(b64_path)
|
||||
nm = instance_ref['name']
|
||||
msg = _('instance %(nm)s: injecting file to %(plain_path)s') % locals()
|
||||
LOG.audit(msg)
|
||||
self.driver.inject_file(instance_ref, b64_path, b64_contents)
|
||||
|
||||
@exception.wrap_exception
|
||||
@checks_instance_lock
|
||||
@@ -629,7 +646,7 @@ class ComputeManager(manager.Manager):
|
||||
def get_ajax_console(self, context, instance_id):
|
||||
"""Return connection information for an ajax console"""
|
||||
context = context.elevated()
|
||||
logging.debug(_("instance %s: getting ajax console"), instance_id)
|
||||
LOG.debug(_("instance %s: getting ajax console"), instance_id)
|
||||
instance_ref = self.db.instance_get(context, instance_id)
|
||||
|
||||
return self.driver.get_ajax_console(instance_ref)
|
||||
|
||||
+29
-1
@@ -603,7 +603,7 @@ def project_get_network(context, project_id, associate=True):
|
||||
|
||||
"""
|
||||
|
||||
return IMPL.project_get_network(context, project_id)
|
||||
return IMPL.project_get_network(context, project_id, associate)
|
||||
|
||||
|
||||
def project_get_network_v6(context, project_id):
|
||||
@@ -1027,3 +1027,31 @@ def console_get_all_by_instance(context, instance_id):
|
||||
def console_get(context, console_id, instance_id=None):
|
||||
"""Get a specific console (possibly on a given instance)."""
|
||||
return IMPL.console_get(context, console_id, instance_id)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
def zone_create(context, values):
|
||||
"""Create a new child Zone entry."""
|
||||
return IMPL.zone_create(context, values)
|
||||
|
||||
|
||||
def zone_update(context, zone_id, values):
|
||||
"""Update a child Zone entry."""
|
||||
return IMPL.zone_update(context, values)
|
||||
|
||||
|
||||
def zone_delete(context, zone_id):
|
||||
"""Delete a child Zone."""
|
||||
return IMPL.zone_delete(context, zone_id)
|
||||
|
||||
|
||||
def zone_get(context, zone_id):
|
||||
"""Get a specific child Zone."""
|
||||
return IMPL.zone_get(context, zone_id)
|
||||
|
||||
|
||||
def zone_get_all(context):
|
||||
"""Get all child Zones."""
|
||||
return IMPL.zone_get_all(context)
|
||||
|
||||
@@ -2114,3 +2114,47 @@ def console_get(context, console_id, instance_id=None):
|
||||
raise exception.NotFound(_("No console with id %(console_id)s"
|
||||
" %(idesc)s") % locals())
|
||||
return result
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def zone_create(context, values):
|
||||
zone = models.Zone()
|
||||
zone.update(values)
|
||||
zone.save()
|
||||
return zone
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def zone_update(context, zone_id, values):
|
||||
zone = session.query(models.Zone).filter_by(id=zone_id).first()
|
||||
if not zone:
|
||||
raise exception.NotFound(_("No zone with id %(zone_id)s") % locals())
|
||||
zone.update(values)
|
||||
zone.save()
|
||||
return zone
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def zone_delete(context, zone_id):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
session.execute('delete from zones '
|
||||
'where id=:id', {'id': zone_id})
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def zone_get(context, zone_id):
|
||||
session = get_session()
|
||||
result = session.query(models.Zone).filter_by(id=zone_id).first()
|
||||
if not result:
|
||||
raise exception.NotFound(_("No zone with id %(zone_id)s") % locals())
|
||||
return result
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def zone_get_all(context):
|
||||
session = get_session()
|
||||
return session.query(models.Zone).all()
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
|
||||
from sqlalchemy import *
|
||||
from migrate import *
|
||||
|
||||
from nova import log as logging
|
||||
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
|
||||
#
|
||||
# New Tables
|
||||
#
|
||||
zones = Table('zones', meta,
|
||||
Column('created_at', DateTime(timezone=False)),
|
||||
Column('updated_at', DateTime(timezone=False)),
|
||||
Column('deleted_at', DateTime(timezone=False)),
|
||||
Column('deleted', Boolean(create_constraint=True, name=None)),
|
||||
Column('id', Integer(), primary_key=True, nullable=False),
|
||||
Column('api_url',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False)),
|
||||
Column('username',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False)),
|
||||
Column('password',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False)),
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Tables to alter
|
||||
#
|
||||
|
||||
# (none currently)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
# Upgrade operations go here. Don't create your own engine;
|
||||
# bind migrate_engine to your metadata
|
||||
meta.bind = migrate_engine
|
||||
for table in (zones, ):
|
||||
try:
|
||||
table.create()
|
||||
except Exception:
|
||||
logging.info(repr(table))
|
||||
@@ -55,8 +55,8 @@ def db_version():
|
||||
engine = sqlalchemy.create_engine(FLAGS.sql_connection, echo=False)
|
||||
meta.reflect(bind=engine)
|
||||
try:
|
||||
for table in ('auth_tokens', 'export_devices', 'fixed_ips',
|
||||
'floating_ips', 'instances',
|
||||
for table in ('auth_tokens', 'zones', 'export_devices',
|
||||
'fixed_ips', 'floating_ips', 'instances',
|
||||
'key_pairs', 'networks', 'projects', 'quotas',
|
||||
'security_group_instance_association',
|
||||
'security_group_rules', 'security_groups',
|
||||
|
||||
@@ -548,6 +548,15 @@ class Console(BASE, NovaBase):
|
||||
pool = relationship(ConsolePool, backref=backref('consoles'))
|
||||
|
||||
|
||||
class Zone(BASE, NovaBase):
|
||||
"""Represents a child zone of this zone."""
|
||||
__tablename__ = 'zones'
|
||||
id = Column(Integer, primary_key=True)
|
||||
api_url = Column(String(255))
|
||||
username = Column(String(255))
|
||||
password = Column(String(255))
|
||||
|
||||
|
||||
def register_models():
|
||||
"""Register Models and create metadata.
|
||||
|
||||
@@ -560,8 +569,7 @@ def register_models():
|
||||
Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp,
|
||||
Network, SecurityGroup, SecurityGroupIngressRule,
|
||||
SecurityGroupInstanceAssociation, AuthToken, User,
|
||||
Project, Certificate, ConsolePool, Console,
|
||||
Migration) # , Image, Host
|
||||
Project, Certificate, ConsolePool, Console, Zone, Migration)
|
||||
engine = create_engine(FLAGS.sql_connection, echo=False)
|
||||
for model in models:
|
||||
model.metadata.create_all(engine)
|
||||
|
||||
+2
-2
@@ -94,7 +94,7 @@ critical = logging.critical
|
||||
log = logging.log
|
||||
# handlers
|
||||
StreamHandler = logging.StreamHandler
|
||||
RotatingFileHandler = logging.handlers.RotatingFileHandler
|
||||
WatchedFileHandler = logging.handlers.WatchedFileHandler
|
||||
# logging.SysLogHandler is nicer than logging.logging.handler.SysLogHandler.
|
||||
SysLogHandler = logging.handlers.SysLogHandler
|
||||
|
||||
@@ -139,7 +139,7 @@ def basicConfig():
|
||||
logging.root.addHandler(syslog)
|
||||
logpath = get_log_file_path()
|
||||
if logpath:
|
||||
logfile = RotatingFileHandler(logpath)
|
||||
logfile = WatchedFileHandler(logpath)
|
||||
logfile.setFormatter(_formatter)
|
||||
logging.root.addHandler(logfile)
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ flags.DEFINE_string('dhcp_domain',
|
||||
|
||||
flags.DEFINE_string('networks_path', '$state_path/networks',
|
||||
'Location to keep network config files')
|
||||
flags.DEFINE_string('public_interface', 'vlan1',
|
||||
flags.DEFINE_string('public_interface', 'eth0',
|
||||
'Interface for public IP addresses')
|
||||
flags.DEFINE_string('vlan_interface', 'eth0',
|
||||
'network device for vlans')
|
||||
|
||||
+16
-13
@@ -110,6 +110,7 @@ class NetworkManager(manager.Manager):
|
||||
|
||||
This class must be subclassed to support specific topologies.
|
||||
"""
|
||||
timeout_fixed_ips = True
|
||||
|
||||
def __init__(self, network_driver=None, *args, **kwargs):
|
||||
if not network_driver:
|
||||
@@ -138,6 +139,19 @@ class NetworkManager(manager.Manager):
|
||||
self.driver.ensure_floating_forward(floating_ip['address'],
|
||||
fixed_address)
|
||||
|
||||
def periodic_tasks(self, context=None):
|
||||
"""Tasks to be run at a periodic interval."""
|
||||
super(NetworkManager, self).periodic_tasks(context)
|
||||
if self.timeout_fixed_ips:
|
||||
now = utils.utcnow()
|
||||
timeout = FLAGS.fixed_ip_disassociate_timeout
|
||||
time = now - datetime.timedelta(seconds=timeout)
|
||||
num = self.db.fixed_ip_disassociate_all_by_timeout(context,
|
||||
self.host,
|
||||
time)
|
||||
if num:
|
||||
LOG.debug(_("Dissassociated %s stale fixed ip(s)"), num)
|
||||
|
||||
def set_network_host(self, context, network_id):
|
||||
"""Safely sets the host of the network."""
|
||||
LOG.debug(_("setting network host"), context=context)
|
||||
@@ -306,6 +320,7 @@ class FlatManager(NetworkManager):
|
||||
not do any setup in this mode, it must be done manually. Requests to
|
||||
169.254.169.254 port 80 will need to be forwarded to the api server.
|
||||
"""
|
||||
timeout_fixed_ips = False
|
||||
|
||||
def allocate_fixed_ip(self, context, instance_id, *args, **kwargs):
|
||||
"""Gets a fixed ip from the pool."""
|
||||
@@ -457,18 +472,6 @@ class VlanManager(NetworkManager):
|
||||
instances in its subnet.
|
||||
"""
|
||||
|
||||
def periodic_tasks(self, context=None):
|
||||
"""Tasks to be run at a periodic interval."""
|
||||
super(VlanManager, self).periodic_tasks(context)
|
||||
now = datetime.datetime.utcnow()
|
||||
timeout = FLAGS.fixed_ip_disassociate_timeout
|
||||
time = now - datetime.timedelta(seconds=timeout)
|
||||
num = self.db.fixed_ip_disassociate_all_by_timeout(context,
|
||||
self.host,
|
||||
time)
|
||||
if num:
|
||||
LOG.debug(_("Dissassociated %s stale fixed ip(s)"), num)
|
||||
|
||||
def init_host(self):
|
||||
"""Do any initialization that needs to be run if this is a
|
||||
standalone service.
|
||||
@@ -509,7 +512,7 @@ class VlanManager(NetworkManager):
|
||||
network_ref['bridge'])
|
||||
|
||||
def create_networks(self, context, cidr, num_networks, network_size,
|
||||
cidr_v6, vlan_start, vpn_start):
|
||||
cidr_v6, vlan_start, vpn_start, **kwargs):
|
||||
"""Create networks based on parameters."""
|
||||
# Check that num_networks + vlan_start is not > 4094, fixes lp708025
|
||||
if num_networks + vlan_start > 4094:
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# 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.
|
||||
|
||||
import unittest
|
||||
|
||||
import stubout
|
||||
import webob
|
||||
import json
|
||||
|
||||
import nova.db
|
||||
from nova import context
|
||||
from nova import flags
|
||||
from nova.api.openstack import zones
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.verbose = True
|
||||
|
||||
|
||||
def zone_get(context, zone_id):
|
||||
return dict(id=1, api_url='http://foo.com', username='bob',
|
||||
password='xxx')
|
||||
|
||||
|
||||
def zone_create(context, values):
|
||||
zone = dict(id=1)
|
||||
zone.update(values)
|
||||
return zone
|
||||
|
||||
|
||||
def zone_update(context, zone_id, values):
|
||||
zone = dict(id=zone_id, api_url='http://foo.com', username='bob',
|
||||
password='xxx')
|
||||
zone.update(values)
|
||||
return zone
|
||||
|
||||
|
||||
def zone_delete(context, zone_id):
|
||||
pass
|
||||
|
||||
|
||||
def zone_get_all(context):
|
||||
return [
|
||||
dict(id=1, api_url='http://foo.com', username='bob',
|
||||
password='xxx'),
|
||||
dict(id=2, api_url='http://blah.com', username='alice',
|
||||
password='qwerty')
|
||||
]
|
||||
|
||||
|
||||
class ZonesTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
fakes.FakeAuthManager.auth_data = {}
|
||||
fakes.FakeAuthDatabase.data = {}
|
||||
fakes.stub_out_networking(self.stubs)
|
||||
fakes.stub_out_rate_limiting(self.stubs)
|
||||
fakes.stub_out_auth(self.stubs)
|
||||
|
||||
self.allow_admin = FLAGS.allow_admin_api
|
||||
FLAGS.allow_admin_api = True
|
||||
|
||||
self.stubs.Set(nova.db, 'zone_get', zone_get)
|
||||
self.stubs.Set(nova.db, 'zone_get_all', zone_get_all)
|
||||
self.stubs.Set(nova.db, 'zone_update', zone_update)
|
||||
self.stubs.Set(nova.db, 'zone_create', zone_create)
|
||||
self.stubs.Set(nova.db, 'zone_delete', zone_delete)
|
||||
|
||||
def tearDown(self):
|
||||
self.stubs.UnsetAll()
|
||||
FLAGS.allow_admin_api = self.allow_admin
|
||||
|
||||
def test_get_zone_list(self):
|
||||
req = webob.Request.blank('/v1.0/zones')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
|
||||
self.assertEqual(res.status_int, 200)
|
||||
self.assertEqual(len(res_dict['zones']), 2)
|
||||
|
||||
def test_get_zone_by_id(self):
|
||||
req = webob.Request.blank('/v1.0/zones/1')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
|
||||
self.assertEqual(res_dict['zone']['id'], 1)
|
||||
self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com')
|
||||
self.assertFalse('password' in res_dict['zone'])
|
||||
self.assertEqual(res.status_int, 200)
|
||||
|
||||
def test_zone_delete(self):
|
||||
req = webob.Request.blank('/v1.0/zones/1')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
|
||||
self.assertEqual(res.status_int, 200)
|
||||
|
||||
def test_zone_create(self):
|
||||
body = dict(zone=dict(api_url='http://blah.zoo', username='fred',
|
||||
password='fubar'))
|
||||
req = webob.Request.blank('/v1.0/zones')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
|
||||
self.assertEqual(res.status_int, 200)
|
||||
self.assertEqual(res_dict['zone']['id'], 1)
|
||||
self.assertEqual(res_dict['zone']['api_url'], 'http://blah.zoo')
|
||||
self.assertFalse('username' in res_dict['zone'])
|
||||
|
||||
def test_zone_update(self):
|
||||
body = dict(zone=dict(username='zeb', password='sneaky'))
|
||||
req = webob.Request.blank('/v1.0/zones/1')
|
||||
req.method = 'PUT'
|
||||
req.body = json.dumps(body)
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
|
||||
self.assertEqual(res.status_int, 200)
|
||||
self.assertEqual(res_dict['zone']['id'], 1)
|
||||
self.assertEqual(res_dict['zone']['api_url'], 'http://foo.com')
|
||||
self.assertFalse('username' in res_dict['zone'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -203,6 +203,14 @@ class ComputeTestCase(test.TestCase):
|
||||
self.compute.set_admin_password(self.context, instance_id)
|
||||
self.compute.terminate_instance(self.context, instance_id)
|
||||
|
||||
def test_inject_file(self):
|
||||
"""Ensure we can write a file to an instance"""
|
||||
instance_id = self._create_instance()
|
||||
self.compute.run_instance(self.context, instance_id)
|
||||
self.compute.inject_file(self.context, instance_id, "/tmp/test",
|
||||
"File Contents")
|
||||
self.compute.terminate_instance(self.context, instance_id)
|
||||
|
||||
def test_snapshot(self):
|
||||
"""Ensure instance can be snapshotted"""
|
||||
instance_id = self._create_instance()
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
System-level utilities and helper functions.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
import string
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
@@ -235,6 +237,15 @@ def generate_mac():
|
||||
return ':'.join(map(lambda x: "%02x" % x, mac))
|
||||
|
||||
|
||||
def generate_password(length=20):
|
||||
"""Generate a random sequence of letters and digits
|
||||
to be used as a password. Note that this is not intended
|
||||
to represent the ultimate in security.
|
||||
"""
|
||||
chrs = string.letters + string.digits
|
||||
return "".join([random.choice(chrs) for i in xrange(length)])
|
||||
|
||||
|
||||
def last_octet(address):
|
||||
return int(address.split(".")[-1])
|
||||
|
||||
@@ -476,3 +487,15 @@ def dumps(value):
|
||||
|
||||
def loads(s):
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def ensure_b64_encoding(val):
|
||||
"""Safety method to ensure that values expected to be base64-encoded
|
||||
actually are. If they are, the value is returned unchanged. Otherwise,
|
||||
the encoded value is returned.
|
||||
"""
|
||||
try:
|
||||
dummy = base64.decode(val)
|
||||
return val
|
||||
except TypeError:
|
||||
return base64.b64encode(val)
|
||||
|
||||
@@ -170,6 +170,21 @@ class FakeConnection(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def inject_file(self, instance, b64_path, b64_contents):
|
||||
"""
|
||||
Writes a file on the specified instance.
|
||||
|
||||
The first parameter is an instance of nova.compute.service.Instance,
|
||||
and so the instance is being specified as instance.name. The second
|
||||
parameter is the base64-encoded path to which the file is to be
|
||||
written on the instance; the third is the contents of the file, also
|
||||
base64-encoded.
|
||||
|
||||
The work will be done asynchronously. This function returns a
|
||||
task that allows the caller to detect when it is complete.
|
||||
"""
|
||||
pass
|
||||
|
||||
def rescue(self, instance):
|
||||
"""
|
||||
Rescue the specified instance.
|
||||
|
||||
@@ -102,6 +102,7 @@ class VMOps(object):
|
||||
vdi_uuid = VMHelper.fetch_image(self._session, instance.id,
|
||||
instance.image_id, user, project, disk_image_type)
|
||||
vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid)
|
||||
|
||||
else:
|
||||
vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', disk)
|
||||
|
||||
@@ -169,6 +170,21 @@ class VMOps(object):
|
||||
LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.')
|
||||
% locals())
|
||||
|
||||
def _inject_onset_files():
|
||||
onset_files = instance.onset_files
|
||||
if onset_files:
|
||||
# Check if this is a JSON-encoded string and convert if needed.
|
||||
if isinstance(onset_files, basestring):
|
||||
try:
|
||||
onset_files = json.loads(onset_files)
|
||||
except ValueError:
|
||||
LOG.exception(_("Invalid value for onset_files: '%s'")
|
||||
% onset_files)
|
||||
onset_files = []
|
||||
# Inject any files, if specified
|
||||
for path, contents in instance.onset_files:
|
||||
LOG.debug(_("Injecting file path: '%s'") % path)
|
||||
self.inject_file(instance, path, contents)
|
||||
# NOTE(armando): Do we really need to do this in virt?
|
||||
# NOTE(tr3buchet): not sure but wherever we do it, we need to call
|
||||
# reset_network afterwards
|
||||
@@ -182,6 +198,8 @@ class VMOps(object):
|
||||
if state == power_state.RUNNING:
|
||||
LOG.debug(_('Instance %s: booted'), instance['name'])
|
||||
timer.stop()
|
||||
_inject_onset_files()
|
||||
return True
|
||||
except Exception, exc:
|
||||
LOG.warn(exc)
|
||||
LOG.exception(_('instance %s: failed to boot'),
|
||||
@@ -190,6 +208,7 @@ class VMOps(object):
|
||||
instance['id'],
|
||||
power_state.SHUTDOWN)
|
||||
timer.stop()
|
||||
return False
|
||||
|
||||
timer.f = _wait_for_boot
|
||||
|
||||
@@ -402,6 +421,32 @@ class VMOps(object):
|
||||
raise RuntimeError(resp_dict['message'])
|
||||
return resp_dict['message']
|
||||
|
||||
def inject_file(self, instance, b64_path, b64_contents):
|
||||
"""Write a file to the VM instance. The path to which it is to be
|
||||
written and the contents of the file need to be supplied; both should
|
||||
be base64-encoded to prevent errors with non-ASCII characters being
|
||||
transmitted. If the agent does not support file injection, or the user
|
||||
has disabled it, a NotImplementedError will be raised.
|
||||
"""
|
||||
# Files/paths *should* be base64-encoded at this point, but
|
||||
# double-check to make sure.
|
||||
b64_path = utils.ensure_b64_encoding(b64_path)
|
||||
b64_contents = utils.ensure_b64_encoding(b64_contents)
|
||||
|
||||
# Need to uniquely identify this request.
|
||||
transaction_id = str(uuid.uuid4())
|
||||
args = {'id': transaction_id, 'b64_path': b64_path,
|
||||
'b64_contents': b64_contents}
|
||||
# If the agent doesn't support file injection, a NotImplementedError
|
||||
# will be raised with the appropriate message.
|
||||
resp = self._make_agent_call('inject_file', instance, '', args)
|
||||
resp_dict = json.loads(resp)
|
||||
if resp_dict['returncode'] != '0':
|
||||
# There was some other sort of error; the message will contain
|
||||
# a description of the error.
|
||||
raise RuntimeError(resp_dict['message'])
|
||||
return resp_dict['message']
|
||||
|
||||
def _shutdown(self, instance, vm, method='hard'):
|
||||
"""Shutdown an instance """
|
||||
state = self.get_info(instance['name'])['state']
|
||||
@@ -620,6 +665,11 @@ class VMOps(object):
|
||||
if 'TIMEOUT:' in err_msg:
|
||||
LOG.error(_('TIMEOUT: The call to %(method)s timed out. '
|
||||
'VM id=%(instance_id)s; args=%(strargs)s') % locals())
|
||||
elif 'NOT IMPLEMENTED:' in err_msg:
|
||||
LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not'
|
||||
' supported by the agent. VM id=%(instance_id)s;'
|
||||
' args=%(strargs)s') % locals())
|
||||
raise NotImplementedError(err_msg)
|
||||
else:
|
||||
LOG.error(_('The call to %(method)s returned an error: %(e)s. '
|
||||
'VM id=%(instance_id)s; args=%(strargs)s') % locals())
|
||||
|
||||
@@ -172,6 +172,12 @@ class XenAPIConnection(object):
|
||||
"""Set the root/admin password on the VM instance"""
|
||||
self._vmops.set_admin_password(instance, new_pass)
|
||||
|
||||
def inject_file(self, instance, b64_path, b64_contents):
|
||||
"""Create a file on the VM instance. The file path and contents
|
||||
should be base64-encoded.
|
||||
"""
|
||||
self._vmops.inject_file(instance, b64_path, b64_contents)
|
||||
|
||||
def destroy(self, instance):
|
||||
"""Destroy VM instance"""
|
||||
self._vmops.destroy(instance)
|
||||
|
||||
@@ -93,9 +93,8 @@ def password(self, arg_dict):
|
||||
|
||||
@jsonify
|
||||
def resetnetwork(self, arg_dict):
|
||||
"""
|
||||
Writes a resquest to xenstore that tells the agent to reset networking.
|
||||
|
||||
"""Writes a resquest to xenstore that tells the agent
|
||||
to reset networking.
|
||||
"""
|
||||
arg_dict['value'] = json.dumps({'name': 'resetnetwork', 'value': ''})
|
||||
request_id = arg_dict['id']
|
||||
|
||||
@@ -36,7 +36,15 @@ pluginlib.configure_logging("xenstore")
|
||||
|
||||
def jsonify(fnc):
|
||||
def wrapper(*args, **kwargs):
|
||||
return json.dumps(fnc(*args, **kwargs))
|
||||
ret = fnc(*args, **kwargs)
|
||||
try:
|
||||
json.loads(ret)
|
||||
except ValueError:
|
||||
# Value should already be JSON-encoded, but some operations
|
||||
# may write raw sting values; this will catch those and
|
||||
# properly encode them.
|
||||
ret = json.dumps(ret)
|
||||
return ret
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user