merged with volume_types. no code refactoring yet
This commit is contained in:
2
Authors
2
Authors
@@ -18,6 +18,7 @@ Chiradeep Vittal <chiradeep@cloud.com>
|
||||
Chmouel Boudjnah <chmouel@chmouel.com>
|
||||
Chris Behrens <cbehrens@codestud.com>
|
||||
Christian Berendt <berendt@b1-systems.de>
|
||||
Christopher MacGown <chris@pistoncloud.com>
|
||||
Chuck Short <zulcss@ubuntu.com>
|
||||
Cory Wright <corywright@gmail.com>
|
||||
Dan Prince <dan.prince@rackspace.com>
|
||||
@@ -101,6 +102,7 @@ Stephanie Reese <reese.sm@gmail.com>
|
||||
Thierry Carrez <thierry@openstack.org>
|
||||
Todd Willey <todd@ansolabs.com>
|
||||
Trey Morris <trey.morris@rackspace.com>
|
||||
Troy Toman <troy.toman@rackspace.com>
|
||||
Tushar Patil <tushar.vitthal.patil@gmail.com>
|
||||
Vasiliy Shlykov <vash@vasiliyshlykov.org>
|
||||
Vishvananda Ishaya <vishvananda@gmail.com>
|
||||
|
||||
@@ -24,7 +24,6 @@ from eventlet import greenthread
|
||||
from eventlet.green import urllib2
|
||||
|
||||
import exceptions
|
||||
import gettext
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
@@ -38,11 +37,11 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import rpc
|
||||
from nova import service
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
|
||||
@@ -141,5 +140,5 @@ if __name__ == '__main__':
|
||||
acp = AjaxConsoleProxy()
|
||||
acp.register_listeners()
|
||||
server = wsgi.Server("AJAX Console Proxy", acp, port=acp_port)
|
||||
server.start()
|
||||
server.wait()
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
43
bin/nova-api
43
bin/nova-api
@@ -19,12 +19,14 @@
|
||||
|
||||
"""Starter script for Nova API.
|
||||
|
||||
Starts both the EC2 and OpenStack APIs in separate processes.
|
||||
Starts both the EC2 and OpenStack APIs in separate greenthreads.
|
||||
|
||||
"""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
|
||||
@@ -33,32 +35,19 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(
|
||||
if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
import nova.service
|
||||
import nova.utils
|
||||
|
||||
from nova import flags
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
def main():
|
||||
"""Launch EC2 and OSAPI services."""
|
||||
nova.utils.Bootstrapper.bootstrap_binary(sys.argv)
|
||||
|
||||
launcher = nova.service.Launcher()
|
||||
|
||||
for api in FLAGS.enabled_apis:
|
||||
service = nova.service.WSGIService(api)
|
||||
launcher.launch_service(service)
|
||||
|
||||
signal.signal(signal.SIGTERM, lambda *_: launcher.stop())
|
||||
|
||||
try:
|
||||
launcher.wait()
|
||||
except KeyboardInterrupt:
|
||||
launcher.stop()
|
||||
|
||||
from nova import log as logging
|
||||
from nova import service
|
||||
from nova import utils
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
utils.monkey_patch()
|
||||
servers = []
|
||||
for api in flags.FLAGS.enabled_apis:
|
||||
servers.append(service.WSGIService(api))
|
||||
service.serve(*servers)
|
||||
service.wait()
|
||||
|
||||
47
bin/nova-api-ec2
Executable file
47
bin/nova-api-ec2
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Starter script for Nova EC2 API."""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
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, "nova", "__init__.py")):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import service
|
||||
from nova import utils
|
||||
|
||||
if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
utils.monkey_patch()
|
||||
server = service.WSGIService('ec2')
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
47
bin/nova-api-os
Executable file
47
bin/nova-api-os
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Starter script for Nova OS API."""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
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, "nova", "__init__.py")):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import service
|
||||
from nova import utils
|
||||
|
||||
if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
utils.monkey_patch()
|
||||
server = service.WSGIService('osapi')
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
@@ -22,7 +22,6 @@
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -34,7 +33,6 @@ POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, POSSIBLE_TOPDIR)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
@@ -45,5 +43,7 @@ if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
service.serve()
|
||||
utils.monkey_patch()
|
||||
server = service.Service.create(binary='nova-compute')
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -33,7 +32,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
@@ -44,5 +42,6 @@ if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
service.serve()
|
||||
server = service.Service.create(binary='nova-console')
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -52,7 +52,7 @@ flags.DECLARE('update_dhcp_on_disassociate', 'nova.network.manager')
|
||||
LOG = logging.getLogger('nova.dhcpbridge')
|
||||
|
||||
|
||||
def add_lease(mac, ip_address, _interface):
|
||||
def add_lease(mac, ip_address):
|
||||
"""Set the IP that was assigned by the DHCP server."""
|
||||
if FLAGS.fake_rabbit:
|
||||
LOG.debug(_("leasing ip"))
|
||||
@@ -66,13 +66,13 @@ def add_lease(mac, ip_address, _interface):
|
||||
"args": {"address": ip_address}})
|
||||
|
||||
|
||||
def old_lease(mac, ip_address, interface):
|
||||
def old_lease(mac, ip_address):
|
||||
"""Update just as add lease."""
|
||||
LOG.debug(_("Adopted old lease or got a change of mac"))
|
||||
add_lease(mac, ip_address, interface)
|
||||
add_lease(mac, ip_address)
|
||||
|
||||
|
||||
def del_lease(mac, ip_address, _interface):
|
||||
def del_lease(mac, ip_address):
|
||||
"""Called when a lease expires."""
|
||||
if FLAGS.fake_rabbit:
|
||||
LOG.debug(_("releasing ip"))
|
||||
@@ -99,8 +99,6 @@ def main():
|
||||
utils.default_flagfile(flagfile)
|
||||
argv = FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
# check ENV first so we don't break any older deploys
|
||||
network_id = int(os.environ.get('NETWORK_ID'))
|
||||
|
||||
if int(os.environ.get('TESTING', '0')):
|
||||
from nova.tests import fake_flags
|
||||
@@ -115,11 +113,19 @@ def main():
|
||||
if action in ['add', 'del', 'old']:
|
||||
mac = argv[2]
|
||||
ip = argv[3]
|
||||
msg = _("Called %(action)s for mac %(mac)s with ip %(ip)s"
|
||||
" on interface %(interface)s") % locals()
|
||||
msg = _("Called '%(action)s' for mac '%(mac)s' with ip '%(ip)s'") % \
|
||||
{"action": action,
|
||||
"mac": mac,
|
||||
"ip": ip}
|
||||
LOG.debug(msg)
|
||||
globals()[action + '_lease'](mac, ip, interface)
|
||||
globals()[action + '_lease'](mac, ip)
|
||||
else:
|
||||
try:
|
||||
network_id = int(os.environ.get('NETWORK_ID'))
|
||||
except TypeError:
|
||||
LOG.error(_("Environment variable 'NETWORK_ID' must be set."))
|
||||
sys.exit(1)
|
||||
|
||||
print init_leases(network_id)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
|
||||
"""Starter script for Nova Direct API."""
|
||||
|
||||
import gettext
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -32,12 +34,12 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import compute
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import network
|
||||
from nova import service
|
||||
from nova import utils
|
||||
from nova import volume
|
||||
from nova import wsgi
|
||||
@@ -97,5 +99,6 @@ if __name__ == '__main__':
|
||||
with_auth,
|
||||
host=FLAGS.direct_host,
|
||||
port=FLAGS.direct_port)
|
||||
server.start()
|
||||
server.wait()
|
||||
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -140,7 +140,7 @@ class VpnCommands(object):
|
||||
help='Project name')
|
||||
def list(self, project=None):
|
||||
"""Print a listing of the VPN data for one or all projects."""
|
||||
|
||||
print "WARNING: This method only works with deprecated auth"
|
||||
print "%-12s\t" % 'project',
|
||||
print "%-20s\t" % 'ip:port',
|
||||
print "%-20s\t" % 'private_ip',
|
||||
@@ -176,17 +176,22 @@ class VpnCommands(object):
|
||||
|
||||
def spawn(self):
|
||||
"""Run all VPNs."""
|
||||
print "WARNING: This method only works with deprecated auth"
|
||||
for p in reversed(self.manager.get_projects()):
|
||||
if not self._vpn_for(p.id):
|
||||
print 'spawning %s' % p.id
|
||||
self.pipe.launch_vpn_instance(p.id)
|
||||
self.pipe.launch_vpn_instance(p.id, p.project_manager_id)
|
||||
time.sleep(10)
|
||||
|
||||
@args('--project', dest="project_id", metavar='<Project name>',
|
||||
help='Project name')
|
||||
def run(self, project_id):
|
||||
"""Start the VPN for a given project."""
|
||||
self.pipe.launch_vpn_instance(project_id)
|
||||
@args('--user', dest="user_id", metavar='<user name>', help='User name')
|
||||
def run(self, project_id, user_id):
|
||||
"""Start the VPN for a given project and user."""
|
||||
if not user_id:
|
||||
print "WARNING: This method only works with deprecated auth"
|
||||
user_id = self.manager.get_project(project_id).project_manager_id
|
||||
self.pipe.launch_vpn_instance(project_id, user_id)
|
||||
|
||||
@args('--project', dest="project_id", metavar='<Project name>',
|
||||
help='Project name')
|
||||
@@ -201,10 +206,6 @@ class VpnCommands(object):
|
||||
"""
|
||||
# TODO(tr3buchet): perhaps this shouldn't update all networks
|
||||
# associated with a project in the future
|
||||
project = self.manager.get_project(project_id)
|
||||
if not project:
|
||||
print 'No project %s' % (project_id)
|
||||
return
|
||||
admin_context = context.get_admin_context()
|
||||
networks = db.project_get_networks(admin_context, project_id)
|
||||
for network in networks:
|
||||
@@ -617,6 +618,8 @@ class FixedIpCommands(object):
|
||||
|
||||
try:
|
||||
fixed_ip = db.fixed_ip_get_by_address(ctxt, address)
|
||||
if fixed_ip is None:
|
||||
raise exception.NotFound('Could not find address')
|
||||
db.fixed_ip_update(ctxt, fixed_ip['address'],
|
||||
{'reserved': reserved})
|
||||
except exception.NotFound as ex:
|
||||
@@ -769,23 +772,26 @@ class NetworkCommands(object):
|
||||
|
||||
def list(self):
|
||||
"""List all created networks"""
|
||||
print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (
|
||||
_('IPv4'),
|
||||
_('IPv6'),
|
||||
_('start address'),
|
||||
_('DNS1'),
|
||||
_('DNS2'),
|
||||
_('VlanID'),
|
||||
'project')
|
||||
_fmt = "%-5s\t%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s"
|
||||
print _fmt % (_('id'),
|
||||
_('IPv4'),
|
||||
_('IPv6'),
|
||||
_('start address'),
|
||||
_('DNS1'),
|
||||
_('DNS2'),
|
||||
_('VlanID'),
|
||||
_('project'),
|
||||
_("uuid"))
|
||||
for network in db.network_get_all(context.get_admin_context()):
|
||||
print "%-18s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s\t%-15s" % (
|
||||
network.cidr,
|
||||
network.cidr_v6,
|
||||
network.dhcp_start,
|
||||
network.dns1,
|
||||
network.dns2,
|
||||
network.vlan,
|
||||
network.project_id)
|
||||
print _fmt % (network.id,
|
||||
network.cidr,
|
||||
network.cidr_v6,
|
||||
network.dhcp_start,
|
||||
network.dns1,
|
||||
network.dns2,
|
||||
network.vlan,
|
||||
network.project_id,
|
||||
network.uuid)
|
||||
|
||||
@args('--network', dest="fixed_range", metavar='<x.x.x.x/yy>',
|
||||
help='Network to delete')
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -34,7 +33,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
@@ -45,5 +43,7 @@ if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
service.serve()
|
||||
utils.monkey_patch()
|
||||
server = service.Service.create(binary='nova-network')
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Daemon for nova objectstore. Supports S3 API.
|
||||
"""
|
||||
"""Daemon for nova objectstore. Supports S3 API."""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -33,10 +33,10 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import service
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
from nova.objectstore import s3server
|
||||
@@ -49,10 +49,11 @@ if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
utils.monkey_patch()
|
||||
router = s3server.S3Application(FLAGS.buckets_path)
|
||||
server = wsgi.Server("S3 Objectstore",
|
||||
router,
|
||||
port=FLAGS.s3_port,
|
||||
host=FLAGS.s3_host)
|
||||
server.start()
|
||||
server.wait()
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -45,5 +45,7 @@ if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
service.serve()
|
||||
utils.monkey_patch()
|
||||
server = service.Service.create(binary='nova-scheduler')
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"""VNC Console Proxy Server."""
|
||||
|
||||
import eventlet
|
||||
import gettext
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -29,7 +30,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
@@ -41,7 +41,7 @@ from nova.vnc import auth
|
||||
from nova.vnc import proxy
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.vnc-proxy')
|
||||
LOG = logging.getLogger('nova.vncproxy')
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
@@ -81,7 +81,7 @@ if __name__ == "__main__":
|
||||
FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
|
||||
LOG.audit(_("Starting nova-vnc-proxy node (version %s)"),
|
||||
LOG.audit(_("Starting nova-vncproxy node (version %s)"),
|
||||
version.version_string_with_vcs())
|
||||
|
||||
if not (os.path.exists(FLAGS.vncproxy_wwwroot) and
|
||||
@@ -107,13 +107,10 @@ if __name__ == "__main__":
|
||||
else:
|
||||
with_auth = auth.VNCNovaAuthMiddleware(with_logging)
|
||||
|
||||
service.serve()
|
||||
|
||||
server = wsgi.Server("VNC Proxy",
|
||||
with_auth,
|
||||
host=FLAGS.vncproxy_host,
|
||||
port=FLAGS.vncproxy_port)
|
||||
server.start()
|
||||
server.start_tcp(handle_flash_socket_policy, 843, host=FLAGS.vncproxy_host)
|
||||
|
||||
server.wait()
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -34,7 +33,6 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
gettext.install('nova', unicode=1)
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
@@ -45,5 +43,7 @@ if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
service.serve()
|
||||
utils.monkey_patch()
|
||||
server = service.Service.create(binary='nova-volume')
|
||||
service.serve(server)
|
||||
service.wait()
|
||||
|
||||
@@ -31,9 +31,9 @@ This is the purpose of the Distributed Scheduler (DS). The DS utilizes the Capab
|
||||
|
||||
So, how does this all work?
|
||||
|
||||
This document will explain the strategy employed by the `ZoneAwareScheduler` and its derivations. You should read the :doc:`devguide/zones` documentation before reading this.
|
||||
This document will explain the strategy employed by the `BaseScheduler`, which is the base for all schedulers designed to work across zones, and its derivations. You should read the :doc:`devguide/zones` documentation before reading this.
|
||||
|
||||
.. image:: /images/zone_aware_scheduler.png
|
||||
.. image:: /images/base_scheduler.png
|
||||
|
||||
Costs & Weights
|
||||
---------------
|
||||
@@ -52,9 +52,9 @@ This Weight is computed for each Instance requested. If the customer asked for 1
|
||||
|
||||
.. image:: /images/costs_weights.png
|
||||
|
||||
nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler
|
||||
nova.scheduler.base_scheduler.BaseScheduler
|
||||
------------------------------------------------------
|
||||
As we explained in the Zones documentation, each Scheduler has a `ZoneManager` object that collects "Capabilities" about child Zones and each of the services running in the current Zone. The `ZoneAwareScheduler` uses this information to make its decisions.
|
||||
As we explained in the Zones documentation, each Scheduler has a `ZoneManager` object that collects "Capabilities" about child Zones and each of the services running in the current Zone. The `BaseScheduler` uses this information to make its decisions.
|
||||
|
||||
Here is how it works:
|
||||
|
||||
@@ -65,19 +65,19 @@ Here is how it works:
|
||||
5. The parent Zone sorts and aggregates all the weights and a final build plan is constructed.
|
||||
6. The build plan is executed upon. Concurrently, instance create requests are sent to each of the selected hosts, be they local or in a child zone. Child Zones may forward the requests to their child Zones as needed.
|
||||
|
||||
.. image:: /images/zone_aware_overview.png
|
||||
.. image:: /images/zone_overview.png
|
||||
|
||||
`ZoneAwareScheduler` by itself is not capable of handling all the provisioning itself. Derived classes are used to select which host filtering and weighing strategy will be used.
|
||||
`BaseScheduler` by itself is not capable of handling all the provisioning itself. You should also specify the filter classes and weighting classes to be used in determining which host is selected for new instance creation.
|
||||
|
||||
Filtering and Weighing
|
||||
----------------------
|
||||
The filtering (excluding compute nodes incapable of fulfilling the request) and weighing (computing the relative "fitness" of a compute node to fulfill the request) rules used are very subjective operations ... Service Providers will probably have a very different set of filtering and weighing rules than private cloud administrators. The filtering and weighing aspects of the `ZoneAwareScheduler` are flexible and extensible.
|
||||
The filtering (excluding compute nodes incapable of fulfilling the request) and weighing (computing the relative "fitness" of a compute node to fulfill the request) rules used are very subjective operations ... Service Providers will probably have a very different set of filtering and weighing rules than private cloud administrators. The filtering and weighing aspects of the `BaseScheduler` are flexible and extensible.
|
||||
|
||||
.. image:: /images/filtering.png
|
||||
|
||||
Requesting a new instance
|
||||
-------------------------
|
||||
Prior to the `ZoneAwareScheduler`, to request a new instance, a call was made to `nova.compute.api.create()`. The type of instance created depended on the value of the `InstanceType` record being passed in. The `InstanceType` determined the amount of disk, CPU, RAM and network required for the instance. Administrators can add new `InstanceType` records to suit their needs. For more complicated instance requests we need to go beyond the default fields in the `InstanceType` table.
|
||||
Prior to the `BaseScheduler`, to request a new instance, a call was made to `nova.compute.api.create()`. The type of instance created depended on the value of the `InstanceType` record being passed in. The `InstanceType` determined the amount of disk, CPU, RAM and network required for the instance. Administrators can add new `InstanceType` records to suit their needs. For more complicated instance requests we need to go beyond the default fields in the `InstanceType` table.
|
||||
|
||||
`nova.compute.api.create()` performed the following actions:
|
||||
1. it validated all the fields passed into it.
|
||||
@@ -89,11 +89,11 @@ Prior to the `ZoneAwareScheduler`, to request a new instance, a call was made to
|
||||
|
||||
.. image:: /images/nova.compute.api.create.png
|
||||
|
||||
Generally, the standard schedulers (like `ChanceScheduler` and `AvailabilityZoneScheduler`) only operate in the current Zone. They have no concept of child Zones.
|
||||
Generally, the simplest schedulers (like `ChanceScheduler` and `AvailabilityZoneScheduler`) only operate in the current Zone. They have no concept of child Zones.
|
||||
|
||||
The problem with this approach is each request is scattered amongst each of the schedulers. If we are asking for 1000 instances, each scheduler gets the requests one-at-a-time. There is no possability of optimizing the requests to take into account all 1000 instances as a group. We call this Single-Shot vs. All-at-Once.
|
||||
|
||||
For the `ZoneAwareScheduler` we need to use the All-at-Once approach. We need to consider all the hosts across all the Zones before deciding where they should reside. In order to handle this we have a new method `nova.compute.api.create_all_at_once()`. This method does things a little differently:
|
||||
For the `BaseScheduler` we need to use the All-at-Once approach. We need to consider all the hosts across all the Zones before deciding where they should reside. In order to handle this we have a new method `nova.compute.api.create_all_at_once()`. This method does things a little differently:
|
||||
1. it validates all the fields passed into it.
|
||||
2. it creates a single `reservation_id` for all of instances created. This is a UUID.
|
||||
3. it creates a single `run_instance` request in the scheduler queue
|
||||
@@ -109,21 +109,19 @@ For the `ZoneAwareScheduler` we need to use the All-at-Once approach. We need to
|
||||
|
||||
The Catch
|
||||
---------
|
||||
This all seems pretty straightforward but, like most things, there's a catch. Zones are expected to operate in complete isolation from each other. Each Zone has its own AMQP service, database and set of Nova services. But, for security reasons Zones should never leak information about the architectural layout internally. That means Zones cannot leak information about hostnames or service IP addresses outside of its world.
|
||||
This all seems pretty straightforward but, like most things, there's a catch. Zones are expected to operate in complete isolation from each other. Each Zone has its own AMQP service, database and set of Nova services. But for security reasons Zones should never leak information about the architectural layout internally. That means Zones cannot leak information about hostnames or service IP addresses outside of its world.
|
||||
|
||||
When `POST /zones/select` is called to estimate which compute node to use, time passes until the `POST /servers` call is issued. If we only passed the weight back from the `select` we would have to re-compute the appropriate compute node for the create command ... and we could end up with a different host. Somehow we need to remember the results of our computations and pass them outside of the Zone. Now, we could store this information in the local database and return a reference to it, but remember that the vast majority of weights are going to be ignored. Storing them in the database would result in a flood of disk access and then we have to clean up all these entries periodically. Recall that there are going to be many many `select` calls issued to child Zones asking for estimates.
|
||||
When `POST /zones/select` is called to estimate which compute node to use, time passes until the `POST /servers` call is issued. If we only passed the weight back from the `select` we would have to re-compute the appropriate compute node for the create command ... and we could end up with a different host. Somehow we need to remember the results of our computations and pass them outside of the Zone. Now, we could store this information in the local database and return a reference to it, but remember that the vast majority of weights are going to be ignored. Storing them in the database would result in a flood of disk access and then we have to clean up all these entries periodically. Recall that there are going to be many, many `select` calls issued to child Zones asking for estimates.
|
||||
|
||||
Instead, we take a rather innovative approach to the problem. We encrypt all the child zone internal details and pass them back the to parent Zone. If the parent zone decides to use a child Zone for the instance it simply passes the encrypted data back to the child during the `POST /servers` call as an extra parameter. The child Zone can then decrypt the hint and go directly to the Compute node previously selected. If the estimate isn't used, it is simply discarded by the parent. It's for this reason that it is so important that each Zone defines a unique encryption key via `--build_plan_encryption_key`
|
||||
Instead, we take a rather innovative approach to the problem. We encrypt all the child Zone internal details and pass them back the to parent Zone. In the case of a nested Zone layout, each nesting layer will encrypt the data from all of its children and pass that to its parent Zone. In the case of nested child Zones, each Zone re-encrypts the weighted list results and passes those values to the parent. Every Zone interface adds another layer of encryption, using its unique key.
|
||||
|
||||
In the case of nested child Zones, each Zone re-encrypts the weighted list results and passes those values to the parent.
|
||||
Once a host is selected, it will either be local to the Zone that received the initial API call, or one of its child Zones. In the latter case, the parent Zone it simply passes the encrypted data for the selected host back to each of its child Zones during the `POST /servers` call as an extra parameter. If the child Zone can decrypt the data, then it is the correct Zone for the selected host; all other Zones will not be able to decrypt the data and will discard the request. This is why it is critical that each Zone has a unique value specified in its config in `--build_plan_encryption_key`: it controls the ability to locate the selected host without having to hard-code path information or other identifying information. The child Zone can then act on the decrypted data and either go directly to the Compute node previously selected if it is located in that Zone, or repeat the process with its child Zones until the target Zone containing the selected host is reached.
|
||||
|
||||
Throughout the `nova.api.openstack.servers`, `nova.api.openstack.zones`, `nova.compute.api.create*` and `nova.scheduler.zone_aware_scheduler` code you'll see references to `blob` and `child_blob`. These are the encrypted hints about which Compute node to use.
|
||||
Throughout the `nova.api.openstack.servers`, `nova.api.openstack.zones`, `nova.compute.api.create*` and `nova.scheduler.base_scheduler` code you'll see references to `blob` and `child_blob`. These are the encrypted hints about which Compute node to use.
|
||||
|
||||
Reservation IDs
|
||||
---------------
|
||||
|
||||
NOTE: The features described in this section are related to the up-coming 'merge-4' branch.
|
||||
|
||||
The OpenStack API allows a user to list all the instances they own via the `GET /servers/` command or the details on a particular instance via `GET /servers/###`. This mechanism is usually sufficient since OS API only allows for creating one instance at a time, unlike the EC2 API which allows you to specify a quantity of instances to be created.
|
||||
|
||||
NOTE: currently the `GET /servers` command is not Zone-aware since all operations done in child Zones are done via a single administrative account. Therefore, asking a child Zone to `GET /servers` would return all the active instances ... and that would not be what the user intended. Later, when the Keystone Auth system is integrated with Nova, this functionality will be enabled.
|
||||
@@ -137,23 +135,23 @@ Finally, we need to give the user a way to get information on each of the instan
|
||||
Host Filter
|
||||
-----------
|
||||
|
||||
As we mentioned earlier, filtering hosts is a very deployment-specific process. Service Providers may have a different set of criteria for filtering Compute nodes than a University. To faciliate this the `nova.scheduler.host_filter` module supports a variety of filtering strategies as well as an easy means for plugging in your own algorithms.
|
||||
As we mentioned earlier, filtering hosts is a very deployment-specific process. Service Providers may have a different set of criteria for filtering Compute nodes than a University. To faciliate this the `nova.scheduler.filters` module supports a variety of filtering strategies as well as an easy means for plugging in your own algorithms.
|
||||
|
||||
The filter used is determined by the `--default_host_filter` flag, which points to a Python Class. By default this flag is set to `nova.scheduler.host_filter.AllHostsFilter` which simply returns all available hosts. But there are others:
|
||||
The filter used is determined by the `--default_host_filters` flag, which points to a Python Class. By default this flag is set to `[AllHostsFilter]` which simply returns all available hosts. But there are others:
|
||||
|
||||
* `nova.scheduler.host_filter.InstanceTypeFilter` provides host filtering based on the memory and disk size specified in the `InstanceType` record passed into `run_instance`.
|
||||
* `InstanceTypeFilter` provides host filtering based on the memory and disk size specified in the `InstanceType` record passed into `run_instance`.
|
||||
|
||||
* `nova.scheduler.host_filter.JSONFilter` filters hosts based on simple JSON expression grammar. Using a LISP-like JSON structure the caller can request instances based on criteria well beyond what `InstanceType` specifies. See `nova.tests.test_host_filter` for examples.
|
||||
* `JSONFilter` filters hosts based on simple JSON expression grammar. Using a LISP-like JSON structure the caller can request instances based on criteria well beyond what `InstanceType` specifies. See `nova.tests.test_host_filter` for examples.
|
||||
|
||||
To create your own `HostFilter` the user simply has to derive from `nova.scheduler.host_filter.HostFilter` and implement two methods: `instance_type_to_filter` and `filter_hosts`. Since Nova is currently dependent on the `InstanceType` structure, the `instance_type_to_filter` method should take an `InstanceType` and turn it into an internal data structure usable by your filter. This is for backward compatibility with existing OpenStack and EC2 API calls. If you decide to create your own call for creating instances not based on `Flavors` or `InstanceTypes` you can ignore this method. The real work is done in `filter_hosts` which must return a list of host tuples for each appropriate host. The set of all available hosts is in the `ZoneManager` object passed into the call as well as the filter query. The host tuple contains (`<hostname>`, `<additional data>`) where `<additional data>` is whatever you want it to be.
|
||||
To create your own `HostFilter` the user simply has to derive from `nova.scheduler.filters.AbstractHostFilter` and implement two methods: `instance_type_to_filter` and `filter_hosts`. Since Nova is currently dependent on the `InstanceType` structure, the `instance_type_to_filter` method should take an `InstanceType` and turn it into an internal data structure usable by your filter. This is for backward compatibility with existing OpenStack and EC2 API calls. If you decide to create your own call for creating instances not based on `Flavors` or `InstanceTypes` you can ignore this method. The real work is done in `filter_hosts` which must return a list of host tuples for each appropriate host. The set of available hosts is in the `host_list` parameter passed into the call as well as the filter query. The host tuple contains (`<hostname>`, `<additional data>`) where `<additional data>` is whatever you want it to be. By default, it is the capabilities reported by the host.
|
||||
|
||||
Cost Scheduler Weighing
|
||||
-----------------------
|
||||
Every `ZoneAwareScheduler` derivation must also override the `weigh_hosts` method. This takes the list of filtered hosts (generated by the `filter_hosts` method) and returns a list of weight dicts. The weight dicts must contain two keys: `weight` and `hostname` where `weight` is simply an integer (lower is better) and `hostname` is the name of the host. The list does not need to be sorted, this will be done by the `ZoneAwareScheduler` base class when all the results have been assembled.
|
||||
Every `BaseScheduler` subclass should also override the `weigh_hosts` method. This takes the list of filtered hosts (generated by the `filter_hosts` method) and returns a list of weight dicts. The weight dicts must contain two keys: `weight` and `hostname` where `weight` is simply an integer (lower is better) and `hostname` is the name of the host. The list does not need to be sorted, this will be done by the `BaseScheduler` when all the results have been assembled.
|
||||
|
||||
Simple Zone Aware Scheduling
|
||||
Simple Scheduling Across Zones
|
||||
----------------------------
|
||||
The easiest way to get started with the `ZoneAwareScheduler` is to use the `nova.scheduler.host_filter.HostFilterScheduler`. This scheduler uses the default Host Filter and the `weight_hosts` method simply returns a weight of 1 for all hosts. But, from this, you can see calls being routed from Zone to Zone and follow the flow of things.
|
||||
The `BaseScheduler` uses the default `filter_hosts` method, which will use either any filters specified in the request's `filter` parameter, or, if that is not specified, the filters specified in the `FLAGS.default_host_filters` setting. Its `weight_hosts` method simply returns a weight of 1 for all hosts. But, from this, you can see calls being routed from Zone to Zone and follow the flow of things.
|
||||
|
||||
The `--scheduler_driver` flag is how you specify the scheduler class name.
|
||||
|
||||
@@ -168,14 +166,14 @@ All this Zone and Distributed Scheduler stuff can seem a little daunting to conf
|
||||
--enable_zone_routing=true
|
||||
--zone_name=zone1
|
||||
--build_plan_encryption_key=c286696d887c9aa0611bbb3e2025a45b
|
||||
--scheduler_driver=nova.scheduler.host_filter.HostFilterScheduler
|
||||
--default_host_filter=nova.scheduler.host_filter.AllHostsFilter
|
||||
--scheduler_driver=nova.scheduler.base_scheduler.BaseScheduler
|
||||
--default_host_filter=nova.scheduler.filters.AllHostsFilter
|
||||
|
||||
`--allow_admin_api` must be set for OS API to enable the new `/zones/*` commands.
|
||||
`--enable_zone_routing` must be set for OS API commands such as `create()`, `pause()` and `delete()` to get routed from Zone to Zone when looking for instances.
|
||||
`--zone_name` is only required in child Zones. The default Zone name is `nova`, but you may want to name your child Zones something useful. Duplicate Zone names are not an issue.
|
||||
`build_plan_encryption_key` is the SHA-256 key for encrypting/decrypting the Host information when it leaves a Zone. Be sure to change this key for each Zone you create. Do not duplicate keys.
|
||||
`scheduler_driver` is the real workhorse of the operation. For Distributed Scheduler, you need to specify a class derived from `nova.scheduler.zone_aware_scheduler.ZoneAwareScheduler`.
|
||||
`scheduler_driver` is the real workhorse of the operation. For Distributed Scheduler, you need to specify a class derived from `nova.scheduler.base_scheduler.BaseScheduler`.
|
||||
`default_host_filter` is the host filter to be used for filtering candidate Compute nodes.
|
||||
|
||||
Some optional flags which are handy for debugging are:
|
||||
|
||||
BIN
doc/source/images/base_scheduler.png
Normal file
BIN
doc/source/images/base_scheduler.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
doc/source/images/zone_overview.png
Executable file
BIN
doc/source/images/zone_overview.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -19,8 +19,11 @@ use = egg:Paste#urlmap
|
||||
/1.0: ec2metadata
|
||||
|
||||
[pipeline:ec2cloud]
|
||||
pipeline = logrequest authenticate cloudrequest authorizer ec2executor
|
||||
#pipeline = logrequest ec2lockout authenticate cloudrequest authorizer ec2executor
|
||||
pipeline = logrequest ec2noauth cloudrequest authorizer ec2executor
|
||||
# NOTE(vish): use the following pipeline for deprecated auth
|
||||
#pipeline = logrequest authenticate cloudrequest authorizer ec2executor
|
||||
# NOTE(vish): use the following pipeline for keystone
|
||||
# pipeline = logrequest totoken authtoken keystonecontext cloudrequest authorizer ec2executor
|
||||
|
||||
[pipeline:ec2admin]
|
||||
pipeline = logrequest authenticate adminrequest authorizer ec2executor
|
||||
@@ -37,6 +40,12 @@ paste.filter_factory = nova.api.ec2:RequestLogging.factory
|
||||
[filter:ec2lockout]
|
||||
paste.filter_factory = nova.api.ec2:Lockout.factory
|
||||
|
||||
[filter:totoken]
|
||||
paste.filter_factory = nova.api.ec2:ToToken.factory
|
||||
|
||||
[filter:ec2noauth]
|
||||
paste.filter_factory = nova.api.ec2:NoAuth.factory
|
||||
|
||||
[filter:authenticate]
|
||||
paste.filter_factory = nova.api.ec2:Authenticate.factory
|
||||
|
||||
@@ -71,10 +80,18 @@ use = egg:Paste#urlmap
|
||||
/v1.1: openstackapi11
|
||||
|
||||
[pipeline:openstackapi10]
|
||||
pipeline = faultwrap auth ratelimit osapiapp10
|
||||
pipeline = faultwrap noauth ratelimit osapiapp10
|
||||
# NOTE(vish): use the following pipeline for deprecated auth
|
||||
# pipeline = faultwrap auth ratelimit osapiapp10
|
||||
# NOTE(vish): use the following pipeline for keystone
|
||||
#pipeline = faultwrap authtoken keystonecontext ratelimit osapiapp10
|
||||
|
||||
[pipeline:openstackapi11]
|
||||
pipeline = faultwrap auth ratelimit extensions osapiapp11
|
||||
pipeline = faultwrap noauth ratelimit extensions osapiapp11
|
||||
# NOTE(vish): use the following pipeline for deprecated auth
|
||||
# pipeline = faultwrap auth ratelimit extensions osapiapp11
|
||||
# NOTE(vish): use the following pipeline for keystone
|
||||
# pipeline = faultwrap authtoken keystonecontext ratelimit extensions osapiapp11
|
||||
|
||||
[filter:faultwrap]
|
||||
paste.filter_factory = nova.api.openstack:FaultWrapper.factory
|
||||
@@ -82,6 +99,9 @@ paste.filter_factory = nova.api.openstack:FaultWrapper.factory
|
||||
[filter:auth]
|
||||
paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory
|
||||
|
||||
[filter:noauth]
|
||||
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory
|
||||
|
||||
[filter:ratelimit]
|
||||
paste.filter_factory = nova.api.openstack.limits:RateLimitingMiddleware.factory
|
||||
|
||||
@@ -99,3 +119,22 @@ pipeline = faultwrap osversionapp
|
||||
|
||||
[app:osversionapp]
|
||||
paste.app_factory = nova.api.openstack.versions:Versions.factory
|
||||
|
||||
##########
|
||||
# Shared #
|
||||
##########
|
||||
|
||||
[filter:keystonecontext]
|
||||
paste.filter_factory = nova.api.auth:KeystoneContext.factory
|
||||
|
||||
[filter:authtoken]
|
||||
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
||||
service_protocol = http
|
||||
service_host = 127.0.0.1
|
||||
service_port = 808
|
||||
auth_host = 127.0.0.1
|
||||
auth_port = 5001
|
||||
auth_protocol = http
|
||||
auth_uri = http://127.0.0.1:5000/
|
||||
admin_token = 999888777666
|
||||
|
||||
|
||||
76
nova/api/auth.py
Normal file
76
nova/api/auth.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 OpenStack, LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Common Auth Middleware.
|
||||
|
||||
"""
|
||||
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova import context
|
||||
from nova import flags
|
||||
from nova import wsgi
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_boolean('use_forwarded_for', False,
|
||||
'Treat X-Forwarded-For as the canonical remote address. '
|
||||
'Only enable this if you have a sanitizing proxy.')
|
||||
|
||||
|
||||
class InjectContext(wsgi.Middleware):
|
||||
"""Add a 'nova.context' to WSGI environ."""
|
||||
|
||||
def __init__(self, context, *args, **kwargs):
|
||||
self.context = context
|
||||
super(InjectContext, self).__init__(*args, **kwargs)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
req.environ['nova.context'] = self.context
|
||||
return self.application
|
||||
|
||||
|
||||
class KeystoneContext(wsgi.Middleware):
|
||||
"""Make a request context from keystone headers"""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
try:
|
||||
user_id = req.headers['X_USER']
|
||||
except KeyError:
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
# get the roles
|
||||
roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
|
||||
project_id = req.headers['X_TENANT']
|
||||
# Get the auth token
|
||||
auth_token = req.headers.get('X_AUTH_TOKEN',
|
||||
req.headers.get('X_STORAGE_TOKEN'))
|
||||
|
||||
# Build a context, including the auth_token...
|
||||
remote_address = getattr(req, 'remote_address', '127.0.0.1')
|
||||
remote_address = req.remote_addr
|
||||
if FLAGS.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
roles=roles,
|
||||
auth_token=auth_token,
|
||||
remote_address=remote_address)
|
||||
|
||||
req.environ['nova.context'] = ctx
|
||||
return self.application
|
||||
@@ -20,6 +20,7 @@ Starting point for routing EC2 requests.
|
||||
|
||||
"""
|
||||
|
||||
import httplib2
|
||||
import webob
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
@@ -37,15 +38,16 @@ from nova.auth import manager
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
LOG = logging.getLogger("nova.api")
|
||||
flags.DEFINE_boolean('use_forwarded_for', False,
|
||||
'Treat X-Forwarded-For as the canonical remote address. '
|
||||
'Only enable this if you have a sanitizing proxy.')
|
||||
flags.DEFINE_integer('lockout_attempts', 5,
|
||||
'Number of failed auths before lockout.')
|
||||
flags.DEFINE_integer('lockout_minutes', 15,
|
||||
'Number of minutes to lockout if triggered.')
|
||||
flags.DEFINE_integer('lockout_window', 15,
|
||||
'Number of minutes for lockout window.')
|
||||
flags.DEFINE_string('keystone_ec2_url',
|
||||
'http://localhost:5000/v2.0/ec2tokens',
|
||||
'URL to get token from ec2 request.')
|
||||
flags.DECLARE('use_forwarded_for', 'nova.api.auth')
|
||||
|
||||
|
||||
class RequestLogging(wsgi.Middleware):
|
||||
@@ -138,6 +140,70 @@ class Lockout(wsgi.Middleware):
|
||||
return res
|
||||
|
||||
|
||||
class ToToken(wsgi.Middleware):
|
||||
"""Authenticate an EC2 request with keystone and convert to token."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
# Read request signature and access id.
|
||||
try:
|
||||
signature = req.params['Signature']
|
||||
access = req.params['AWSAccessKeyId']
|
||||
except KeyError:
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
|
||||
# Make a copy of args for authentication and signature verification.
|
||||
auth_params = dict(req.params)
|
||||
# Not part of authentication args
|
||||
auth_params.pop('Signature')
|
||||
|
||||
# Authenticate the request.
|
||||
client = httplib2.Http()
|
||||
creds = {'ec2Credentials': {'access': access,
|
||||
'signature': signature,
|
||||
'host': req.host,
|
||||
'verb': req.method,
|
||||
'path': req.path,
|
||||
'params': auth_params,
|
||||
}}
|
||||
headers = {'Content-Type': 'application/json'},
|
||||
resp, content = client.request(FLAGS.keystone_ec2_url,
|
||||
'POST',
|
||||
headers=headers,
|
||||
body=utils.dumps(creds))
|
||||
# NOTE(vish): We could save a call to keystone by
|
||||
# having keystone return token, tenant,
|
||||
# user, and roles from this call.
|
||||
result = utils.loads(content)
|
||||
# TODO(vish): check for errors
|
||||
token_id = result['auth']['token']['id']
|
||||
|
||||
# Authenticated!
|
||||
req.headers['X-Auth-Token'] = token_id
|
||||
return self.application
|
||||
|
||||
|
||||
class NoAuth(wsgi.Middleware):
|
||||
"""Add user:project as 'nova.context' to WSGI environ."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
if 'AWSAccessKeyId' not in req.params:
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
user_id, _sep, project_id = req.params['AWSAccessKeyId'].partition(':')
|
||||
project_id = project_id or user_id
|
||||
remote_address = getattr(req, 'remote_address', '127.0.0.1')
|
||||
if FLAGS.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
is_admin=True,
|
||||
remote_address=remote_address)
|
||||
|
||||
req.environ['nova.context'] = ctx
|
||||
return self.application
|
||||
|
||||
|
||||
class Authenticate(wsgi.Middleware):
|
||||
"""Authenticate an EC2 request and add 'nova.context' to WSGI environ."""
|
||||
|
||||
@@ -147,7 +213,7 @@ class Authenticate(wsgi.Middleware):
|
||||
try:
|
||||
signature = req.params['Signature']
|
||||
access = req.params['AWSAccessKeyId']
|
||||
except KeyError, e:
|
||||
except KeyError:
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
|
||||
# Make a copy of args for authentication and signature verification.
|
||||
|
||||
@@ -283,8 +283,10 @@ class AdminController(object):
|
||||
# NOTE(vish) import delayed because of __init__.py
|
||||
from nova.cloudpipe import pipelib
|
||||
pipe = pipelib.CloudPipe()
|
||||
proj = manager.AuthManager().get_project(project)
|
||||
user_id = proj.project_manager_id
|
||||
try:
|
||||
pipe.launch_vpn_instance(project)
|
||||
pipe.launch_vpn_instance(project, user_id)
|
||||
except db.NoMoreNetworks:
|
||||
raise exception.ApiError("Unable to claim IP for VPN instance"
|
||||
", ensure it isn't running, and try "
|
||||
|
||||
@@ -30,6 +30,7 @@ from nova.api.ec2 import cloud
|
||||
|
||||
LOG = logging.getLogger('nova.api.ec2.metadata')
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DECLARE('use_forwarded_for', 'nova.api.auth')
|
||||
|
||||
|
||||
class MetadataRequestHandler(wsgi.Application):
|
||||
|
||||
@@ -68,6 +68,22 @@ class FaultWrapper(base_wsgi.Middleware):
|
||||
return faults.Fault(exc)
|
||||
|
||||
|
||||
class ProjectMapper(routes.Mapper):
|
||||
|
||||
def resource(self, member_name, collection_name, **kwargs):
|
||||
if not ('parent_resource' in kwargs):
|
||||
kwargs['path_prefix'] = '{project_id}/'
|
||||
else:
|
||||
parent_resource = kwargs['parent_resource']
|
||||
p_collection = parent_resource['collection_name']
|
||||
p_member = parent_resource['member_name']
|
||||
kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection,
|
||||
p_member)
|
||||
routes.Mapper.resource(self, member_name,
|
||||
collection_name,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class APIRouter(base_wsgi.Router):
|
||||
"""
|
||||
Routes requests on the OpenStack API to the appropriate controller
|
||||
@@ -81,10 +97,13 @@ class APIRouter(base_wsgi.Router):
|
||||
|
||||
def __init__(self, ext_mgr=None):
|
||||
self.server_members = {}
|
||||
mapper = routes.Mapper()
|
||||
mapper = self._mapper()
|
||||
self._setup_routes(mapper)
|
||||
super(APIRouter, self).__init__(mapper)
|
||||
|
||||
def _mapper(self):
|
||||
return routes.Mapper()
|
||||
|
||||
def _setup_routes(self, mapper):
|
||||
raise NotImplementedError(_("You must implement _setup_routes."))
|
||||
|
||||
@@ -174,6 +193,9 @@ class APIRouterV10(APIRouter):
|
||||
class APIRouterV11(APIRouter):
|
||||
"""Define routes specific to OpenStack API V1.1."""
|
||||
|
||||
def _mapper(self):
|
||||
return ProjectMapper()
|
||||
|
||||
def _setup_routes(self, mapper):
|
||||
self._setup_base_routes(mapper, '1.1')
|
||||
|
||||
@@ -184,7 +206,7 @@ class APIRouterV11(APIRouter):
|
||||
parent_resource=dict(member_name='image',
|
||||
collection_name='images'))
|
||||
|
||||
mapper.connect("metadata", "/images/{image_id}/metadata",
|
||||
mapper.connect("metadata", "/{project_id}/images/{image_id}/metadata",
|
||||
controller=image_metadata_controller,
|
||||
action='update_all',
|
||||
conditions={"method": ['PUT']})
|
||||
@@ -196,7 +218,8 @@ class APIRouterV11(APIRouter):
|
||||
parent_resource=dict(member_name='server',
|
||||
collection_name='servers'))
|
||||
|
||||
mapper.connect("metadata", "/servers/{server_id}/metadata",
|
||||
mapper.connect("metadata",
|
||||
"/{project_id}/servers/{server_id}/metadata",
|
||||
controller=server_metadata_controller,
|
||||
action='update_all',
|
||||
conditions={"method": ['PUT']})
|
||||
|
||||
@@ -28,10 +28,51 @@ from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import faults
|
||||
|
||||
LOG = logging.getLogger('nova.api.openstack')
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DECLARE('use_forwarded_for', 'nova.api.auth')
|
||||
|
||||
|
||||
class NoAuthMiddleware(wsgi.Middleware):
|
||||
"""Return a fake token if one isn't specified."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
if 'X-Auth-Token' not in req.headers:
|
||||
os_url = req.url
|
||||
version = common.get_version_from_href(os_url)
|
||||
user_id = req.headers.get('X-Auth-User', 'admin')
|
||||
project_id = req.headers.get('X-Auth-Project-Id', 'admin')
|
||||
if version == '1.1':
|
||||
os_url += '/' + project_id
|
||||
res = webob.Response()
|
||||
# NOTE(vish): This is expecting and returning Auth(1.1), whereas
|
||||
# keystone uses 2.0 auth. We should probably allow
|
||||
# 2.0 auth here as well.
|
||||
res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id)
|
||||
res.headers['X-Server-Management-Url'] = os_url
|
||||
res.headers['X-Storage-Url'] = ''
|
||||
res.headers['X-CDN-Management-Url'] = ''
|
||||
res.content_type = 'text/plain'
|
||||
res.status = '204'
|
||||
return res
|
||||
|
||||
token = req.headers['X-Auth-Token']
|
||||
user_id, _sep, project_id = token.partition(':')
|
||||
project_id = project_id or user_id
|
||||
remote_address = getattr(req, 'remote_address', '127.0.0.1')
|
||||
if FLAGS.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
is_admin=True,
|
||||
remote_address=remote_address)
|
||||
|
||||
req.environ['nova.context'] = ctx
|
||||
return self.application
|
||||
|
||||
|
||||
class AuthMiddleware(wsgi.Middleware):
|
||||
@@ -55,21 +96,44 @@ class AuthMiddleware(wsgi.Middleware):
|
||||
LOG.warn(msg % locals())
|
||||
return faults.Fault(webob.exc.HTTPUnauthorized())
|
||||
|
||||
try:
|
||||
project_id = req.headers["X-Auth-Project-Id"]
|
||||
except KeyError:
|
||||
# FIXME(usrleon): It needed only for compatibility
|
||||
# while osapi clients don't use this header
|
||||
projects = self.auth.get_projects(user_id)
|
||||
if projects:
|
||||
project_id = projects[0].id
|
||||
else:
|
||||
# Get all valid projects for the user
|
||||
projects = self.auth.get_projects(user_id)
|
||||
if not projects:
|
||||
return faults.Fault(webob.exc.HTTPUnauthorized())
|
||||
|
||||
project_id = ""
|
||||
path_parts = req.path.split('/')
|
||||
# TODO(wwolf): this v1.1 check will be temporary as
|
||||
# keystone should be taking this over at some point
|
||||
if len(path_parts) > 1 and path_parts[1] == 'v1.1':
|
||||
project_id = path_parts[2]
|
||||
# Check that the project for project_id exists, and that user
|
||||
# is authorized to use it
|
||||
try:
|
||||
project = self.auth.get_project(project_id)
|
||||
except exception.ProjectNotFound:
|
||||
return faults.Fault(webob.exc.HTTPUnauthorized())
|
||||
if project_id not in [p.id for p in projects]:
|
||||
return faults.Fault(webob.exc.HTTPUnauthorized())
|
||||
else:
|
||||
# As a fallback, set project_id from the headers, which is the v1.0
|
||||
# behavior. As a last resort, be forgiving to the user and set
|
||||
# project_id based on a valid project of theirs.
|
||||
try:
|
||||
project_id = req.headers["X-Auth-Project-Id"]
|
||||
except KeyError:
|
||||
project_id = projects[0].id
|
||||
|
||||
is_admin = self.auth.is_admin(user_id)
|
||||
req.environ['nova.context'] = context.RequestContext(user_id,
|
||||
project_id,
|
||||
is_admin)
|
||||
remote_address = getattr(req, 'remote_address', '127.0.0.1')
|
||||
if FLAGS.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
is_admin=is_admin,
|
||||
remote_address=remote_address)
|
||||
req.environ['nova.context'] = ctx
|
||||
|
||||
if not is_admin and not self.auth.is_project_member(user_id,
|
||||
project_id):
|
||||
msg = _("%(user_id)s must be an admin or a "
|
||||
@@ -95,12 +159,19 @@ class AuthMiddleware(wsgi.Middleware):
|
||||
LOG.warn(msg)
|
||||
return faults.Fault(webob.exc.HTTPUnauthorized(explanation=msg))
|
||||
|
||||
def _get_auth_header(key):
|
||||
"""Ensures that the KeyError returned is meaningful."""
|
||||
try:
|
||||
return req.headers[key]
|
||||
except KeyError as ex:
|
||||
raise KeyError(key)
|
||||
try:
|
||||
username = req.headers['X-Auth-User']
|
||||
key = req.headers['X-Auth-Key']
|
||||
username = _get_auth_header('X-Auth-User')
|
||||
key = _get_auth_header('X-Auth-Key')
|
||||
except KeyError as ex:
|
||||
LOG.warn(_("Could not find %s in request.") % ex)
|
||||
return faults.Fault(webob.exc.HTTPUnauthorized())
|
||||
msg = _("Could not find %s in request.") % ex
|
||||
LOG.warn(msg)
|
||||
return faults.Fault(webob.exc.HTTPUnauthorized(explanation=msg))
|
||||
|
||||
token, user = self._authorize_user(username, key, req)
|
||||
if user and token:
|
||||
@@ -149,6 +220,16 @@ class AuthMiddleware(wsgi.Middleware):
|
||||
"""
|
||||
ctxt = context.get_admin_context()
|
||||
|
||||
project_id = req.headers.get('X-Auth-Project-Id')
|
||||
if project_id is None:
|
||||
# If the project_id is not provided in the headers, be forgiving to
|
||||
# the user and set project_id based on a valid project of theirs.
|
||||
user = self.auth.get_user_from_access_key(key)
|
||||
projects = self.auth.get_projects(user.id)
|
||||
if not projects:
|
||||
raise webob.exc.HTTPUnauthorized()
|
||||
project_id = projects[0].id
|
||||
|
||||
try:
|
||||
user = self.auth.get_user_from_access_key(key)
|
||||
except exception.NotFound:
|
||||
@@ -162,7 +243,10 @@ class AuthMiddleware(wsgi.Middleware):
|
||||
token_dict['token_hash'] = token_hash
|
||||
token_dict['cdn_management_url'] = ''
|
||||
os_url = req.url
|
||||
token_dict['server_management_url'] = os_url
|
||||
token_dict['server_management_url'] = os_url.strip('/')
|
||||
version = common.get_version_from_href(os_url)
|
||||
if version == '1.1':
|
||||
token_dict['server_management_url'] += '/' + project_id
|
||||
token_dict['storage_url'] = ''
|
||||
token_dict['user_id'] = user.id
|
||||
token = self.db.auth_token_create(ctxt, token_dict)
|
||||
|
||||
@@ -241,7 +241,8 @@ def check_img_metadata_quota_limit(context, metadata):
|
||||
quota_metadata = quota.allowed_metadata_items(context, num_metadata)
|
||||
if quota_metadata < num_metadata:
|
||||
expl = _("Image metadata limit exceeded")
|
||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(explanation=expl,
|
||||
headers={'Retry-After': 0})
|
||||
|
||||
|
||||
class MetadataXMLDeserializer(wsgi.XMLDeserializer):
|
||||
|
||||
66
nova/api/openstack/contrib/createserverext.py
Normal file
66
nova/api/openstack/contrib/createserverext.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License
|
||||
|
||||
from nova.api.openstack import create_instance_helper as helper
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import servers
|
||||
from nova.api.openstack import wsgi
|
||||
|
||||
|
||||
class Createserverext(extensions.ExtensionDescriptor):
|
||||
"""The servers create ext
|
||||
|
||||
Exposes addFixedIp and removeFixedIp actions on servers.
|
||||
|
||||
"""
|
||||
def get_name(self):
|
||||
return "Createserverext"
|
||||
|
||||
def get_alias(self):
|
||||
return "os-create-server-ext"
|
||||
|
||||
def get_description(self):
|
||||
return "Extended support to the Create Server v1.1 API"
|
||||
|
||||
def get_namespace(self):
|
||||
return "http://docs.openstack.org/ext/createserverext/api/v1.1"
|
||||
|
||||
def get_updated(self):
|
||||
return "2011-07-19T00:00:00+00:00"
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
|
||||
headers_serializer = servers.HeadersSerializer()
|
||||
body_serializers = {
|
||||
'application/xml': servers.ServerXMLSerializer(),
|
||||
}
|
||||
|
||||
body_deserializers = {
|
||||
'application/xml': helper.ServerXMLDeserializerV11(),
|
||||
}
|
||||
|
||||
serializer = wsgi.ResponseSerializer(body_serializers,
|
||||
headers_serializer)
|
||||
deserializer = wsgi.RequestDeserializer(body_deserializers)
|
||||
|
||||
res = extensions.ResourceExtension('os-create-server-ext',
|
||||
controller=servers.ControllerV11(),
|
||||
deserializer=deserializer,
|
||||
serializer=serializer)
|
||||
resources.append(res)
|
||||
|
||||
return resources
|
||||
@@ -15,8 +15,9 @@
|
||||
# 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 webob import exc
|
||||
import webob
|
||||
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
from nova import log as logging
|
||||
from nova import network
|
||||
@@ -71,18 +72,22 @@ class FloatingIPController(object):
|
||||
try:
|
||||
floating_ip = self.network_api.get_floating_ip(context, id)
|
||||
except exception.NotFound:
|
||||
return faults.Fault(exc.HTTPNotFound())
|
||||
return faults.Fault(webob.exc.HTTPNotFound())
|
||||
|
||||
return _translate_floating_ip_view(floating_ip)
|
||||
|
||||
def index(self, req):
|
||||
context = req.environ['nova.context']
|
||||
|
||||
floating_ips = self.network_api.list_floating_ips(context)
|
||||
try:
|
||||
# FIXME(ja) - why does self.network_api.list_floating_ips raise?
|
||||
floating_ips = self.network_api.list_floating_ips(context)
|
||||
except exception.FloatingIpNotFoundForProject:
|
||||
floating_ips = []
|
||||
|
||||
return _translate_floating_ips_view(floating_ips)
|
||||
|
||||
def create(self, req):
|
||||
def create(self, req, body=None):
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
@@ -95,56 +100,19 @@ class FloatingIPController(object):
|
||||
else:
|
||||
raise
|
||||
|
||||
return {'allocated': {
|
||||
"id": ip['id'],
|
||||
"floating_ip": ip['address']}}
|
||||
return _translate_floating_ip_view(ip)
|
||||
|
||||
def delete(self, req, id):
|
||||
context = req.environ['nova.context']
|
||||
ip = self.network_api.get_floating_ip(context, id)
|
||||
|
||||
if 'fixed_ip' in ip:
|
||||
self.disassociate(req, id)
|
||||
|
||||
self.network_api.release_floating_ip(context, address=ip['address'])
|
||||
|
||||
return {'released': {
|
||||
"id": ip['id'],
|
||||
"floating_ip": ip['address']}}
|
||||
|
||||
def associate(self, req, id, body):
|
||||
""" /floating_ips/{id}/associate fixed ip in body """
|
||||
context = req.environ['nova.context']
|
||||
floating_ip = self._get_ip_by_id(context, id)
|
||||
|
||||
fixed_ip = body['associate_address']['fixed_ip']
|
||||
|
||||
try:
|
||||
self.network_api.associate_floating_ip(context,
|
||||
floating_ip, fixed_ip)
|
||||
except rpc.RemoteError:
|
||||
raise
|
||||
|
||||
return {'associated':
|
||||
{
|
||||
"floating_ip_id": id,
|
||||
"floating_ip": floating_ip,
|
||||
"fixed_ip": fixed_ip}}
|
||||
|
||||
def disassociate(self, req, id, body=None):
|
||||
""" POST /floating_ips/{id}/disassociate """
|
||||
context = req.environ['nova.context']
|
||||
floating_ip = self.network_api.get_floating_ip(context, id)
|
||||
address = floating_ip['address']
|
||||
fixed_ip = floating_ip['fixed_ip']['address']
|
||||
|
||||
try:
|
||||
self.network_api.disassociate_floating_ip(context, address)
|
||||
except rpc.RemoteError:
|
||||
raise
|
||||
if 'fixed_ip' in floating_ip:
|
||||
self.network_api.disassociate_floating_ip(context,
|
||||
floating_ip['address'])
|
||||
|
||||
return {'disassociated': {'floating_ip': address,
|
||||
'fixed_ip': fixed_ip}}
|
||||
self.network_api.release_floating_ip(context,
|
||||
address=floating_ip['address'])
|
||||
return webob.exc.HTTPAccepted()
|
||||
|
||||
def _get_ip_by_id(self, context, value):
|
||||
"""Checks that value is id and then returns its address."""
|
||||
@@ -152,6 +120,47 @@ class FloatingIPController(object):
|
||||
|
||||
|
||||
class Floating_ips(extensions.ExtensionDescriptor):
|
||||
def __init__(self):
|
||||
self.compute_api = compute.API()
|
||||
self.network_api = network.API()
|
||||
super(Floating_ips, self).__init__()
|
||||
|
||||
def _add_floating_ip(self, input_dict, req, instance_id):
|
||||
"""Associate floating_ip to an instance."""
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
address = input_dict['addFloatingIp']['address']
|
||||
except TypeError:
|
||||
msg = _("Missing parameter dict")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
except KeyError:
|
||||
msg = _("Address not specified")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
self.compute_api.associate_floating_ip(context, instance_id, address)
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
def _remove_floating_ip(self, input_dict, req, instance_id):
|
||||
"""Dissociate floating_ip from an instance."""
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
address = input_dict['removeFloatingIp']['address']
|
||||
except TypeError:
|
||||
msg = _("Missing parameter dict")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
except KeyError:
|
||||
msg = _("Address not specified")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
floating_ip = self.network_api.get_floating_ip_by_ip(context, address)
|
||||
if 'fixed_ip' in floating_ip:
|
||||
self.network_api.disassociate_floating_ip(context, address)
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
def get_name(self):
|
||||
return "Floating_ips"
|
||||
|
||||
@@ -172,9 +181,18 @@ class Floating_ips(extensions.ExtensionDescriptor):
|
||||
|
||||
res = extensions.ResourceExtension('os-floating-ips',
|
||||
FloatingIPController(),
|
||||
member_actions={
|
||||
'associate': 'POST',
|
||||
'disassociate': 'POST'})
|
||||
member_actions={})
|
||||
resources.append(res)
|
||||
|
||||
return resources
|
||||
|
||||
def get_actions(self):
|
||||
"""Return the actions the extension adds, as required by contract."""
|
||||
actions = [
|
||||
extensions.ActionExtension("servers", "addFloatingIp",
|
||||
self._add_floating_ip),
|
||||
extensions.ActionExtension("servers", "removeFloatingIp",
|
||||
self._remove_floating_ip),
|
||||
]
|
||||
|
||||
return actions
|
||||
|
||||
83
nova/api/openstack/contrib/rescue.py
Normal file
83
nova/api/openstack/contrib/rescue.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Copyright 2011 Openstack, LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""The rescue mode extension."""
|
||||
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from nova import compute
|
||||
from nova import log as logging
|
||||
from nova.api.openstack import extensions as exts
|
||||
from nova.api.openstack import faults
|
||||
|
||||
|
||||
LOG = logging.getLogger("nova.api.contrib.rescue")
|
||||
|
||||
|
||||
def wrap_errors(fn):
|
||||
""""Ensure errors are not passed along."""
|
||||
def wrapped(*args):
|
||||
try:
|
||||
fn(*args)
|
||||
except Exception, e:
|
||||
return faults.Fault(exc.HTTPInternalServerError())
|
||||
return wrapped
|
||||
|
||||
|
||||
class Rescue(exts.ExtensionDescriptor):
|
||||
"""The Rescue controller for the OpenStack API."""
|
||||
def __init__(self):
|
||||
super(Rescue, self).__init__()
|
||||
self.compute_api = compute.API()
|
||||
|
||||
@wrap_errors
|
||||
def _rescue(self, input_dict, req, instance_id):
|
||||
"""Rescue an instance."""
|
||||
context = req.environ["nova.context"]
|
||||
self.compute_api.rescue(context, instance_id)
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wrap_errors
|
||||
def _unrescue(self, input_dict, req, instance_id):
|
||||
"""Unrescue an instance."""
|
||||
context = req.environ["nova.context"]
|
||||
self.compute_api.unrescue(context, instance_id)
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
def get_name(self):
|
||||
return "Rescue"
|
||||
|
||||
def get_alias(self):
|
||||
return "os-rescue"
|
||||
|
||||
def get_description(self):
|
||||
return "Instance rescue mode"
|
||||
|
||||
def get_namespace(self):
|
||||
return "http://docs.openstack.org/ext/rescue/api/v1.1"
|
||||
|
||||
def get_updated(self):
|
||||
return "2011-08-18T00:00:00+00:00"
|
||||
|
||||
def get_actions(self):
|
||||
"""Return the actions the extension adds, as required by contract."""
|
||||
actions = [
|
||||
exts.ActionExtension("servers", "rescue", self._rescue),
|
||||
exts.ActionExtension("servers", "unrescue", self._unrescue),
|
||||
]
|
||||
|
||||
return actions
|
||||
@@ -25,10 +25,11 @@ from nova import db
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import rpc
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
|
||||
from nova.compute import power_state
|
||||
|
||||
from xml.dom import minidom
|
||||
|
||||
@@ -73,33 +74,28 @@ class SecurityGroupController(object):
|
||||
context, rule)]
|
||||
return security_group
|
||||
|
||||
def show(self, req, id):
|
||||
"""Return data about the given security group."""
|
||||
context = req.environ['nova.context']
|
||||
def _get_security_group(self, context, id):
|
||||
try:
|
||||
id = int(id)
|
||||
security_group = db.security_group_get(context, id)
|
||||
except ValueError:
|
||||
msg = _("Security group id is not integer")
|
||||
return exc.HTTPBadRequest(explanation=msg)
|
||||
msg = _("Security group id should be integer")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
except exception.NotFound as exp:
|
||||
return exc.HTTPNotFound(explanation=unicode(exp))
|
||||
raise exc.HTTPNotFound(explanation=unicode(exp))
|
||||
return security_group
|
||||
|
||||
def show(self, req, id):
|
||||
"""Return data about the given security group."""
|
||||
context = req.environ['nova.context']
|
||||
security_group = self._get_security_group(context, id)
|
||||
return {'security_group': self._format_security_group(context,
|
||||
security_group)}
|
||||
|
||||
def delete(self, req, id):
|
||||
"""Delete a security group."""
|
||||
context = req.environ['nova.context']
|
||||
try:
|
||||
id = int(id)
|
||||
security_group = db.security_group_get(context, id)
|
||||
except ValueError:
|
||||
msg = _("Security group id is not integer")
|
||||
return exc.HTTPBadRequest(explanation=msg)
|
||||
except exception.SecurityGroupNotFound as exp:
|
||||
return exc.HTTPNotFound(explanation=unicode(exp))
|
||||
|
||||
security_group = self._get_security_group(context, id)
|
||||
LOG.audit(_("Delete security group %s"), id, context=context)
|
||||
db.security_group_destroy(context, security_group.id)
|
||||
|
||||
@@ -226,9 +222,9 @@ class SecurityGroupRulesController(SecurityGroupController):
|
||||
security_group_rule = db.security_group_rule_create(context, values)
|
||||
|
||||
self.compute_api.trigger_security_group_rules_refresh(context,
|
||||
security_group_id=security_group['id'])
|
||||
security_group_id=security_group['id'])
|
||||
|
||||
return {'security_group_rule': self._format_security_group_rule(
|
||||
return {"security_group_rule": self._format_security_group_rule(
|
||||
context,
|
||||
security_group_rule)}
|
||||
|
||||
@@ -336,6 +332,11 @@ class SecurityGroupRulesController(SecurityGroupController):
|
||||
|
||||
|
||||
class Security_groups(extensions.ExtensionDescriptor):
|
||||
|
||||
def __init__(self):
|
||||
self.compute_api = compute.API()
|
||||
super(Security_groups, self).__init__()
|
||||
|
||||
def get_name(self):
|
||||
return "SecurityGroups"
|
||||
|
||||
@@ -351,6 +352,82 @@ class Security_groups(extensions.ExtensionDescriptor):
|
||||
def get_updated(self):
|
||||
return "2011-07-21T00:00:00+00:00"
|
||||
|
||||
def _addSecurityGroup(self, input_dict, req, instance_id):
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
body = input_dict['addSecurityGroup']
|
||||
group_name = body['name']
|
||||
instance_id = int(instance_id)
|
||||
except ValueError:
|
||||
msg = _("Server id should be integer")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
except TypeError:
|
||||
msg = _("Missing parameter dict")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
except KeyError:
|
||||
msg = _("Security group not specified")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if not group_name or group_name.strip() == '':
|
||||
msg = _("Security group name cannot be empty")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
try:
|
||||
self.compute_api.add_security_group(context, instance_id,
|
||||
group_name)
|
||||
except exception.SecurityGroupNotFound as exp:
|
||||
return exc.HTTPNotFound(explanation=unicode(exp))
|
||||
except exception.InstanceNotFound as exp:
|
||||
return exc.HTTPNotFound(explanation=unicode(exp))
|
||||
except exception.Invalid as exp:
|
||||
return exc.HTTPBadRequest(explanation=unicode(exp))
|
||||
|
||||
return exc.HTTPAccepted()
|
||||
|
||||
def _removeSecurityGroup(self, input_dict, req, instance_id):
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
body = input_dict['removeSecurityGroup']
|
||||
group_name = body['name']
|
||||
instance_id = int(instance_id)
|
||||
except ValueError:
|
||||
msg = _("Server id should be integer")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
except TypeError:
|
||||
msg = _("Missing parameter dict")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
except KeyError:
|
||||
msg = _("Security group not specified")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
if not group_name or group_name.strip() == '':
|
||||
msg = _("Security group name cannot be empty")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
try:
|
||||
self.compute_api.remove_security_group(context, instance_id,
|
||||
group_name)
|
||||
except exception.SecurityGroupNotFound as exp:
|
||||
return exc.HTTPNotFound(explanation=unicode(exp))
|
||||
except exception.InstanceNotFound as exp:
|
||||
return exc.HTTPNotFound(explanation=unicode(exp))
|
||||
except exception.Invalid as exp:
|
||||
return exc.HTTPBadRequest(explanation=unicode(exp))
|
||||
|
||||
return exc.HTTPAccepted()
|
||||
|
||||
def get_actions(self):
|
||||
"""Return the actions the extensions adds"""
|
||||
actions = [
|
||||
extensions.ActionExtension("servers", "addSecurityGroup",
|
||||
self._addSecurityGroup),
|
||||
extensions.ActionExtension("servers", "removeSecurityGroup",
|
||||
self._removeSecurityGroup)
|
||||
]
|
||||
return actions
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
|
||||
|
||||
108
nova/api/openstack/contrib/virtual_interfaces.py
Normal file
108
nova/api/openstack/contrib/virtual_interfaces.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# Copyright (C) 2011 Midokura KK
|
||||
# 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 virtual interfaces extension."""
|
||||
|
||||
from webob import exc
|
||||
import webob
|
||||
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
from nova import log as logging
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import faults
|
||||
from nova.api.openstack import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger("nova.api.virtual_interfaces")
|
||||
|
||||
|
||||
def _translate_vif_summary_view(_context, vif):
|
||||
"""Maps keys for VIF summary view."""
|
||||
d = {}
|
||||
d['id'] = vif['uuid']
|
||||
d['mac_address'] = vif['address']
|
||||
return d
|
||||
|
||||
|
||||
def _get_metadata():
|
||||
metadata = {
|
||||
"attributes": {
|
||||
'virtual_interface': ["id", "mac_address"]}}
|
||||
return metadata
|
||||
|
||||
|
||||
class ServerVirtualInterfaceController(object):
|
||||
"""The instance VIF API controller for the Openstack API.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.compute_api = compute.API()
|
||||
super(ServerVirtualInterfaceController, self).__init__()
|
||||
|
||||
def _items(self, req, server_id, entity_maker):
|
||||
"""Returns a list of VIFs, transformed through entity_maker."""
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
instance = self.compute_api.get(context, server_id)
|
||||
except exception.NotFound:
|
||||
return faults.Fault(exc.HTTPNotFound())
|
||||
|
||||
vifs = instance['virtual_interfaces']
|
||||
limited_list = common.limited(vifs, req)
|
||||
res = [entity_maker(context, vif) for vif in limited_list]
|
||||
return {'virtual_interfaces': res}
|
||||
|
||||
def index(self, req, server_id):
|
||||
"""Returns the list of VIFs for a given instance."""
|
||||
return self._items(req, server_id,
|
||||
entity_maker=_translate_vif_summary_view)
|
||||
|
||||
|
||||
class Virtual_interfaces(extensions.ExtensionDescriptor):
|
||||
|
||||
def get_name(self):
|
||||
return "VirtualInterfaces"
|
||||
|
||||
def get_alias(self):
|
||||
return "virtual_interfaces"
|
||||
|
||||
def get_description(self):
|
||||
return "Virtual interface support"
|
||||
|
||||
def get_namespace(self):
|
||||
return "http://docs.openstack.org/ext/virtual_interfaces/api/v1.1"
|
||||
|
||||
def get_updated(self):
|
||||
return "2011-08-17T00:00:00+00:00"
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
|
||||
metadata = _get_metadata()
|
||||
body_serializers = {
|
||||
'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
|
||||
xmlns=wsgi.XMLNS_V11)}
|
||||
serializer = wsgi.ResponseSerializer(body_serializers, None)
|
||||
res = extensions.ResourceExtension(
|
||||
'os-virtual-interfaces',
|
||||
controller=ServerVirtualInterfaceController(),
|
||||
parent=dict(member_name='server', collection_name='servers'),
|
||||
serializer=serializer)
|
||||
resources.append(res)
|
||||
|
||||
return resources
|
||||
@@ -24,6 +24,7 @@ from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import quota
|
||||
from nova import volume
|
||||
from nova.volume import volume_types
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import faults
|
||||
@@ -63,6 +64,22 @@ def translate_volume_summary_view(context, vol):
|
||||
|
||||
d['displayName'] = vol['display_name']
|
||||
d['displayDescription'] = vol['display_description']
|
||||
|
||||
if vol['volume_type_id'] and vol.get('volume_type'):
|
||||
d['volumeType'] = vol['volume_type']['name']
|
||||
else:
|
||||
d['volumeType'] = vol['volume_type_id']
|
||||
|
||||
LOG.audit(_("vol=%s"), vol, context=context)
|
||||
|
||||
if vol.get('volume_metadata'):
|
||||
meta_dict = {}
|
||||
for i in vol['volume_metadata']:
|
||||
meta_dict[i['key']] = i['value']
|
||||
d['metadata'] = meta_dict
|
||||
else:
|
||||
d['metadata'] = {}
|
||||
|
||||
return d
|
||||
|
||||
|
||||
@@ -80,6 +97,8 @@ class VolumeController(object):
|
||||
"createdAt",
|
||||
"displayName",
|
||||
"displayDescription",
|
||||
"volumeType",
|
||||
"metadata",
|
||||
]}}}
|
||||
|
||||
def __init__(self):
|
||||
@@ -136,12 +155,25 @@ class VolumeController(object):
|
||||
vol = body['volume']
|
||||
size = vol['size']
|
||||
LOG.audit(_("Create volume of %s GB"), size, context=context)
|
||||
|
||||
vol_type = vol.get('volume_type', None)
|
||||
if vol_type:
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type_by_name(context,
|
||||
vol_type)
|
||||
except exception.NotFound:
|
||||
return faults.Fault(exc.HTTPNotFound())
|
||||
|
||||
metadata = vol.get('metadata', None)
|
||||
|
||||
new_volume = self.volume_api.create(context, size, None,
|
||||
vol.get('display_name'),
|
||||
vol.get('display_description'))
|
||||
vol.get('display_description'),
|
||||
volume_type=vol_type,
|
||||
metadata=metadata)
|
||||
|
||||
# Work around problem that instance is lazy-loaded...
|
||||
new_volume['instance'] = None
|
||||
new_volume = self.volume_api.get(context, new_volume['id'])
|
||||
|
||||
retval = translate_volume_detail_view(context, new_volume)
|
||||
|
||||
|
||||
197
nova/api/openstack/contrib/volumetypes.py
Normal file
197
nova/api/openstack/contrib/volumetypes.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Zadara Storage Inc.
|
||||
# Copyright (c) 2011 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
""" The volume type & volume types extra specs extension"""
|
||||
|
||||
from webob import exc
|
||||
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova import quota
|
||||
from nova.volume import volume_types
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import faults
|
||||
from nova.api.openstack import wsgi
|
||||
|
||||
|
||||
class VolumeTypesController(object):
|
||||
""" The volume types API controller for the Openstack API """
|
||||
|
||||
def index(self, req):
|
||||
""" Returns the list of volume types """
|
||||
context = req.environ['nova.context']
|
||||
return volume_types.get_all_types(context)
|
||||
|
||||
def create(self, req, body):
|
||||
"""Creates a new volume type."""
|
||||
context = req.environ['nova.context']
|
||||
|
||||
if not body or body == "":
|
||||
return faults.Fault(exc.HTTPUnprocessableEntity())
|
||||
|
||||
vol_type = body.get('volume_type', None)
|
||||
if vol_type is None or vol_type == "":
|
||||
return faults.Fault(exc.HTTPUnprocessableEntity())
|
||||
|
||||
name = vol_type.get('name', None)
|
||||
specs = vol_type.get('extra_specs', {})
|
||||
|
||||
if name is None or name == "":
|
||||
return faults.Fault(exc.HTTPUnprocessableEntity())
|
||||
|
||||
try:
|
||||
volume_types.create(context, name, specs)
|
||||
vol_type = volume_types.get_volume_type_by_name(context, name)
|
||||
except quota.QuotaError as error:
|
||||
self._handle_quota_error(error)
|
||||
except exception.NotFound:
|
||||
return faults.Fault(exc.HTTPNotFound())
|
||||
|
||||
return {'volume_type': vol_type}
|
||||
|
||||
def show(self, req, id):
|
||||
""" Return a single volume type item """
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
except exception.NotFound or exception.ApiError:
|
||||
return faults.Fault(exc.HTTPNotFound())
|
||||
|
||||
return {'volume_type': vol_type}
|
||||
|
||||
def delete(self, req, id):
|
||||
""" Deletes an existing volume type """
|
||||
context = req.environ['nova.context']
|
||||
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
volume_types.destroy(context, vol_type['name'])
|
||||
except exception.NotFound:
|
||||
return faults.Fault(exc.HTTPNotFound())
|
||||
|
||||
def _handle_quota_error(self, error):
|
||||
"""Reraise quota errors as api-specific http exceptions."""
|
||||
if error.code == "MetadataLimitExceeded":
|
||||
raise exc.HTTPBadRequest(explanation=error.message)
|
||||
raise error
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecsController(object):
|
||||
""" The volume type extra specs API controller for the Openstack API """
|
||||
|
||||
def _get_extra_specs(self, context, vol_type_id):
|
||||
extra_specs = db.api.volume_type_extra_specs_get(context, vol_type_id)
|
||||
specs_dict = {}
|
||||
for key, value in extra_specs.iteritems():
|
||||
specs_dict[key] = value
|
||||
return dict(extra_specs=specs_dict)
|
||||
|
||||
def _check_body(self, body):
|
||||
if body == None or body == "":
|
||||
expl = _('No Request Body')
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
|
||||
def index(self, req, vol_type_id):
|
||||
""" Returns the list of extra specs for a given volume type """
|
||||
context = req.environ['nova.context']
|
||||
return self._get_extra_specs(context, vol_type_id)
|
||||
|
||||
def create(self, req, vol_type_id, body):
|
||||
self._check_body(body)
|
||||
context = req.environ['nova.context']
|
||||
specs = body.get('extra_specs')
|
||||
try:
|
||||
db.api.volume_type_extra_specs_update_or_create(context,
|
||||
vol_type_id,
|
||||
specs)
|
||||
except quota.QuotaError as error:
|
||||
self._handle_quota_error(error)
|
||||
return body
|
||||
|
||||
def update(self, req, vol_type_id, id, body):
|
||||
self._check_body(body)
|
||||
context = req.environ['nova.context']
|
||||
if not id in body:
|
||||
expl = _('Request body and URI mismatch')
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
if len(body) > 1:
|
||||
expl = _('Request body contains too many items')
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
try:
|
||||
db.api.volume_type_extra_specs_update_or_create(context,
|
||||
vol_type_id,
|
||||
body)
|
||||
except quota.QuotaError as error:
|
||||
self._handle_quota_error(error)
|
||||
|
||||
return body
|
||||
|
||||
def show(self, req, vol_type_id, id):
|
||||
""" Return a single extra spec item """
|
||||
context = req.environ['nova.context']
|
||||
specs = self._get_extra_specs(context, vol_type_id)
|
||||
if id in specs['extra_specs']:
|
||||
return {id: specs['extra_specs'][id]}
|
||||
else:
|
||||
return faults.Fault(exc.HTTPNotFound())
|
||||
|
||||
def delete(self, req, vol_type_id, id):
|
||||
""" Deletes an existing extra spec """
|
||||
context = req.environ['nova.context']
|
||||
db.api.volume_type_extra_specs_delete(context, vol_type_id, id)
|
||||
|
||||
def _handle_quota_error(self, error):
|
||||
"""Reraise quota errors as api-specific http exceptions."""
|
||||
if error.code == "MetadataLimitExceeded":
|
||||
raise exc.HTTPBadRequest(explanation=error.message)
|
||||
raise error
|
||||
|
||||
|
||||
class Volumetypes(extensions.ExtensionDescriptor):
|
||||
|
||||
def get_name(self):
|
||||
return "VolumeTypes"
|
||||
|
||||
def get_alias(self):
|
||||
return "os-volume-types"
|
||||
|
||||
def get_description(self):
|
||||
return "Volume types support"
|
||||
|
||||
def get_namespace(self):
|
||||
return \
|
||||
"http://docs.openstack.org/ext/volume_types/api/v1.1"
|
||||
|
||||
def get_updated(self):
|
||||
return "2011-08-24T00:00:00+00:00"
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
res = extensions.ResourceExtension(
|
||||
'os-volume-types',
|
||||
VolumeTypesController())
|
||||
resources.append(res)
|
||||
|
||||
res = extensions.ResourceExtension('extra_specs',
|
||||
VolumeTypeExtraSpecsController(),
|
||||
parent=dict(
|
||||
member_name='vol_type',
|
||||
collection_name='os-volume-types'))
|
||||
resources.append(res)
|
||||
|
||||
return resources
|
||||
@@ -1,4 +1,5 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@@ -29,7 +30,7 @@ from nova import utils
|
||||
from nova.compute import instance_types
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import wsgi
|
||||
|
||||
from nova.rpc.common import RemoteError
|
||||
|
||||
LOG = logging.getLogger('nova.api.openstack.create_instance_helper')
|
||||
FLAGS = flags.FLAGS
|
||||
@@ -106,11 +107,26 @@ class CreateInstanceHelper(object):
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
personality = server_dict.get('personality')
|
||||
config_drive = server_dict.get('config_drive')
|
||||
|
||||
injected_files = []
|
||||
if personality:
|
||||
injected_files = self._get_injected_files(personality)
|
||||
|
||||
sg_names = []
|
||||
security_groups = server_dict.get('security_groups')
|
||||
if security_groups is not None:
|
||||
sg_names = [sg['name'] for sg in security_groups if sg.get('name')]
|
||||
if not sg_names:
|
||||
sg_names.append('default')
|
||||
|
||||
sg_names = list(set(sg_names))
|
||||
|
||||
requested_networks = server_dict.get('networks')
|
||||
if requested_networks is not None:
|
||||
requested_networks = self._get_requested_networks(
|
||||
requested_networks)
|
||||
|
||||
try:
|
||||
flavor_id = self.controller._flavor_id_from_req_data(body)
|
||||
except ValueError as error:
|
||||
@@ -122,6 +138,7 @@ class CreateInstanceHelper(object):
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
zone_blob = server_dict.get('blob')
|
||||
user_data = server_dict.get('user_data')
|
||||
availability_zone = server_dict.get('availability_zone')
|
||||
name = server_dict['name']
|
||||
self._validate_server_name(name)
|
||||
@@ -144,6 +161,7 @@ class CreateInstanceHelper(object):
|
||||
extra_values = {
|
||||
'instance_type': inst_type,
|
||||
'image_ref': image_href,
|
||||
'config_drive': config_drive,
|
||||
'password': password}
|
||||
|
||||
return (extra_values,
|
||||
@@ -157,13 +175,19 @@ class CreateInstanceHelper(object):
|
||||
key_name=key_name,
|
||||
key_data=key_data,
|
||||
metadata=server_dict.get('metadata', {}),
|
||||
access_ip_v4=server_dict.get('accessIPv4'),
|
||||
access_ip_v6=server_dict.get('accessIPv6'),
|
||||
injected_files=injected_files,
|
||||
admin_password=password,
|
||||
zone_blob=zone_blob,
|
||||
reservation_id=reservation_id,
|
||||
min_count=min_count,
|
||||
max_count=max_count,
|
||||
availability_zone=availability_zone))
|
||||
requested_networks=requested_networks,
|
||||
security_group=sg_names,
|
||||
user_data=user_data,
|
||||
availability_zone=availability_zone,
|
||||
config_drive=config_drive,))
|
||||
except quota.QuotaError as error:
|
||||
self._handle_quota_error(error)
|
||||
except exception.ImageNotFound as error:
|
||||
@@ -172,6 +196,12 @@ class CreateInstanceHelper(object):
|
||||
except exception.FlavorNotFound as error:
|
||||
msg = _("Invalid flavorRef provided.")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
except exception.SecurityGroupNotFound as error:
|
||||
raise exc.HTTPBadRequest(explanation=unicode(error))
|
||||
except RemoteError as err:
|
||||
msg = "%(err_type)s: %(err_msg)s" % \
|
||||
{'err_type': err.exc_type, 'err_msg': err.value}
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
# Let the caller deal with unhandled exceptions.
|
||||
|
||||
def _handle_quota_error(self, error):
|
||||
@@ -180,13 +210,20 @@ class CreateInstanceHelper(object):
|
||||
"""
|
||||
if error.code == "OnsetFileLimitExceeded":
|
||||
expl = _("Personality file limit exceeded")
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
||||
headers={'Retry-After': 0})
|
||||
if error.code == "OnsetFilePathLimitExceeded":
|
||||
expl = _("Personality file path too long")
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
||||
headers={'Retry-After': 0})
|
||||
if error.code == "OnsetFileContentLimitExceeded":
|
||||
expl = _("Personality file content too long")
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
||||
headers={'Retry-After': 0})
|
||||
if error.code == "InstanceLimitExceeded":
|
||||
expl = _("Instance quotas have been exceeded")
|
||||
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
||||
headers={'Retry-After': 0})
|
||||
# if the original error is okay, just reraise it
|
||||
raise error
|
||||
|
||||
@@ -293,6 +330,46 @@ class CreateInstanceHelper(object):
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
return password
|
||||
|
||||
def _get_requested_networks(self, requested_networks):
|
||||
"""
|
||||
Create a list of requested networks from the networks attribute
|
||||
"""
|
||||
networks = []
|
||||
for network in requested_networks:
|
||||
try:
|
||||
network_uuid = network['uuid']
|
||||
|
||||
if not utils.is_uuid_like(network_uuid):
|
||||
msg = _("Bad networks format: network uuid is not in"
|
||||
" proper format (%s)") % network_uuid
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
#fixed IP address is optional
|
||||
#if the fixed IP address is not provided then
|
||||
#it will use one of the available IP address from the network
|
||||
address = network.get('fixed_ip', None)
|
||||
if address is not None and not utils.is_valid_ipv4(address):
|
||||
msg = _("Invalid fixed IP address (%s)") % address
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
# check if the network id is already present in the list,
|
||||
# we don't want duplicate networks to be passed
|
||||
# at the boot time
|
||||
for id, ip in networks:
|
||||
if id == network_uuid:
|
||||
expl = _("Duplicate networks (%s) are not allowed")\
|
||||
% network_uuid
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
|
||||
networks.append((network_uuid, address))
|
||||
except KeyError as key:
|
||||
expl = _('Bad network format: missing %s') % key
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
except TypeError:
|
||||
expl = _('Bad networks format')
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
|
||||
return networks
|
||||
|
||||
|
||||
class ServerXMLDeserializer(wsgi.XMLDeserializer):
|
||||
"""
|
||||
@@ -443,7 +520,8 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer):
|
||||
server = {}
|
||||
server_node = self.find_first_child_named(node, 'server')
|
||||
|
||||
attributes = ["name", "imageRef", "flavorRef", "adminPass"]
|
||||
attributes = ["name", "imageRef", "flavorRef", "adminPass",
|
||||
"accessIPv4", "accessIPv6"]
|
||||
for attr in attributes:
|
||||
if server_node.getAttribute(attr):
|
||||
server[attr] = server_node.getAttribute(attr)
|
||||
@@ -456,6 +534,14 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer):
|
||||
if personality is not None:
|
||||
server["personality"] = personality
|
||||
|
||||
networks = self._extract_networks(server_node)
|
||||
if networks is not None:
|
||||
server["networks"] = networks
|
||||
|
||||
security_groups = self._extract_security_groups(server_node)
|
||||
if security_groups is not None:
|
||||
server["security_groups"] = security_groups
|
||||
|
||||
return server
|
||||
|
||||
def _extract_personality(self, server_node):
|
||||
@@ -472,3 +558,35 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer):
|
||||
return personality
|
||||
else:
|
||||
return None
|
||||
|
||||
def _extract_networks(self, server_node):
|
||||
"""Marshal the networks attribute of a parsed request"""
|
||||
node = self.find_first_child_named(server_node, "networks")
|
||||
if node is not None:
|
||||
networks = []
|
||||
for network_node in self.find_children_named(node,
|
||||
"network"):
|
||||
item = {}
|
||||
if network_node.hasAttribute("uuid"):
|
||||
item["uuid"] = network_node.getAttribute("uuid")
|
||||
if network_node.hasAttribute("fixed_ip"):
|
||||
item["fixed_ip"] = network_node.getAttribute("fixed_ip")
|
||||
networks.append(item)
|
||||
return networks
|
||||
else:
|
||||
return None
|
||||
|
||||
def _extract_security_groups(self, server_node):
|
||||
"""Marshal the security_groups attribute of a parsed request"""
|
||||
node = self.find_first_child_named(server_node, "security_groups")
|
||||
if node is not None:
|
||||
security_groups = []
|
||||
for sg_node in self.find_children_named(node, "security_group"):
|
||||
item = {}
|
||||
name_node = self.find_first_child_named(sg_node, "name")
|
||||
if name_node:
|
||||
item["name"] = self.extract_text(name_node)
|
||||
security_groups.append(item)
|
||||
return security_groups
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -29,6 +29,7 @@ from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import wsgi as base_wsgi
|
||||
import nova.api.openstack
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import faults
|
||||
from nova.api.openstack import wsgi
|
||||
@@ -220,12 +221,13 @@ class ExtensionMiddleware(base_wsgi.Middleware):
|
||||
for action in ext_mgr.get_actions():
|
||||
if not action.collection in action_resources.keys():
|
||||
resource = ActionExtensionResource(application)
|
||||
mapper.connect("/%s/:(id)/action.:(format)" %
|
||||
mapper.connect("/:(project_id)/%s/:(id)/action.:(format)" %
|
||||
action.collection,
|
||||
action='action',
|
||||
controller=resource,
|
||||
conditions=dict(method=['POST']))
|
||||
mapper.connect("/%s/:(id)/action" % action.collection,
|
||||
mapper.connect("/:(project_id)/%s/:(id)/action" %
|
||||
action.collection,
|
||||
action='action',
|
||||
controller=resource,
|
||||
conditions=dict(method=['POST']))
|
||||
@@ -258,7 +260,7 @@ class ExtensionMiddleware(base_wsgi.Middleware):
|
||||
ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path)
|
||||
self.ext_mgr = ext_mgr
|
||||
|
||||
mapper = routes.Mapper()
|
||||
mapper = nova.api.openstack.ProjectMapper()
|
||||
|
||||
serializer = wsgi.ResponseSerializer(
|
||||
{'application/xml': ExtensionsXMLSerializer()})
|
||||
@@ -269,13 +271,17 @@ class ExtensionMiddleware(base_wsgi.Middleware):
|
||||
if resource.serializer is None:
|
||||
resource.serializer = serializer
|
||||
|
||||
mapper.resource(resource.collection, resource.collection,
|
||||
kargs = dict(
|
||||
controller=wsgi.Resource(
|
||||
resource.controller, resource.deserializer,
|
||||
resource.serializer),
|
||||
collection=resource.collection_actions,
|
||||
member=resource.member_actions,
|
||||
parent_resource=resource.parent)
|
||||
member=resource.member_actions)
|
||||
|
||||
if resource.parent:
|
||||
kargs['parent_resource'] = resource.parent
|
||||
|
||||
mapper.resource(resource.collection, resource.collection, **kargs)
|
||||
|
||||
# extended actions
|
||||
action_resources = self._action_ext_resources(application, ext_mgr,
|
||||
|
||||
@@ -72,7 +72,8 @@ class ControllerV11(Controller):
|
||||
|
||||
def _get_view_builder(self, req):
|
||||
base_url = req.application_url
|
||||
return views.flavors.ViewBuilderV11(base_url)
|
||||
project_id = getattr(req.environ['nova.context'], 'project_id', '')
|
||||
return views.flavors.ViewBuilderV11(base_url, project_id)
|
||||
|
||||
|
||||
class FlavorXMLSerializer(wsgi.XMLDictSerializer):
|
||||
|
||||
@@ -166,10 +166,11 @@ class ControllerV10(Controller):
|
||||
class ControllerV11(Controller):
|
||||
"""Version 1.1 specific controller logic."""
|
||||
|
||||
def get_builder(self, request):
|
||||
def get_builder(self, req):
|
||||
"""Property to get the ViewBuilder class we need to use."""
|
||||
base_url = request.application_url
|
||||
return images_view.ViewBuilderV11(base_url)
|
||||
base_url = req.application_url
|
||||
project_id = getattr(req.environ['nova.context'], 'project_id', '')
|
||||
return images_view.ViewBuilderV11(base_url, project_id)
|
||||
|
||||
def index(self, req):
|
||||
"""Return an index listing of images available to the request.
|
||||
|
||||
50
nova/api/openstack/schemas/v1.1/server.rng
Normal file
50
nova/api/openstack/schemas/v1.1/server.rng
Normal file
@@ -0,0 +1,50 @@
|
||||
<element name="server" ns="http://docs.openstack.org/compute/api/v1.1"
|
||||
xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<attribute name="name"> <text/> </attribute>
|
||||
<attribute name="id"> <text/> </attribute>
|
||||
<attribute name="uuid"> <text/> </attribute>
|
||||
<attribute name="updated"> <text/> </attribute>
|
||||
<attribute name="created"> <text/> </attribute>
|
||||
<attribute name="hostId"> <text/> </attribute>
|
||||
<attribute name="accessIPv4"> <text/> </attribute>
|
||||
<attribute name="accessIPv6"> <text/> </attribute>
|
||||
<attribute name="status"> <text/> </attribute>
|
||||
<optional>
|
||||
<attribute name="progress"> <text/> </attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="adminPass"> <text/> </attribute>
|
||||
</optional>
|
||||
<zeroOrMore>
|
||||
<externalRef href="../atom-link.rng"/>
|
||||
</zeroOrMore>
|
||||
<element name="image">
|
||||
<attribute name="id"> <text/> </attribute>
|
||||
<externalRef href="../atom-link.rng"/>
|
||||
</element>
|
||||
<element name="flavor">
|
||||
<attribute name="id"> <text/> </attribute>
|
||||
<externalRef href="../atom-link.rng"/>
|
||||
</element>
|
||||
<element name="metadata">
|
||||
<zeroOrMore>
|
||||
<element name="meta">
|
||||
<attribute name="key"> <text/> </attribute>
|
||||
<text/>
|
||||
</element>
|
||||
</zeroOrMore>
|
||||
</element>
|
||||
<element name="addresses">
|
||||
<zeroOrMore>
|
||||
<element name="network">
|
||||
<attribute name="id"> <text/> </attribute>
|
||||
<zeroOrMore>
|
||||
<element name="ip">
|
||||
<attribute name="version"> <text/> </attribute>
|
||||
<attribute name="addr"> <text/> </attribute>
|
||||
</element>
|
||||
</zeroOrMore>
|
||||
</element>
|
||||
</zeroOrMore>
|
||||
</element>
|
||||
</element>
|
||||
6
nova/api/openstack/schemas/v1.1/servers.rng
Normal file
6
nova/api/openstack/schemas/v1.1/servers.rng
Normal file
@@ -0,0 +1,6 @@
|
||||
<element name="servers" xmlns="http://relaxng.org/ns/structure/1.0"
|
||||
ns="http://docs.openstack.org/compute/api/v1.1">
|
||||
<zeroOrMore>
|
||||
<externalRef href="server.rng"/>
|
||||
</zeroOrMore>
|
||||
</element>
|
||||
12
nova/api/openstack/schemas/v1.1/servers_index.rng
Normal file
12
nova/api/openstack/schemas/v1.1/servers_index.rng
Normal file
@@ -0,0 +1,12 @@
|
||||
<element name="servers" ns="http://docs.openstack.org/compute/api/v1.1"
|
||||
xmlns="http://relaxng.org/ns/structure/1.0">
|
||||
<zeroOrMore>
|
||||
<element name="server">
|
||||
<attribute name="name"> <text/> </attribute>
|
||||
<attribute name="id"> <text/> </attribute>
|
||||
<zeroOrMore>
|
||||
<externalRef href="../atom-link.rng"/>
|
||||
</zeroOrMore>
|
||||
</element>
|
||||
</zeroOrMore>
|
||||
</element>
|
||||
@@ -151,7 +151,8 @@ class Controller(object):
|
||||
def _handle_quota_error(self, error):
|
||||
"""Reraise quota errors as api-specific http exceptions."""
|
||||
if error.code == "MetadataLimitExceeded":
|
||||
raise exc.HTTPBadRequest(explanation=error.message)
|
||||
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
||||
headers={'Retry-After': 0})
|
||||
raise error
|
||||
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ class Controller(object):
|
||||
|
||||
@scheduler_api.redirect_handler
|
||||
def update(self, req, id, body):
|
||||
"""Update server name then pass on to version-specific controller"""
|
||||
"""Update server then pass on to version-specific controller"""
|
||||
if len(req.body) == 0:
|
||||
raise exc.HTTPUnprocessableEntity()
|
||||
|
||||
@@ -178,6 +178,14 @@ class Controller(object):
|
||||
self.helper._validate_server_name(name)
|
||||
update_dict['display_name'] = name.strip()
|
||||
|
||||
if 'accessIPv4' in body['server']:
|
||||
access_ipv4 = body['server']['accessIPv4']
|
||||
update_dict['access_ip_v4'] = access_ipv4.strip()
|
||||
|
||||
if 'accessIPv6' in body['server']:
|
||||
access_ipv6 = body['server']['accessIPv6']
|
||||
update_dict['access_ip_v6'] = access_ipv6.strip()
|
||||
|
||||
try:
|
||||
self.compute_api.update(ctxt, id, **update_dict)
|
||||
except exception.NotFound:
|
||||
@@ -596,8 +604,10 @@ class ControllerV10(Controller):
|
||||
LOG.debug(msg)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
password = utils.generate_password(16)
|
||||
|
||||
try:
|
||||
self.compute_api.rebuild(context, instance_id, image_id)
|
||||
self.compute_api.rebuild(context, instance_id, image_id, password)
|
||||
except exception.BuildInProgress:
|
||||
msg = _("Instance %s is currently being rebuilt.") % instance_id
|
||||
LOG.debug(msg)
|
||||
@@ -642,14 +652,16 @@ class ControllerV11(Controller):
|
||||
return common.get_id_from_href(flavor_ref)
|
||||
|
||||
def _build_view(self, req, instance, is_detail=False):
|
||||
project_id = getattr(req.environ['nova.context'], 'project_id', '')
|
||||
base_url = req.application_url
|
||||
flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
|
||||
base_url)
|
||||
base_url, project_id)
|
||||
image_builder = nova.api.openstack.views.images.ViewBuilderV11(
|
||||
base_url)
|
||||
base_url, project_id)
|
||||
addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
|
||||
builder = nova.api.openstack.views.servers.ViewBuilderV11(
|
||||
addresses_builder, flavor_builder, image_builder, base_url)
|
||||
addresses_builder, flavor_builder, image_builder,
|
||||
base_url, project_id)
|
||||
|
||||
return builder.build(instance, is_detail=is_detail)
|
||||
|
||||
@@ -731,15 +743,26 @@ class ControllerV11(Controller):
|
||||
self._validate_metadata(metadata)
|
||||
self._decode_personalities(personalities)
|
||||
|
||||
password = info["rebuild"].get("adminPass",
|
||||
utils.generate_password(16))
|
||||
|
||||
try:
|
||||
self.compute_api.rebuild(context, instance_id, image_href, name,
|
||||
metadata, personalities)
|
||||
self.compute_api.rebuild(context, instance_id, image_href,
|
||||
password, name=name, metadata=metadata,
|
||||
files_to_inject=personalities)
|
||||
except exception.BuildInProgress:
|
||||
msg = _("Instance %s is currently being rebuilt.") % instance_id
|
||||
LOG.debug(msg)
|
||||
raise exc.HTTPConflict(explanation=msg)
|
||||
except exception.InstanceNotFound:
|
||||
msg = _("Instance %s could not be found") % instance_id
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
instance = self.compute_api.routing_get(context, instance_id)
|
||||
view = self._build_view(request, instance, is_detail=True)
|
||||
view['server']['adminPass'] = password
|
||||
|
||||
return view
|
||||
|
||||
@common.check_snapshots_enabled
|
||||
def _action_create_image(self, input_dict, req, instance_id):
|
||||
@@ -806,6 +829,9 @@ class HeadersSerializer(wsgi.ResponseHeadersSerializer):
|
||||
def delete(self, response, data):
|
||||
response.status_int = 204
|
||||
|
||||
def action(self, response, data):
|
||||
response.status_int = 202
|
||||
|
||||
|
||||
class ServerXMLSerializer(wsgi.XMLDictSerializer):
|
||||
|
||||
@@ -837,6 +863,10 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer):
|
||||
node.setAttribute('created', str(server['created']))
|
||||
node.setAttribute('updated', str(server['updated']))
|
||||
node.setAttribute('status', server['status'])
|
||||
if 'accessIPv4' in server:
|
||||
node.setAttribute('accessIPv4', str(server['accessIPv4']))
|
||||
if 'accessIPv6' in server:
|
||||
node.setAttribute('accessIPv6', str(server['accessIPv6']))
|
||||
if 'progress' in server:
|
||||
node.setAttribute('progress', str(server['progress']))
|
||||
|
||||
@@ -923,6 +953,17 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer):
|
||||
node.setAttribute('adminPass', server_dict['server']['adminPass'])
|
||||
return self.to_xml_string(node, True)
|
||||
|
||||
def action(self, server_dict):
|
||||
#NOTE(bcwaldon): We need a way to serialize actions individually. This
|
||||
# assumes all actions return a server entity
|
||||
return self.create(server_dict)
|
||||
|
||||
def update(self, server_dict):
|
||||
xml_doc = minidom.Document()
|
||||
node = self._server_to_xml_detailed(xml_doc,
|
||||
server_dict['server'])
|
||||
return self.to_xml_string(node, True)
|
||||
|
||||
|
||||
def create_resource(version='1.0'):
|
||||
controller = {
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
|
||||
from nova import flags
|
||||
from nova import utils
|
||||
from nova import log as logging
|
||||
from nova.api.openstack import common
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
LOG = logging.getLogger('nova.api.openstack.views.addresses')
|
||||
|
||||
|
||||
class ViewBuilder(object):
|
||||
@@ -48,7 +50,10 @@ class ViewBuilderV11(ViewBuilder):
|
||||
def build(self, interfaces):
|
||||
networks = {}
|
||||
for interface in interfaces:
|
||||
network_label = interface['network']['label']
|
||||
try:
|
||||
network_label = self._extract_network_label(interface)
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
if network_label not in networks:
|
||||
networks[network_label] = []
|
||||
@@ -64,9 +69,14 @@ class ViewBuilderV11(ViewBuilder):
|
||||
|
||||
return networks
|
||||
|
||||
def build_network(self, interfaces, network_label):
|
||||
def build_network(self, interfaces, requested_network):
|
||||
for interface in interfaces:
|
||||
if interface['network']['label'] == network_label:
|
||||
try:
|
||||
network_label = self._extract_network_label(interface)
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
if network_label == requested_network:
|
||||
ips = list(self._extract_ipv4_addresses(interface))
|
||||
ipv6 = self._extract_ipv6_address(interface)
|
||||
if ipv6 is not None:
|
||||
@@ -74,6 +84,13 @@ class ViewBuilderV11(ViewBuilder):
|
||||
return {network_label: ips}
|
||||
return None
|
||||
|
||||
def _extract_network_label(self, interface):
|
||||
try:
|
||||
return interface['network']['label']
|
||||
except (TypeError, KeyError) as exc:
|
||||
LOG.exception(exc)
|
||||
raise TypeError
|
||||
|
||||
def _extract_ipv4_addresses(self, interface):
|
||||
for fixed_ip in interface['fixed_ips']:
|
||||
yield self._build_ip_entity(fixed_ip['address'], 4)
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os.path
|
||||
|
||||
|
||||
from nova.api.openstack import common
|
||||
|
||||
|
||||
@@ -59,11 +62,12 @@ class ViewBuilder(object):
|
||||
class ViewBuilderV11(ViewBuilder):
|
||||
"""Openstack API v1.1 flavors view builder."""
|
||||
|
||||
def __init__(self, base_url):
|
||||
def __init__(self, base_url, project_id=""):
|
||||
"""
|
||||
:param base_url: url of the root wsgi application
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.project_id = project_id
|
||||
|
||||
def _build_extra(self, flavor_obj):
|
||||
flavor_obj["links"] = self._build_links(flavor_obj)
|
||||
@@ -88,11 +92,10 @@ class ViewBuilderV11(ViewBuilder):
|
||||
|
||||
def generate_href(self, flavor_id):
|
||||
"""Create an url that refers to a specific flavor id."""
|
||||
return "%s/flavors/%s" % (self.base_url, flavor_id)
|
||||
return os.path.join(self.base_url, self.project_id,
|
||||
"flavors", str(flavor_id))
|
||||
|
||||
def generate_bookmark(self, flavor_id):
|
||||
"""Create an url that refers to a specific flavor id."""
|
||||
return "%s/flavors/%s" % (
|
||||
common.remove_version_from_href(self.base_url),
|
||||
flavor_id,
|
||||
)
|
||||
return os.path.join(common.remove_version_from_href(self.base_url),
|
||||
self.project_id, "flavors", str(flavor_id))
|
||||
|
||||
@@ -23,9 +23,10 @@ from nova.api.openstack import common
|
||||
class ViewBuilder(object):
|
||||
"""Base class for generating responses to OpenStack API image requests."""
|
||||
|
||||
def __init__(self, base_url):
|
||||
def __init__(self, base_url, project_id=""):
|
||||
"""Initialize new `ViewBuilder`."""
|
||||
self._url = base_url
|
||||
self.base_url = base_url
|
||||
self.project_id = project_id
|
||||
|
||||
def _format_dates(self, image):
|
||||
"""Update all date fields to ensure standardized formatting."""
|
||||
@@ -54,7 +55,7 @@ class ViewBuilder(object):
|
||||
|
||||
def generate_href(self, image_id):
|
||||
"""Return an href string pointing to this object."""
|
||||
return os.path.join(self._url, "images", str(image_id))
|
||||
return os.path.join(self.base_url, "images", str(image_id))
|
||||
|
||||
def build(self, image_obj, detail=False):
|
||||
"""Return a standardized image structure for display by the API."""
|
||||
@@ -117,6 +118,11 @@ class ViewBuilderV11(ViewBuilder):
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
def generate_href(self, image_id):
|
||||
"""Return an href string pointing to this object."""
|
||||
return os.path.join(self.base_url, self.project_id,
|
||||
"images", str(image_id))
|
||||
|
||||
def build(self, image_obj, detail=False):
|
||||
"""Return a standardized image structure for display by the API."""
|
||||
image = ViewBuilder.build(self, image_obj, detail)
|
||||
@@ -142,5 +148,5 @@ class ViewBuilderV11(ViewBuilder):
|
||||
|
||||
def generate_bookmark(self, image_id):
|
||||
"""Create an url that refers to a specific flavor id."""
|
||||
return os.path.join(common.remove_version_from_href(self._url),
|
||||
"images", str(image_id))
|
||||
return os.path.join(common.remove_version_from_href(self.base_url),
|
||||
self.project_id, "images", str(image_id))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack LLC.
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@@ -128,11 +129,12 @@ class ViewBuilderV10(ViewBuilder):
|
||||
class ViewBuilderV11(ViewBuilder):
|
||||
"""Model an Openstack API V1.0 server response."""
|
||||
def __init__(self, addresses_builder, flavor_builder, image_builder,
|
||||
base_url):
|
||||
base_url, project_id=""):
|
||||
ViewBuilder.__init__(self, addresses_builder)
|
||||
self.flavor_builder = flavor_builder
|
||||
self.image_builder = image_builder
|
||||
self.base_url = base_url
|
||||
self.project_id = project_id
|
||||
|
||||
def _build_detail(self, inst):
|
||||
response = super(ViewBuilderV11, self)._build_detail(inst)
|
||||
@@ -143,6 +145,10 @@ class ViewBuilderV11(ViewBuilder):
|
||||
response['server']['progress'] = 100
|
||||
elif response['server']['status'] == "BUILD":
|
||||
response['server']['progress'] = 0
|
||||
|
||||
response['server']['accessIPv4'] = inst.get('access_ip_v4') or ""
|
||||
response['server']['accessIPv6'] = inst.get('access_ip_v6') or ""
|
||||
|
||||
return response
|
||||
|
||||
def _build_image(self, response, inst):
|
||||
@@ -182,6 +188,7 @@ class ViewBuilderV11(ViewBuilder):
|
||||
def _build_extra(self, response, inst):
|
||||
self._build_links(response, inst)
|
||||
response['uuid'] = inst['uuid']
|
||||
self._build_config_drive(response, inst)
|
||||
|
||||
def _build_links(self, response, inst):
|
||||
href = self.generate_href(inst["id"])
|
||||
@@ -200,11 +207,15 @@ class ViewBuilderV11(ViewBuilder):
|
||||
|
||||
response["links"] = links
|
||||
|
||||
def _build_config_drive(self, response, inst):
|
||||
response['config_drive'] = inst.get('config_drive')
|
||||
|
||||
def generate_href(self, server_id):
|
||||
"""Create an url that refers to a specific server id."""
|
||||
return os.path.join(self.base_url, "servers", str(server_id))
|
||||
return os.path.join(self.base_url, self.project_id,
|
||||
"servers", str(server_id))
|
||||
|
||||
def generate_bookmark(self, server_id):
|
||||
"""Create an url that refers to a specific flavor id."""
|
||||
return os.path.join(common.remove_version_from_href(self.base_url),
|
||||
"servers", str(server_id))
|
||||
self.project_id, "servers", str(server_id))
|
||||
|
||||
@@ -486,6 +486,10 @@ class Resource(wsgi.Application):
|
||||
msg = _("Malformed request body")
|
||||
return faults.Fault(webob.exc.HTTPBadRequest(explanation=msg))
|
||||
|
||||
project_id = args.pop("project_id", None)
|
||||
if 'nova.context' in request.environ and project_id:
|
||||
request.environ['nova.context'].project_id = project_id
|
||||
|
||||
try:
|
||||
action_result = self.dispatch(request, action, args)
|
||||
except webob.exc.HTTPException as ex:
|
||||
@@ -516,6 +520,6 @@ class Resource(wsgi.Application):
|
||||
controller_method = getattr(self.controller, action)
|
||||
try:
|
||||
return controller_method(req=request, **action_args)
|
||||
except TypeError, exc:
|
||||
LOG.debug(str(exc))
|
||||
return webob.exc.HTTPBadRequest()
|
||||
except TypeError as exc:
|
||||
LOG.exception(exc)
|
||||
return faults.Fault(webob.exc.HTTPBadRequest())
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
WARNING: This code is deprecated and will be removed.
|
||||
Keystone is the recommended solution for auth management.
|
||||
|
||||
Nova authentication management
|
||||
"""
|
||||
|
||||
@@ -38,10 +41,13 @@ from nova.auth import signer
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_bool('use_deprecated_auth',
|
||||
False,
|
||||
'This flag must be set to use old style auth')
|
||||
|
||||
flags.DEFINE_list('allowed_roles',
|
||||
['cloudadmin', 'itsec', 'sysadmin', 'netadmin', 'developer'],
|
||||
'Allowed roles for project')
|
||||
|
||||
# NOTE(vish): a user with one of these roles will be a superuser and
|
||||
# have access to all api commands
|
||||
flags.DEFINE_list('superuser_roles', ['cloudadmin'],
|
||||
@@ -811,7 +817,13 @@ class AuthManager(object):
|
||||
s3_host = host
|
||||
ec2_host = host
|
||||
rc = open(FLAGS.credentials_template).read()
|
||||
rc = rc % {'access': user.access,
|
||||
# NOTE(vish): Deprecated auth uses an access key, no auth uses a
|
||||
# the user_id in place of it.
|
||||
if FLAGS.use_deprecated_auth:
|
||||
access = user.access
|
||||
else:
|
||||
access = user.id
|
||||
rc = rc % {'access': access,
|
||||
'project': pid,
|
||||
'secret': user.secret,
|
||||
'ec2': '%s://%s:%s%s' % (FLAGS.ec2_scheme,
|
||||
|
||||
@@ -34,7 +34,6 @@ from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import utils
|
||||
from nova.auth import manager
|
||||
# TODO(eday): Eventually changes these to something not ec2-specific
|
||||
from nova.api.ec2 import cloud
|
||||
|
||||
@@ -57,7 +56,6 @@ LOG = logging.getLogger('nova.cloudpipe')
|
||||
class CloudPipe(object):
|
||||
def __init__(self):
|
||||
self.controller = cloud.CloudController()
|
||||
self.manager = manager.AuthManager()
|
||||
|
||||
def get_encoded_zip(self, project_id):
|
||||
# Make a payload.zip
|
||||
@@ -93,11 +91,10 @@ class CloudPipe(object):
|
||||
zippy.close()
|
||||
return encoded
|
||||
|
||||
def launch_vpn_instance(self, project_id):
|
||||
def launch_vpn_instance(self, project_id, user_id):
|
||||
LOG.debug(_("Launching VPN for %s") % (project_id))
|
||||
project = self.manager.get_project(project_id)
|
||||
ctxt = context.RequestContext(user=project.project_manager_id,
|
||||
project=project.id)
|
||||
ctxt = context.RequestContext(user_id=user_id,
|
||||
project_id=project_id)
|
||||
key_name = self.setup_key_pair(ctxt)
|
||||
group_name = self.setup_security_group(ctxt)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@@ -54,15 +55,15 @@ def generate_default_hostname(instance):
|
||||
"""Default function to generate a hostname given an instance reference."""
|
||||
display_name = instance['display_name']
|
||||
if display_name is None:
|
||||
return 'server_%d' % (instance['id'],)
|
||||
return 'server-%d' % (instance['id'],)
|
||||
table = ''
|
||||
deletions = ''
|
||||
for i in xrange(256):
|
||||
c = chr(i)
|
||||
if ('a' <= c <= 'z') or ('0' <= c <= '9') or (c == '-'):
|
||||
table += c
|
||||
elif c == ' ':
|
||||
table += '_'
|
||||
elif c in " _":
|
||||
table += '-'
|
||||
elif ('A' <= c <= 'Z'):
|
||||
table += c.lower()
|
||||
else:
|
||||
@@ -146,6 +147,16 @@ class API(base.Base):
|
||||
LOG.warn(msg)
|
||||
raise quota.QuotaError(msg, "MetadataLimitExceeded")
|
||||
|
||||
def _check_requested_networks(self, context, requested_networks):
|
||||
""" Check if the networks requested belongs to the project
|
||||
and the fixed IP address for each network provided is within
|
||||
same the network block
|
||||
"""
|
||||
if requested_networks is None:
|
||||
return
|
||||
|
||||
self.network_api.validate_networks(context, requested_networks)
|
||||
|
||||
def _check_create_parameters(self, context, instance_type,
|
||||
image_href, kernel_id=None, ramdisk_id=None,
|
||||
min_count=None, max_count=None,
|
||||
@@ -153,7 +164,8 @@ class API(base.Base):
|
||||
key_name=None, key_data=None, security_group='default',
|
||||
availability_zone=None, user_data=None, metadata=None,
|
||||
injected_files=None, admin_password=None, zone_blob=None,
|
||||
reservation_id=None):
|
||||
reservation_id=None, access_ip_v4=None, access_ip_v6=None,
|
||||
requested_networks=None, config_drive=None,):
|
||||
"""Verify all the input parameters regardless of the provisioning
|
||||
strategy being performed."""
|
||||
|
||||
@@ -182,10 +194,16 @@ class API(base.Base):
|
||||
|
||||
self._check_metadata_properties_quota(context, metadata)
|
||||
self._check_injected_file_quota(context, injected_files)
|
||||
self._check_requested_networks(context, requested_networks)
|
||||
|
||||
(image_service, image_id) = nova.image.get_image_service(image_href)
|
||||
image = image_service.show(context, image_id)
|
||||
|
||||
config_drive_id = None
|
||||
if config_drive and config_drive is not True:
|
||||
# config_drive is volume id
|
||||
config_drive, config_drive_id = None, config_drive
|
||||
|
||||
os_type = None
|
||||
if 'properties' in image and 'os_type' in image['properties']:
|
||||
os_type = image['properties']['os_type']
|
||||
@@ -213,6 +231,8 @@ class API(base.Base):
|
||||
image_service.show(context, kernel_id)
|
||||
if ramdisk_id:
|
||||
image_service.show(context, ramdisk_id)
|
||||
if config_drive_id:
|
||||
image_service.show(context, config_drive_id)
|
||||
|
||||
self.ensure_default_security_group(context)
|
||||
|
||||
@@ -231,6 +251,8 @@ class API(base.Base):
|
||||
'image_ref': image_href,
|
||||
'kernel_id': kernel_id or '',
|
||||
'ramdisk_id': ramdisk_id or '',
|
||||
'config_drive_id': config_drive_id or '',
|
||||
'config_drive': config_drive or '',
|
||||
'state': 0,
|
||||
'state_description': 'scheduling',
|
||||
'user_id': context.user_id,
|
||||
@@ -247,6 +269,8 @@ class API(base.Base):
|
||||
'key_data': key_data,
|
||||
'locked': False,
|
||||
'metadata': metadata,
|
||||
'access_ip_v4': access_ip_v4,
|
||||
'access_ip_v6': access_ip_v6,
|
||||
'availability_zone': availability_zone,
|
||||
'os_type': os_type,
|
||||
'architecture': architecture,
|
||||
@@ -398,9 +422,9 @@ class API(base.Base):
|
||||
def _ask_scheduler_to_create_instance(self, context, base_options,
|
||||
instance_type, zone_blob,
|
||||
availability_zone, injected_files,
|
||||
admin_password,
|
||||
image,
|
||||
instance_id=None, num_instances=1):
|
||||
admin_password, image,
|
||||
instance_id=None, num_instances=1,
|
||||
requested_networks=None):
|
||||
"""Send the run_instance request to the schedulers for processing."""
|
||||
pid = context.project_id
|
||||
uid = context.user_id
|
||||
@@ -411,12 +435,11 @@ class API(base.Base):
|
||||
LOG.debug(_("Casting to scheduler for %(pid)s/%(uid)s's"
|
||||
" (all-at-once)") % locals())
|
||||
|
||||
filter_class = 'nova.scheduler.host_filter.InstanceTypeFilter'
|
||||
request_spec = {
|
||||
'image': image,
|
||||
'instance_properties': base_options,
|
||||
'instance_type': instance_type,
|
||||
'filter': filter_class,
|
||||
'filter': None,
|
||||
'blob': zone_blob,
|
||||
'num_instances': num_instances,
|
||||
}
|
||||
@@ -429,7 +452,8 @@ class API(base.Base):
|
||||
"request_spec": request_spec,
|
||||
"availability_zone": availability_zone,
|
||||
"admin_password": admin_password,
|
||||
"injected_files": injected_files}})
|
||||
"injected_files": injected_files,
|
||||
"requested_networks": requested_networks}})
|
||||
|
||||
def create_all_at_once(self, context, instance_type,
|
||||
image_href, kernel_id=None, ramdisk_id=None,
|
||||
@@ -438,7 +462,9 @@ class API(base.Base):
|
||||
key_name=None, key_data=None, security_group='default',
|
||||
availability_zone=None, user_data=None, metadata=None,
|
||||
injected_files=None, admin_password=None, zone_blob=None,
|
||||
reservation_id=None, block_device_mapping=None):
|
||||
reservation_id=None, block_device_mapping=None,
|
||||
access_ip_v4=None, access_ip_v6=None,
|
||||
requested_networks=None, config_drive=None):
|
||||
"""Provision the instances by passing the whole request to
|
||||
the Scheduler for execution. Returns a Reservation ID
|
||||
related to the creation of all of these instances."""
|
||||
@@ -454,14 +480,15 @@ class API(base.Base):
|
||||
key_name, key_data, security_group,
|
||||
availability_zone, user_data, metadata,
|
||||
injected_files, admin_password, zone_blob,
|
||||
reservation_id)
|
||||
reservation_id, access_ip_v4, access_ip_v6,
|
||||
requested_networks, config_drive)
|
||||
|
||||
self._ask_scheduler_to_create_instance(context, base_options,
|
||||
instance_type, zone_blob,
|
||||
availability_zone, injected_files,
|
||||
admin_password,
|
||||
image,
|
||||
num_instances=num_instances)
|
||||
admin_password, image,
|
||||
num_instances=num_instances,
|
||||
requested_networks=requested_networks)
|
||||
|
||||
return base_options['reservation_id']
|
||||
|
||||
@@ -472,7 +499,9 @@ class API(base.Base):
|
||||
key_name=None, key_data=None, security_group='default',
|
||||
availability_zone=None, user_data=None, metadata=None,
|
||||
injected_files=None, admin_password=None, zone_blob=None,
|
||||
reservation_id=None, block_device_mapping=None):
|
||||
reservation_id=None, block_device_mapping=None,
|
||||
access_ip_v4=None, access_ip_v6=None,
|
||||
requested_networks=None, config_drive=None,):
|
||||
"""
|
||||
Provision the instances by sending off a series of single
|
||||
instance requests to the Schedulers. This is fine for trival
|
||||
@@ -496,7 +525,8 @@ class API(base.Base):
|
||||
key_name, key_data, security_group,
|
||||
availability_zone, user_data, metadata,
|
||||
injected_files, admin_password, zone_blob,
|
||||
reservation_id)
|
||||
reservation_id, access_ip_v4, access_ip_v6,
|
||||
requested_networks, config_drive)
|
||||
|
||||
block_device_mapping = block_device_mapping or []
|
||||
instances = []
|
||||
@@ -510,11 +540,11 @@ class API(base.Base):
|
||||
instance_id = instance['id']
|
||||
|
||||
self._ask_scheduler_to_create_instance(context, base_options,
|
||||
instance_type, zone_blob,
|
||||
availability_zone, injected_files,
|
||||
admin_password,
|
||||
image,
|
||||
instance_id=instance_id)
|
||||
instance_type, zone_blob,
|
||||
availability_zone, injected_files,
|
||||
admin_password, image,
|
||||
instance_id=instance_id,
|
||||
requested_networks=requested_networks)
|
||||
|
||||
return [dict(x.iteritems()) for x in instances]
|
||||
|
||||
@@ -614,6 +644,78 @@ class API(base.Base):
|
||||
self.db.queue_get_for(context, FLAGS.compute_topic, host),
|
||||
{'method': 'refresh_provider_fw_rules', 'args': {}})
|
||||
|
||||
def _is_security_group_associated_with_server(self, security_group,
|
||||
instance_id):
|
||||
"""Check if the security group is already associated
|
||||
with the instance. If Yes, return True.
|
||||
"""
|
||||
|
||||
if not security_group:
|
||||
return False
|
||||
|
||||
instances = security_group.get('instances')
|
||||
if not instances:
|
||||
return False
|
||||
|
||||
inst_id = None
|
||||
for inst_id in (instance['id'] for instance in instances \
|
||||
if instance_id == instance['id']):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_security_group(self, context, instance_id, security_group_name):
|
||||
"""Add security group to the instance"""
|
||||
security_group = db.security_group_get_by_name(context,
|
||||
context.project_id,
|
||||
security_group_name)
|
||||
# check if the server exists
|
||||
inst = db.instance_get(context, instance_id)
|
||||
#check if the security group is associated with the server
|
||||
if self._is_security_group_associated_with_server(security_group,
|
||||
instance_id):
|
||||
raise exception.SecurityGroupExistsForInstance(
|
||||
security_group_id=security_group['id'],
|
||||
instance_id=instance_id)
|
||||
|
||||
#check if the instance is in running state
|
||||
if inst['state'] != power_state.RUNNING:
|
||||
raise exception.InstanceNotRunning(instance_id=instance_id)
|
||||
|
||||
db.instance_add_security_group(context.elevated(),
|
||||
instance_id,
|
||||
security_group['id'])
|
||||
rpc.cast(context,
|
||||
db.queue_get_for(context, FLAGS.compute_topic, inst['host']),
|
||||
{"method": "refresh_security_group_rules",
|
||||
"args": {"security_group_id": security_group['id']}})
|
||||
|
||||
def remove_security_group(self, context, instance_id, security_group_name):
|
||||
"""Remove the security group associated with the instance"""
|
||||
security_group = db.security_group_get_by_name(context,
|
||||
context.project_id,
|
||||
security_group_name)
|
||||
# check if the server exists
|
||||
inst = db.instance_get(context, instance_id)
|
||||
#check if the security group is associated with the server
|
||||
if not self._is_security_group_associated_with_server(security_group,
|
||||
instance_id):
|
||||
raise exception.SecurityGroupNotExistsForInstance(
|
||||
security_group_id=security_group['id'],
|
||||
instance_id=instance_id)
|
||||
|
||||
#check if the instance is in running state
|
||||
if inst['state'] != power_state.RUNNING:
|
||||
raise exception.InstanceNotRunning(instance_id=instance_id)
|
||||
|
||||
db.instance_remove_security_group(context.elevated(),
|
||||
instance_id,
|
||||
security_group['id'])
|
||||
rpc.cast(context,
|
||||
db.queue_get_for(context, FLAGS.compute_topic, inst['host']),
|
||||
{"method": "refresh_security_group_rules",
|
||||
"args": {"security_group_id": security_group['id']}})
|
||||
|
||||
@scheduler_api.reroute_compute("update")
|
||||
def update(self, context, instance_id, **kwargs):
|
||||
"""Updates the instance in the datastore.
|
||||
@@ -921,8 +1023,8 @@ class API(base.Base):
|
||||
self._cast_compute_message('reboot_instance', context, instance_id)
|
||||
|
||||
@scheduler_api.reroute_compute("rebuild")
|
||||
def rebuild(self, context, instance_id, image_href, name=None,
|
||||
metadata=None, files_to_inject=None):
|
||||
def rebuild(self, context, instance_id, image_href, admin_password,
|
||||
name=None, metadata=None, files_to_inject=None):
|
||||
"""Rebuild the given instance with the provided metadata."""
|
||||
instance = db.api.instance_get(context, instance_id)
|
||||
|
||||
@@ -942,6 +1044,7 @@ class API(base.Base):
|
||||
self.db.instance_update(context, instance_id, values)
|
||||
|
||||
rebuild_params = {
|
||||
"new_pass": admin_password,
|
||||
"image_ref": image_href,
|
||||
"injected_files": files_to_inject,
|
||||
}
|
||||
@@ -1068,15 +1171,21 @@ class API(base.Base):
|
||||
"""Unpause the given instance."""
|
||||
self._cast_compute_message('unpause_instance', context, instance_id)
|
||||
|
||||
def _call_compute_message_for_host(self, action, context, host, params):
|
||||
"""Call method deliberately designed to make host/service only calls"""
|
||||
queue = self.db.queue_get_for(context, FLAGS.compute_topic, host)
|
||||
kwargs = {'method': action, 'args': params}
|
||||
return rpc.call(context, queue, kwargs)
|
||||
|
||||
def set_host_enabled(self, context, host, enabled):
|
||||
"""Sets the specified host's ability to accept new instances."""
|
||||
return self._call_compute_message("set_host_enabled", context,
|
||||
return self._call_compute_message_for_host("set_host_enabled", context,
|
||||
host=host, params={"enabled": enabled})
|
||||
|
||||
def host_power_action(self, context, host, action):
|
||||
"""Reboots, shuts down or powers up the host."""
|
||||
return self._call_compute_message("host_power_action", context,
|
||||
host=host, params={"action": action})
|
||||
return self._call_compute_message_for_host("host_power_action",
|
||||
context, host=host, params={"action": action})
|
||||
|
||||
@scheduler_api.reroute_compute("diagnostics")
|
||||
def get_diagnostics(self, context, instance_id):
|
||||
|
||||
@@ -382,6 +382,8 @@ class ComputeManager(manager.SchedulerDependentManager):
|
||||
context = context.elevated()
|
||||
instance = self.db.instance_get(context, instance_id)
|
||||
|
||||
requested_networks = kwargs.get('requested_networks', None)
|
||||
|
||||
if instance['name'] in self.driver.list_instances():
|
||||
raise exception.Error(_("Instance has already been created"))
|
||||
|
||||
@@ -392,12 +394,12 @@ class ComputeManager(manager.SchedulerDependentManager):
|
||||
updates = {}
|
||||
updates['host'] = self.host
|
||||
updates['launched_on'] = self.host
|
||||
# NOTE(vish): used by virt but not in database
|
||||
updates['injected_files'] = kwargs.get('injected_files', [])
|
||||
updates['admin_pass'] = kwargs.get('admin_password', None)
|
||||
instance = self.db.instance_update(context,
|
||||
instance_id,
|
||||
updates)
|
||||
instance['injected_files'] = kwargs.get('injected_files', [])
|
||||
instance['admin_pass'] = kwargs.get('admin_password', None)
|
||||
|
||||
self.db.instance_set_state(context,
|
||||
instance_id,
|
||||
power_state.NOSTATE,
|
||||
@@ -411,7 +413,8 @@ class ComputeManager(manager.SchedulerDependentManager):
|
||||
# will eventually also need to save the address here.
|
||||
if not FLAGS.stub_network:
|
||||
network_info = self.network_api.allocate_for_instance(context,
|
||||
instance, vpn=is_vpn)
|
||||
instance, vpn=is_vpn,
|
||||
requested_networks=requested_networks)
|
||||
LOG.debug(_("instance network_info: |%s|"), network_info)
|
||||
else:
|
||||
# TODO(tr3buchet) not really sure how this should be handled.
|
||||
@@ -524,6 +527,7 @@ class ComputeManager(manager.SchedulerDependentManager):
|
||||
:param context: `nova.RequestContext` object
|
||||
:param instance_id: Instance identifier (integer)
|
||||
:param image_ref: Image identifier (href or integer)
|
||||
:param new_pass: password to set on rebuilt instance
|
||||
"""
|
||||
context = context.elevated()
|
||||
|
||||
@@ -541,6 +545,11 @@ class ComputeManager(manager.SchedulerDependentManager):
|
||||
network_info = self.network_api.get_instance_nw_info(context,
|
||||
instance_ref)
|
||||
bd_mapping = self._setup_block_device_mapping(context, instance_id)
|
||||
|
||||
# pull in new password here since the original password isn't in the db
|
||||
instance_ref.admin_pass = kwargs.get('new_pass',
|
||||
utils.generate_password(FLAGS.password_length))
|
||||
|
||||
self.driver.spawn(context, instance_ref, network_info, bd_mapping)
|
||||
|
||||
self._update_image_ref(context, instance_id, image_ref)
|
||||
|
||||
@@ -324,13 +324,13 @@ def migration_get_by_instance_and_status(context, instance_uuid, status):
|
||||
####################
|
||||
|
||||
|
||||
def fixed_ip_associate(context, address, instance_id):
|
||||
def fixed_ip_associate(context, address, instance_id, network_id=None):
|
||||
"""Associate fixed ip to instance.
|
||||
|
||||
Raises if fixed ip is not available.
|
||||
|
||||
"""
|
||||
return IMPL.fixed_ip_associate(context, address, instance_id)
|
||||
return IMPL.fixed_ip_associate(context, address, instance_id, network_id)
|
||||
|
||||
|
||||
def fixed_ip_associate_pool(context, network_id, instance_id=None, host=None):
|
||||
@@ -397,7 +397,6 @@ def fixed_ip_update(context, address, values):
|
||||
"""Create a fixed ip from the values dictionary."""
|
||||
return IMPL.fixed_ip_update(context, address, values)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
@@ -571,6 +570,12 @@ def instance_add_security_group(context, instance_id, security_group_id):
|
||||
security_group_id)
|
||||
|
||||
|
||||
def instance_remove_security_group(context, instance_id, security_group_id):
|
||||
"""Disassociate the given security group from the given instance."""
|
||||
return IMPL.instance_remove_security_group(context, instance_id,
|
||||
security_group_id)
|
||||
|
||||
|
||||
def instance_action_create(context, values):
|
||||
"""Create an instance action from the values dictionary."""
|
||||
return IMPL.instance_action_create(context, values)
|
||||
@@ -681,7 +686,14 @@ def network_get_all(context):
|
||||
return IMPL.network_get_all(context)
|
||||
|
||||
|
||||
def network_get_all_by_uuids(context, network_uuids, project_id=None):
|
||||
"""Return networks by ids."""
|
||||
return IMPL.network_get_all_by_uuids(context, network_uuids, project_id)
|
||||
|
||||
|
||||
# pylint: disable=C0103
|
||||
|
||||
|
||||
def network_get_associated_fixed_ips(context, network_id):
|
||||
"""Get all network's ips that have been associated."""
|
||||
return IMPL.network_get_associated_fixed_ips(context, network_id)
|
||||
@@ -1437,6 +1449,82 @@ def instance_type_extra_specs_update_or_create(context, instance_type_id,
|
||||
extra_specs)
|
||||
|
||||
|
||||
##################
|
||||
|
||||
|
||||
def volume_metadata_get(context, volume_id):
|
||||
"""Get all metadata for a volume."""
|
||||
return IMPL.volume_metadata_get(context, volume_id)
|
||||
|
||||
|
||||
def volume_metadata_delete(context, volume_id, key):
|
||||
"""Delete the given metadata item."""
|
||||
IMPL.volume_metadata_delete(context, volume_id, key)
|
||||
|
||||
|
||||
def volume_metadata_update(context, volume_id, metadata, delete):
|
||||
"""Update metadata if it exists, otherwise create it."""
|
||||
IMPL.volume_metadata_update(context, volume_id, metadata, delete)
|
||||
|
||||
|
||||
##################
|
||||
|
||||
|
||||
def volume_type_create(context, values):
|
||||
"""Create a new volume type."""
|
||||
return IMPL.volume_type_create(context, values)
|
||||
|
||||
|
||||
def volume_type_get_all(context, inactive=False):
|
||||
"""Get all volume types."""
|
||||
return IMPL.volume_type_get_all(context, inactive)
|
||||
|
||||
|
||||
def volume_type_get(context, id):
|
||||
"""Get volume type by id."""
|
||||
return IMPL.volume_type_get(context, id)
|
||||
|
||||
|
||||
def volume_type_get_by_name(context, name):
|
||||
"""Get volume type by name."""
|
||||
return IMPL.volume_type_get_by_name(context, name)
|
||||
|
||||
|
||||
def volume_type_destroy(context, name):
|
||||
"""Delete a volume type."""
|
||||
return IMPL.volume_type_destroy(context, name)
|
||||
|
||||
|
||||
def volume_type_purge(context, name):
|
||||
"""Purges (removes) a volume type from DB.
|
||||
|
||||
Use volume_type_destroy for most cases
|
||||
|
||||
"""
|
||||
return IMPL.volume_type_purge(context, name)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
def volume_type_extra_specs_get(context, volume_type_id):
|
||||
"""Get all extra specs for a volume type."""
|
||||
return IMPL.volume_type_extra_specs_get(context, volume_type_id)
|
||||
|
||||
|
||||
def volume_type_extra_specs_delete(context, volume_type_id, key):
|
||||
"""Delete the given extra specs item."""
|
||||
IMPL.volume_type_extra_specs_delete(context, volume_type_id, key)
|
||||
|
||||
|
||||
def volume_type_extra_specs_update_or_create(context, volume_type_id,
|
||||
extra_specs):
|
||||
"""Create or update volume type extra specs. This adds or modifies the
|
||||
key/value pairs specified in the extra specs dict argument"""
|
||||
IMPL.volume_type_extra_specs_update_or_create(context, volume_type_id,
|
||||
extra_specs)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
||||
@@ -132,6 +132,20 @@ def require_instance_exists(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_volume_exists(f):
|
||||
"""Decorator to require the specified volume to exist.
|
||||
|
||||
Requres the wrapped function to use context and volume_id as
|
||||
their first two arguments.
|
||||
"""
|
||||
|
||||
def wrapper(context, volume_id, *args, **kwargs):
|
||||
db.api.volume_get(context, volume_id)
|
||||
return f(context, volume_id, *args, **kwargs)
|
||||
wrapper.__name__ = f.__name__
|
||||
return wrapper
|
||||
|
||||
|
||||
###################
|
||||
|
||||
|
||||
@@ -652,23 +666,36 @@ def floating_ip_update(context, address, values):
|
||||
###################
|
||||
|
||||
|
||||
@require_context
|
||||
def fixed_ip_associate(context, address, instance_id):
|
||||
@require_admin_context
|
||||
def fixed_ip_associate(context, address, instance_id, network_id=None):
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
instance = instance_get(context, instance_id, session=session)
|
||||
network_or_none = or_(models.FixedIp.network_id == network_id,
|
||||
models.FixedIp.network_id == None)
|
||||
fixed_ip_ref = session.query(models.FixedIp).\
|
||||
filter_by(address=address).\
|
||||
filter(network_or_none).\
|
||||
filter_by(reserved=False).\
|
||||
filter_by(deleted=False).\
|
||||
filter_by(instance=None).\
|
||||
filter_by(address=address).\
|
||||
with_lockmode('update').\
|
||||
first()
|
||||
# NOTE(vish): if with_lockmode isn't supported, as in sqlite,
|
||||
# then this has concurrency issues
|
||||
if not fixed_ip_ref:
|
||||
raise exception.NoMoreFixedIps()
|
||||
fixed_ip_ref.instance = instance
|
||||
if fixed_ip_ref is None:
|
||||
raise exception.FixedIpNotFoundForNetwork(address=address,
|
||||
network_id=network_id)
|
||||
if fixed_ip_ref.instance is not None:
|
||||
raise exception.FixedIpAlreadyInUse(address=address)
|
||||
|
||||
if not fixed_ip_ref.network:
|
||||
fixed_ip_ref.network = network_get(context,
|
||||
network_id,
|
||||
session=session)
|
||||
fixed_ip_ref.instance = instance_get(context,
|
||||
instance_id,
|
||||
session=session)
|
||||
session.add(fixed_ip_ref)
|
||||
return fixed_ip_ref['address']
|
||||
|
||||
|
||||
@require_admin_context
|
||||
@@ -1006,11 +1033,11 @@ def virtual_interface_delete_by_instance(context, instance_id):
|
||||
###################
|
||||
|
||||
|
||||
def _metadata_refs(metadata_dict):
|
||||
def _metadata_refs(metadata_dict, meta_class):
|
||||
metadata_refs = []
|
||||
if metadata_dict:
|
||||
for k, v in metadata_dict.iteritems():
|
||||
metadata_ref = models.InstanceMetadata()
|
||||
metadata_ref = meta_class()
|
||||
metadata_ref['key'] = k
|
||||
metadata_ref['value'] = v
|
||||
metadata_refs.append(metadata_ref)
|
||||
@@ -1024,8 +1051,8 @@ def instance_create(context, values):
|
||||
context - request context object
|
||||
values - dict containing column values.
|
||||
"""
|
||||
values['metadata'] = _metadata_refs(values.get('metadata'))
|
||||
|
||||
values['metadata'] = _metadata_refs(values.get('metadata'),
|
||||
models.InstanceMetadata)
|
||||
instance_ref = models.Instance()
|
||||
instance_ref['uuid'] = str(utils.gen_uuid())
|
||||
|
||||
@@ -1222,7 +1249,8 @@ def instance_get_all_by_filters(context, filters):
|
||||
options(joinedload('security_groups')).\
|
||||
options(joinedload_all('fixed_ips.network')).\
|
||||
options(joinedload('metadata')).\
|
||||
options(joinedload('instance_type'))
|
||||
options(joinedload('instance_type')).\
|
||||
filter_by(deleted=can_read_deleted(context))
|
||||
|
||||
# Make a copy of the filters dictionary to use going forward, as we'll
|
||||
# be modifying it and we shouldn't affect the caller's use of it.
|
||||
@@ -1500,6 +1528,19 @@ def instance_add_security_group(context, instance_id, security_group_id):
|
||||
instance_ref.save(session=session)
|
||||
|
||||
|
||||
@require_context
|
||||
def instance_remove_security_group(context, instance_id, security_group_id):
|
||||
"""Disassociate the given security group from the given instance"""
|
||||
session = get_session()
|
||||
|
||||
session.query(models.SecurityGroupInstanceAssociation).\
|
||||
filter_by(instance_id=instance_id).\
|
||||
filter_by(security_group_id=security_group_id).\
|
||||
update({'deleted': True,
|
||||
'deleted_at': utils.utcnow(),
|
||||
'updated_at': literal_column('updated_at')})
|
||||
|
||||
|
||||
@require_context
|
||||
def instance_action_create(context, values):
|
||||
"""Create an instance action from the values dictionary."""
|
||||
@@ -1741,6 +1782,40 @@ def network_get_all(context):
|
||||
return result
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def network_get_all_by_uuids(context, network_uuids, project_id=None):
|
||||
session = get_session()
|
||||
project_or_none = or_(models.Network.project_id == project_id,
|
||||
models.Network.project_id == None)
|
||||
result = session.query(models.Network).\
|
||||
filter(models.Network.uuid.in_(network_uuids)).\
|
||||
filter(project_or_none).\
|
||||
filter_by(deleted=False).all()
|
||||
if not result:
|
||||
raise exception.NoNetworksFound()
|
||||
|
||||
#check if host is set to all of the networks
|
||||
# returned in the result
|
||||
for network in result:
|
||||
if network['host'] is None:
|
||||
raise exception.NetworkHostNotSet(network_id=network['id'])
|
||||
|
||||
#check if the result contains all the networks
|
||||
#we are looking for
|
||||
for network_uuid in network_uuids:
|
||||
found = False
|
||||
for network in result:
|
||||
if network['uuid'] == network_uuid:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
if project_id:
|
||||
raise exception.NetworkNotFoundForProject(network_uuid=uuid,
|
||||
project_id=context.project_id)
|
||||
raise exception.NetworkNotFound(network_id=network_uuid)
|
||||
|
||||
return result
|
||||
|
||||
# NOTE(vish): pylint complains because of the long method name, but
|
||||
# it fits with the names of the rest of the methods
|
||||
# pylint: disable=C0103
|
||||
@@ -2083,6 +2158,8 @@ def volume_attached(context, volume_id, instance_id, mountpoint):
|
||||
|
||||
@require_context
|
||||
def volume_create(context, values):
|
||||
values['volume_metadata'] = _metadata_refs(values.get('metadata'),
|
||||
models.VolumeMetadata)
|
||||
volume_ref = models.Volume()
|
||||
volume_ref.update(values)
|
||||
|
||||
@@ -2119,6 +2196,11 @@ def volume_destroy(context, volume_id):
|
||||
session.query(models.IscsiTarget).\
|
||||
filter_by(volume_id=volume_id).\
|
||||
update({'volume_id': None})
|
||||
session.query(models.VolumeMetadata).\
|
||||
filter_by(volume_id=volume_id).\
|
||||
update({'deleted': True,
|
||||
'deleted_at': utils.utcnow(),
|
||||
'updated_at': literal_column('updated_at')})
|
||||
|
||||
|
||||
@require_admin_context
|
||||
@@ -2142,6 +2224,8 @@ def volume_get(context, volume_id, session=None):
|
||||
if is_admin_context(context):
|
||||
result = session.query(models.Volume).\
|
||||
options(joinedload('instance')).\
|
||||
options(joinedload('volume_metadata')).\
|
||||
options(joinedload('volume_type')).\
|
||||
options(joinedload('drive_type')).\
|
||||
filter_by(id=volume_id).\
|
||||
filter_by(deleted=can_read_deleted(context)).\
|
||||
@@ -2149,6 +2233,8 @@ def volume_get(context, volume_id, session=None):
|
||||
elif is_user_context(context):
|
||||
result = session.query(models.Volume).\
|
||||
options(joinedload('instance')).\
|
||||
options(joinedload('volume_metadata')).\
|
||||
options(joinedload('volume_type')).\
|
||||
options(joinedload('drive_type')).\
|
||||
filter_by(project_id=context.project_id).\
|
||||
filter_by(id=volume_id).\
|
||||
@@ -2165,6 +2251,8 @@ def volume_get_all(context):
|
||||
session = get_session()
|
||||
return session.query(models.Volume).\
|
||||
options(joinedload('instance')).\
|
||||
options(joinedload('volume_metadata')).\
|
||||
options(joinedload('volume_type')).\
|
||||
options(joinedload('drive_type')).\
|
||||
filter_by(deleted=can_read_deleted(context)).\
|
||||
all()
|
||||
@@ -2175,6 +2263,8 @@ def volume_get_all_by_host(context, host):
|
||||
session = get_session()
|
||||
return session.query(models.Volume).\
|
||||
options(joinedload('instance')).\
|
||||
options(joinedload('volume_metadata')).\
|
||||
options(joinedload('volume_type')).\
|
||||
options(joinedload('drive_type')).\
|
||||
filter_by(host=host).\
|
||||
filter_by(deleted=can_read_deleted(context)).\
|
||||
@@ -2185,6 +2275,8 @@ def volume_get_all_by_host(context, host):
|
||||
def volume_get_all_by_instance(context, instance_id):
|
||||
session = get_session()
|
||||
result = session.query(models.Volume).\
|
||||
options(joinedload('volume_metadata')).\
|
||||
options(joinedload('volume_type')).\
|
||||
options(joinedload('drive_type')).\
|
||||
filter_by(instance_id=instance_id).\
|
||||
filter_by(deleted=False).\
|
||||
@@ -2223,6 +2315,8 @@ def volume_get_all_by_project(context, project_id):
|
||||
session = get_session()
|
||||
return session.query(models.Volume).\
|
||||
options(joinedload('instance')).\
|
||||
options(joinedload('volume_metadata')).\
|
||||
options(joinedload('volume_type')).\
|
||||
options(joinedload('drive_type')).\
|
||||
filter_by(project_id=project_id).\
|
||||
filter_by(deleted=can_read_deleted(context)).\
|
||||
@@ -2236,6 +2330,8 @@ def volume_get_instance(context, volume_id):
|
||||
filter_by(id=volume_id).\
|
||||
filter_by(deleted=can_read_deleted(context)).\
|
||||
options(joinedload('instance')).\
|
||||
options(joinedload('volume_metadata')).\
|
||||
options(joinedload('volume_type')).\
|
||||
options(joinedload('drive_type')).\
|
||||
first()
|
||||
if not result:
|
||||
@@ -2271,12 +2367,115 @@ def volume_get_iscsi_target_num(context, volume_id):
|
||||
@require_context
|
||||
def volume_update(context, volume_id, values):
|
||||
session = get_session()
|
||||
metadata = values.get('metadata')
|
||||
if metadata is not None:
|
||||
volume_metadata_update(context,
|
||||
volume_id,
|
||||
values.pop('metadata'),
|
||||
delete=True)
|
||||
with session.begin():
|
||||
volume_ref = volume_get(context, volume_id, session=session)
|
||||
volume_ref.update(values)
|
||||
volume_ref.save(session=session)
|
||||
return volume_ref
|
||||
|
||||
####################
|
||||
|
||||
|
||||
@require_context
|
||||
@require_volume_exists
|
||||
def volume_metadata_get(context, volume_id):
|
||||
session = get_session()
|
||||
|
||||
meta_results = session.query(models.VolumeMetadata).\
|
||||
filter_by(volume_id=volume_id).\
|
||||
filter_by(deleted=False).\
|
||||
all()
|
||||
|
||||
meta_dict = {}
|
||||
for i in meta_results:
|
||||
meta_dict[i['key']] = i['value']
|
||||
return meta_dict
|
||||
|
||||
|
||||
@require_context
|
||||
@require_volume_exists
|
||||
def volume_metadata_delete(context, volume_id, key):
|
||||
session = get_session()
|
||||
session.query(models.VolumeMetadata).\
|
||||
filter_by(volume_id=volume_id).\
|
||||
filter_by(key=key).\
|
||||
filter_by(deleted=False).\
|
||||
update({'deleted': True,
|
||||
'deleted_at': utils.utcnow(),
|
||||
'updated_at': literal_column('updated_at')})
|
||||
|
||||
|
||||
@require_context
|
||||
@require_volume_exists
|
||||
def volume_metadata_delete_all(context, volume_id):
|
||||
session = get_session()
|
||||
session.query(models.VolumeMetadata).\
|
||||
filter_by(volume_id=volume_id).\
|
||||
filter_by(deleted=False).\
|
||||
update({'deleted': True,
|
||||
'deleted_at': utils.utcnow(),
|
||||
'updated_at': literal_column('updated_at')})
|
||||
|
||||
|
||||
@require_context
|
||||
@require_volume_exists
|
||||
def volume_metadata_get_item(context, volume_id, key, session=None):
|
||||
if not session:
|
||||
session = get_session()
|
||||
|
||||
meta_result = session.query(models.VolumeMetadata).\
|
||||
filter_by(volume_id=volume_id).\
|
||||
filter_by(key=key).\
|
||||
filter_by(deleted=False).\
|
||||
first()
|
||||
|
||||
if not meta_result:
|
||||
raise exception.VolumeMetadataNotFound(metadata_key=key,
|
||||
volume_id=volume_id)
|
||||
return meta_result
|
||||
|
||||
|
||||
@require_context
|
||||
@require_volume_exists
|
||||
def volume_metadata_update(context, volume_id, metadata, delete):
|
||||
session = get_session()
|
||||
|
||||
# Set existing metadata to deleted if delete argument is True
|
||||
if delete:
|
||||
original_metadata = volume_metadata_get(context, volume_id)
|
||||
for meta_key, meta_value in original_metadata.iteritems():
|
||||
if meta_key not in metadata:
|
||||
meta_ref = volume_metadata_get_item(context, volume_id,
|
||||
meta_key, session)
|
||||
meta_ref.update({'deleted': True})
|
||||
meta_ref.save(session=session)
|
||||
|
||||
meta_ref = None
|
||||
|
||||
# Now update all existing items with new values, or create new meta objects
|
||||
for meta_key, meta_value in metadata.iteritems():
|
||||
|
||||
# update the value whether it exists or not
|
||||
item = {"value": meta_value}
|
||||
|
||||
try:
|
||||
meta_ref = volume_metadata_get_item(context, volume_id,
|
||||
meta_key, session)
|
||||
except exception.VolumeMetadataNotFound, e:
|
||||
meta_ref = models.VolumeMetadata()
|
||||
item.update({"key": meta_key, "volume_id": volume_id})
|
||||
|
||||
meta_ref.update(item)
|
||||
meta_ref.save(session=session)
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
###################
|
||||
|
||||
@@ -2466,6 +2665,7 @@ def security_group_get(context, security_group_id, session=None):
|
||||
filter_by(deleted=can_read_deleted(context),).\
|
||||
filter_by(id=security_group_id).\
|
||||
options(joinedload_all('rules')).\
|
||||
options(joinedload_all('instances')).\
|
||||
first()
|
||||
else:
|
||||
result = session.query(models.SecurityGroup).\
|
||||
@@ -2473,6 +2673,7 @@ def security_group_get(context, security_group_id, session=None):
|
||||
filter_by(id=security_group_id).\
|
||||
filter_by(project_id=context.project_id).\
|
||||
options(joinedload_all('rules')).\
|
||||
options(joinedload_all('instances')).\
|
||||
first()
|
||||
if not result:
|
||||
raise exception.SecurityGroupNotFound(
|
||||
@@ -3110,7 +3311,7 @@ def instance_type_create(_context, values):
|
||||
|
||||
|
||||
def _dict_with_extra_specs(inst_type_query):
|
||||
"""Takes an instance type query returned by sqlalchemy
|
||||
"""Takes an instance OR volume type query returned by sqlalchemy
|
||||
and returns it as a dictionary, converting the extra_specs
|
||||
entry from a list of dicts:
|
||||
|
||||
@@ -3494,6 +3695,179 @@ def instance_type_extra_specs_update_or_create(context, instance_type_id,
|
||||
return specs
|
||||
|
||||
|
||||
##################
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_create(_context, values):
|
||||
"""Create a new instance type. In order to pass in extra specs,
|
||||
the values dict should contain a 'extra_specs' key/value pair:
|
||||
|
||||
{'extra_specs' : {'k1': 'v1', 'k2': 'v2', ...}}
|
||||
|
||||
"""
|
||||
try:
|
||||
specs = values.get('extra_specs')
|
||||
|
||||
values['extra_specs'] = _metadata_refs(values.get('extra_specs'),
|
||||
models.VolumeTypeExtraSpecs)
|
||||
volume_type_ref = models.VolumeTypes()
|
||||
volume_type_ref.update(values)
|
||||
volume_type_ref.save()
|
||||
except Exception, e:
|
||||
raise exception.DBError(e)
|
||||
return volume_type_ref
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_get_all(context, inactive=False, filters={}):
|
||||
"""
|
||||
Returns a dict describing all volume_types with name as key.
|
||||
"""
|
||||
session = get_session()
|
||||
if inactive:
|
||||
vol_types = session.query(models.VolumeTypes).\
|
||||
options(joinedload('extra_specs')).\
|
||||
order_by("name").\
|
||||
all()
|
||||
else:
|
||||
vol_types = session.query(models.VolumeTypes).\
|
||||
options(joinedload('extra_specs')).\
|
||||
filter_by(deleted=False).\
|
||||
order_by("name").\
|
||||
all()
|
||||
vol_dict = {}
|
||||
if vol_types:
|
||||
for i in vol_types:
|
||||
vol_dict[i['name']] = _dict_with_extra_specs(i)
|
||||
return vol_dict
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_get(context, id):
|
||||
"""Returns a dict describing specific volume_type"""
|
||||
session = get_session()
|
||||
vol_type = session.query(models.VolumeTypes).\
|
||||
options(joinedload('extra_specs')).\
|
||||
filter_by(id=id).\
|
||||
first()
|
||||
|
||||
if not vol_type:
|
||||
raise exception.VolumeTypeNotFound(volume_type=id)
|
||||
else:
|
||||
return _dict_with_extra_specs(vol_type)
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_get_by_name(context, name):
|
||||
"""Returns a dict describing specific volume_type"""
|
||||
session = get_session()
|
||||
vol_type = session.query(models.VolumeTypes).\
|
||||
options(joinedload('extra_specs')).\
|
||||
filter_by(name=name).\
|
||||
first()
|
||||
if not vol_type:
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
else:
|
||||
return _dict_with_extra_specs(vol_type)
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_destroy(context, name):
|
||||
""" Marks specific volume_type as deleted"""
|
||||
session = get_session()
|
||||
volume_type_ref = session.query(models.VolumeTypes).\
|
||||
filter_by(name=name)
|
||||
records = volume_type_ref.update(dict(deleted=True))
|
||||
if records == 0:
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
else:
|
||||
return volume_type_ref
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_purge(context, name):
|
||||
""" Removes specific volume_type from DB
|
||||
Usually volume_type_destroy should be used
|
||||
"""
|
||||
session = get_session()
|
||||
volume_type_ref = session.query(models.VolumeTypes).\
|
||||
filter_by(name=name)
|
||||
records = volume_type_ref.delete()
|
||||
if records == 0:
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
else:
|
||||
return volume_type_ref
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_extra_specs_get(context, volume_type_id):
|
||||
session = get_session()
|
||||
|
||||
spec_results = session.query(models.VolumeTypeExtraSpecs).\
|
||||
filter_by(volume_type_id=volume_type_id).\
|
||||
filter_by(deleted=False).\
|
||||
all()
|
||||
|
||||
spec_dict = {}
|
||||
for i in spec_results:
|
||||
spec_dict[i['key']] = i['value']
|
||||
return spec_dict
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_extra_specs_delete(context, volume_type_id, key):
|
||||
session = get_session()
|
||||
session.query(models.VolumeTypeExtraSpecs).\
|
||||
filter_by(volume_type_id=volume_type_id).\
|
||||
filter_by(key=key).\
|
||||
filter_by(deleted=False).\
|
||||
update({'deleted': True,
|
||||
'deleted_at': utils.utcnow(),
|
||||
'updated_at': literal_column('updated_at')})
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_extra_specs_get_item(context, volume_type_id, key,
|
||||
session=None):
|
||||
|
||||
if not session:
|
||||
session = get_session()
|
||||
|
||||
spec_result = session.query(models.VolumeTypeExtraSpecs).\
|
||||
filter_by(volume_type_id=volume_type_id).\
|
||||
filter_by(key=key).\
|
||||
filter_by(deleted=False).\
|
||||
first()
|
||||
|
||||
if not spec_result:
|
||||
raise exception.\
|
||||
VolumeTypeExtraSpecsNotFound(extra_specs_key=key,
|
||||
volume_type_id=volume_type_id)
|
||||
return spec_result
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_extra_specs_update_or_create(context, volume_type_id,
|
||||
specs):
|
||||
session = get_session()
|
||||
spec_ref = None
|
||||
for key, value in specs.iteritems():
|
||||
try:
|
||||
spec_ref = volume_type_extra_specs_get_item(
|
||||
context, volume_type_id, key, session)
|
||||
except exception.VolumeTypeExtraSpecsNotFound, e:
|
||||
spec_ref = models.VolumeTypeExtraSpecs()
|
||||
spec_ref.update({"key": key, "value": value,
|
||||
"volume_type_id": volume_type_id,
|
||||
"deleted": 0})
|
||||
spec_ref.save(session=session)
|
||||
return specs
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from sqlalchemy import Column, MetaData, Table, String
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
admin_pass = Column(
|
||||
'admin_pass',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False),
|
||||
nullable=True)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
instances = Table('instances', meta, autoload=True,
|
||||
autoload_with=migrate_engine)
|
||||
instances.drop_column('admin_pass')
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
instances = Table('instances', meta, autoload=True,
|
||||
autoload_with=migrate_engine)
|
||||
instances.create_column(admin_pass)
|
||||
@@ -0,0 +1,44 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (C) 2011 Midokura KK
|
||||
#
|
||||
# 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 Column, Integer, MetaData, String, Table
|
||||
|
||||
from nova import utils
|
||||
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
virtual_interfaces = Table("virtual_interfaces", meta,
|
||||
Column("id", Integer(), primary_key=True,
|
||||
nullable=False))
|
||||
uuid_column = Column("uuid", String(36))
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
virtual_interfaces.create_column(uuid_column)
|
||||
|
||||
rows = migrate_engine.execute(virtual_interfaces.select())
|
||||
for row in rows:
|
||||
vif_uuid = str(utils.gen_uuid())
|
||||
migrate_engine.execute(virtual_interfaces.update()\
|
||||
.where(virtual_interfaces.c.id == row[0])\
|
||||
.values(uuid=vif_uuid))
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
virtual_interfaces.drop_column(uuid_column)
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from sqlalchemy import Column, Integer, MetaData, Table, String
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
accessIPv4 = Column(
|
||||
'access_ip_v4',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False),
|
||||
nullable=True)
|
||||
|
||||
accessIPv6 = Column(
|
||||
'access_ip_v6',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False),
|
||||
nullable=True)
|
||||
|
||||
instances = Table('instances', meta,
|
||||
Column('id', Integer(), primary_key=True, nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
# Upgrade operations go here. Don't create your own engine;
|
||||
# bind migrate_engine to your metadata
|
||||
meta.bind = migrate_engine
|
||||
instances.create_column(accessIPv4)
|
||||
instances.create_column(accessIPv6)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
# Operations to reverse the above upgrade go here.
|
||||
meta.bind = migrate_engine
|
||||
instances.drop_column('access_ip_v4')
|
||||
instances.drop_column('access_ip_v6')
|
||||
@@ -0,0 +1,43 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from sqlalchemy import Column, Integer, MetaData, String, Table
|
||||
|
||||
from nova import utils
|
||||
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
networks = Table("networks", meta,
|
||||
Column("id", Integer(), primary_key=True, nullable=False))
|
||||
uuid_column = Column("uuid", String(36))
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
networks.create_column(uuid_column)
|
||||
|
||||
rows = migrate_engine.execute(networks.select())
|
||||
for row in rows:
|
||||
networks_uuid = str(utils.gen_uuid())
|
||||
migrate_engine.execute(networks.update()\
|
||||
.where(networks.c.id == row[0])\
|
||||
.values(uuid=networks_uuid))
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
networks.drop_column(uuid_column)
|
||||
@@ -0,0 +1,38 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from sqlalchemy import Column, Integer, MetaData, String, Table
|
||||
|
||||
from nova import utils
|
||||
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
instances = Table("instances", meta,
|
||||
Column("id", Integer(), primary_key=True, nullable=False))
|
||||
|
||||
# matches the size of an image_ref
|
||||
config_drive_column = Column("config_drive", String(255), nullable=True)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
instances.create_column(config_drive_column)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
instances.drop_column(config_drive_column)
|
||||
@@ -0,0 +1,115 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Zadara Storage Inc.
|
||||
# Copyright (c) 2011 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table
|
||||
from sqlalchemy import Text, Boolean, ForeignKey
|
||||
|
||||
from nova import log as logging
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
# Just for the ForeignKey and column creation to succeed, these are not the
|
||||
# actual definitions of tables .
|
||||
#
|
||||
|
||||
volumes = Table('volumes', meta,
|
||||
Column('id', Integer(), primary_key=True, nullable=False),
|
||||
)
|
||||
|
||||
volume_type_id = Column('volume_type_id', Integer(), nullable=True)
|
||||
|
||||
|
||||
# New Tables
|
||||
#
|
||||
|
||||
volume_types = Table('volume_types', 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('name',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False),
|
||||
unique=True))
|
||||
|
||||
volume_type_extra_specs_table = Table('volume_type_extra_specs', 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('volume_type_id',
|
||||
Integer(),
|
||||
ForeignKey('volume_types.id'),
|
||||
nullable=False),
|
||||
Column('key',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False)),
|
||||
Column('value',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False)))
|
||||
|
||||
|
||||
volume_metadata_table = Table('volume_metadata', 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('volume_id',
|
||||
Integer(),
|
||||
ForeignKey('volumes.id'),
|
||||
nullable=False),
|
||||
Column('key',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False)),
|
||||
Column('value',
|
||||
String(length=255, convert_unicode=False, assert_unicode=None,
|
||||
unicode_error=None, _warn_on_bytestring=False)))
|
||||
|
||||
|
||||
new_tables = (volume_types,
|
||||
volume_type_extra_specs_table,
|
||||
volume_metadata_table)
|
||||
|
||||
#
|
||||
# Tables to alter
|
||||
#
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
for table in new_tables:
|
||||
try:
|
||||
table.create()
|
||||
except Exception:
|
||||
logging.info(repr(table))
|
||||
logging.exception('Exception while creating table')
|
||||
raise
|
||||
|
||||
volumes.create_column(volume_type_id)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
volumes.drop_column(volume_type_id)
|
||||
|
||||
for table in new_tables:
|
||||
table.drop()
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@@ -173,7 +174,6 @@ class Instance(BASE, NovaBase):
|
||||
base_name += "-rescue"
|
||||
return base_name
|
||||
|
||||
admin_pass = Column(String(255))
|
||||
user_id = Column(String(255))
|
||||
project_id = Column(String(255))
|
||||
|
||||
@@ -231,6 +231,12 @@ class Instance(BASE, NovaBase):
|
||||
uuid = Column(String(36))
|
||||
|
||||
root_device_name = Column(String(255))
|
||||
config_drive = Column(String(255))
|
||||
|
||||
# User editable field meant to represent what ip should be used
|
||||
# to connect to the instance
|
||||
access_ip_v4 = Column(String(255))
|
||||
access_ip_v6 = Column(String(255))
|
||||
|
||||
# TODO(vish): see Ewan's email about state improvements, probably
|
||||
# should be in a driver base class or some such
|
||||
@@ -344,6 +350,8 @@ class Volume(BASE, NovaBase):
|
||||
provider_location = Column(String(255))
|
||||
provider_auth = Column(String(255))
|
||||
|
||||
volume_type_id = Column(Integer)
|
||||
|
||||
to_vsa_id = Column(Integer,
|
||||
ForeignKey('virtual_storage_arrays.id'), nullable=True)
|
||||
from_vsa_id = Column(Integer,
|
||||
@@ -352,6 +360,48 @@ class Volume(BASE, NovaBase):
|
||||
ForeignKey('drive_types.id'), nullable=True)
|
||||
|
||||
|
||||
class VolumeMetadata(BASE, NovaBase):
|
||||
"""Represents a metadata key/value pair for a volume"""
|
||||
__tablename__ = 'volume_metadata'
|
||||
id = Column(Integer, primary_key=True)
|
||||
key = Column(String(255))
|
||||
value = Column(String(255))
|
||||
volume_id = Column(Integer, ForeignKey('volumes.id'), nullable=False)
|
||||
volume = relationship(Volume, backref="volume_metadata",
|
||||
foreign_keys=volume_id,
|
||||
primaryjoin='and_('
|
||||
'VolumeMetadata.volume_id == Volume.id,'
|
||||
'VolumeMetadata.deleted == False)')
|
||||
|
||||
|
||||
class VolumeTypes(BASE, NovaBase):
|
||||
"""Represent possible volume_types of volumes offered"""
|
||||
__tablename__ = "volume_types"
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255), unique=True)
|
||||
|
||||
volumes = relationship(Volume,
|
||||
backref=backref('volume_type', uselist=False),
|
||||
foreign_keys=id,
|
||||
primaryjoin='and_(Volume.volume_type_id == '
|
||||
'VolumeTypes.id)')
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecs(BASE, NovaBase):
|
||||
"""Represents additional specs as key/value pairs for a volume_type"""
|
||||
__tablename__ = 'volume_type_extra_specs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
key = Column(String(255))
|
||||
value = Column(String(255))
|
||||
volume_type_id = Column(Integer, ForeignKey('volume_types.id'),
|
||||
nullable=False)
|
||||
volume_type = relationship(VolumeTypes, backref="extra_specs",
|
||||
foreign_keys=volume_type_id,
|
||||
primaryjoin='and_('
|
||||
'VolumeTypeExtraSpecs.volume_type_id == VolumeTypes.id,'
|
||||
'VolumeTypeExtraSpecs.deleted == False)')
|
||||
|
||||
|
||||
class DriveTypes(BASE, NovaBase):
|
||||
"""Represents the known drive types (storage media)."""
|
||||
__tablename__ = 'drive_types'
|
||||
@@ -628,6 +678,7 @@ class Network(BASE, NovaBase):
|
||||
|
||||
project_id = Column(String(255))
|
||||
host = Column(String(255)) # , ForeignKey('hosts.id'))
|
||||
uuid = Column(String(36))
|
||||
|
||||
|
||||
class VirtualInterface(BASE, NovaBase):
|
||||
@@ -642,6 +693,8 @@ class VirtualInterface(BASE, NovaBase):
|
||||
instance_id = Column(Integer, ForeignKey('instances.id'), nullable=False)
|
||||
instance = relationship(Instance, backref=backref('virtual_interfaces'))
|
||||
|
||||
uuid = Column(String(36))
|
||||
|
||||
@property
|
||||
def fixed_ipv6(self):
|
||||
cidr_v6 = self.network.cidr_v6
|
||||
@@ -865,7 +918,6 @@ def register_models():
|
||||
Network, SecurityGroup, SecurityGroupIngressRule,
|
||||
SecurityGroupInstanceAssociation, AuthToken, User,
|
||||
Project, Certificate, ConsolePool, Console, Zone,
|
||||
VirtualStorageArray, DriveTypes,
|
||||
AgentBuild, InstanceMetadata, InstanceTypeExtraSpecs, Migration)
|
||||
engine = create_engine(FLAGS.sql_connection, echo=False)
|
||||
for model in models:
|
||||
|
||||
@@ -71,9 +71,11 @@ def get_engine():
|
||||
|
||||
elif MySQLdb and "mysql" in connection_dict.drivername:
|
||||
LOG.info(_("Using mysql/eventlet db_pool."))
|
||||
# MySQLdb won't accept 'None' in the password field
|
||||
password = connection_dict.password or ''
|
||||
pool_args = {
|
||||
"db": connection_dict.database,
|
||||
"passwd": connection_dict.password,
|
||||
"passwd": password,
|
||||
"host": connection_dict.host,
|
||||
"user": connection_dict.username,
|
||||
"min_size": FLAGS.sql_min_pool_size,
|
||||
|
||||
@@ -197,6 +197,10 @@ class InvalidInstanceType(Invalid):
|
||||
message = _("Invalid instance type %(instance_type)s.")
|
||||
|
||||
|
||||
class InvalidVolumeType(Invalid):
|
||||
message = _("Invalid volume type %(volume_type)s.")
|
||||
|
||||
|
||||
class InvalidPortRange(Invalid):
|
||||
message = _("Invalid port range %(from_port)s:%(to_port)s.")
|
||||
|
||||
@@ -338,6 +342,29 @@ class VolumeNotFoundForInstance(VolumeNotFound):
|
||||
message = _("Volume not found for instance %(instance_id)s.")
|
||||
|
||||
|
||||
class VolumeMetadataNotFound(NotFound):
|
||||
message = _("Volume %(volume_id)s has no metadata with "
|
||||
"key %(metadata_key)s.")
|
||||
|
||||
|
||||
class NoVolumeTypesFound(NotFound):
|
||||
message = _("Zero volume types found.")
|
||||
|
||||
|
||||
class VolumeTypeNotFound(NotFound):
|
||||
message = _("Volume type %(volume_type_id)s could not be found.")
|
||||
|
||||
|
||||
class VolumeTypeNotFoundByName(VolumeTypeNotFound):
|
||||
message = _("Volume type with name %(volume_type_name)s "
|
||||
"could not be found.")
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecsNotFound(NotFound):
|
||||
message = _("Volume Type %(volume_type_id)s has no extra specs with "
|
||||
"key %(extra_specs_key)s.")
|
||||
|
||||
|
||||
class VolumeNotFoundForVsa(VolumeNotFound):
|
||||
message = _("Volume not found for vsa %(vsa_id)s.")
|
||||
|
||||
@@ -427,6 +454,15 @@ class NoNetworksFound(NotFound):
|
||||
message = _("No networks defined.")
|
||||
|
||||
|
||||
class NetworkNotFoundForProject(NotFound):
|
||||
message = _("Either Network uuid %(network_uuid)s is not present or "
|
||||
"is not assigned to the project %(project_id)s.")
|
||||
|
||||
|
||||
class NetworkHostNotSet(NovaException):
|
||||
message = _("Host is not set to the network (%(network_id)s).")
|
||||
|
||||
|
||||
class DatastoreNotFound(NotFound):
|
||||
message = _("Could not find the datastore reference(s) which the VM uses.")
|
||||
|
||||
@@ -460,6 +496,19 @@ class FixedIpNotFoundForHost(FixedIpNotFound):
|
||||
message = _("Host %(host)s has zero fixed ips.")
|
||||
|
||||
|
||||
class FixedIpNotFoundForNetwork(FixedIpNotFound):
|
||||
message = _("Fixed IP address (%(address)s) does not exist in "
|
||||
"network (%(network_uuid)s).")
|
||||
|
||||
|
||||
class FixedIpAlreadyInUse(NovaException):
|
||||
message = _("Fixed IP address %(address)s is already in use.")
|
||||
|
||||
|
||||
class FixedIpInvalid(Invalid):
|
||||
message = _("Fixed IP address %(address)s is invalid.")
|
||||
|
||||
|
||||
class NoMoreFixedIps(Error):
|
||||
message = _("Zero fixed ips available.")
|
||||
|
||||
@@ -545,6 +594,16 @@ class SecurityGroupNotFoundForRule(SecurityGroupNotFound):
|
||||
message = _("Security group with rule %(rule_id)s not found.")
|
||||
|
||||
|
||||
class SecurityGroupExistsForInstance(Invalid):
|
||||
message = _("Security group %(security_group_id)s is already associated"
|
||||
" with the instance %(instance_id)s")
|
||||
|
||||
|
||||
class SecurityGroupNotExistsForInstance(Invalid):
|
||||
message = _("Security group %(security_group_id)s is not associated with"
|
||||
" the instance %(instance_id)s")
|
||||
|
||||
|
||||
class MigrationNotFound(NotFound):
|
||||
message = _("Migration %(migration_id)s could not be found.")
|
||||
|
||||
|
||||
@@ -414,3 +414,14 @@ DEFINE_bool('resume_guests_state_on_host_boot', False,
|
||||
|
||||
DEFINE_string('root_helper', 'sudo',
|
||||
'Command prefix to use for running commands as root')
|
||||
|
||||
DEFINE_bool('use_ipv6', False, 'use ipv6')
|
||||
|
||||
DEFINE_bool('monkey_patch', False,
|
||||
'Whether to log monkey patching')
|
||||
|
||||
DEFINE_list('monkey_patch_modules',
|
||||
['nova.api.ec2.cloud:nova.notifier.api.notify_decorator',
|
||||
'nova.compute.api:nova.notifier.api.notify_decorator'],
|
||||
'Module list representing monkey '
|
||||
'patched module and decorator')
|
||||
|
||||
@@ -34,8 +34,12 @@ def to_global(prefix, mac, project_id):
|
||||
mac_addr = netaddr.IPAddress(int_addr)
|
||||
maskIP = netaddr.IPNetwork(prefix).ip
|
||||
return (project_hash ^ static_num ^ mac_addr | maskIP).format()
|
||||
except TypeError:
|
||||
except netaddr.AddrFormatError:
|
||||
raise TypeError(_('Bad mac for to_global_ipv6: %s') % mac)
|
||||
except TypeError:
|
||||
raise TypeError(_('Bad prefix for to_global_ipv6: %s') % prefix)
|
||||
except NameError:
|
||||
raise TypeError(_('Bad project_id for to_global_ipv6: %s') % project_id)
|
||||
|
||||
|
||||
def to_mac(ipv6_address):
|
||||
|
||||
@@ -30,8 +30,10 @@ def to_global(prefix, mac, project_id):
|
||||
maskIP = netaddr.IPNetwork(prefix).ip
|
||||
return (mac64_addr ^ netaddr.IPAddress('::0200:0:0:0') | maskIP).\
|
||||
format()
|
||||
except TypeError:
|
||||
except netaddr.AddrFormatError:
|
||||
raise TypeError(_('Bad mac for to_global_ipv6: %s') % mac)
|
||||
except TypeError:
|
||||
raise TypeError(_('Bad prefix for to_global_ipv6: %s') % prefix)
|
||||
|
||||
|
||||
def to_mac(ipv6_address):
|
||||
|
||||
@@ -195,3 +195,12 @@ class API(base.Base):
|
||||
return rpc.call(context, FLAGS.network_topic,
|
||||
{'method': 'get_instance_nw_info',
|
||||
'args': args})
|
||||
|
||||
def validate_networks(self, context, requested_networks):
|
||||
"""validate the networks passed at the time of creating
|
||||
the server
|
||||
"""
|
||||
args = {'networks': requested_networks}
|
||||
return rpc.call(context, FLAGS.network_topic,
|
||||
{'method': 'validate_networks',
|
||||
'args': args})
|
||||
|
||||
@@ -106,8 +106,6 @@ flags.DEFINE_integer('create_unique_mac_address_attempts', 5,
|
||||
'Number of attempts to create unique mac address')
|
||||
flags.DEFINE_bool('auto_assign_floating_ip', False,
|
||||
'Autoassigning floating ip to VM')
|
||||
flags.DEFINE_bool('use_ipv6', False,
|
||||
'use the ipv6')
|
||||
flags.DEFINE_string('network_host', socket.gethostname(),
|
||||
'Network host to use for ip allocation in flat modes')
|
||||
flags.DEFINE_bool('fake_call', False,
|
||||
@@ -131,7 +129,15 @@ class RPCAllocateFixedIP(object):
|
||||
green_pool = greenpool.GreenPool()
|
||||
|
||||
vpn = kwargs.pop('vpn')
|
||||
requested_networks = kwargs.pop('requested_networks')
|
||||
|
||||
for network in networks:
|
||||
address = None
|
||||
if requested_networks is not None:
|
||||
for address in (fixed_ip for (uuid, fixed_ip) in \
|
||||
requested_networks if network['uuid'] == uuid):
|
||||
break
|
||||
|
||||
# NOTE(vish): if we are not multi_host pass to the network host
|
||||
if not network['multi_host']:
|
||||
host = network['host']
|
||||
@@ -148,6 +154,7 @@ class RPCAllocateFixedIP(object):
|
||||
args = {}
|
||||
args['instance_id'] = instance_id
|
||||
args['network_id'] = network['id']
|
||||
args['address'] = address
|
||||
args['vpn'] = vpn
|
||||
|
||||
green_pool.spawn_n(rpc.call, context, topic,
|
||||
@@ -155,7 +162,8 @@ class RPCAllocateFixedIP(object):
|
||||
'args': args})
|
||||
else:
|
||||
# i am the correct host, run here
|
||||
self.allocate_fixed_ip(context, instance_id, network, vpn=vpn)
|
||||
self.allocate_fixed_ip(context, instance_id, network,
|
||||
vpn=vpn, address=address)
|
||||
|
||||
# wait for all of the allocates (if any) to finish
|
||||
green_pool.waitall()
|
||||
@@ -199,6 +207,7 @@ class FloatingIP(object):
|
||||
"""
|
||||
instance_id = kwargs.get('instance_id')
|
||||
project_id = kwargs.get('project_id')
|
||||
requested_networks = kwargs.get('requested_networks')
|
||||
LOG.debug(_("floating IP allocation for instance |%s|"), instance_id,
|
||||
context=context)
|
||||
# call the next inherited class's allocate_for_instance()
|
||||
@@ -380,16 +389,21 @@ class NetworkManager(manager.SchedulerDependentManager):
|
||||
self.compute_api.trigger_security_group_members_refresh(admin_context,
|
||||
group_ids)
|
||||
|
||||
def _get_networks_for_instance(self, context, instance_id, project_id):
|
||||
def _get_networks_for_instance(self, context, instance_id, project_id,
|
||||
requested_networks=None):
|
||||
"""Determine & return which networks an instance should connect to."""
|
||||
# TODO(tr3buchet) maybe this needs to be updated in the future if
|
||||
# there is a better way to determine which networks
|
||||
# a non-vlan instance should connect to
|
||||
try:
|
||||
networks = self.db.network_get_all(context)
|
||||
except exception.NoNetworksFound:
|
||||
return []
|
||||
|
||||
if requested_networks is not None and len(requested_networks) != 0:
|
||||
network_uuids = [uuid for (uuid, fixed_ip) in requested_networks]
|
||||
networks = self.db.network_get_all_by_uuids(context,
|
||||
network_uuids)
|
||||
else:
|
||||
try:
|
||||
networks = self.db.network_get_all(context)
|
||||
except exception.NoNetworksFound:
|
||||
return []
|
||||
# return only networks which are not vlan networks
|
||||
return [network for network in networks if
|
||||
not network['vlan']]
|
||||
@@ -403,16 +417,18 @@ class NetworkManager(manager.SchedulerDependentManager):
|
||||
host = kwargs.pop('host')
|
||||
project_id = kwargs.pop('project_id')
|
||||
type_id = kwargs.pop('instance_type_id')
|
||||
requested_networks = kwargs.get('requested_networks')
|
||||
vpn = kwargs.pop('vpn')
|
||||
admin_context = context.elevated()
|
||||
LOG.debug(_("network allocations for instance %s"), instance_id,
|
||||
context=context)
|
||||
networks = self._get_networks_for_instance(admin_context, instance_id,
|
||||
project_id)
|
||||
LOG.warn(networks)
|
||||
networks = self._get_networks_for_instance(admin_context,
|
||||
instance_id, project_id,
|
||||
requested_networks=requested_networks)
|
||||
self._allocate_mac_addresses(context, instance_id, networks)
|
||||
self._allocate_fixed_ips(admin_context, instance_id, host, networks,
|
||||
vpn=vpn)
|
||||
self._allocate_fixed_ips(admin_context, instance_id,
|
||||
host, networks, vpn=vpn,
|
||||
requested_networks=requested_networks)
|
||||
return self.get_instance_nw_info(context, instance_id, type_id, host)
|
||||
|
||||
def deallocate_for_instance(self, context, **kwargs):
|
||||
@@ -500,6 +516,7 @@ class NetworkManager(manager.SchedulerDependentManager):
|
||||
'dhcp_server': dhcp_server,
|
||||
'broadcast': network['broadcast'],
|
||||
'mac': vif['address'],
|
||||
'vif_uuid': vif['uuid'],
|
||||
'rxtx_cap': flavor['rxtx_cap'],
|
||||
'dns': [],
|
||||
'ips': [ip_dict(ip) for ip in network_IPs],
|
||||
@@ -524,7 +541,8 @@ class NetworkManager(manager.SchedulerDependentManager):
|
||||
for network in networks:
|
||||
vif = {'address': self.generate_mac_address(),
|
||||
'instance_id': instance_id,
|
||||
'network_id': network['id']}
|
||||
'network_id': network['id'],
|
||||
'uuid': str(utils.gen_uuid())}
|
||||
# try FLAG times to create a vif record with a unique mac_address
|
||||
for i in range(FLAGS.create_unique_mac_address_attempts):
|
||||
try:
|
||||
@@ -568,9 +586,15 @@ class NetworkManager(manager.SchedulerDependentManager):
|
||||
# network_get_by_compute_host
|
||||
address = None
|
||||
if network['cidr']:
|
||||
address = self.db.fixed_ip_associate_pool(context.elevated(),
|
||||
network['id'],
|
||||
instance_id)
|
||||
address = kwargs.get('address', None)
|
||||
if address:
|
||||
address = self.db.fixed_ip_associate(context,
|
||||
address, instance_id,
|
||||
network['id'])
|
||||
else:
|
||||
address = self.db.fixed_ip_associate_pool(context.elevated(),
|
||||
network['id'],
|
||||
instance_id)
|
||||
self._do_trigger_security_group_members_refresh_for_instance(
|
||||
instance_id)
|
||||
get_vif = self.db.virtual_interface_get_by_instance_and_network
|
||||
@@ -796,6 +820,35 @@ class NetworkManager(manager.SchedulerDependentManager):
|
||||
"""Sets up network on this host."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate_networks(self, context, networks):
|
||||
"""check if the networks exists and host
|
||||
is set to each network.
|
||||
"""
|
||||
if networks is None or len(networks) == 0:
|
||||
return
|
||||
|
||||
network_uuids = [uuid for (uuid, fixed_ip) in networks]
|
||||
|
||||
self._get_networks_by_uuids(context, network_uuids)
|
||||
|
||||
for network_uuid, address in networks:
|
||||
# check if the fixed IP address is valid and
|
||||
# it actually belongs to the network
|
||||
if address is not None:
|
||||
if not utils.is_valid_ipv4(address):
|
||||
raise exception.FixedIpInvalid(address=address)
|
||||
|
||||
fixed_ip_ref = self.db.fixed_ip_get_by_address(context,
|
||||
address)
|
||||
if fixed_ip_ref['network']['uuid'] != network_uuid:
|
||||
raise exception.FixedIpNotFoundForNetwork(address=address,
|
||||
network_uuid=network_uuid)
|
||||
if fixed_ip_ref['instance'] is not None:
|
||||
raise exception.FixedIpAlreadyInUse(address=address)
|
||||
|
||||
def _get_networks_by_uuids(self, context, network_uuids):
|
||||
return self.db.network_get_all_by_uuids(context, network_uuids)
|
||||
|
||||
|
||||
class FlatManager(NetworkManager):
|
||||
"""Basic network where no vlans are used.
|
||||
@@ -830,8 +883,16 @@ class FlatManager(NetworkManager):
|
||||
def _allocate_fixed_ips(self, context, instance_id, host, networks,
|
||||
**kwargs):
|
||||
"""Calls allocate_fixed_ip once for each network."""
|
||||
requested_networks = kwargs.pop('requested_networks')
|
||||
for network in networks:
|
||||
self.allocate_fixed_ip(context, instance_id, network)
|
||||
address = None
|
||||
if requested_networks is not None:
|
||||
for address in (fixed_ip for (uuid, fixed_ip) in \
|
||||
requested_networks if network['uuid'] == uuid):
|
||||
break
|
||||
|
||||
self.allocate_fixed_ip(context, instance_id,
|
||||
network, address=address)
|
||||
|
||||
def deallocate_fixed_ip(self, context, address, **kwargs):
|
||||
"""Returns a fixed ip to the pool."""
|
||||
@@ -925,9 +986,15 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager):
|
||||
address,
|
||||
instance_id)
|
||||
else:
|
||||
address = self.db.fixed_ip_associate_pool(context,
|
||||
network['id'],
|
||||
instance_id)
|
||||
address = kwargs.get('address', None)
|
||||
if address:
|
||||
address = self.db.fixed_ip_associate(context, address,
|
||||
instance_id,
|
||||
network['id'])
|
||||
else:
|
||||
address = self.db.fixed_ip_associate_pool(context,
|
||||
network['id'],
|
||||
instance_id)
|
||||
self._do_trigger_security_group_members_refresh_for_instance(
|
||||
instance_id)
|
||||
vif = self.db.virtual_interface_get_by_instance_and_network(context,
|
||||
@@ -943,10 +1010,18 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager):
|
||||
"""Force adds another network to a project."""
|
||||
self.db.network_associate(context, project_id, force=True)
|
||||
|
||||
def _get_networks_for_instance(self, context, instance_id, project_id):
|
||||
def _get_networks_for_instance(self, context, instance_id, project_id,
|
||||
requested_networks=None):
|
||||
"""Determine which networks an instance should connect to."""
|
||||
# get networks associated with project
|
||||
return self.db.project_get_networks(context, project_id)
|
||||
if requested_networks is not None and len(requested_networks) != 0:
|
||||
network_uuids = [uuid for (uuid, fixed_ip) in requested_networks]
|
||||
networks = self.db.network_get_all_by_uuids(context,
|
||||
network_uuids,
|
||||
project_id)
|
||||
else:
|
||||
networks = self.db.project_get_networks(context, project_id)
|
||||
return networks
|
||||
|
||||
def create_networks(self, context, **kwargs):
|
||||
"""Create networks based on parameters."""
|
||||
@@ -995,6 +1070,10 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager):
|
||||
self.db.network_update(context, network_ref['id'],
|
||||
{'gateway_v6': gateway})
|
||||
|
||||
def _get_networks_by_uuids(self, context, network_uuids):
|
||||
return self.db.network_get_all_by_uuids(context, network_uuids,
|
||||
context.project_id)
|
||||
|
||||
@property
|
||||
def _bottom_reserved_ips(self):
|
||||
"""Number of reserved ips at the bottom of the range."""
|
||||
|
||||
@@ -25,6 +25,9 @@ FLAGS = flags.FLAGS
|
||||
|
||||
flags.DEFINE_string('default_notification_level', 'INFO',
|
||||
'Default notification level for outgoing notifications')
|
||||
flags.DEFINE_string('default_publisher_id', FLAGS.host,
|
||||
'Default publisher_id for outgoing notifications')
|
||||
|
||||
|
||||
WARN = 'WARN'
|
||||
INFO = 'INFO'
|
||||
@@ -39,6 +42,30 @@ class BadPriorityException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def notify_decorator(name, fn):
|
||||
""" decorator for notify which is used from utils.monkey_patch()
|
||||
|
||||
:param name: name of the function
|
||||
:param function: - object of the function
|
||||
:returns: function -- decorated function
|
||||
|
||||
"""
|
||||
def wrapped_func(*args, **kwarg):
|
||||
body = {}
|
||||
body['args'] = []
|
||||
body['kwarg'] = {}
|
||||
for arg in args:
|
||||
body['args'].append(arg)
|
||||
for key in kwarg:
|
||||
body['kwarg'][key] = kwarg[key]
|
||||
notify(FLAGS.default_publisher_id,
|
||||
name,
|
||||
FLAGS.default_notification_level,
|
||||
body)
|
||||
return fn(*args, **kwarg)
|
||||
return wrapped_func
|
||||
|
||||
|
||||
def publisher_id(service, host=None):
|
||||
if not host:
|
||||
host = FLAGS.host
|
||||
|
||||
@@ -164,5 +164,5 @@ def allowed_injected_file_path_bytes(context):
|
||||
|
||||
|
||||
class QuotaError(exception.ApiError):
|
||||
"""Quota Exceeeded."""
|
||||
"""Quota Exceeded."""
|
||||
pass
|
||||
|
||||
@@ -21,5 +21,7 @@
|
||||
.. automodule:: nova.scheduler
|
||||
:platform: Unix
|
||||
:synopsis: Module that picks a compute node to run a VM instance.
|
||||
.. moduleauthor:: Sandy Walsh <sandy.walsh@rackspace.com>
|
||||
.. moduleauthor:: Ed Leafe <ed@leafe.com>
|
||||
.. moduleauthor:: Chris Behrens <cbehrens@codestud.com>
|
||||
"""
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
The AbsractScheduler is a base class Scheduler for creating instances
|
||||
across zones. There are two expansion points to this class for:
|
||||
1. Assigning Weights to hosts for requested instances
|
||||
2. Filtering Hosts based on required instance capabilities
|
||||
The AbsractScheduler is an abstract class Scheduler for creating instances
|
||||
locally or across zones. Two methods should be overridden in order to
|
||||
customize the behavior: filter_hosts() and weigh_hosts(). The default
|
||||
behavior is to simply select all hosts and weight them the same.
|
||||
"""
|
||||
|
||||
import operator
|
||||
@@ -45,44 +45,44 @@ LOG = logging.getLogger('nova.scheduler.abstract_scheduler')
|
||||
|
||||
class InvalidBlob(exception.NovaException):
|
||||
message = _("Ill-formed or incorrectly routed 'blob' data sent "
|
||||
"to instance create request.")
|
||||
"to instance create request.")
|
||||
|
||||
|
||||
class AbstractScheduler(driver.Scheduler):
|
||||
"""Base class for creating Schedulers that can work across any nova
|
||||
deployment, from simple designs to multiply-nested zones.
|
||||
"""
|
||||
|
||||
def _call_zone_method(self, context, method, specs, zones):
|
||||
"""Call novaclient zone method. Broken out for testing."""
|
||||
return api.call_zone_method(context, method, specs=specs, zones=zones)
|
||||
|
||||
def _provision_resource_locally(self, context, build_plan_item,
|
||||
request_spec, kwargs):
|
||||
request_spec, kwargs):
|
||||
"""Create the requested resource in this Zone."""
|
||||
host = build_plan_item['hostname']
|
||||
base_options = request_spec['instance_properties']
|
||||
image = request_spec['image']
|
||||
instance_type = request_spec.get('instance_type')
|
||||
|
||||
# TODO(sandy): I guess someone needs to add block_device_mapping
|
||||
# support at some point? Also, OS API has no concept of security
|
||||
# groups.
|
||||
instance = compute_api.API().create_db_entry_for_new_instance(context,
|
||||
image, base_options, None, [])
|
||||
instance_type, image, base_options, None, [])
|
||||
|
||||
instance_id = instance['id']
|
||||
kwargs['instance_id'] = instance_id
|
||||
|
||||
rpc.cast(context,
|
||||
db.queue_get_for(context, "compute", host),
|
||||
{"method": "run_instance",
|
||||
"args": kwargs})
|
||||
queue = db.queue_get_for(context, "compute", host)
|
||||
params = {"method": "run_instance", "args": kwargs}
|
||||
rpc.cast(context, queue, params)
|
||||
LOG.debug(_("Provisioning locally via compute node %(host)s")
|
||||
% locals())
|
||||
% locals())
|
||||
|
||||
def _decrypt_blob(self, blob):
|
||||
"""Returns the decrypted blob or None if invalid. Broken out
|
||||
for testing."""
|
||||
for testing.
|
||||
"""
|
||||
decryptor = crypto.decryptor(FLAGS.build_plan_encryption_key)
|
||||
try:
|
||||
json_entry = decryptor(blob)
|
||||
@@ -92,15 +92,15 @@ class AbstractScheduler(driver.Scheduler):
|
||||
return None
|
||||
|
||||
def _ask_child_zone_to_create_instance(self, context, zone_info,
|
||||
request_spec, kwargs):
|
||||
request_spec, kwargs):
|
||||
"""Once we have determined that the request should go to one
|
||||
of our children, we need to fabricate a new POST /servers/
|
||||
call with the same parameters that were passed into us.
|
||||
|
||||
Note that we have to reverse engineer from our args to get back the
|
||||
image, flavor, ipgroup, etc. since the original call could have
|
||||
come in from EC2 (which doesn't use these things)."""
|
||||
|
||||
come in from EC2 (which doesn't use these things).
|
||||
"""
|
||||
instance_type = request_spec['instance_type']
|
||||
instance_properties = request_spec['instance_properties']
|
||||
|
||||
@@ -109,30 +109,26 @@ class AbstractScheduler(driver.Scheduler):
|
||||
meta = instance_properties['metadata']
|
||||
flavor_id = instance_type['flavorid']
|
||||
reservation_id = instance_properties['reservation_id']
|
||||
|
||||
files = kwargs['injected_files']
|
||||
ipgroup = None # Not supported in OS API ... yet
|
||||
|
||||
child_zone = zone_info['child_zone']
|
||||
child_blob = zone_info['child_blob']
|
||||
zone = db.zone_get(context, child_zone)
|
||||
url = zone.api_url
|
||||
LOG.debug(_("Forwarding instance create call to child zone %(url)s"
|
||||
". ReservationID=%(reservation_id)s")
|
||||
% locals())
|
||||
". ReservationID=%(reservation_id)s") % locals())
|
||||
nova = None
|
||||
try:
|
||||
nova = novaclient.Client(zone.username, zone.password, None, url)
|
||||
nova.authenticate()
|
||||
except novaclient_exceptions.BadRequest, e:
|
||||
raise exception.NotAuthorized(_("Bad credentials attempting "
|
||||
"to talk to zone at %(url)s.") % locals())
|
||||
|
||||
"to talk to zone at %(url)s.") % locals())
|
||||
nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files,
|
||||
child_blob, reservation_id=reservation_id)
|
||||
child_blob, reservation_id=reservation_id)
|
||||
|
||||
def _provision_resource_from_blob(self, context, build_plan_item,
|
||||
instance_id, request_spec, kwargs):
|
||||
instance_id, request_spec, kwargs):
|
||||
"""Create the requested resource locally or in a child zone
|
||||
based on what is stored in the zone blob info.
|
||||
|
||||
@@ -145,8 +141,8 @@ class AbstractScheduler(driver.Scheduler):
|
||||
means we gathered the info from one of our children.
|
||||
It's possible that, when we decrypt the 'blob' field, it
|
||||
contains "child_blob" data. In which case we forward the
|
||||
request."""
|
||||
|
||||
request.
|
||||
"""
|
||||
host_info = None
|
||||
if "blob" in build_plan_item:
|
||||
# Request was passed in from above. Is it for us?
|
||||
@@ -161,21 +157,20 @@ class AbstractScheduler(driver.Scheduler):
|
||||
# Valid data ... is it for us?
|
||||
if 'child_zone' in host_info and 'child_blob' in host_info:
|
||||
self._ask_child_zone_to_create_instance(context, host_info,
|
||||
request_spec, kwargs)
|
||||
request_spec, kwargs)
|
||||
else:
|
||||
self._provision_resource_locally(context, host_info, request_spec,
|
||||
kwargs)
|
||||
kwargs)
|
||||
|
||||
def _provision_resource(self, context, build_plan_item, instance_id,
|
||||
request_spec, kwargs):
|
||||
request_spec, kwargs):
|
||||
"""Create the requested resource in this Zone or a child zone."""
|
||||
if "hostname" in build_plan_item:
|
||||
self._provision_resource_locally(context, build_plan_item,
|
||||
request_spec, kwargs)
|
||||
request_spec, kwargs)
|
||||
return
|
||||
|
||||
self._provision_resource_from_blob(context, build_plan_item,
|
||||
instance_id, request_spec, kwargs)
|
||||
instance_id, request_spec, kwargs)
|
||||
|
||||
def _adjust_child_weights(self, child_results, zones):
|
||||
"""Apply the Scale and Offset values from the Zone definition
|
||||
@@ -185,13 +180,11 @@ class AbstractScheduler(driver.Scheduler):
|
||||
for zone_id, result in child_results:
|
||||
if not result:
|
||||
continue
|
||||
|
||||
assert isinstance(zone_id, int)
|
||||
|
||||
for zone_rec in zones:
|
||||
if zone_rec['id'] != zone_id:
|
||||
continue
|
||||
|
||||
for item in result:
|
||||
try:
|
||||
offset = zone_rec['weight_offset']
|
||||
@@ -202,10 +195,10 @@ class AbstractScheduler(driver.Scheduler):
|
||||
item['raw_weight'] = raw_weight
|
||||
except KeyError:
|
||||
LOG.exception(_("Bad child zone scaling values "
|
||||
"for Zone: %(zone_id)s") % locals())
|
||||
"for Zone: %(zone_id)s") % locals())
|
||||
|
||||
def schedule_run_instance(self, context, instance_id, request_spec,
|
||||
*args, **kwargs):
|
||||
*args, **kwargs):
|
||||
"""This method is called from nova.compute.api to provision
|
||||
an instance. However we need to look at the parameters being
|
||||
passed in to see if this is a request to:
|
||||
@@ -214,13 +207,11 @@ class AbstractScheduler(driver.Scheduler):
|
||||
to simply create the instance (either in this zone or
|
||||
a child zone).
|
||||
"""
|
||||
|
||||
# TODO(sandy): We'll have to look for richer specs at some point.
|
||||
|
||||
blob = request_spec.get('blob')
|
||||
if blob:
|
||||
self._provision_resource(context, request_spec, instance_id,
|
||||
request_spec, kwargs)
|
||||
request_spec, kwargs)
|
||||
return None
|
||||
|
||||
num_instances = request_spec.get('num_instances', 1)
|
||||
@@ -235,10 +226,9 @@ class AbstractScheduler(driver.Scheduler):
|
||||
for num in xrange(num_instances):
|
||||
if not build_plan:
|
||||
break
|
||||
|
||||
build_plan_item = build_plan.pop(0)
|
||||
self._provision_resource(context, build_plan_item, instance_id,
|
||||
request_spec, kwargs)
|
||||
request_spec, kwargs)
|
||||
|
||||
# Returning None short-circuits the routing to Compute (since
|
||||
# we've already done it here)
|
||||
@@ -251,58 +241,44 @@ class AbstractScheduler(driver.Scheduler):
|
||||
anything about the children.
|
||||
"""
|
||||
return self._schedule(context, "compute", request_spec,
|
||||
*args, **kwargs)
|
||||
*args, **kwargs)
|
||||
|
||||
# TODO(sandy): We're only focused on compute instances right now,
|
||||
# so we don't implement the default "schedule()" method required
|
||||
# of Schedulers.
|
||||
def schedule(self, context, topic, request_spec, *args, **kwargs):
|
||||
"""The schedule() contract requires we return the one
|
||||
best-suited host for this request.
|
||||
"""
|
||||
raise driver.NoValidHost(_('No hosts were available'))
|
||||
# TODO(sandy): We're only focused on compute instances right now,
|
||||
# so we don't implement the default "schedule()" method required
|
||||
# of Schedulers.
|
||||
msg = _("No host selection for %s defined." % topic)
|
||||
raise driver.NoValidHost(msg)
|
||||
|
||||
def _schedule(self, context, topic, request_spec, *args, **kwargs):
|
||||
"""Returns a list of hosts that meet the required specs,
|
||||
ordered by their fitness.
|
||||
"""
|
||||
|
||||
if topic != "compute":
|
||||
raise NotImplementedError(_("Scheduler only understands"
|
||||
" Compute nodes (for now)"))
|
||||
msg = _("Scheduler only understands Compute nodes (for now)")
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
num_instances = request_spec.get('num_instances', 1)
|
||||
instance_type = request_spec['instance_type']
|
||||
# Get all available hosts.
|
||||
all_hosts = self.zone_manager.service_states.iteritems()
|
||||
unfiltered_hosts = [(host, services[topic])
|
||||
for host, services in all_hosts
|
||||
if topic in services]
|
||||
|
||||
weighted = []
|
||||
host_list = None
|
||||
# Filter local hosts based on requirements ...
|
||||
filtered_hosts = self.filter_hosts(topic, request_spec,
|
||||
unfiltered_hosts)
|
||||
if not filtered_hosts:
|
||||
LOG.warn(_("No hosts available"))
|
||||
return []
|
||||
|
||||
for i in xrange(num_instances):
|
||||
# Filter local hosts based on requirements ...
|
||||
#
|
||||
# The first pass through here will pass 'None' as the
|
||||
# host_list.. which tells the filter to build the full
|
||||
# list of hosts.
|
||||
# On a 2nd pass, the filter can modify the host_list with
|
||||
# any updates it needs to make based on resources that
|
||||
# may have been consumed from a previous build..
|
||||
host_list = self.filter_hosts(topic, request_spec, host_list)
|
||||
if not host_list:
|
||||
LOG.warn(_("Filter returned no hosts after processing "
|
||||
"%(i)d of %(num_instances)d instances") % locals())
|
||||
break
|
||||
|
||||
# then weigh the selected hosts.
|
||||
# weighted = [{weight=weight, hostname=hostname,
|
||||
# capabilities=capabs}, ...]
|
||||
weights = self.weigh_hosts(topic, request_spec, host_list)
|
||||
weights.sort(key=operator.itemgetter('weight'))
|
||||
best_weight = weights[0]
|
||||
weighted.append(best_weight)
|
||||
self.consume_resources(topic, best_weight['capabilities'],
|
||||
instance_type)
|
||||
|
||||
# Next, tack on the best weights from the child zones ...
|
||||
# weigh the selected hosts.
|
||||
# weighted_hosts = [{weight=weight, hostname=hostname,
|
||||
# capabilities=capabs}, ...]
|
||||
weighted_hosts = self.weigh_hosts(topic, request_spec, filtered_hosts)
|
||||
# Next, tack on the host weights from the child zones
|
||||
json_spec = json.dumps(request_spec)
|
||||
all_zones = db.zone_get_all(context)
|
||||
child_results = self._call_zone_method(context, "select",
|
||||
@@ -314,90 +290,32 @@ class AbstractScheduler(driver.Scheduler):
|
||||
# it later if needed. This implicitly builds a zone
|
||||
# path structure.
|
||||
host_dict = {"weight": weighting["weight"],
|
||||
"child_zone": child_zone,
|
||||
"child_blob": weighting["blob"]}
|
||||
weighted.append(host_dict)
|
||||
"child_zone": child_zone,
|
||||
"child_blob": weighting["blob"]}
|
||||
weighted_hosts.append(host_dict)
|
||||
weighted_hosts.sort(key=operator.itemgetter('weight'))
|
||||
return weighted_hosts
|
||||
|
||||
weighted.sort(key=operator.itemgetter('weight'))
|
||||
return weighted
|
||||
def filter_hosts(self, topic, request_spec, host_list):
|
||||
"""Filter the full host list returned from the ZoneManager. By default,
|
||||
this method only applies the basic_ram_filter(), meaning all hosts
|
||||
with at least enough RAM for the requested instance are returned.
|
||||
|
||||
def compute_filter(self, hostname, capabilities, request_spec):
|
||||
"""Return whether or not we can schedule to this compute node.
|
||||
Derived classes should override this and return True if the host
|
||||
is acceptable for scheduling.
|
||||
Override in subclasses to provide greater selectivity.
|
||||
"""
|
||||
instance_type = request_spec['instance_type']
|
||||
requested_mem = instance_type['memory_mb'] * 1024 * 1024
|
||||
return capabilities['host_memory_free'] >= requested_mem
|
||||
def basic_ram_filter(hostname, capabilities, request_spec):
|
||||
"""Only return hosts with sufficient available RAM."""
|
||||
instance_type = request_spec['instance_type']
|
||||
requested_mem = instance_type['memory_mb'] * 1024 * 1024
|
||||
return capabilities['host_memory_free'] >= requested_mem
|
||||
|
||||
def hold_filter_hosts(self, topic, request_spec, hosts=None):
|
||||
"""Filter the full host list (from the ZoneManager)"""
|
||||
# NOTE(dabo): The logic used by the current _schedule() method
|
||||
# is incorrect. Since this task is just to refactor the classes,
|
||||
# I'm not fixing the logic now - that will be the next task.
|
||||
# So for now this method is just renamed; afterwards this will
|
||||
# become the filter_hosts() method, and the one below will
|
||||
# be removed.
|
||||
filter_name = request_spec.get('filter', None)
|
||||
# Make sure that the requested filter is legitimate.
|
||||
selected_filter = host_filter.choose_host_filter(filter_name)
|
||||
|
||||
# TODO(sandy): We're only using InstanceType-based specs
|
||||
# currently. Later we'll need to snoop for more detailed
|
||||
# host filter requests.
|
||||
instance_type = request_spec['instance_type']
|
||||
name, query = selected_filter.instance_type_to_filter(instance_type)
|
||||
return selected_filter.filter_hosts(self.zone_manager, query)
|
||||
|
||||
def filter_hosts(self, topic, request_spec, host_list=None):
|
||||
"""Return a list of hosts which are acceptable for scheduling.
|
||||
Return value should be a list of (hostname, capability_dict)s.
|
||||
Derived classes may override this, but may find the
|
||||
'<topic>_filter' function more appropriate.
|
||||
"""
|
||||
def _default_filter(self, hostname, capabilities, request_spec):
|
||||
"""Default filter function if there's no <topic>_filter"""
|
||||
# NOTE(sirp): The default logic is the equivalent to
|
||||
# AllHostsFilter
|
||||
return True
|
||||
|
||||
filter_func = getattr(self, '%s_filter' % topic, _default_filter)
|
||||
|
||||
if host_list is None:
|
||||
first_run = True
|
||||
host_list = self.zone_manager.service_states.iteritems()
|
||||
else:
|
||||
first_run = False
|
||||
|
||||
filtered_hosts = []
|
||||
for host, services in host_list:
|
||||
if first_run:
|
||||
if topic not in services:
|
||||
continue
|
||||
services = services[topic]
|
||||
if filter_func(host, services, request_spec):
|
||||
filtered_hosts.append((host, services))
|
||||
return filtered_hosts
|
||||
return [(host, services) for host, services in host_list
|
||||
if basic_ram_filter(host, services, request_spec)]
|
||||
|
||||
def weigh_hosts(self, topic, request_spec, hosts):
|
||||
"""Derived classes may override this to provide more sophisticated
|
||||
scheduling objectives
|
||||
"""This version assigns a weight of 1 to all hosts, making selection
|
||||
of any host basically a random event. Override this method in your
|
||||
subclass to add logic to prefer one potential host over another.
|
||||
"""
|
||||
# NOTE(sirp): The default logic is the same as the NoopCostFunction
|
||||
return [dict(weight=1, hostname=hostname, capabilities=capabilities)
|
||||
for hostname, capabilities in hosts]
|
||||
|
||||
def compute_consume(self, capabilities, instance_type):
|
||||
"""Consume compute resources for selected host"""
|
||||
|
||||
requested_mem = max(instance_type['memory_mb'], 0) * 1024 * 1024
|
||||
capabilities['host_memory_free'] -= requested_mem
|
||||
|
||||
def consume_resources(self, topic, capabilities, instance_type):
|
||||
"""Consume resources for a specific host. 'host' is a tuple
|
||||
of the hostname and the services"""
|
||||
|
||||
consume_func = getattr(self, '%s_consume' % topic, None)
|
||||
if not consume_func:
|
||||
return
|
||||
consume_func(capabilities, instance_type)
|
||||
|
||||
59
nova/scheduler/base_scheduler.py
Normal file
59
nova/scheduler/base_scheduler.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
The BaseScheduler is the base class Scheduler for creating instances
|
||||
across zones. There are two expansion points to this class for:
|
||||
1. Assigning Weights to hosts for requested instances
|
||||
2. Filtering Hosts based on required instance capabilities
|
||||
"""
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
|
||||
from nova.scheduler import abstract_scheduler
|
||||
from nova.scheduler import host_filter
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
LOG = logging.getLogger('nova.scheduler.base_scheduler')
|
||||
|
||||
|
||||
class BaseScheduler(abstract_scheduler.AbstractScheduler):
|
||||
"""Base class for creating Schedulers that can work across any nova
|
||||
deployment, from simple designs to multiply-nested zones.
|
||||
"""
|
||||
def filter_hosts(self, topic, request_spec, hosts=None):
|
||||
"""Filter the full host list (from the ZoneManager)"""
|
||||
filter_name = request_spec.get('filter', None)
|
||||
# Make sure that the requested filter is legitimate.
|
||||
selected_filter = host_filter.choose_host_filter(filter_name)
|
||||
|
||||
# TODO(sandy): We're only using InstanceType-based specs
|
||||
# currently. Later we'll need to snoop for more detailed
|
||||
# host filter requests.
|
||||
instance_type = request_spec.get("instance_type", None)
|
||||
if instance_type is None:
|
||||
# No way to select; return the specified hosts
|
||||
return hosts or []
|
||||
name, query = selected_filter.instance_type_to_filter(instance_type)
|
||||
return selected_filter.filter_hosts(self.zone_manager, query)
|
||||
|
||||
def weigh_hosts(self, topic, request_spec, hosts):
|
||||
"""Derived classes may override this to provide more sophisticated
|
||||
scheduling objectives
|
||||
"""
|
||||
# NOTE(sirp): The default logic is the same as the NoopCostFunction
|
||||
return [dict(weight=1, hostname=hostname, capabilities=capabilities)
|
||||
for hostname, capabilities in hosts]
|
||||
36
nova/scheduler/filters/__init__.py
Normal file
36
nova/scheduler/filters/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
There are three filters included: AllHosts, InstanceType & JSON.
|
||||
|
||||
AllHosts just returns the full, unfiltered list of hosts.
|
||||
InstanceType is a hard coded matching mechanism based on flavor criteria.
|
||||
JSON is an ad-hoc filter grammar.
|
||||
|
||||
Why JSON? The requests for instances may come in through the
|
||||
REST interface from a user or a parent Zone.
|
||||
Currently InstanceTypes are used for specifing the type of instance desired.
|
||||
Specific Nova users have noted a need for a more expressive way of specifying
|
||||
instance requirements. Since we don't want to get into building full DSL,
|
||||
this filter is a simple form as an example of how this could be done.
|
||||
In reality, most consumers will use the more rigid filters such as the
|
||||
InstanceType filter.
|
||||
"""
|
||||
|
||||
from abstract_filter import AbstractHostFilter
|
||||
from all_hosts_filter import AllHostsFilter
|
||||
from instance_type_filter import InstanceTypeFilter
|
||||
from json_filter import JsonFilter
|
||||
37
nova/scheduler/filters/abstract_filter.py
Normal file
37
nova/scheduler/filters/abstract_filter.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import nova.scheduler
|
||||
from nova import flags
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('default_host_filter', 'AllHostsFilter',
|
||||
'Which filter to use for filtering hosts')
|
||||
|
||||
|
||||
class AbstractHostFilter(object):
|
||||
"""Base class for host filters."""
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Convert instance_type into a filter for most common use-case."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts that fulfill the filter."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _full_name(self):
|
||||
"""module.classname of the filter."""
|
||||
return "%s.%s" % (self.__module__, self.__class__.__name__)
|
||||
32
nova/scheduler/filters/all_hosts_filter.py
Normal file
32
nova/scheduler/filters/all_hosts_filter.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import nova.scheduler
|
||||
from nova.scheduler.filters import abstract_filter
|
||||
|
||||
|
||||
class AllHostsFilter(abstract_filter.AbstractHostFilter):
|
||||
"""NOP host filter. Returns all hosts in ZoneManager."""
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Return anything to prevent base-class from raising
|
||||
exception.
|
||||
"""
|
||||
return (self._full_name(), instance_type)
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts from ZoneManager list."""
|
||||
return [(host, services)
|
||||
for host, services in zone_manager.service_states.iteritems()]
|
||||
87
nova/scheduler/filters/instance_type_filter.py
Normal file
87
nova/scheduler/filters/instance_type_filter.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import nova.scheduler
|
||||
from nova.scheduler.filters import abstract_filter
|
||||
|
||||
|
||||
class InstanceTypeFilter(abstract_filter.AbstractHostFilter):
|
||||
"""HostFilter hard-coded to work with InstanceType records."""
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Use instance_type to filter hosts."""
|
||||
return (self._full_name(), instance_type)
|
||||
|
||||
def _satisfies_extra_specs(self, capabilities, instance_type):
|
||||
"""Check that the capabilities provided by the compute service
|
||||
satisfy the extra specs associated with the instance type"""
|
||||
if 'extra_specs' not in instance_type:
|
||||
return True
|
||||
# NOTE(lorinh): For now, we are just checking exact matching on the
|
||||
# values. Later on, we want to handle numerical
|
||||
# values so we can represent things like number of GPU cards
|
||||
try:
|
||||
for key, value in instance_type['extra_specs'].iteritems():
|
||||
if capabilities[key] != value:
|
||||
return False
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts that can create instance_type."""
|
||||
instance_type = query
|
||||
selected_hosts = []
|
||||
for host, services in zone_manager.service_states.iteritems():
|
||||
capabilities = services.get('compute', {})
|
||||
if not capabilities:
|
||||
continue
|
||||
host_ram_mb = capabilities['host_memory_free']
|
||||
disk_bytes = capabilities['disk_available']
|
||||
spec_ram = instance_type['memory_mb']
|
||||
spec_disk = instance_type['local_gb']
|
||||
extra_specs = instance_type['extra_specs']
|
||||
|
||||
if ((host_ram_mb >= spec_ram) and (disk_bytes >= spec_disk) and
|
||||
self._satisfies_extra_specs(capabilities, instance_type)):
|
||||
selected_hosts.append((host, capabilities))
|
||||
return selected_hosts
|
||||
|
||||
|
||||
# host entries (currently) are like:
|
||||
# {'host_name-description': 'Default install of XenServer',
|
||||
# 'host_hostname': 'xs-mini',
|
||||
# 'host_memory_total': 8244539392,
|
||||
# 'host_memory_overhead': 184225792,
|
||||
# 'host_memory_free': 3868327936,
|
||||
# 'host_memory_free_computed': 3840843776,
|
||||
# 'host_other_config': {},
|
||||
# 'host_ip_address': '192.168.1.109',
|
||||
# 'host_cpu_info': {},
|
||||
# 'disk_available': 32954957824,
|
||||
# 'disk_total': 50394562560,
|
||||
# 'disk_used': 17439604736,
|
||||
# 'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f',
|
||||
# 'host_name_label': 'xs-mini'}
|
||||
|
||||
# instance_type table has:
|
||||
# name = Column(String(255), unique=True)
|
||||
# memory_mb = Column(Integer)
|
||||
# vcpus = Column(Integer)
|
||||
# local_gb = Column(Integer)
|
||||
# flavorid = Column(Integer, unique=True)
|
||||
# swap = Column(Integer, nullable=False, default=0)
|
||||
# rxtx_quota = Column(Integer, nullable=False, default=0)
|
||||
# rxtx_cap = Column(Integer, nullable=False, default=0)
|
||||
146
nova/scheduler/filters/json_filter.py
Normal file
146
nova/scheduler/filters/json_filter.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import json
|
||||
import operator
|
||||
|
||||
import nova.scheduler
|
||||
from nova.scheduler.filters import abstract_filter
|
||||
|
||||
|
||||
class JsonFilter(abstract_filter.AbstractHostFilter):
|
||||
"""Host Filter to allow simple JSON-based grammar for
|
||||
selecting hosts.
|
||||
"""
|
||||
def _op_compare(self, args, op):
|
||||
"""Returns True if the specified operator can successfully
|
||||
compare the first item in the args with all the rest. Will
|
||||
return False if only one item is in the list.
|
||||
"""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
if op is operator.contains:
|
||||
bad = not args[0] in args[1:]
|
||||
else:
|
||||
bad = [arg for arg in args[1:]
|
||||
if not op(args[0], arg)]
|
||||
return not bool(bad)
|
||||
|
||||
def _equals(self, args):
|
||||
"""First term is == all the other terms."""
|
||||
return self._op_compare(args, operator.eq)
|
||||
|
||||
def _less_than(self, args):
|
||||
"""First term is < all the other terms."""
|
||||
return self._op_compare(args, operator.lt)
|
||||
|
||||
def _greater_than(self, args):
|
||||
"""First term is > all the other terms."""
|
||||
return self._op_compare(args, operator.gt)
|
||||
|
||||
def _in(self, args):
|
||||
"""First term is in set of remaining terms"""
|
||||
return self._op_compare(args, operator.contains)
|
||||
|
||||
def _less_than_equal(self, args):
|
||||
"""First term is <= all the other terms."""
|
||||
return self._op_compare(args, operator.le)
|
||||
|
||||
def _greater_than_equal(self, args):
|
||||
"""First term is >= all the other terms."""
|
||||
return self._op_compare(args, operator.ge)
|
||||
|
||||
def _not(self, args):
|
||||
"""Flip each of the arguments."""
|
||||
return [not arg for arg in args]
|
||||
|
||||
def _or(self, args):
|
||||
"""True if any arg is True."""
|
||||
return any(args)
|
||||
|
||||
def _and(self, args):
|
||||
"""True if all args are True."""
|
||||
return all(args)
|
||||
|
||||
commands = {
|
||||
'=': _equals,
|
||||
'<': _less_than,
|
||||
'>': _greater_than,
|
||||
'in': _in,
|
||||
'<=': _less_than_equal,
|
||||
'>=': _greater_than_equal,
|
||||
'not': _not,
|
||||
'or': _or,
|
||||
'and': _and,
|
||||
}
|
||||
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Convert instance_type into JSON filter object."""
|
||||
required_ram = instance_type['memory_mb']
|
||||
required_disk = instance_type['local_gb']
|
||||
query = ['and',
|
||||
['>=', '$compute.host_memory_free', required_ram],
|
||||
['>=', '$compute.disk_available', required_disk]]
|
||||
return (self._full_name(), json.dumps(query))
|
||||
|
||||
def _parse_string(self, string, host, services):
|
||||
"""Strings prefixed with $ are capability lookups in the
|
||||
form '$service.capability[.subcap*]'.
|
||||
"""
|
||||
if not string:
|
||||
return None
|
||||
if not string.startswith("$"):
|
||||
return string
|
||||
|
||||
path = string[1:].split(".")
|
||||
for item in path:
|
||||
services = services.get(item, None)
|
||||
if not services:
|
||||
return None
|
||||
return services
|
||||
|
||||
def _process_filter(self, zone_manager, query, host, services):
|
||||
"""Recursively parse the query structure."""
|
||||
if not query:
|
||||
return True
|
||||
cmd = query[0]
|
||||
method = self.commands[cmd]
|
||||
cooked_args = []
|
||||
for arg in query[1:]:
|
||||
if isinstance(arg, list):
|
||||
arg = self._process_filter(zone_manager, arg, host, services)
|
||||
elif isinstance(arg, basestring):
|
||||
arg = self._parse_string(arg, host, services)
|
||||
if arg is not None:
|
||||
cooked_args.append(arg)
|
||||
result = method(self, cooked_args)
|
||||
return result
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts that can fulfill the requirements
|
||||
specified in the query.
|
||||
"""
|
||||
expanded = json.loads(query)
|
||||
filtered_hosts = []
|
||||
for host, services in zone_manager.service_states.iteritems():
|
||||
result = self._process_filter(zone_manager, expanded, host,
|
||||
services)
|
||||
if isinstance(result, list):
|
||||
# If any succeeded, include the host
|
||||
result = any(result)
|
||||
if result:
|
||||
filtered_hosts.append((host, services))
|
||||
return filtered_hosts
|
||||
@@ -20,283 +20,33 @@ either incompatible or insufficient to accept a newly-requested instance
|
||||
are removed by Host Filter classes from consideration. Those that pass
|
||||
the filter are then passed on for weighting or other process for ordering.
|
||||
|
||||
Three filters are included: AllHosts, Flavor & JSON. AllHosts just
|
||||
returns the full, unfiltered list of hosts. Flavor is a hard coded
|
||||
matching mechanism based on flavor criteria and JSON is an ad-hoc
|
||||
filter grammar.
|
||||
|
||||
Why JSON? The requests for instances may come in through the
|
||||
REST interface from a user or a parent Zone.
|
||||
Currently Flavors and/or InstanceTypes are used for
|
||||
specifing the type of instance desired. Specific Nova users have
|
||||
noted a need for a more expressive way of specifying instances.
|
||||
Since we don't want to get into building full DSL this is a simple
|
||||
form as an example of how this could be done. In reality, most
|
||||
consumers will use the more rigid filters such as FlavorFilter.
|
||||
Filters are in the 'filters' directory that is off the 'scheduler'
|
||||
directory of nova. Additional filters can be created and added to that
|
||||
directory; be sure to add them to the filters/__init__.py file so that
|
||||
they are part of the nova.schedulers.filters namespace.
|
||||
"""
|
||||
|
||||
import json
|
||||
import types
|
||||
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import utils
|
||||
import nova.scheduler
|
||||
|
||||
LOG = logging.getLogger('nova.scheduler.host_filter')
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('default_host_filter',
|
||||
'nova.scheduler.host_filter.AllHostsFilter',
|
||||
'Which filter to use for filtering hosts.')
|
||||
|
||||
|
||||
class HostFilter(object):
|
||||
"""Base class for host filters."""
|
||||
def _get_filters():
|
||||
# Imported here to avoid circular imports
|
||||
from nova.scheduler import filters
|
||||
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Convert instance_type into a filter for most common use-case."""
|
||||
raise NotImplementedError()
|
||||
def get_itm(nm):
|
||||
return getattr(filters, nm)
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts that fulfill the filter."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _full_name(self):
|
||||
"""module.classname of the filter."""
|
||||
return "%s.%s" % (self.__module__, self.__class__.__name__)
|
||||
|
||||
|
||||
class AllHostsFilter(HostFilter):
|
||||
""" NOP host filter. Returns all hosts in ZoneManager.
|
||||
This essentially does what the old Scheduler+Chance used
|
||||
to give us.
|
||||
"""
|
||||
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Return anything to prevent base-class from raising
|
||||
exception."""
|
||||
return (self._full_name(), instance_type)
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts from ZoneManager list."""
|
||||
return [(host, services)
|
||||
for host, services in zone_manager.service_states.iteritems()]
|
||||
|
||||
|
||||
class InstanceTypeFilter(HostFilter):
|
||||
"""HostFilter hard-coded to work with InstanceType records."""
|
||||
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Use instance_type to filter hosts."""
|
||||
return (self._full_name(), instance_type)
|
||||
|
||||
def _satisfies_extra_specs(self, capabilities, instance_type):
|
||||
"""Check that the capabilities provided by the compute service
|
||||
satisfy the extra specs associated with the instance type"""
|
||||
|
||||
if 'extra_specs' not in instance_type:
|
||||
return True
|
||||
|
||||
# Note(lorinh): For now, we are just checking exact matching on the
|
||||
# values. Later on, we want to handle numerical
|
||||
# values so we can represent things like number of GPU cards
|
||||
|
||||
try:
|
||||
for key, value in instance_type['extra_specs'].iteritems():
|
||||
if capabilities[key] != value:
|
||||
return False
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts that can create instance_type."""
|
||||
instance_type = query
|
||||
selected_hosts = []
|
||||
for host, services in zone_manager.service_states.iteritems():
|
||||
capabilities = services.get('compute', {})
|
||||
host_ram_mb = capabilities['host_memory_free']
|
||||
disk_bytes = capabilities['disk_available']
|
||||
spec_ram = instance_type['memory_mb']
|
||||
spec_disk = instance_type['local_gb']
|
||||
extra_specs = instance_type['extra_specs']
|
||||
|
||||
if ((host_ram_mb >= spec_ram) and (disk_bytes >= spec_disk) and
|
||||
self._satisfies_extra_specs(capabilities, instance_type)):
|
||||
selected_hosts.append((host, capabilities))
|
||||
return selected_hosts
|
||||
|
||||
#host entries (currently) are like:
|
||||
# {'host_name-description': 'Default install of XenServer',
|
||||
# 'host_hostname': 'xs-mini',
|
||||
# 'host_memory_total': 8244539392,
|
||||
# 'host_memory_overhead': 184225792,
|
||||
# 'host_memory_free': 3868327936,
|
||||
# 'host_memory_free_computed': 3840843776,
|
||||
# 'host_other_config': {},
|
||||
# 'host_ip_address': '192.168.1.109',
|
||||
# 'host_cpu_info': {},
|
||||
# 'disk_available': 32954957824,
|
||||
# 'disk_total': 50394562560,
|
||||
# 'disk_used': 17439604736,
|
||||
# 'host_uuid': 'cedb9b39-9388-41df-8891-c5c9a0c0fe5f',
|
||||
# 'host_name_label': 'xs-mini'}
|
||||
|
||||
# instance_type table has:
|
||||
#name = Column(String(255), unique=True)
|
||||
#memory_mb = Column(Integer)
|
||||
#vcpus = Column(Integer)
|
||||
#local_gb = Column(Integer)
|
||||
#flavorid = Column(Integer, unique=True)
|
||||
#swap = Column(Integer, nullable=False, default=0)
|
||||
#rxtx_quota = Column(Integer, nullable=False, default=0)
|
||||
#rxtx_cap = Column(Integer, nullable=False, default=0)
|
||||
|
||||
|
||||
class JsonFilter(HostFilter):
|
||||
"""Host Filter to allow simple JSON-based grammar for
|
||||
selecting hosts.
|
||||
"""
|
||||
|
||||
def _equals(self, args):
|
||||
"""First term is == all the other terms."""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
lhs = args[0]
|
||||
for rhs in args[1:]:
|
||||
if lhs != rhs:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _less_than(self, args):
|
||||
"""First term is < all the other terms."""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
lhs = args[0]
|
||||
for rhs in args[1:]:
|
||||
if lhs >= rhs:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _greater_than(self, args):
|
||||
"""First term is > all the other terms."""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
lhs = args[0]
|
||||
for rhs in args[1:]:
|
||||
if lhs <= rhs:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _in(self, args):
|
||||
"""First term is in set of remaining terms"""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
return args[0] in args[1:]
|
||||
|
||||
def _less_than_equal(self, args):
|
||||
"""First term is <= all the other terms."""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
lhs = args[0]
|
||||
for rhs in args[1:]:
|
||||
if lhs > rhs:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _greater_than_equal(self, args):
|
||||
"""First term is >= all the other terms."""
|
||||
if len(args) < 2:
|
||||
return False
|
||||
lhs = args[0]
|
||||
for rhs in args[1:]:
|
||||
if lhs < rhs:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _not(self, args):
|
||||
"""Flip each of the arguments."""
|
||||
if len(args) == 0:
|
||||
return False
|
||||
return [not arg for arg in args]
|
||||
|
||||
def _or(self, args):
|
||||
"""True if any arg is True."""
|
||||
return True in args
|
||||
|
||||
def _and(self, args):
|
||||
"""True if all args are True."""
|
||||
return False not in args
|
||||
|
||||
commands = {
|
||||
'=': _equals,
|
||||
'<': _less_than,
|
||||
'>': _greater_than,
|
||||
'in': _in,
|
||||
'<=': _less_than_equal,
|
||||
'>=': _greater_than_equal,
|
||||
'not': _not,
|
||||
'or': _or,
|
||||
'and': _and,
|
||||
}
|
||||
|
||||
def instance_type_to_filter(self, instance_type):
|
||||
"""Convert instance_type into JSON filter object."""
|
||||
required_ram = instance_type['memory_mb']
|
||||
required_disk = instance_type['local_gb']
|
||||
query = ['and',
|
||||
['>=', '$compute.host_memory_free', required_ram],
|
||||
['>=', '$compute.disk_available', required_disk]]
|
||||
return (self._full_name(), json.dumps(query))
|
||||
|
||||
def _parse_string(self, string, host, services):
|
||||
"""Strings prefixed with $ are capability lookups in the
|
||||
form '$service.capability[.subcap*]'
|
||||
"""
|
||||
if not string:
|
||||
return None
|
||||
if string[0] != '$':
|
||||
return string
|
||||
|
||||
path = string[1:].split('.')
|
||||
for item in path:
|
||||
services = services.get(item, None)
|
||||
if not services:
|
||||
return None
|
||||
return services
|
||||
|
||||
def _process_filter(self, zone_manager, query, host, services):
|
||||
"""Recursively parse the query structure."""
|
||||
if len(query) == 0:
|
||||
return True
|
||||
cmd = query[0]
|
||||
method = self.commands[cmd] # Let exception fly.
|
||||
cooked_args = []
|
||||
for arg in query[1:]:
|
||||
if isinstance(arg, list):
|
||||
arg = self._process_filter(zone_manager, arg, host, services)
|
||||
elif isinstance(arg, basestring):
|
||||
arg = self._parse_string(arg, host, services)
|
||||
if arg != None:
|
||||
cooked_args.append(arg)
|
||||
result = method(self, cooked_args)
|
||||
return result
|
||||
|
||||
def filter_hosts(self, zone_manager, query):
|
||||
"""Return a list of hosts that can fulfill filter."""
|
||||
expanded = json.loads(query)
|
||||
hosts = []
|
||||
for host, services in zone_manager.service_states.iteritems():
|
||||
r = self._process_filter(zone_manager, expanded, host, services)
|
||||
if isinstance(r, list):
|
||||
r = True in r
|
||||
if r:
|
||||
hosts.append((host, services))
|
||||
return hosts
|
||||
|
||||
|
||||
FILTERS = [AllHostsFilter, InstanceTypeFilter, JsonFilter]
|
||||
return [get_itm(itm) for itm in dir(filters)
|
||||
if (type(get_itm(itm)) is types.TypeType)
|
||||
and issubclass(get_itm(itm), filters.AbstractHostFilter)
|
||||
and get_itm(itm) is not filters.AbstractHostFilter]
|
||||
|
||||
|
||||
def choose_host_filter(filter_name=None):
|
||||
@@ -307,8 +57,7 @@ def choose_host_filter(filter_name=None):
|
||||
"""
|
||||
if not filter_name:
|
||||
filter_name = FLAGS.default_host_filter
|
||||
for filter_class in FILTERS:
|
||||
host_match = "%s.%s" % (filter_class.__module__, filter_class.__name__)
|
||||
if host_match == filter_name:
|
||||
for filter_class in _get_filters():
|
||||
if filter_class.__name__ == filter_name:
|
||||
return filter_class()
|
||||
raise exception.SchedulerHostFilterNotFound(filter_name=filter_name)
|
||||
|
||||
@@ -22,14 +22,12 @@ The cost-function and weights are tabulated, and the host with the least cost
|
||||
is then selected for provisioning.
|
||||
"""
|
||||
|
||||
# TODO(dabo): This class will be removed in the next merge prop; it remains now
|
||||
# because much of the code will be refactored into different classes.
|
||||
|
||||
import collections
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova.scheduler import abstract_scheduler
|
||||
from nova.scheduler import base_scheduler
|
||||
from nova import utils
|
||||
from nova import exception
|
||||
|
||||
@@ -37,14 +35,16 @@ LOG = logging.getLogger('nova.scheduler.least_cost')
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_list('least_cost_scheduler_cost_functions',
|
||||
['nova.scheduler.least_cost.noop_cost_fn'],
|
||||
'Which cost functions the LeastCostScheduler should use.')
|
||||
['nova.scheduler.least_cost.noop_cost_fn'],
|
||||
'Which cost functions the LeastCostScheduler should use.')
|
||||
|
||||
|
||||
# TODO(sirp): Once we have enough of these rules, we can break them out into a
|
||||
# cost_functions.py file (perhaps in a least_cost_scheduler directory)
|
||||
flags.DEFINE_integer('noop_cost_fn_weight', 1,
|
||||
'How much weight to give the noop cost function')
|
||||
'How much weight to give the noop cost function')
|
||||
flags.DEFINE_integer('compute_fill_first_cost_fn_weight', 1,
|
||||
'How much weight to give the fill-first cost function')
|
||||
|
||||
|
||||
def noop_cost_fn(host):
|
||||
@@ -52,87 +52,20 @@ def noop_cost_fn(host):
|
||||
return 1
|
||||
|
||||
|
||||
flags.DEFINE_integer('compute_fill_first_cost_fn_weight', 1,
|
||||
'How much weight to give the fill-first cost function')
|
||||
|
||||
|
||||
def compute_fill_first_cost_fn(host):
|
||||
"""Prefer hosts that have less ram available, filter_hosts will exclude
|
||||
hosts that don't have enough ram"""
|
||||
hostname, caps = host
|
||||
free_mem = caps['host_memory_free']
|
||||
hosts that don't have enough ram.
|
||||
"""
|
||||
hostname, service = host
|
||||
caps = service.get("compute", {})
|
||||
free_mem = caps.get("host_memory_free", 0)
|
||||
return free_mem
|
||||
|
||||
|
||||
class LeastCostScheduler(abstract_scheduler.AbstractScheduler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cost_fns_cache = {}
|
||||
super(LeastCostScheduler, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_cost_fns(self, topic):
|
||||
"""Returns a list of tuples containing weights and cost functions to
|
||||
use for weighing hosts
|
||||
"""
|
||||
|
||||
if topic in self.cost_fns_cache:
|
||||
return self.cost_fns_cache[topic]
|
||||
|
||||
cost_fns = []
|
||||
for cost_fn_str in FLAGS.least_cost_scheduler_cost_functions:
|
||||
if '.' in cost_fn_str:
|
||||
short_name = cost_fn_str.split('.')[-1]
|
||||
else:
|
||||
short_name = cost_fn_str
|
||||
cost_fn_str = "%s.%s.%s" % (
|
||||
__name__, self.__class__.__name__, short_name)
|
||||
|
||||
if not (short_name.startswith('%s_' % topic) or
|
||||
short_name.startswith('noop')):
|
||||
continue
|
||||
|
||||
try:
|
||||
# NOTE(sirp): import_class is somewhat misnamed since it can
|
||||
# any callable from a module
|
||||
cost_fn = utils.import_class(cost_fn_str)
|
||||
except exception.ClassNotFound:
|
||||
raise exception.SchedulerCostFunctionNotFound(
|
||||
cost_fn_str=cost_fn_str)
|
||||
|
||||
try:
|
||||
flag_name = "%s_weight" % cost_fn.__name__
|
||||
weight = getattr(FLAGS, flag_name)
|
||||
except AttributeError:
|
||||
raise exception.SchedulerWeightFlagNotFound(
|
||||
flag_name=flag_name)
|
||||
|
||||
cost_fns.append((weight, cost_fn))
|
||||
|
||||
self.cost_fns_cache[topic] = cost_fns
|
||||
return cost_fns
|
||||
|
||||
def weigh_hosts(self, topic, request_spec, hosts):
|
||||
"""Returns a list of dictionaries of form:
|
||||
[ {weight: weight, hostname: hostname, capabilities: capabs} ]
|
||||
"""
|
||||
|
||||
cost_fns = self.get_cost_fns(topic)
|
||||
costs = weighted_sum(domain=hosts, weighted_fns=cost_fns)
|
||||
|
||||
weighted = []
|
||||
weight_log = []
|
||||
for cost, (hostname, caps) in zip(costs, hosts):
|
||||
weight_log.append("%s: %s" % (hostname, "%.2f" % cost))
|
||||
weight_dict = dict(weight=cost, hostname=hostname,
|
||||
capabilities=caps)
|
||||
weighted.append(weight_dict)
|
||||
|
||||
LOG.debug(_("Weighted Costs => %s") % weight_log)
|
||||
return weighted
|
||||
|
||||
|
||||
def normalize_list(L):
|
||||
"""Normalize an array of numbers such that each element satisfies:
|
||||
0 <= e <= 1"""
|
||||
0 <= e <= 1
|
||||
"""
|
||||
if not L:
|
||||
return L
|
||||
max_ = max(L)
|
||||
@@ -160,12 +93,10 @@ def weighted_sum(domain, weighted_fns, normalize=True):
|
||||
score_table = collections.defaultdict(list)
|
||||
for weight, fn in weighted_fns:
|
||||
scores = [fn(elem) for elem in domain]
|
||||
|
||||
if normalize:
|
||||
norm_scores = normalize_list(scores)
|
||||
else:
|
||||
norm_scores = scores
|
||||
|
||||
for idx, score in enumerate(norm_scores):
|
||||
weighted_score = score * weight
|
||||
score_table[idx].append(weighted_score)
|
||||
@@ -175,5 +106,66 @@ def weighted_sum(domain, weighted_fns, normalize=True):
|
||||
for idx in sorted(score_table):
|
||||
elem_score = sum(score_table[idx])
|
||||
domain_scores.append(elem_score)
|
||||
|
||||
return domain_scores
|
||||
|
||||
|
||||
class LeastCostScheduler(base_scheduler.BaseScheduler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cost_fns_cache = {}
|
||||
super(LeastCostScheduler, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_cost_fns(self, topic):
|
||||
"""Returns a list of tuples containing weights and cost functions to
|
||||
use for weighing hosts
|
||||
"""
|
||||
if topic in self.cost_fns_cache:
|
||||
return self.cost_fns_cache[topic]
|
||||
cost_fns = []
|
||||
for cost_fn_str in FLAGS.least_cost_scheduler_cost_functions:
|
||||
if '.' in cost_fn_str:
|
||||
short_name = cost_fn_str.split('.')[-1]
|
||||
else:
|
||||
short_name = cost_fn_str
|
||||
cost_fn_str = "%s.%s.%s" % (
|
||||
__name__, self.__class__.__name__, short_name)
|
||||
if not (short_name.startswith('%s_' % topic) or
|
||||
short_name.startswith('noop')):
|
||||
continue
|
||||
|
||||
try:
|
||||
# NOTE(sirp): import_class is somewhat misnamed since it can
|
||||
# any callable from a module
|
||||
cost_fn = utils.import_class(cost_fn_str)
|
||||
except exception.ClassNotFound:
|
||||
raise exception.SchedulerCostFunctionNotFound(
|
||||
cost_fn_str=cost_fn_str)
|
||||
|
||||
try:
|
||||
flag_name = "%s_weight" % cost_fn.__name__
|
||||
weight = getattr(FLAGS, flag_name)
|
||||
except AttributeError:
|
||||
raise exception.SchedulerWeightFlagNotFound(
|
||||
flag_name=flag_name)
|
||||
cost_fns.append((weight, cost_fn))
|
||||
|
||||
self.cost_fns_cache[topic] = cost_fns
|
||||
return cost_fns
|
||||
|
||||
def weigh_hosts(self, topic, request_spec, hosts):
|
||||
"""Returns a list of dictionaries of form:
|
||||
[ {weight: weight, hostname: hostname, capabilities: capabs} ]
|
||||
"""
|
||||
cost_fns = self.get_cost_fns(topic)
|
||||
costs = weighted_sum(domain=hosts, weighted_fns=cost_fns)
|
||||
|
||||
weighted = []
|
||||
weight_log = []
|
||||
for cost, (hostname, service) in zip(costs, hosts):
|
||||
caps = service[topic]
|
||||
weight_log.append("%s: %s" % (hostname, "%.2f" % cost))
|
||||
weight_dict = dict(weight=cost, hostname=hostname,
|
||||
capabilities=caps)
|
||||
weighted.append(weight_dict)
|
||||
|
||||
LOG.debug(_("Weighted Costs => %s") % weight_log)
|
||||
return weighted
|
||||
|
||||
@@ -20,13 +20,11 @@
|
||||
"""Generic Node baseclass for all workers that run on hosts."""
|
||||
|
||||
import inspect
|
||||
import multiprocessing
|
||||
import os
|
||||
|
||||
import eventlet
|
||||
import greenlet
|
||||
|
||||
from eventlet import greenthread
|
||||
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import exception
|
||||
@@ -69,30 +67,25 @@ class Launcher(object):
|
||||
self._services = []
|
||||
|
||||
@staticmethod
|
||||
def run_service(service):
|
||||
"""Start and wait for a service to finish.
|
||||
def run_server(server):
|
||||
"""Start and wait for a server to finish.
|
||||
|
||||
:param service: Service to run and wait for.
|
||||
:param service: Server to run and wait for.
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
service.start()
|
||||
try:
|
||||
service.wait()
|
||||
except KeyboardInterrupt:
|
||||
service.stop()
|
||||
server.start()
|
||||
server.wait()
|
||||
|
||||
def launch_service(self, service):
|
||||
"""Load and start the given service.
|
||||
def launch_server(self, server):
|
||||
"""Load and start the given server.
|
||||
|
||||
:param service: The service you would like to start.
|
||||
:param server: The server you would like to start.
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
process = multiprocessing.Process(target=self.run_service,
|
||||
args=(service,))
|
||||
process.start()
|
||||
self._services.append(process)
|
||||
gt = eventlet.spawn(self.run_server, server)
|
||||
self._services.append(gt)
|
||||
|
||||
def stop(self):
|
||||
"""Stop all services which are currently running.
|
||||
@@ -101,8 +94,7 @@ class Launcher(object):
|
||||
|
||||
"""
|
||||
for service in self._services:
|
||||
if service.is_alive():
|
||||
service.terminate()
|
||||
service.kill()
|
||||
|
||||
def wait(self):
|
||||
"""Waits until all services have been stopped, and then returns.
|
||||
@@ -111,11 +103,18 @@ class Launcher(object):
|
||||
|
||||
"""
|
||||
for service in self._services:
|
||||
service.join()
|
||||
try:
|
||||
service.wait()
|
||||
except greenlet.GreenletExit:
|
||||
pass
|
||||
|
||||
|
||||
class Service(object):
|
||||
"""Base class for workers that run on hosts."""
|
||||
"""Service object for binaries running on hosts.
|
||||
|
||||
A service takes a manager and enables rpc by listening to queues based
|
||||
on topic. It also periodically runs tasks on the manager and reports
|
||||
it state to the database services table."""
|
||||
|
||||
def __init__(self, host, binary, topic, manager, report_interval=None,
|
||||
periodic_interval=None, *args, **kwargs):
|
||||
@@ -173,7 +172,7 @@ class Service(object):
|
||||
finally:
|
||||
consumer_set.close()
|
||||
|
||||
self.consumer_set_thread = greenthread.spawn(_wait)
|
||||
self.consumer_set_thread = eventlet.spawn(_wait)
|
||||
|
||||
if self.report_interval:
|
||||
pulse = utils.LoopingCall(self.report_state)
|
||||
@@ -293,9 +292,9 @@ class WSGIService(object):
|
||||
"""Provides ability to launch API from a 'paste' configuration."""
|
||||
|
||||
def __init__(self, name, loader=None):
|
||||
"""Initialize, but do not start the WSGI service.
|
||||
"""Initialize, but do not start the WSGI server.
|
||||
|
||||
:param name: The name of the WSGI service given to the loader.
|
||||
:param name: The name of the WSGI server given to the loader.
|
||||
:param loader: Loads the WSGI application using the given name.
|
||||
:returns: None
|
||||
|
||||
@@ -339,32 +338,32 @@ class WSGIService(object):
|
||||
self.server.wait()
|
||||
|
||||
|
||||
def serve(*services):
|
||||
try:
|
||||
if not services:
|
||||
services = [Service.create()]
|
||||
except Exception:
|
||||
logging.exception('in Service.create()')
|
||||
raise
|
||||
finally:
|
||||
# After we've loaded up all our dynamic bits, check
|
||||
# whether we should print help
|
||||
flags.DEFINE_flag(flags.HelpFlag())
|
||||
flags.DEFINE_flag(flags.HelpshortFlag())
|
||||
flags.DEFINE_flag(flags.HelpXMLFlag())
|
||||
FLAGS.ParseNewFlags()
|
||||
# NOTE(vish): the global launcher is to maintain the existing
|
||||
# functionality of calling service.serve +
|
||||
# service.wait
|
||||
_launcher = None
|
||||
|
||||
name = '_'.join(x.binary for x in services)
|
||||
logging.debug(_('Serving %s'), name)
|
||||
|
||||
def serve(*servers):
|
||||
global _launcher
|
||||
if not _launcher:
|
||||
_launcher = Launcher()
|
||||
for server in servers:
|
||||
_launcher.launch_server(server)
|
||||
|
||||
|
||||
def wait():
|
||||
# After we've loaded up all our dynamic bits, check
|
||||
# whether we should print help
|
||||
flags.DEFINE_flag(flags.HelpFlag())
|
||||
flags.DEFINE_flag(flags.HelpshortFlag())
|
||||
flags.DEFINE_flag(flags.HelpXMLFlag())
|
||||
FLAGS.ParseNewFlags()
|
||||
logging.debug(_('Full set of FLAGS:'))
|
||||
for flag in FLAGS:
|
||||
flag_get = FLAGS.get(flag, None)
|
||||
logging.debug('%(flag)s : %(flag_get)s' % locals())
|
||||
|
||||
for x in services:
|
||||
x.start()
|
||||
|
||||
|
||||
def wait():
|
||||
while True:
|
||||
greenthread.sleep(5)
|
||||
try:
|
||||
_launcher.wait()
|
||||
except KeyboardInterrupt:
|
||||
_launcher.stop()
|
||||
|
||||
@@ -44,14 +44,15 @@ class RateLimitingMiddlewareTest(test.TestCase):
|
||||
action = middleware.get_action_name(req)
|
||||
self.assertEqual(action, action_name)
|
||||
|
||||
verify('PUT', '/servers/4', 'PUT')
|
||||
verify('DELETE', '/servers/4', 'DELETE')
|
||||
verify('POST', '/images/4', 'POST')
|
||||
verify('POST', '/servers/4', 'POST servers')
|
||||
verify('GET', '/foo?a=4&changes-since=never&b=5', 'GET changes-since')
|
||||
verify('GET', '/foo?a=4&monkeys-since=never&b=5', None)
|
||||
verify('GET', '/servers/4', None)
|
||||
verify('HEAD', '/servers/4', None)
|
||||
verify('PUT', '/fake/servers/4', 'PUT')
|
||||
verify('DELETE', '/fake/servers/4', 'DELETE')
|
||||
verify('POST', '/fake/images/4', 'POST')
|
||||
verify('POST', '/fake/servers/4', 'POST servers')
|
||||
verify('GET', '/fake/foo?a=4&changes-since=never&b=5',
|
||||
'GET changes-since')
|
||||
verify('GET', '/fake/foo?a=4&monkeys-since=never&b=5', None)
|
||||
verify('GET', '/fake/servers/4', None)
|
||||
verify('HEAD', '/fake/servers/4', None)
|
||||
|
||||
def exhaust(self, middleware, method, url, username, times):
|
||||
req = Request.blank(url, dict(REQUEST_METHOD=method),
|
||||
@@ -67,13 +68,13 @@ class RateLimitingMiddlewareTest(test.TestCase):
|
||||
|
||||
def test_single_action(self):
|
||||
middleware = RateLimitingMiddleware(simple_wsgi)
|
||||
self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100)
|
||||
self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100)
|
||||
self.exhaust(middleware, 'DELETE', '/fake/servers/4', 'usr1', 100)
|
||||
self.exhaust(middleware, 'DELETE', '/fake/servers/4', 'usr2', 100)
|
||||
|
||||
def test_POST_servers_action_implies_POST_action(self):
|
||||
middleware = RateLimitingMiddleware(simple_wsgi)
|
||||
self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10)
|
||||
self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10)
|
||||
self.exhaust(middleware, 'POST', '/fake/servers/4', 'usr1', 10)
|
||||
self.exhaust(middleware, 'POST', '/fake/images/4', 'usr2', 10)
|
||||
self.assertTrue(set(middleware.limiter._levels) == \
|
||||
set(['usr1:POST', 'usr1:POST servers', 'usr2:POST']))
|
||||
|
||||
@@ -81,11 +82,11 @@ class RateLimitingMiddlewareTest(test.TestCase):
|
||||
middleware = RateLimitingMiddleware(simple_wsgi)
|
||||
# Use up all of our "POST" allowance for the minute, 5 times
|
||||
for i in range(5):
|
||||
self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10)
|
||||
self.exhaust(middleware, 'POST', '/fake/servers/4', 'usr1', 10)
|
||||
# Reset the 'POST' action counter.
|
||||
del middleware.limiter._levels['usr1:POST']
|
||||
# All 50 daily "POST servers" actions should be all used up
|
||||
self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0)
|
||||
self.exhaust(middleware, 'POST', '/fake/servers/4', 'usr1', 0)
|
||||
|
||||
def test_proxy_ctor_works(self):
|
||||
middleware = RateLimitingMiddleware(simple_wsgi)
|
||||
|
||||
306
nova/tests/api/openstack/contrib/test_createserverext.py
Normal file
306
nova/tests/api/openstack/contrib/test_createserverext.py
Normal file
@@ -0,0 +1,306 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import base64
|
||||
import json
|
||||
import unittest
|
||||
from xml.dom import minidom
|
||||
|
||||
import stubout
|
||||
import webob
|
||||
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova import test
|
||||
from nova import utils
|
||||
import nova.api.openstack
|
||||
from nova.api.openstack import servers
|
||||
from nova.api.openstack.contrib import createserverext
|
||||
import nova.compute.api
|
||||
|
||||
import nova.scheduler.api
|
||||
import nova.image.fake
|
||||
import nova.rpc
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.verbose = True
|
||||
|
||||
FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
|
||||
FAKE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '10.0.2.12')]
|
||||
|
||||
DUPLICATE_NETWORKS = [('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12'),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '10.0.1.12')]
|
||||
|
||||
INVALID_NETWORKS = [('invalid', 'invalid-ip-address')]
|
||||
|
||||
|
||||
class CreateserverextTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CreateserverextTest, self).setUp()
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
fakes.FakeAuthManager.auth_data = {}
|
||||
fakes.FakeAuthDatabase.data = {}
|
||||
fakes.stub_out_auth(self.stubs)
|
||||
fakes.stub_out_image_service(self.stubs)
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
self.allow_admin = FLAGS.allow_admin_api
|
||||
|
||||
def tearDown(self):
|
||||
self.stubs.UnsetAll()
|
||||
FLAGS.allow_admin_api = self.allow_admin
|
||||
super(CreateserverextTest, self).tearDown()
|
||||
|
||||
def _setup_mock_compute_api(self):
|
||||
|
||||
class MockComputeAPI(nova.compute.API):
|
||||
|
||||
def __init__(self):
|
||||
self.injected_files = None
|
||||
self.networks = None
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
if 'injected_files' in kwargs:
|
||||
self.injected_files = kwargs['injected_files']
|
||||
else:
|
||||
self.injected_files = None
|
||||
|
||||
if 'requested_networks' in kwargs:
|
||||
self.networks = kwargs['requested_networks']
|
||||
else:
|
||||
self.networks = None
|
||||
return [{'id': '1234', 'display_name': 'fakeinstance',
|
||||
'uuid': FAKE_UUID,
|
||||
'created_at': "",
|
||||
'updated_at': ""}]
|
||||
|
||||
def set_admin_password(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def make_stub_method(canned_return):
|
||||
def stub_method(*args, **kwargs):
|
||||
return canned_return
|
||||
return stub_method
|
||||
|
||||
compute_api = MockComputeAPI()
|
||||
self.stubs.Set(nova.compute, 'API', make_stub_method(compute_api))
|
||||
self.stubs.Set(
|
||||
nova.api.openstack.create_instance_helper.CreateInstanceHelper,
|
||||
'_get_kernel_ramdisk_from_image', make_stub_method((1, 1)))
|
||||
return compute_api
|
||||
|
||||
def _create_networks_request_dict(self, networks):
|
||||
server = {}
|
||||
server['name'] = 'new-server-test'
|
||||
server['imageRef'] = 1
|
||||
server['flavorRef'] = 1
|
||||
if networks is not None:
|
||||
network_list = []
|
||||
for uuid, fixed_ip in networks:
|
||||
network_list.append({'uuid': uuid, 'fixed_ip': fixed_ip})
|
||||
server['networks'] = network_list
|
||||
return {'server': server}
|
||||
|
||||
def _get_create_request_json(self, body_dict):
|
||||
req = webob.Request.blank('/v1.1/123/os-create-server-ext')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body_dict)
|
||||
return req
|
||||
|
||||
def _run_create_instance_with_mock_compute_api(self, request):
|
||||
compute_api = self._setup_mock_compute_api()
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
return compute_api, response
|
||||
|
||||
def _format_xml_request_body(self, body_dict):
|
||||
server = body_dict['server']
|
||||
body_parts = []
|
||||
body_parts.extend([
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<server xmlns="http://docs.rackspacecloud.com/servers/api/v1.1"',
|
||||
' name="%s" imageRef="%s" flavorRef="%s">' % (
|
||||
server['name'], server['imageRef'], server['flavorRef'])])
|
||||
if 'metadata' in server:
|
||||
metadata = server['metadata']
|
||||
body_parts.append('<metadata>')
|
||||
for item in metadata.iteritems():
|
||||
body_parts.append('<meta key="%s">%s</meta>' % item)
|
||||
body_parts.append('</metadata>')
|
||||
if 'personality' in server:
|
||||
personalities = server['personality']
|
||||
body_parts.append('<personality>')
|
||||
for file in personalities:
|
||||
item = (file['path'], file['contents'])
|
||||
body_parts.append('<file path="%s">%s</file>' % item)
|
||||
body_parts.append('</personality>')
|
||||
if 'networks' in server:
|
||||
networks = server['networks']
|
||||
body_parts.append('<networks>')
|
||||
for network in networks:
|
||||
item = (network['uuid'], network['fixed_ip'])
|
||||
body_parts.append('<network uuid="%s" fixed_ip="%s"></network>'
|
||||
% item)
|
||||
body_parts.append('</networks>')
|
||||
body_parts.append('</server>')
|
||||
return ''.join(body_parts)
|
||||
|
||||
def _get_create_request_xml(self, body_dict):
|
||||
req = webob.Request.blank('/v1.1/123/os-create-server-ext')
|
||||
req.content_type = 'application/xml'
|
||||
req.accept = 'application/xml'
|
||||
req.method = 'POST'
|
||||
req.body = self._format_xml_request_body(body_dict)
|
||||
return req
|
||||
|
||||
def _create_instance_with_networks_json(self, networks):
|
||||
body_dict = self._create_networks_request_dict(networks)
|
||||
request = self._get_create_request_json(body_dict)
|
||||
compute_api, response = \
|
||||
self._run_create_instance_with_mock_compute_api(request)
|
||||
return request, response, compute_api.networks
|
||||
|
||||
def _create_instance_with_networks_xml(self, networks):
|
||||
body_dict = self._create_networks_request_dict(networks)
|
||||
request = self._get_create_request_xml(body_dict)
|
||||
compute_api, response = \
|
||||
self._run_create_instance_with_mock_compute_api(request)
|
||||
return request, response, compute_api.networks
|
||||
|
||||
def test_create_instance_with_no_networks(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_json(networks=None)
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_no_networks_xml(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_xml(networks=None)
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_one_network(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_json([FAKE_NETWORKS[0]])
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(networks, [FAKE_NETWORKS[0]])
|
||||
|
||||
def test_create_instance_with_one_network_xml(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_xml([FAKE_NETWORKS[0]])
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(networks, [FAKE_NETWORKS[0]])
|
||||
|
||||
def test_create_instance_with_two_networks(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_json(FAKE_NETWORKS)
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(networks, FAKE_NETWORKS)
|
||||
|
||||
def test_create_instance_with_two_networks_xml(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_xml(FAKE_NETWORKS)
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(networks, FAKE_NETWORKS)
|
||||
|
||||
def test_create_instance_with_duplicate_networks(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_json(DUPLICATE_NETWORKS)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_duplicate_networks_xml(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_xml(DUPLICATE_NETWORKS)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_network_no_id(self):
|
||||
body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
|
||||
del body_dict['server']['networks'][0]['uuid']
|
||||
request = self._get_create_request_json(body_dict)
|
||||
compute_api, response = \
|
||||
self._run_create_instance_with_mock_compute_api(request)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(compute_api.networks, None)
|
||||
|
||||
def test_create_instance_with_network_no_id_xml(self):
|
||||
body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
|
||||
request = self._get_create_request_xml(body_dict)
|
||||
uuid = ' uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"'
|
||||
request.body = request.body.replace(uuid, '')
|
||||
compute_api, response = \
|
||||
self._run_create_instance_with_mock_compute_api(request)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(compute_api.networks, None)
|
||||
|
||||
def test_create_instance_with_network_invalid_id(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_json(INVALID_NETWORKS)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_network_invalid_id_xml(self):
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_xml(INVALID_NETWORKS)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_network_empty_fixed_ip(self):
|
||||
networks = [('1', '')]
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_json(networks)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_network_non_string_fixed_ip(self):
|
||||
networks = [('1', 12345)]
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_json(networks)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_network_empty_fixed_ip_xml(self):
|
||||
networks = [('1', '')]
|
||||
request, response, networks = \
|
||||
self._create_instance_with_networks_xml(networks)
|
||||
self.assertEquals(response.status_int, 400)
|
||||
self.assertEquals(networks, None)
|
||||
|
||||
def test_create_instance_with_network_no_fixed_ip(self):
|
||||
body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
|
||||
del body_dict['server']['networks'][0]['fixed_ip']
|
||||
request = self._get_create_request_json(body_dict)
|
||||
compute_api, response = \
|
||||
self._run_create_instance_with_mock_compute_api(request)
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(compute_api.networks,
|
||||
[('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)])
|
||||
|
||||
def test_create_instance_with_network_no_fixed_ip_xml(self):
|
||||
body_dict = self._create_networks_request_dict([FAKE_NETWORKS[0]])
|
||||
request = self._get_create_request_xml(body_dict)
|
||||
request.body = request.body.replace(' fixed_ip="10.0.1.12"', '')
|
||||
compute_api, response = \
|
||||
self._run_create_instance_with_mock_compute_api(request)
|
||||
self.assertEquals(response.status_int, 202)
|
||||
self.assertEquals(compute_api.networks,
|
||||
[('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', None)])
|
||||
@@ -17,6 +17,7 @@ import json
|
||||
import stubout
|
||||
import webob
|
||||
|
||||
from nova import compute
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import test
|
||||
@@ -29,6 +30,11 @@ from nova.api.openstack.contrib.floating_ips import _translate_floating_ip_view
|
||||
|
||||
|
||||
def network_api_get_floating_ip(self, context, id):
|
||||
return {'id': 1, 'address': '10.10.10.10',
|
||||
'fixed_ip': None}
|
||||
|
||||
|
||||
def network_api_get_floating_ip_by_ip(self, context, address):
|
||||
return {'id': 1, 'address': '10.10.10.10',
|
||||
'fixed_ip': {'address': '11.0.0.1'}}
|
||||
|
||||
@@ -50,7 +56,7 @@ def network_api_release(self, context, address):
|
||||
pass
|
||||
|
||||
|
||||
def network_api_associate(self, context, floating_ip, fixed_ip):
|
||||
def compute_api_associate(self, context, instance_id, floating_ip):
|
||||
pass
|
||||
|
||||
|
||||
@@ -78,14 +84,16 @@ class FloatingIpTest(test.TestCase):
|
||||
fakes.stub_out_rate_limiting(self.stubs)
|
||||
self.stubs.Set(network.api.API, "get_floating_ip",
|
||||
network_api_get_floating_ip)
|
||||
self.stubs.Set(network.api.API, "get_floating_ip_by_ip",
|
||||
network_api_get_floating_ip)
|
||||
self.stubs.Set(network.api.API, "list_floating_ips",
|
||||
network_api_list_floating_ips)
|
||||
self.stubs.Set(network.api.API, "allocate_floating_ip",
|
||||
network_api_allocate)
|
||||
self.stubs.Set(network.api.API, "release_floating_ip",
|
||||
network_api_release)
|
||||
self.stubs.Set(network.api.API, "associate_floating_ip",
|
||||
network_api_associate)
|
||||
self.stubs.Set(compute.api.API, "associate_floating_ip",
|
||||
compute_api_associate)
|
||||
self.stubs.Set(network.api.API, "disassociate_floating_ip",
|
||||
network_api_disassociate)
|
||||
self.context = context.get_admin_context()
|
||||
@@ -112,7 +120,7 @@ class FloatingIpTest(test.TestCase):
|
||||
self.assertTrue('floating_ip' in view)
|
||||
|
||||
def test_floating_ips_list(self):
|
||||
req = webob.Request.blank('/v1.1/os-floating-ips')
|
||||
req = webob.Request.blank('/v1.1/123/os-floating-ips')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
res_dict = json.loads(res.body)
|
||||
@@ -127,65 +135,91 @@ class FloatingIpTest(test.TestCase):
|
||||
self.assertEqual(res_dict, response)
|
||||
|
||||
def test_floating_ip_show(self):
|
||||
req = webob.Request.blank('/v1.1/os-floating-ips/1')
|
||||
req = webob.Request.blank('/v1.1/123/os-floating-ips/1')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEqual(res_dict['floating_ip']['id'], 1)
|
||||
self.assertEqual(res_dict['floating_ip']['ip'], '10.10.10.10')
|
||||
self.assertEqual(res_dict['floating_ip']['fixed_ip'], '11.0.0.1')
|
||||
self.assertEqual(res_dict['floating_ip']['instance_id'], None)
|
||||
|
||||
def test_floating_ip_allocate(self):
|
||||
req = webob.Request.blank('/v1.1/os-floating-ips')
|
||||
req = webob.Request.blank('/v1.1/123/os-floating-ips')
|
||||
req.method = 'POST'
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
print res
|
||||
self.assertEqual(res.status_int, 200)
|
||||
ip = json.loads(res.body)['allocated']
|
||||
ip = json.loads(res.body)['floating_ip']
|
||||
|
||||
expected = {
|
||||
"id": 1,
|
||||
"floating_ip": '10.10.10.10'}
|
||||
"instance_id": None,
|
||||
"ip": "10.10.10.10",
|
||||
"fixed_ip": None}
|
||||
self.assertEqual(ip, expected)
|
||||
|
||||
def test_floating_ip_release(self):
|
||||
req = webob.Request.blank('/v1.1/os-floating-ips/1')
|
||||
req = webob.Request.blank('/v1.1/123/os-floating-ips/1')
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
actual = json.loads(res.body)['released']
|
||||
expected = {
|
||||
"id": 1,
|
||||
"floating_ip": '10.10.10.10'}
|
||||
self.assertEqual(actual, expected)
|
||||
self.assertEqual(res.status_int, 202)
|
||||
|
||||
def test_floating_ip_associate(self):
|
||||
body = dict(associate_address=dict(fixed_ip='1.2.3.4'))
|
||||
req = webob.Request.blank('/v1.1/os-floating-ips/1/associate')
|
||||
req.method = 'POST'
|
||||
def test_add_floating_ip_to_instance(self):
|
||||
body = dict(addFloatingIp=dict(address='11.0.0.1'))
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
actual = json.loads(res.body)['associated']
|
||||
expected = {
|
||||
"floating_ip_id": '1',
|
||||
"floating_ip": "10.10.10.10",
|
||||
"fixed_ip": "1.2.3.4"}
|
||||
self.assertEqual(actual, expected)
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 202)
|
||||
|
||||
def test_floating_ip_disassociate(self):
|
||||
body = dict()
|
||||
req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate')
|
||||
req.method = 'POST'
|
||||
def test_remove_floating_ip_from_instance(self):
|
||||
body = dict(removeFloatingIp=dict(address='11.0.0.1'))
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
ip = json.loads(res.body)['disassociated']
|
||||
expected = {
|
||||
"floating_ip": '10.10.10.10',
|
||||
"fixed_ip": '11.0.0.1'}
|
||||
self.assertEqual(ip, expected)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 202)
|
||||
|
||||
def test_bad_address_param_in_remove_floating_ip(self):
|
||||
body = dict(removeFloatingIp=dict(badparam='11.0.0.1'))
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
|
||||
def test_missing_dict_param_in_remove_floating_ip(self):
|
||||
body = dict(removeFloatingIp='11.0.0.1')
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
|
||||
def test_bad_address_param_in_add_floating_ip(self):
|
||||
body = dict(addFloatingIp=dict(badparam='11.0.0.1'))
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
|
||||
def test_missing_dict_param_in_add_floating_ip(self):
|
||||
body = dict(addFloatingIp='11.0.0.1')
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 400)
|
||||
|
||||
@@ -58,7 +58,7 @@ class KeypairsTest(test.TestCase):
|
||||
self.context = context.get_admin_context()
|
||||
|
||||
def test_keypair_list(self):
|
||||
req = webob.Request.blank('/v1.1/os-keypairs')
|
||||
req = webob.Request.blank('/v1.1/123/os-keypairs')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
res_dict = json.loads(res.body)
|
||||
@@ -67,7 +67,7 @@ class KeypairsTest(test.TestCase):
|
||||
|
||||
def test_keypair_create(self):
|
||||
body = {'keypair': {'name': 'create_test'}}
|
||||
req = webob.Request.blank('/v1.1/os-keypairs')
|
||||
req = webob.Request.blank('/v1.1/123/os-keypairs')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
@@ -93,7 +93,7 @@ class KeypairsTest(test.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/os-keypairs')
|
||||
req = webob.Request.blank('/v1.1/123/os-keypairs')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
@@ -105,7 +105,7 @@ class KeypairsTest(test.TestCase):
|
||||
self.assertFalse('private_key' in res_dict['keypair'])
|
||||
|
||||
def test_keypair_delete(self):
|
||||
req = webob.Request.blank('/v1.1/os-keypairs/FAKE')
|
||||
req = webob.Request.blank('/v1.1/123/os-keypairs/FAKE')
|
||||
req.method = 'DELETE'
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
|
||||
@@ -55,7 +55,7 @@ class FixedIpTest(test.TestCase):
|
||||
last_add_fixed_ip = (None, None)
|
||||
|
||||
body = dict(addFixedIp=dict(networkId='test_net'))
|
||||
req = webob.Request.blank('/v1.1/servers/test_inst/action')
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers['content-type'] = 'application/json'
|
||||
@@ -69,7 +69,7 @@ class FixedIpTest(test.TestCase):
|
||||
last_add_fixed_ip = (None, None)
|
||||
|
||||
body = dict(addFixedIp=dict())
|
||||
req = webob.Request.blank('/v1.1/servers/test_inst/action')
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers['content-type'] = 'application/json'
|
||||
@@ -83,7 +83,7 @@ class FixedIpTest(test.TestCase):
|
||||
last_remove_fixed_ip = (None, None)
|
||||
|
||||
body = dict(removeFixedIp=dict(address='10.10.10.1'))
|
||||
req = webob.Request.blank('/v1.1/servers/test_inst/action')
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers['content-type'] = 'application/json'
|
||||
@@ -97,7 +97,7 @@ class FixedIpTest(test.TestCase):
|
||||
last_remove_fixed_ip = (None, None)
|
||||
|
||||
body = dict(removeFixedIp=dict())
|
||||
req = webob.Request.blank('/v1.1/servers/test_inst/action')
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers['content-type'] = 'application/json'
|
||||
|
||||
@@ -78,7 +78,8 @@ class QuotaSetsTest(test.TestCase):
|
||||
self.assertEqual(qs['injected_file_content_bytes'], 10240)
|
||||
|
||||
def test_quotas_defaults(self):
|
||||
req = webob.Request.blank('/v1.1/os-quota-sets/fake_tenant/defaults')
|
||||
uri = '/v1.1/fake_tenant/os-quota-sets/fake_tenant/defaults'
|
||||
req = webob.Request.blank(uri)
|
||||
req.method = 'GET'
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
@@ -99,7 +100,7 @@ class QuotaSetsTest(test.TestCase):
|
||||
self.assertEqual(json.loads(res.body), expected)
|
||||
|
||||
def test_quotas_show_as_admin(self):
|
||||
req = webob.Request.blank('/v1.1/os-quota-sets/1234')
|
||||
req = webob.Request.blank('/v1.1/1234/os-quota-sets/1234')
|
||||
req.method = 'GET'
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
res = req.get_response(fakes.wsgi_app(
|
||||
@@ -109,7 +110,7 @@ class QuotaSetsTest(test.TestCase):
|
||||
self.assertEqual(json.loads(res.body), quota_set('1234'))
|
||||
|
||||
def test_quotas_show_as_unauthorized_user(self):
|
||||
req = webob.Request.blank('/v1.1/os-quota-sets/1234')
|
||||
req = webob.Request.blank('/v1.1/fake/os-quota-sets/1234')
|
||||
req.method = 'GET'
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
res = req.get_response(fakes.wsgi_app(
|
||||
@@ -124,7 +125,7 @@ class QuotaSetsTest(test.TestCase):
|
||||
'metadata_items': 128, 'injected_files': 5,
|
||||
'injected_file_content_bytes': 10240}}
|
||||
|
||||
req = webob.Request.blank('/v1.1/os-quota-sets/update_me')
|
||||
req = webob.Request.blank('/v1.1/1234/os-quota-sets/update_me')
|
||||
req.method = 'PUT'
|
||||
req.body = json.dumps(updated_quota_set)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
@@ -141,7 +142,7 @@ class QuotaSetsTest(test.TestCase):
|
||||
'metadata_items': 128, 'injected_files': 5,
|
||||
'injected_file_content_bytes': 10240}}
|
||||
|
||||
req = webob.Request.blank('/v1.1/os-quota-sets/update_me')
|
||||
req = webob.Request.blank('/v1.1/1234/os-quota-sets/update_me')
|
||||
req.method = 'PUT'
|
||||
req.body = json.dumps(updated_quota_set)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
|
||||
55
nova/tests/api/openstack/contrib/test_rescue.py
Normal file
55
nova/tests/api/openstack/contrib/test_rescue.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import webob
|
||||
|
||||
from nova import compute
|
||||
from nova import test
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
def rescue(self, context, instance_id):
|
||||
pass
|
||||
|
||||
|
||||
def unrescue(self, context, instance_id):
|
||||
pass
|
||||
|
||||
|
||||
class RescueTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(RescueTest, self).setUp()
|
||||
self.stubs.Set(compute.api.API, "rescue", rescue)
|
||||
self.stubs.Set(compute.api.API, "unrescue", unrescue)
|
||||
|
||||
def test_rescue(self):
|
||||
body = dict(rescue=None)
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
|
||||
def test_unrescue(self):
|
||||
body = dict(unrescue=None)
|
||||
req = webob.Request.blank('/v1.1/123/servers/test_inst/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
resp = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(resp.status_int, 200)
|
||||
@@ -15,17 +15,20 @@
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import mox
|
||||
import nova
|
||||
import unittest
|
||||
import webob
|
||||
from xml.dom import minidom
|
||||
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.api.openstack.contrib import security_groups
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
def _get_create_request_json(body_dict):
|
||||
req = webob.Request.blank('/v1.1/os-security-groups')
|
||||
req = webob.Request.blank('/v1.1/123/os-security-groups')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body_dict)
|
||||
@@ -51,6 +54,28 @@ def _create_security_group_request_dict(security_group):
|
||||
return {'security_group': sg}
|
||||
|
||||
|
||||
def return_server(context, server_id):
|
||||
return {'id': server_id, 'state': 0x01, 'host': "localhost"}
|
||||
|
||||
|
||||
def return_non_running_server(context, server_id):
|
||||
return {'id': server_id, 'state': 0x02,
|
||||
'host': "localhost"}
|
||||
|
||||
|
||||
def return_security_group(context, project_id, group_name):
|
||||
return {'id': 1, 'name': group_name, "instances": [
|
||||
{'id': 1}]}
|
||||
|
||||
|
||||
def return_security_group_without_instances(context, project_id, group_name):
|
||||
return {'id': 1, 'name': group_name}
|
||||
|
||||
|
||||
def return_server_nonexistant(context, server_id):
|
||||
raise exception.InstanceNotFound(instance_id=server_id)
|
||||
|
||||
|
||||
class TestSecurityGroups(test.TestCase):
|
||||
def setUp(self):
|
||||
super(TestSecurityGroups, self).setUp()
|
||||
@@ -84,7 +109,7 @@ class TestSecurityGroups(test.TestCase):
|
||||
return ''.join(body_parts)
|
||||
|
||||
def _get_create_request_xml(self, body_dict):
|
||||
req = webob.Request.blank('/v1.1/os-security-groups')
|
||||
req = webob.Request.blank('/v1.1/123/os-security-groups')
|
||||
req.headers['Content-Type'] = 'application/xml'
|
||||
req.content_type = 'application/xml'
|
||||
req.accept = 'application/xml'
|
||||
@@ -99,7 +124,7 @@ class TestSecurityGroups(test.TestCase):
|
||||
return response
|
||||
|
||||
def _delete_security_group(self, id):
|
||||
request = webob.Request.blank('/v1.1/os-security-groups/%s'
|
||||
request = webob.Request.blank('/v1.1/123/os-security-groups/%s'
|
||||
% id)
|
||||
request.method = 'DELETE'
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
@@ -238,7 +263,7 @@ class TestSecurityGroups(test.TestCase):
|
||||
security_group['description'] = "group-description"
|
||||
response = _create_security_group_json(security_group)
|
||||
|
||||
req = webob.Request.blank('/v1.1/os-security-groups')
|
||||
req = webob.Request.blank('/v1.1/123/os-security-groups')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'GET'
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
@@ -247,7 +272,7 @@ class TestSecurityGroups(test.TestCase):
|
||||
expected = {'security_groups': [
|
||||
{'id': 1,
|
||||
'name':"default",
|
||||
'tenant_id': "fake",
|
||||
'tenant_id': "123",
|
||||
"description":"default",
|
||||
"rules": []
|
||||
},
|
||||
@@ -257,7 +282,7 @@ class TestSecurityGroups(test.TestCase):
|
||||
{
|
||||
'id': 2,
|
||||
'name': "test",
|
||||
'tenant_id': "fake",
|
||||
'tenant_id': "123",
|
||||
"description": "group-description",
|
||||
"rules": []
|
||||
}
|
||||
@@ -272,7 +297,7 @@ class TestSecurityGroups(test.TestCase):
|
||||
response = _create_security_group_json(security_group)
|
||||
|
||||
res_dict = json.loads(response.body)
|
||||
req = webob.Request.blank('/v1.1/os-security-groups/%s' %
|
||||
req = webob.Request.blank('/v1.1/123/os-security-groups/%s' %
|
||||
res_dict['security_group']['id'])
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'GET'
|
||||
@@ -283,23 +308,22 @@ class TestSecurityGroups(test.TestCase):
|
||||
'security_group': {
|
||||
'id': 2,
|
||||
'name': "test",
|
||||
'tenant_id': "fake",
|
||||
'tenant_id': "123",
|
||||
'description': "group-description",
|
||||
'rules': []
|
||||
}
|
||||
}
|
||||
self.assertEquals(response.status_int, 200)
|
||||
self.assertEquals(res_dict, expected)
|
||||
|
||||
def test_get_security_group_by_invalid_id(self):
|
||||
req = webob.Request.blank('/v1.1/os-security-groups/invalid')
|
||||
req = webob.Request.blank('/v1.1/123/os-security-groups/invalid')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'GET'
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_get_security_group_by_non_existing_id(self):
|
||||
req = webob.Request.blank('/v1.1/os-security-groups/111111111')
|
||||
req = webob.Request.blank('/v1.1/123/os-security-groups/111111111')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'GET'
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
@@ -325,6 +349,252 @@ class TestSecurityGroups(test.TestCase):
|
||||
response = self._delete_security_group(11111111)
|
||||
self.assertEquals(response.status_int, 404)
|
||||
|
||||
def test_associate_by_non_existing_security_group_name(self):
|
||||
body = dict(addSecurityGroup=dict(name='non-existing'))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 404)
|
||||
|
||||
def test_associate_by_invalid_server_id(self):
|
||||
body = dict(addSecurityGroup=dict(name='test'))
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
req = webob.Request.blank('/v1.1/123/servers/invalid/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_associate_without_body(self):
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
body = dict(addSecurityGroup=None)
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_associate_no_security_group_name(self):
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
body = dict(addSecurityGroup=dict())
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_associate_security_group_name_with_whitespaces(self):
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
body = dict(addSecurityGroup=dict(name=" "))
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_associate_non_existing_instance(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant)
|
||||
body = dict(addSecurityGroup=dict(name="test"))
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
req = webob.Request.blank('/v1.1/123/servers/10000/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 404)
|
||||
|
||||
def test_associate_non_running_instance(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_non_running_server)
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group_without_instances)
|
||||
body = dict(addSecurityGroup=dict(name="test"))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_associate_already_associated_security_group_to_instance(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
body = dict(addSecurityGroup=dict(name="test"))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_associate(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
self.mox.StubOutWithMock(nova.db, 'instance_add_security_group')
|
||||
nova.db.instance_add_security_group(mox.IgnoreArg(),
|
||||
mox.IgnoreArg(),
|
||||
mox.IgnoreArg())
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group_without_instances)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
body = dict(addSecurityGroup=dict(name="test"))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 202)
|
||||
|
||||
def test_associate_xml(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
self.mox.StubOutWithMock(nova.db, 'instance_add_security_group')
|
||||
nova.db.instance_add_security_group(mox.IgnoreArg(),
|
||||
mox.IgnoreArg(),
|
||||
mox.IgnoreArg())
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group_without_instances)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/xml'
|
||||
req.method = 'POST'
|
||||
req.body = """<addSecurityGroup>
|
||||
<name>test</name>
|
||||
</addSecurityGroup>"""
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 202)
|
||||
|
||||
def test_disassociate_by_non_existing_security_group_name(self):
|
||||
body = dict(removeSecurityGroup=dict(name='non-existing'))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 404)
|
||||
|
||||
def test_disassociate_by_invalid_server_id(self):
|
||||
body = dict(removeSecurityGroup=dict(name='test'))
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
req = webob.Request.blank('/v1.1/123/servers/invalid/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_disassociate_without_body(self):
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
body = dict(removeSecurityGroup=None)
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_disassociate_no_security_group_name(self):
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
body = dict(removeSecurityGroup=dict())
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_disassociate_security_group_name_with_whitespaces(self):
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
body = dict(removeSecurityGroup=dict(name=" "))
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_disassociate_non_existing_instance(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant)
|
||||
body = dict(removeSecurityGroup=dict(name="test"))
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
req = webob.Request.blank('/v1.1/123/servers/10000/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 404)
|
||||
|
||||
def test_disassociate_non_running_instance(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_non_running_server)
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
body = dict(removeSecurityGroup=dict(name="test"))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_disassociate_already_associated_security_group_to_instance(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group_without_instances)
|
||||
body = dict(removeSecurityGroup=dict(name="test"))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_disassociate(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group')
|
||||
nova.db.instance_remove_security_group(mox.IgnoreArg(),
|
||||
mox.IgnoreArg(),
|
||||
mox.IgnoreArg())
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
body = dict(removeSecurityGroup=dict(name="test"))
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/json'
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 202)
|
||||
|
||||
def test_disassociate_xml(self):
|
||||
self.stubs.Set(nova.db, 'instance_get', return_server)
|
||||
self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group')
|
||||
nova.db.instance_remove_security_group(mox.IgnoreArg(),
|
||||
mox.IgnoreArg(),
|
||||
mox.IgnoreArg())
|
||||
self.stubs.Set(nova.db, 'security_group_get_by_name',
|
||||
return_security_group)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/action')
|
||||
req.headers['Content-Type'] = 'application/xml'
|
||||
req.method = 'POST'
|
||||
req.body = """<removeSecurityGroup>
|
||||
<name>test</name>
|
||||
</removeSecurityGroup>"""
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEquals(response.status_int, 202)
|
||||
|
||||
|
||||
class TestSecurityGroupRules(test.TestCase):
|
||||
def setUp(self):
|
||||
@@ -354,7 +624,7 @@ class TestSecurityGroupRules(test.TestCase):
|
||||
super(TestSecurityGroupRules, self).tearDown()
|
||||
|
||||
def _create_security_group_rule_json(self, rules):
|
||||
request = webob.Request.blank('/v1.1/os-security-group-rules')
|
||||
request = webob.Request.blank('/v1.1/123/os-security-group-rules')
|
||||
request.headers['Content-Type'] = 'application/json'
|
||||
request.method = 'POST'
|
||||
request.body = json.dumps(rules)
|
||||
@@ -362,7 +632,7 @@ class TestSecurityGroupRules(test.TestCase):
|
||||
return response
|
||||
|
||||
def _delete_security_group_rule(self, id):
|
||||
request = webob.Request.blank('/v1.1/os-security-group-rules/%s'
|
||||
request = webob.Request.blank('/v1.1/123/os-security-group-rules/%s'
|
||||
% id)
|
||||
request.method = 'DELETE'
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
@@ -420,7 +690,7 @@ class TestSecurityGroupRules(test.TestCase):
|
||||
self.assertEquals(response.status_int, 400)
|
||||
|
||||
def test_create_with_no_body_json(self):
|
||||
request = webob.Request.blank('/v1.1/os-security-group-rules')
|
||||
request = webob.Request.blank('/v1.1/123/os-security-group-rules')
|
||||
request.headers['Content-Type'] = 'application/json'
|
||||
request.method = 'POST'
|
||||
request.body = json.dumps(None)
|
||||
@@ -428,7 +698,7 @@ class TestSecurityGroupRules(test.TestCase):
|
||||
self.assertEquals(response.status_int, 422)
|
||||
|
||||
def test_create_with_no_security_group_rule_in_body_json(self):
|
||||
request = webob.Request.blank('/v1.1/os-security-group-rules')
|
||||
request = webob.Request.blank('/v1.1/123/os-security-group-rules')
|
||||
request.headers['Content-Type'] = 'application/json'
|
||||
request.method = 'POST'
|
||||
body_dict = {'test': "test"}
|
||||
|
||||
55
nova/tests/api/openstack/contrib/test_virtual_interfaces.py
Normal file
55
nova/tests/api/openstack/contrib/test_virtual_interfaces.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# Copyright (C) 2011 Midokura KK
|
||||
# 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 json
|
||||
import stubout
|
||||
import webob
|
||||
|
||||
from nova import test
|
||||
from nova import compute
|
||||
from nova.tests.api.openstack import fakes
|
||||
from nova.api.openstack.contrib.virtual_interfaces import \
|
||||
ServerVirtualInterfaceController
|
||||
|
||||
|
||||
def compute_api_get(self, context, server_id):
|
||||
return {'virtual_interfaces': [
|
||||
{'uuid': '00000000-0000-0000-0000-00000000000000000',
|
||||
'address': '00-00-00-00-00-00'},
|
||||
{'uuid': '11111111-1111-1111-1111-11111111111111111',
|
||||
'address': '11-11-11-11-11-11'}]}
|
||||
|
||||
|
||||
class ServerVirtualInterfaceTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ServerVirtualInterfaceTest, self).setUp()
|
||||
self.controller = ServerVirtualInterfaceController()
|
||||
self.stubs.Set(compute.api.API, "get", compute_api_get)
|
||||
|
||||
def tearDown(self):
|
||||
super(ServerVirtualInterfaceTest, self).tearDown()
|
||||
|
||||
def test_get_virtual_interfaces_list(self):
|
||||
req = webob.Request.blank('/v1.1/123/servers/1/os-virtual-interfaces')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
res_dict = json.loads(res.body)
|
||||
response = {'virtual_interfaces': [
|
||||
{'id': '00000000-0000-0000-0000-00000000000000000',
|
||||
'mac_address': '00-00-00-00-00-00'},
|
||||
{'id': '11111111-1111-1111-1111-11111111111111111',
|
||||
'mac_address': '11-11-11-11-11-11'}]}
|
||||
self.assertEqual(res_dict, response)
|
||||
@@ -72,8 +72,9 @@ class Foxinsocks(object):
|
||||
res.body = json.dumps(data)
|
||||
return res
|
||||
|
||||
req_ext1 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)',
|
||||
_goose_handler)
|
||||
req_ext1 = extensions.RequestExtension('GET',
|
||||
'/v1.1/:(project_id)/flavors/:(id)',
|
||||
_goose_handler)
|
||||
request_exts.append(req_ext1)
|
||||
|
||||
def _bands_handler(req, res):
|
||||
@@ -84,8 +85,9 @@ class Foxinsocks(object):
|
||||
res.body = json.dumps(data)
|
||||
return res
|
||||
|
||||
req_ext2 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)',
|
||||
_bands_handler)
|
||||
req_ext2 = extensions.RequestExtension('GET',
|
||||
'/v1.1/:(project_id)/flavors/:(id)',
|
||||
_bands_handler)
|
||||
request_exts.append(req_ext2)
|
||||
return request_exts
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from nova import utils
|
||||
from nova import wsgi
|
||||
import nova.api.openstack.auth
|
||||
from nova.api import openstack
|
||||
from nova.api import auth as api_auth
|
||||
from nova.api.openstack import auth
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import versions
|
||||
@@ -83,9 +84,9 @@ def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True,
|
||||
ctxt = fake_auth_context
|
||||
else:
|
||||
ctxt = context.RequestContext('fake', 'fake')
|
||||
api10 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
|
||||
api10 = openstack.FaultWrapper(api_auth.InjectContext(ctxt,
|
||||
limits.RateLimitingMiddleware(inner_app10)))
|
||||
api11 = openstack.FaultWrapper(wsgi.InjectContext(ctxt,
|
||||
api11 = openstack.FaultWrapper(api_auth.InjectContext(ctxt,
|
||||
limits.RateLimitingMiddleware(
|
||||
extensions.ExtensionMiddleware(inner_app11))))
|
||||
else:
|
||||
|
||||
@@ -53,6 +53,7 @@ class Test(test.TestCase):
|
||||
req = webob.Request.blank('/v1.0/')
|
||||
req.headers['X-Auth-User'] = 'user1'
|
||||
req.headers['X-Auth-Key'] = 'user1_key'
|
||||
req.headers['X-Auth-Project-Id'] = 'user1_project'
|
||||
result = req.get_response(fakes.wsgi_app(fake_auth=False))
|
||||
self.assertEqual(result.status, '204 No Content')
|
||||
self.assertEqual(len(result.headers['X-Auth-Token']), 40)
|
||||
@@ -73,14 +74,14 @@ class Test(test.TestCase):
|
||||
self.assertEqual(result.status, '204 No Content')
|
||||
self.assertEqual(len(result.headers['X-Auth-Token']), 40)
|
||||
self.assertEqual(result.headers['X-Server-Management-Url'],
|
||||
"http://foo/v1.0/")
|
||||
"http://foo/v1.0")
|
||||
self.assertEqual(result.headers['X-CDN-Management-Url'],
|
||||
"")
|
||||
self.assertEqual(result.headers['X-Storage-Url'], "")
|
||||
|
||||
token = result.headers['X-Auth-Token']
|
||||
self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter)
|
||||
req = webob.Request.blank('/v1.0/fake')
|
||||
req = webob.Request.blank('/v1.0/user1_project')
|
||||
req.headers['X-Auth-Token'] = token
|
||||
result = req.get_response(fakes.wsgi_app(fake_auth=False))
|
||||
self.assertEqual(result.status, '200 OK')
|
||||
@@ -125,7 +126,7 @@ class Test(test.TestCase):
|
||||
|
||||
token = result.headers['X-Auth-Token']
|
||||
self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter)
|
||||
req = webob.Request.blank('/v1.0/fake')
|
||||
req = webob.Request.blank('/v1.0/')
|
||||
req.headers['X-Auth-Token'] = token
|
||||
req.headers['X-Auth-Project-Id'] = 'user2_project'
|
||||
result = req.get_response(fakes.wsgi_app(fake_auth=False))
|
||||
@@ -136,6 +137,7 @@ class Test(test.TestCase):
|
||||
req = webob.Request.blank('/v1.0/')
|
||||
req.headers['X-Auth-User'] = 'unknown_user'
|
||||
req.headers['X-Auth-Key'] = 'unknown_user_key'
|
||||
req.headers['X-Auth-Project-Id'] = 'user_project'
|
||||
result = req.get_response(fakes.wsgi_app(fake_auth=False))
|
||||
self.assertEqual(result.status, '401 Unauthorized')
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ class ExtensionControllerTest(test.TestCase):
|
||||
self.flags(osapi_extensions_path=ext_path)
|
||||
self.ext_list = [
|
||||
"DriveTypes",
|
||||
"Createserverext",
|
||||
"FlavorExtraSpecs",
|
||||
"Floating_ips",
|
||||
"Fox In Socks",
|
||||
@@ -93,16 +94,19 @@ class ExtensionControllerTest(test.TestCase):
|
||||
"Keypairs",
|
||||
"Multinic",
|
||||
"Quotas",
|
||||
"Rescue",
|
||||
"SecurityGroups",
|
||||
"VirtualInterfaces",
|
||||
"VSAs",
|
||||
"Volumes",
|
||||
"VolumeTypes",
|
||||
]
|
||||
self.ext_list.sort()
|
||||
|
||||
def test_list_extensions_json(self):
|
||||
app = openstack.APIRouterV11()
|
||||
ext_midware = extensions.ExtensionMiddleware(app)
|
||||
request = webob.Request.blank("/extensions")
|
||||
request = webob.Request.blank("/123/extensions")
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
@@ -128,7 +132,7 @@ class ExtensionControllerTest(test.TestCase):
|
||||
def test_get_extension_json(self):
|
||||
app = openstack.APIRouterV11()
|
||||
ext_midware = extensions.ExtensionMiddleware(app)
|
||||
request = webob.Request.blank("/extensions/FOXNSOX")
|
||||
request = webob.Request.blank("/123/extensions/FOXNSOX")
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
@@ -144,7 +148,7 @@ class ExtensionControllerTest(test.TestCase):
|
||||
def test_list_extensions_xml(self):
|
||||
app = openstack.APIRouterV11()
|
||||
ext_midware = extensions.ExtensionMiddleware(app)
|
||||
request = webob.Request.blank("/extensions")
|
||||
request = webob.Request.blank("/123/extensions")
|
||||
request.accept = "application/xml"
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
@@ -171,7 +175,7 @@ class ExtensionControllerTest(test.TestCase):
|
||||
def test_get_extension_xml(self):
|
||||
app = openstack.APIRouterV11()
|
||||
ext_midware = extensions.ExtensionMiddleware(app)
|
||||
request = webob.Request.blank("/extensions/FOXNSOX")
|
||||
request = webob.Request.blank("/123/extensions/FOXNSOX")
|
||||
request.accept = "application/xml"
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
@@ -212,7 +216,7 @@ class ResourceExtensionTest(test.TestCase):
|
||||
manager = StubExtensionManager(res_ext)
|
||||
app = openstack.APIRouterV11()
|
||||
ext_midware = extensions.ExtensionMiddleware(app, manager)
|
||||
request = webob.Request.blank("/tweedles")
|
||||
request = webob.Request.blank("/123/tweedles")
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual(response_body, response.body)
|
||||
@@ -223,7 +227,7 @@ class ResourceExtensionTest(test.TestCase):
|
||||
manager = StubExtensionManager(res_ext)
|
||||
app = openstack.APIRouterV11()
|
||||
ext_midware = extensions.ExtensionMiddleware(app, manager)
|
||||
request = webob.Request.blank("/tweedles")
|
||||
request = webob.Request.blank("/123/tweedles")
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual(response_body, response.body)
|
||||
@@ -247,7 +251,7 @@ class ExtensionManagerTest(test.TestCase):
|
||||
def test_get_resources(self):
|
||||
app = openstack.APIRouterV11()
|
||||
ext_midware = extensions.ExtensionMiddleware(app)
|
||||
request = webob.Request.blank("/foxnsocks")
|
||||
request = webob.Request.blank("/123/foxnsocks")
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual(response_body, response.body)
|
||||
@@ -280,23 +284,26 @@ class ActionExtensionTest(test.TestCase):
|
||||
|
||||
def test_extended_action(self):
|
||||
body = dict(add_tweedle=dict(name="test"))
|
||||
response = self._send_server_action_request("/servers/1/action", body)
|
||||
url = "/123/servers/1/action"
|
||||
response = self._send_server_action_request(url, body)
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual("Tweedle Beetle Added.", response.body)
|
||||
|
||||
body = dict(delete_tweedle=dict(name="test"))
|
||||
response = self._send_server_action_request("/servers/1/action", body)
|
||||
response = self._send_server_action_request(url, body)
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual("Tweedle Beetle Deleted.", response.body)
|
||||
|
||||
def test_invalid_action_body(self):
|
||||
body = dict(blah=dict(name="test")) # Doesn't exist
|
||||
response = self._send_server_action_request("/servers/1/action", body)
|
||||
url = "/123/servers/1/action"
|
||||
response = self._send_server_action_request(url, body)
|
||||
self.assertEqual(400, response.status_int)
|
||||
|
||||
def test_invalid_action(self):
|
||||
body = dict(blah=dict(name="test"))
|
||||
response = self._send_server_action_request("/fdsa/1/action", body)
|
||||
url = "/123/fdsa/1/action"
|
||||
response = self._send_server_action_request(url, body)
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
|
||||
@@ -317,13 +324,13 @@ class RequestExtensionTest(test.TestCase):
|
||||
return res
|
||||
|
||||
req_ext = extensions.RequestExtension('GET',
|
||||
'/v1.1/flavors/:(id)',
|
||||
'/v1.1/123/flavors/:(id)',
|
||||
_req_handler)
|
||||
|
||||
manager = StubExtensionManager(None, None, req_ext)
|
||||
app = fakes.wsgi_app()
|
||||
ext_midware = extensions.ExtensionMiddleware(app, manager)
|
||||
request = webob.Request.blank("/v1.1/flavors/1?chewing=bluegoo")
|
||||
request = webob.Request.blank("/v1.1/123/flavors/1?chewing=bluegoo")
|
||||
request.environ['api.version'] = '1.1'
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
@@ -334,7 +341,7 @@ class RequestExtensionTest(test.TestCase):
|
||||
|
||||
app = fakes.wsgi_app()
|
||||
ext_midware = extensions.ExtensionMiddleware(app)
|
||||
request = webob.Request.blank("/v1.1/flavors/1?chewing=newblue")
|
||||
request = webob.Request.blank("/v1.1/123/flavors/1?chewing=newblue")
|
||||
request.environ['api.version'] = '1.1'
|
||||
response = request.get_response(ext_midware)
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
@@ -138,7 +138,7 @@ class FlavorsTest(test.TestCase):
|
||||
self.assertEqual(res.status_int, 404)
|
||||
|
||||
def test_get_flavor_by_id_v1_1(self):
|
||||
req = webob.Request.blank('/v1.1/flavors/12')
|
||||
req = webob.Request.blank('/v1.1/fake/flavors/12')
|
||||
req.environ['api.version'] = '1.1'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
@@ -152,11 +152,11 @@ class FlavorsTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/12",
|
||||
"href": "http://localhost/v1.1/fake/flavors/12",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/12",
|
||||
"href": "http://localhost/fake/flavors/12",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -164,7 +164,7 @@ class FlavorsTest(test.TestCase):
|
||||
self.assertEqual(flavor, expected)
|
||||
|
||||
def test_get_flavor_list_v1_1(self):
|
||||
req = webob.Request.blank('/v1.1/flavors')
|
||||
req = webob.Request.blank('/v1.1/fake/flavors')
|
||||
req.environ['api.version'] = '1.1'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
@@ -177,11 +177,11 @@ class FlavorsTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/1",
|
||||
"href": "http://localhost/v1.1/fake/flavors/1",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/1",
|
||||
"href": "http://localhost/fake/flavors/1",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -191,11 +191,11 @@ class FlavorsTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/2",
|
||||
"href": "http://localhost/v1.1/fake/flavors/2",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/2",
|
||||
"href": "http://localhost/fake/flavors/2",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -204,7 +204,7 @@ class FlavorsTest(test.TestCase):
|
||||
self.assertEqual(flavor, expected)
|
||||
|
||||
def test_get_flavor_list_detail_v1_1(self):
|
||||
req = webob.Request.blank('/v1.1/flavors/detail')
|
||||
req = webob.Request.blank('/v1.1/fake/flavors/detail')
|
||||
req.environ['api.version'] = '1.1'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
@@ -219,11 +219,11 @@ class FlavorsTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/1",
|
||||
"href": "http://localhost/v1.1/fake/flavors/1",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/1",
|
||||
"href": "http://localhost/fake/flavors/1",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -235,11 +235,11 @@ class FlavorsTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/2",
|
||||
"href": "http://localhost/v1.1/fake/flavors/2",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/2",
|
||||
"href": "http://localhost/fake/flavors/2",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -252,7 +252,7 @@ class FlavorsTest(test.TestCase):
|
||||
return {}
|
||||
self.stubs.Set(nova.db.api, "instance_type_get_all", _return_empty)
|
||||
|
||||
req = webob.Request.blank('/v1.1/flavors')
|
||||
req = webob.Request.blank('/v1.1/fake/flavors')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 200)
|
||||
flavors = json.loads(res.body)["flavors"]
|
||||
@@ -274,11 +274,11 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/12",
|
||||
"href": "http://localhost/v1.1/fake/flavors/12",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/12",
|
||||
"href": "http://localhost/fake/flavors/12",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -294,8 +294,10 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
name="asdf"
|
||||
ram="256"
|
||||
disk="10">
|
||||
<atom:link href="http://localhost/v1.1/flavors/12" rel="self"/>
|
||||
<atom:link href="http://localhost/flavors/12" rel="bookmark"/>
|
||||
<atom:link href="http://localhost/v1.1/fake/flavors/12"
|
||||
rel="self"/>
|
||||
<atom:link href="http://localhost/fake/flavors/12"
|
||||
rel="bookmark"/>
|
||||
</flavor>
|
||||
""".replace(" ", ""))
|
||||
|
||||
@@ -313,11 +315,11 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/12",
|
||||
"href": "http://localhost/v1.1/fake/flavors/12",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/12",
|
||||
"href": "http://localhost/fake/flavors/12",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -333,8 +335,10 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
name="asdf"
|
||||
ram="256"
|
||||
disk="10">
|
||||
<atom:link href="http://localhost/v1.1/flavors/12" rel="self"/>
|
||||
<atom:link href="http://localhost/flavors/12" rel="bookmark"/>
|
||||
<atom:link href="http://localhost/v1.1/fake/flavors/12"
|
||||
rel="self"/>
|
||||
<atom:link href="http://localhost/fake/flavors/12"
|
||||
rel="bookmark"/>
|
||||
</flavor>
|
||||
""".replace(" ", ""))
|
||||
|
||||
@@ -353,11 +357,11 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/23",
|
||||
"href": "http://localhost/v1.1/fake/flavors/23",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/23",
|
||||
"href": "http://localhost/fake/flavors/23",
|
||||
},
|
||||
],
|
||||
}, {
|
||||
@@ -368,11 +372,11 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/13",
|
||||
"href": "http://localhost/v1.1/fake/flavors/13",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/13",
|
||||
"href": "http://localhost/fake/flavors/13",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -389,15 +393,19 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
name="flavor 23"
|
||||
ram="512"
|
||||
disk="20">
|
||||
<atom:link href="http://localhost/v1.1/flavors/23" rel="self"/>
|
||||
<atom:link href="http://localhost/flavors/23" rel="bookmark"/>
|
||||
<atom:link href="http://localhost/v1.1/fake/flavors/23"
|
||||
rel="self"/>
|
||||
<atom:link href="http://localhost/fake/flavors/23"
|
||||
rel="bookmark"/>
|
||||
</flavor>
|
||||
<flavor id="13"
|
||||
name="flavor 13"
|
||||
ram="256"
|
||||
disk="10">
|
||||
<atom:link href="http://localhost/v1.1/flavors/13" rel="self"/>
|
||||
<atom:link href="http://localhost/flavors/13" rel="bookmark"/>
|
||||
<atom:link href="http://localhost/v1.1/fake/flavors/13"
|
||||
rel="self"/>
|
||||
<atom:link href="http://localhost/fake/flavors/13"
|
||||
rel="bookmark"/>
|
||||
</flavor>
|
||||
</flavors>
|
||||
""".replace(" ", "") % locals())
|
||||
@@ -417,11 +425,11 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/23",
|
||||
"href": "http://localhost/v1.1/fake/flavors/23",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/23",
|
||||
"href": "http://localhost/fake/flavors/23",
|
||||
},
|
||||
],
|
||||
}, {
|
||||
@@ -432,11 +440,11 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/flavors/13",
|
||||
"href": "http://localhost/v1.1/fake/flavors/13",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/flavors/13",
|
||||
"href": "http://localhost/fake/flavors/13",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -450,12 +458,16 @@ class FlavorsXMLSerializationTest(test.TestCase):
|
||||
<flavors xmlns="http://docs.openstack.org/compute/api/v1.1"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<flavor id="23" name="flavor 23">
|
||||
<atom:link href="http://localhost/v1.1/flavors/23" rel="self"/>
|
||||
<atom:link href="http://localhost/flavors/23" rel="bookmark"/>
|
||||
<atom:link href="http://localhost/v1.1/fake/flavors/23"
|
||||
rel="self"/>
|
||||
<atom:link href="http://localhost/fake/flavors/23"
|
||||
rel="bookmark"/>
|
||||
</flavor>
|
||||
<flavor id="13" name="flavor 13">
|
||||
<atom:link href="http://localhost/v1.1/flavors/13" rel="self"/>
|
||||
<atom:link href="http://localhost/flavors/13" rel="bookmark"/>
|
||||
<atom:link href="http://localhost/v1.1/fake/flavors/13"
|
||||
rel="self"/>
|
||||
<atom:link href="http://localhost/fake/flavors/13"
|
||||
rel="bookmark"/>
|
||||
</flavor>
|
||||
</flavors>
|
||||
""".replace(" ", "") % locals())
|
||||
|
||||
@@ -63,7 +63,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
def test_index(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
|
||||
return_flavor_extra_specs)
|
||||
request = webob.Request.blank('/v1.1/flavors/1/os-extra_specs')
|
||||
request = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs')
|
||||
res = request.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(200, res.status_int)
|
||||
res_dict = json.loads(res.body)
|
||||
@@ -73,7 +73,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
def test_index_no_data(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
|
||||
return_empty_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEqual(200, res.status_int)
|
||||
@@ -83,7 +83,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
def test_show(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
|
||||
return_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key5')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key5')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(200, res.status_int)
|
||||
res_dict = json.loads(res.body)
|
||||
@@ -93,7 +93,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
def test_show_spec_not_found(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_type_extra_specs_get',
|
||||
return_empty_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key6')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key6')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEqual(404, res.status_int)
|
||||
@@ -101,7 +101,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
def test_delete(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_type_extra_specs_delete',
|
||||
delete_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key5')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key5')
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(200, res.status_int)
|
||||
@@ -110,7 +110,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
self.stubs.Set(nova.db.api,
|
||||
'instance_type_extra_specs_update_or_create',
|
||||
return_create_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs')
|
||||
req.method = 'POST'
|
||||
req.body = '{"extra_specs": {"key1": "value1"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -124,7 +124,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
self.stubs.Set(nova.db.api,
|
||||
'instance_type_extra_specs_update_or_create',
|
||||
return_create_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs')
|
||||
req.method = 'POST'
|
||||
req.headers["content-type"] = "application/json"
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
@@ -134,7 +134,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
self.stubs.Set(nova.db.api,
|
||||
'instance_type_extra_specs_update_or_create',
|
||||
return_create_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key1')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"key1": "value1"}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -148,7 +148,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
self.stubs.Set(nova.db.api,
|
||||
'instance_type_extra_specs_update_or_create',
|
||||
return_create_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key1')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key1')
|
||||
req.method = 'PUT'
|
||||
req.headers["content-type"] = "application/json"
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
@@ -158,7 +158,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
self.stubs.Set(nova.db.api,
|
||||
'instance_type_extra_specs_update_or_create',
|
||||
return_create_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/key1')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"key1": "value1", "key2": "value2"}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -169,7 +169,7 @@ class FlavorsExtraSpecsTest(test.TestCase):
|
||||
self.stubs.Set(nova.db.api,
|
||||
'instance_type_extra_specs_update_or_create',
|
||||
return_create_flavor_extra_specs)
|
||||
req = webob.Request.blank('/v1.1/flavors/1/os-extra_specs/bad')
|
||||
req = webob.Request.blank('/v1.1/123/flavors/1/os-extra_specs/bad')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"key1": "value1"}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
@@ -90,7 +90,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
fakes.stub_out_glance(self.stubs, self.IMAGE_FIXTURES)
|
||||
|
||||
def test_index(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/123/images/1/metadata')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEqual(200, res.status_int)
|
||||
@@ -100,7 +100,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(value, res_dict['metadata'][key])
|
||||
|
||||
def test_show(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEqual(200, res.status_int)
|
||||
@@ -109,12 +109,12 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual('value1', res_dict['meta']['key1'])
|
||||
|
||||
def test_show_not_found(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata/key9')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata/key9')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, res.status_int)
|
||||
|
||||
def test_create(self):
|
||||
req = webob.Request.blank('/v1.1/images/2/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/images/2/metadata')
|
||||
req.method = 'POST'
|
||||
req.body = '{"metadata": {"key9": "value9"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -134,7 +134,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(expected_output, actual_output)
|
||||
|
||||
def test_update_all(self):
|
||||
req = webob.Request.blank('/v1.1/images/2/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"metadata": {"key9": "value9"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -152,7 +152,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(expected_output, actual_output)
|
||||
|
||||
def test_update_item(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"key1": "zz"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -168,7 +168,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_update_item_bad_body(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"key1": "zz"}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -176,7 +176,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_update_item_too_many_keys(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"key1": "value1", "key2": "value2"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -184,7 +184,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_update_item_body_uri_mismatch(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata/bad')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata/bad')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"key1": "value1"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -192,7 +192,7 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_update_item_xml(self):
|
||||
req = webob.Request.blank('/v1.1/images/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '<meta key="key1">five</meta>'
|
||||
req.headers["content-type"] = "application/xml"
|
||||
@@ -208,14 +208,14 @@ class ImageMetaDataTest(test.TestCase):
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_delete(self):
|
||||
req = webob.Request.blank('/v1.1/images/2/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/images/2/metadata/key1')
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(204, res.status_int)
|
||||
self.assertEqual('', res.body)
|
||||
|
||||
def test_delete_not_found(self):
|
||||
req = webob.Request.blank('/v1.1/images/2/metadata/blah')
|
||||
req = webob.Request.blank('/v1.1/fake/images/2/metadata/blah')
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, res.status_int)
|
||||
@@ -225,17 +225,17 @@ class ImageMetaDataTest(test.TestCase):
|
||||
for num in range(FLAGS.quota_metadata_items + 1):
|
||||
data['metadata']['key%i' % num] = "blah"
|
||||
json_string = str(data).replace("\'", "\"")
|
||||
req = webob.Request.blank('/v1.1/images/2/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/images/2/metadata')
|
||||
req.method = 'POST'
|
||||
req.body = json_string
|
||||
req.headers["content-type"] = "application/json"
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(400, res.status_int)
|
||||
self.assertEqual(413, res.status_int)
|
||||
|
||||
def test_too_many_metadata_items_on_put(self):
|
||||
req = webob.Request.blank('/v1.1/images/3/metadata/blah')
|
||||
req = webob.Request.blank('/v1.1/fake/images/3/metadata/blah')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"blah": "blah"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(400, res.status_int)
|
||||
self.assertEqual(413, res.status_int)
|
||||
|
||||
@@ -339,6 +339,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
self.stubs.UnsetAll()
|
||||
super(ImageControllerWithGlanceServiceTest, self).tearDown()
|
||||
|
||||
def _get_fake_context(self):
|
||||
class Context(object):
|
||||
project_id = 'fake'
|
||||
return Context()
|
||||
|
||||
def _applicable_fixture(self, fixture, user_id):
|
||||
"""Determine if this fixture is applicable for given user id."""
|
||||
is_public = fixture["is_public"]
|
||||
@@ -386,13 +391,13 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
self.assertEqual(expected_image, actual_image)
|
||||
|
||||
def test_get_image_v1_1(self):
|
||||
request = webob.Request.blank('/v1.1/images/124')
|
||||
request = webob.Request.blank('/v1.1/fake/images/124')
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
|
||||
actual_image = json.loads(response.body)
|
||||
|
||||
href = "http://localhost/v1.1/images/124"
|
||||
bookmark = "http://localhost/images/124"
|
||||
href = "http://localhost/v1.1/fake/images/124"
|
||||
bookmark = "http://localhost/fake/images/124"
|
||||
server_href = "http://localhost/v1.1/servers/42"
|
||||
server_bookmark = "http://localhost/servers/42"
|
||||
|
||||
@@ -508,7 +513,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
self.assertEqual(expected.toxml(), actual.toxml())
|
||||
|
||||
def test_get_image_404_v1_1_json(self):
|
||||
request = webob.Request.blank('/v1.1/images/NonExistantImage')
|
||||
request = webob.Request.blank('/v1.1/fake/images/NonExistantImage')
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
@@ -524,7 +529,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_get_image_404_v1_1_xml(self):
|
||||
request = webob.Request.blank('/v1.1/images/NonExistantImage')
|
||||
request = webob.Request.blank('/v1.1/fake/images/NonExistantImage')
|
||||
request.accept = "application/xml"
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, response.status_int)
|
||||
@@ -545,7 +550,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
self.assertEqual(expected.toxml(), actual.toxml())
|
||||
|
||||
def test_get_image_index_v1_1(self):
|
||||
request = webob.Request.blank('/v1.1/images')
|
||||
request = webob.Request.blank('/v1.1/fake/images')
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
|
||||
response_dict = json.loads(response.body)
|
||||
@@ -558,8 +563,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
fixtures.remove(image)
|
||||
continue
|
||||
|
||||
href = "http://localhost/v1.1/images/%s" % image["id"]
|
||||
bookmark = "http://localhost/images/%s" % image["id"]
|
||||
href = "http://localhost/v1.1/fake/images/%s" % image["id"]
|
||||
bookmark = "http://localhost/fake/images/%s" % image["id"]
|
||||
test_image = {
|
||||
"id": image["id"],
|
||||
"name": image["name"],
|
||||
@@ -637,7 +642,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
self.assertDictListMatch(expected, response_list)
|
||||
|
||||
def test_get_image_details_v1_1(self):
|
||||
request = webob.Request.blank('/v1.1/images/detail')
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail')
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
|
||||
response_dict = json.loads(response.body)
|
||||
@@ -655,11 +660,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
'progress': 100,
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/images/123",
|
||||
"href": "http://localhost/v1.1/fake/images/123",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/images/123",
|
||||
"href": "http://localhost/fake/images/123",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -686,11 +691,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
},
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/images/124",
|
||||
"href": "http://localhost/v1.1/fake/images/124",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/images/124",
|
||||
"href": "http://localhost/fake/images/124",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -717,11 +722,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
},
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/images/125",
|
||||
"href": "http://localhost/v1.1/fake/images/125",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/images/125",
|
||||
"href": "http://localhost/fake/images/125",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -748,11 +753,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
},
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/images/126",
|
||||
"href": "http://localhost/v1.1/fake/images/126",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/images/126",
|
||||
"href": "http://localhost/fake/images/126",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -779,11 +784,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
},
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/images/127",
|
||||
"href": "http://localhost/v1.1/fake/images/127",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/images/127",
|
||||
"href": "http://localhost/fake/images/127",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -796,11 +801,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
'progress': 100,
|
||||
"links": [{
|
||||
"rel": "self",
|
||||
"href": "http://localhost/v1.1/images/129",
|
||||
"href": "http://localhost/v1.1/fake/images/129",
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": "http://localhost/images/129",
|
||||
"href": "http://localhost/fake/images/129",
|
||||
}],
|
||||
},
|
||||
]
|
||||
@@ -809,7 +814,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_filter_with_name(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'name': 'testname'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
@@ -821,7 +826,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_filter_with_status(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'status': 'ACTIVE'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
@@ -833,7 +838,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_filter_with_property(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'property-test': '3'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
@@ -845,7 +850,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_filter_server(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
# 'server' should be converted to 'property-instance_ref'
|
||||
filters = {'property-instance_ref': 'http://localhost:8774/servers/12'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
@@ -859,7 +864,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_filter_changes_since(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'changes-since': '2011-01-24T17:08Z'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
@@ -872,7 +877,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_filter_with_type(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'property-image_type': 'BASE'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
@@ -884,7 +889,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_filter_not_supported(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'status': 'ACTIVE'}
|
||||
image_service.detail(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
@@ -897,7 +902,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_no_filters(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {}
|
||||
image_service.index(
|
||||
context, filters=filters).AndReturn([])
|
||||
@@ -911,11 +916,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_filter_with_name(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'name': 'testname'}
|
||||
image_service.detail(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail?name=testname')
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail?name=testname')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
controller.detail(request)
|
||||
@@ -923,11 +928,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_filter_with_status(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'status': 'ACTIVE'}
|
||||
image_service.detail(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail?status=ACTIVE')
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail?status=ACTIVE')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
controller.detail(request)
|
||||
@@ -935,11 +940,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_filter_with_property(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'property-test': '3'}
|
||||
image_service.detail(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail?property-test=3')
|
||||
request = webob.Request.blank(
|
||||
'/v1.1/fake/images/detail?property-test=3')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
controller.detail(request)
|
||||
@@ -947,12 +953,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_filter_server(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
# 'server' should be converted to 'property-instance_ref'
|
||||
filters = {'property-instance_ref': 'http://localhost:8774/servers/12'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail?server='
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail?server='
|
||||
'http://localhost:8774/servers/12')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
@@ -961,11 +967,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_filter_changes_since(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'changes-since': '2011-01-24T17:08Z'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail?changes-since='
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail?changes-since='
|
||||
'2011-01-24T17:08Z')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
@@ -974,11 +980,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_filter_with_type(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'property-image_type': 'BASE'}
|
||||
image_service.index(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail?type=BASE')
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail?type=BASE')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
controller.index(request)
|
||||
@@ -986,11 +992,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_filter_not_supported(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {'status': 'ACTIVE'}
|
||||
image_service.detail(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail?status=ACTIVE&'
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail?status=ACTIVE&'
|
||||
'UNSUPPORTEDFILTER=testname')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
@@ -999,11 +1005,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
|
||||
|
||||
def test_image_detail_no_filters(self):
|
||||
image_service = self.mox.CreateMockAnything()
|
||||
context = object()
|
||||
context = self._get_fake_context()
|
||||
filters = {}
|
||||
image_service.detail(context, filters=filters).AndReturn([])
|
||||
self.mox.ReplayAll()
|
||||
request = webob.Request.blank('/v1.1/images/detail')
|
||||
request = webob.Request.blank('/v1.1/fake/images/detail')
|
||||
request.environ['nova.context'] = context
|
||||
controller = images.ControllerV11(image_service=image_service)
|
||||
controller.detail(request)
|
||||
@@ -1123,8 +1129,8 @@ class ImageXMLSerializationTest(test.TestCase):
|
||||
TIMESTAMP = "2010-10-11T10:30:22Z"
|
||||
SERVER_HREF = 'http://localhost/v1.1/servers/123'
|
||||
SERVER_BOOKMARK = 'http://localhost/servers/123'
|
||||
IMAGE_HREF = 'http://localhost/v1.1/images/%s'
|
||||
IMAGE_BOOKMARK = 'http://localhost/images/%s'
|
||||
IMAGE_HREF = 'http://localhost/v1.1/fake/images/%s'
|
||||
IMAGE_BOOKMARK = 'http://localhost/fake/images/%s'
|
||||
|
||||
def test_show(self):
|
||||
serializer = images.ImageXMLSerializer()
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import unittest
|
||||
from xml.dom import minidom
|
||||
|
||||
import stubout
|
||||
import webob
|
||||
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import utils
|
||||
from nova import exception
|
||||
from nova import flags
|
||||
from nova.api.openstack import create_instance_helper
|
||||
from nova.compute import instance_types
|
||||
@@ -23,25 +22,81 @@ FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
def return_server_by_id(context, id):
|
||||
return _get_instance()
|
||||
return stub_instance(id)
|
||||
|
||||
|
||||
def instance_update(context, instance_id, kwargs):
|
||||
return _get_instance()
|
||||
return stub_instance(instance_id)
|
||||
|
||||
|
||||
def return_server_with_attributes(**kwargs):
|
||||
def _return_server(context, id):
|
||||
return stub_instance(id, **kwargs)
|
||||
return _return_server
|
||||
|
||||
|
||||
def return_server_with_power_state(power_state):
|
||||
def _return_server(context, id):
|
||||
instance = _get_instance()
|
||||
instance['state'] = power_state
|
||||
return instance
|
||||
return _return_server
|
||||
return return_server_with_attributes(power_state=power_state)
|
||||
|
||||
|
||||
def return_server_with_uuid_and_power_state(power_state):
|
||||
def _return_server(context, id):
|
||||
return return_server_with_power_state(power_state)
|
||||
return _return_server
|
||||
return return_server_with_power_state(power_state)
|
||||
|
||||
|
||||
def stub_instance(id, power_state=0, metadata=None,
|
||||
image_ref="10", flavor_id="1", name=None):
|
||||
|
||||
if metadata is not None:
|
||||
metadata_items = [{'key':k, 'value':v} for k, v in metadata.items()]
|
||||
else:
|
||||
metadata_items = [{'key':'seq', 'value':id}]
|
||||
|
||||
inst_type = instance_types.get_instance_type_by_flavor_id(int(flavor_id))
|
||||
|
||||
instance = {
|
||||
"id": int(id),
|
||||
"created_at": datetime.datetime(2010, 10, 10, 12, 0, 0),
|
||||
"updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0),
|
||||
"admin_pass": "",
|
||||
"user_id": "fake",
|
||||
"project_id": "fake",
|
||||
"image_ref": image_ref,
|
||||
"kernel_id": "",
|
||||
"ramdisk_id": "",
|
||||
"launch_index": 0,
|
||||
"key_name": "",
|
||||
"key_data": "",
|
||||
"state": power_state,
|
||||
"state_description": "",
|
||||
"memory_mb": 0,
|
||||
"vcpus": 0,
|
||||
"local_gb": 0,
|
||||
"hostname": "",
|
||||
"host": "",
|
||||
"instance_type": dict(inst_type),
|
||||
"user_data": "",
|
||||
"reservation_id": "",
|
||||
"mac_address": "",
|
||||
"scheduled_at": utils.utcnow(),
|
||||
"launched_at": utils.utcnow(),
|
||||
"terminated_at": utils.utcnow(),
|
||||
"availability_zone": "",
|
||||
"display_name": name or "server%s" % id,
|
||||
"display_description": "",
|
||||
"locked": False,
|
||||
"metadata": metadata_items,
|
||||
"access_ip_v4": "",
|
||||
"access_ip_v6": "",
|
||||
"uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
"virtual_interfaces": [],
|
||||
}
|
||||
|
||||
instance["fixed_ips"] = {
|
||||
"address": '192.168.0.1',
|
||||
"floating_ips": [],
|
||||
}
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class MockSetAdminPassword(object):
|
||||
@@ -54,48 +109,6 @@ class MockSetAdminPassword(object):
|
||||
self.password = password
|
||||
|
||||
|
||||
def _get_instance():
|
||||
instance = {
|
||||
"id": 1,
|
||||
"created_at": "2010-10-10 12:00:00",
|
||||
"updated_at": "2010-11-11 11:00:00",
|
||||
"admin_pass": "",
|
||||
"user_id": "",
|
||||
"project_id": "",
|
||||
"image_ref": "5",
|
||||
"kernel_id": "",
|
||||
"ramdisk_id": "",
|
||||
"launch_index": 0,
|
||||
"key_name": "",
|
||||
"key_data": "",
|
||||
"state": 0,
|
||||
"state_description": "",
|
||||
"memory_mb": 0,
|
||||
"vcpus": 0,
|
||||
"local_gb": 0,
|
||||
"hostname": "",
|
||||
"host": "",
|
||||
"instance_type": {
|
||||
"flavorid": 1,
|
||||
},
|
||||
"user_data": "",
|
||||
"reservation_id": "",
|
||||
"mac_address": "",
|
||||
"scheduled_at": utils.utcnow(),
|
||||
"launched_at": utils.utcnow(),
|
||||
"terminated_at": utils.utcnow(),
|
||||
"availability_zone": "",
|
||||
"display_name": "test_server",
|
||||
"display_description": "",
|
||||
"locked": False,
|
||||
"metadata": [],
|
||||
#"address": ,
|
||||
#"floating_ips": [{"address":ip} for ip in public_addresses]}
|
||||
"uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"}
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class ServerActionsTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -103,8 +116,6 @@ class ServerActionsTest(test.TestCase):
|
||||
super(ServerActionsTest, self).setUp()
|
||||
self.flags(verbose=True)
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
fakes.FakeAuthManager.reset_fake_data()
|
||||
fakes.FakeAuthDatabase.data = {}
|
||||
fakes.stub_out_auth(self.stubs)
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
|
||||
self.stubs.Set(nova.db.api, 'instance_update', instance_update)
|
||||
@@ -392,7 +403,7 @@ class ServerActionsTest(test.TestCase):
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertEqual(413, response.status_int)
|
||||
|
||||
def test_create_backup_no_name(self):
|
||||
"""Name is required for backups"""
|
||||
@@ -468,8 +479,6 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.maxDiff = None
|
||||
super(ServerActionsTestV11, self).setUp()
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
fakes.FakeAuthManager.reset_fake_data()
|
||||
fakes.FakeAuthDatabase.data = {}
|
||||
fakes.stub_out_auth(self.stubs)
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_by_id)
|
||||
self.stubs.Set(nova.db.api, 'instance_update', instance_update)
|
||||
@@ -489,7 +498,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_bad_body(self):
|
||||
body = {}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -498,7 +507,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_unknown_action(self):
|
||||
body = {'sockTheFox': {'fakekey': '1234'}}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -509,7 +518,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
mock_method = MockSetAdminPassword()
|
||||
self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
|
||||
body = {'changePassword': {'adminPass': '1234pass'}}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -521,7 +530,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
def test_server_change_password_xml(self):
|
||||
mock_method = MockSetAdminPassword()
|
||||
self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = "application/xml"
|
||||
req.body = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -535,7 +544,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_change_password_not_a_string(self):
|
||||
body = {'changePassword': {'adminPass': 1234}}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -544,7 +553,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_change_password_bad_request(self):
|
||||
body = {'changePassword': {'pass': '12345'}}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -553,7 +562,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_change_password_empty_string(self):
|
||||
body = {'changePassword': {'adminPass': ''}}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -562,7 +571,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_change_password_none(self):
|
||||
body = {'changePassword': {'adminPass': None}}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -571,7 +580,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_reboot_hard(self):
|
||||
body = dict(reboot=dict(type="HARD"))
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -580,7 +589,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_reboot_soft(self):
|
||||
body = dict(reboot=dict(type="SOFT"))
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -589,7 +598,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_reboot_incorrect_type(self):
|
||||
body = dict(reboot=dict(type="NOT_A_TYPE"))
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -598,7 +607,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
|
||||
def test_server_reboot_missing_type(self):
|
||||
body = dict(reboot=dict())
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -606,19 +615,25 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.assertEqual(res.status_int, 400)
|
||||
|
||||
def test_server_rebuild_accepted_minimum(self):
|
||||
new_return_server = return_server_with_attributes(image_ref='2')
|
||||
self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
|
||||
|
||||
body = {
|
||||
"rebuild": {
|
||||
"imageRef": "http://localhost/images/2",
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 202)
|
||||
body = json.loads(res.body)
|
||||
self.assertEqual(body['server']['image']['id'], '2')
|
||||
self.assertEqual(len(body['server']['adminPass']), 16)
|
||||
|
||||
def test_server_rebuild_rejected_when_building(self):
|
||||
body = {
|
||||
@@ -633,7 +648,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.stubs.Set(nova.db, 'instance_get_by_uuid',
|
||||
return_server_with_uuid_and_power_state(state))
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -642,22 +657,27 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.assertEqual(res.status_int, 409)
|
||||
|
||||
def test_server_rebuild_accepted_with_metadata(self):
|
||||
metadata = {'new': 'metadata'}
|
||||
|
||||
new_return_server = return_server_with_attributes(metadata=metadata)
|
||||
self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
|
||||
|
||||
body = {
|
||||
"rebuild": {
|
||||
"imageRef": "http://localhost/images/2",
|
||||
"metadata": {
|
||||
"new": "metadata",
|
||||
},
|
||||
"metadata": metadata,
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 202)
|
||||
body = json.loads(res.body)
|
||||
self.assertEqual(body['server']['metadata'], metadata)
|
||||
|
||||
def test_server_rebuild_accepted_with_bad_metadata(self):
|
||||
body = {
|
||||
@@ -667,7 +687,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -682,7 +702,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -701,7 +721,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
@@ -720,17 +740,60 @@ class ServerActionsTestV11(test.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 202)
|
||||
body = json.loads(res.body)
|
||||
self.assertTrue('personality' not in body['server'])
|
||||
|
||||
def test_server_rebuild_admin_pass(self):
|
||||
new_return_server = return_server_with_attributes(image_ref='2')
|
||||
self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
|
||||
|
||||
body = {
|
||||
"rebuild": {
|
||||
"imageRef": "http://localhost/images/2",
|
||||
"adminPass": "asdf",
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 202)
|
||||
body = json.loads(res.body)
|
||||
self.assertEqual(body['server']['image']['id'], '2')
|
||||
self.assertEqual(body['server']['adminPass'], 'asdf')
|
||||
|
||||
def test_server_rebuild_server_not_found(self):
|
||||
def server_not_found(self, instance_id):
|
||||
raise exception.InstanceNotFound(instance_id=instance_id)
|
||||
self.stubs.Set(nova.db.api, 'instance_get', server_not_found)
|
||||
|
||||
body = {
|
||||
"rebuild": {
|
||||
"imageRef": "http://localhost/images/2",
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 404)
|
||||
|
||||
def test_resize_server(self):
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.content_type = 'application/json'
|
||||
req.method = 'POST'
|
||||
body_dict = dict(resize=dict(flavorRef="http://localhost/3"))
|
||||
@@ -748,7 +811,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.assertEqual(self.resize_called, True)
|
||||
|
||||
def test_resize_server_no_flavor(self):
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.content_type = 'application/json'
|
||||
req.method = 'POST'
|
||||
body_dict = dict(resize=dict())
|
||||
@@ -758,7 +821,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.assertEqual(res.status_int, 400)
|
||||
|
||||
def test_resize_server_no_flavor_ref(self):
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.content_type = 'application/json'
|
||||
req.method = 'POST'
|
||||
body_dict = dict(resize=dict(flavorRef=None))
|
||||
@@ -768,7 +831,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.assertEqual(res.status_int, 400)
|
||||
|
||||
def test_confirm_resize_server(self):
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.content_type = 'application/json'
|
||||
req.method = 'POST'
|
||||
body_dict = dict(confirmResize=None)
|
||||
@@ -786,7 +849,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
self.assertEqual(self.confirm_resize_called, True)
|
||||
|
||||
def test_revert_resize_server(self):
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.content_type = 'application/json'
|
||||
req.method = 'POST'
|
||||
body_dict = dict(revertResize=None)
|
||||
@@ -809,7 +872,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
'name': 'Snapshot 1',
|
||||
},
|
||||
}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -828,7 +891,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
'name': 'Snapshot 1',
|
||||
},
|
||||
}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -842,7 +905,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
'metadata': {'key': 'asdf'},
|
||||
},
|
||||
}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -860,18 +923,18 @@ class ServerActionsTestV11(test.TestCase):
|
||||
}
|
||||
for num in range(FLAGS.quota_metadata_items + 1):
|
||||
body['createImage']['metadata']['foo%i' % num] = "bar"
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
response = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertEqual(413, response.status_int)
|
||||
|
||||
def test_create_image_no_name(self):
|
||||
body = {
|
||||
'createImage': {},
|
||||
}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -885,7 +948,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
'metadata': 'henry',
|
||||
},
|
||||
}
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -904,7 +967,7 @@ class ServerActionsTestV11(test.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
req = webob.Request.blank('/v1.1/servers/1/action')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/action')
|
||||
req.method = 'POST'
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
@@ -83,7 +83,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_index(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_get',
|
||||
return_server_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(200, res.status_int)
|
||||
res_dict = json.loads(res.body)
|
||||
@@ -100,7 +100,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_index_xml(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_get',
|
||||
return_server_metadata)
|
||||
request = webob.Request.blank("/v1.1/servers/1/metadata")
|
||||
request = webob.Request.blank("/v1.1/fake/servers/1/metadata")
|
||||
request.accept = "application/xml"
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(200, response.status_int)
|
||||
@@ -120,14 +120,14 @@ class ServerMetaDataTest(test.TestCase):
|
||||
|
||||
def test_index_nonexistant_server(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, res.status_int)
|
||||
|
||||
def test_index_no_data(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_get',
|
||||
return_empty_server_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(200, res.status_int)
|
||||
res_dict = json.loads(res.body)
|
||||
@@ -137,7 +137,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_show(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_get',
|
||||
return_server_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key2')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
res_dict = json.loads(res.body)
|
||||
self.assertEqual(200, res.status_int)
|
||||
@@ -147,7 +147,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_show_xml(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_get',
|
||||
return_server_metadata)
|
||||
request = webob.Request.blank("/v1.1/servers/1/metadata/key2")
|
||||
request = webob.Request.blank("/v1.1/fake/servers/1/metadata/key2")
|
||||
request.accept = "application/xml"
|
||||
response = request.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(200, response.status_int)
|
||||
@@ -164,14 +164,14 @@ class ServerMetaDataTest(test.TestCase):
|
||||
|
||||
def test_show_nonexistant_server(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key2')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, res.status_int)
|
||||
|
||||
def test_show_meta_not_found(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_get',
|
||||
return_empty_server_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key6')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key6')
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, res.status_int)
|
||||
|
||||
@@ -180,7 +180,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
return_server_metadata)
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_delete',
|
||||
delete_server_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key2')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key2')
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(204, res.status_int)
|
||||
@@ -188,7 +188,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
|
||||
def test_delete_nonexistant_server(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1')
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, res.status_int)
|
||||
@@ -196,7 +196,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_delete_meta_not_found(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_get',
|
||||
return_empty_server_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key6')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key6')
|
||||
req.method = 'DELETE'
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(404, res.status_int)
|
||||
@@ -206,7 +206,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
return_server_metadata)
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
req.method = 'POST'
|
||||
req.content_type = "application/json"
|
||||
input = {"metadata": {"key9": "value9"}}
|
||||
@@ -227,7 +227,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
return_server_metadata)
|
||||
self.stubs.Set(nova.db.api, "instance_metadata_update",
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank("/v1.1/servers/1/metadata")
|
||||
req = webob.Request.blank("/v1.1/fake/servers/1/metadata")
|
||||
req.method = "POST"
|
||||
req.content_type = "application/xml"
|
||||
req.accept = "application/xml"
|
||||
@@ -258,7 +258,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_create_empty_body(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
req.method = 'POST'
|
||||
req.headers["content-type"] = "application/json"
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
@@ -266,7 +266,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
|
||||
def test_create_nonexistant_server(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
|
||||
req = webob.Request.blank('/v1.1/servers/100/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/100/metadata')
|
||||
req.method = 'POST'
|
||||
req.body = '{"metadata": {"key1": "value1"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -276,7 +276,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_all(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
req.method = 'PUT'
|
||||
req.content_type = "application/json"
|
||||
expected = {
|
||||
@@ -294,7 +294,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_all_empty_container(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
req.method = 'PUT'
|
||||
req.content_type = "application/json"
|
||||
expected = {'metadata': {}}
|
||||
@@ -307,7 +307,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_all_malformed_container(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
req.method = 'PUT'
|
||||
req.content_type = "application/json"
|
||||
expected = {'meta': {}}
|
||||
@@ -318,7 +318,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_all_malformed_data(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
req.method = 'PUT'
|
||||
req.content_type = "application/json"
|
||||
expected = {'metadata': ['asdf']}
|
||||
@@ -328,7 +328,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
|
||||
def test_update_all_nonexistant_server(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
|
||||
req = webob.Request.blank('/v1.1/servers/100/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/100/metadata')
|
||||
req.method = 'PUT'
|
||||
req.content_type = "application/json"
|
||||
req.body = json.dumps({'metadata': {'key10': 'value10'}})
|
||||
@@ -338,7 +338,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_item(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"key1": "value1"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -352,7 +352,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_item_xml(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key9')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key9')
|
||||
req.method = 'PUT'
|
||||
req.accept = "application/json"
|
||||
req.content_type = "application/xml"
|
||||
@@ -369,7 +369,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
|
||||
def test_update_item_nonexistant_server(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_get', return_server_nonexistant)
|
||||
req = webob.Request.blank('/v1.1/servers/asdf/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/asdf/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta":{"key1": "value1"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -379,7 +379,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_item_empty_body(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.headers["content-type"] = "application/json"
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
@@ -388,7 +388,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_item_too_many_keys(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"key1": "value1", "key2": "value2"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -398,7 +398,7 @@ class ServerMetaDataTest(test.TestCase):
|
||||
def test_update_item_body_uri_mismatch(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/bad')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/bad')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"key1": "value1"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
@@ -412,17 +412,17 @@ class ServerMetaDataTest(test.TestCase):
|
||||
for num in range(FLAGS.quota_metadata_items + 1):
|
||||
data['metadata']['key%i' % num] = "blah"
|
||||
json_string = str(data).replace("\'", "\"")
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata')
|
||||
req.method = 'POST'
|
||||
req.body = json_string
|
||||
req.headers["content-type"] = "application/json"
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(400, res.status_int)
|
||||
self.assertEqual(413, res.status_int)
|
||||
|
||||
def test_to_many_metadata_items_on_update_item(self):
|
||||
def test_too_many_metadata_items_on_update_item(self):
|
||||
self.stubs.Set(nova.db.api, 'instance_metadata_update',
|
||||
return_create_instance_metadata_max)
|
||||
req = webob.Request.blank('/v1.1/servers/1/metadata/key1')
|
||||
req = webob.Request.blank('/v1.1/fake/servers/1/metadata/key1')
|
||||
req.method = 'PUT'
|
||||
req.body = '{"meta": {"a new key": "a new value"}}'
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user