fixed a bug which prevents suspend/resume after block-migration
This commit is contained in:
4
.mailmap
4
.mailmap
@@ -47,3 +47,7 @@
|
|||||||
<vishvananda@gmail.com> <root@mirror.nasanebula.net>
|
<vishvananda@gmail.com> <root@mirror.nasanebula.net>
|
||||||
<vishvananda@gmail.com> <root@ubuntu>
|
<vishvananda@gmail.com> <root@ubuntu>
|
||||||
<vishvananda@gmail.com> <vishvananda@yahoo.com>
|
<vishvananda@gmail.com> <vishvananda@yahoo.com>
|
||||||
|
<ilyaalekseyev@acm.org> <ialekseev@griddynamics.com>
|
||||||
|
<ilyaalekseyev@acm.org> <ilya@oscloud.ru>
|
||||||
|
<reldan@oscloud.ru> <enugaev@griddynamics.com>
|
||||||
|
<kshileev@gmail.com> <kshileev@griddynamics.com>
|
5
Authors
5
Authors
@@ -22,14 +22,14 @@ David Pravec <David.Pravec@danix.org>
|
|||||||
Dean Troyer <dtroyer@gmail.com>
|
Dean Troyer <dtroyer@gmail.com>
|
||||||
Devin Carlen <devin.carlen@gmail.com>
|
Devin Carlen <devin.carlen@gmail.com>
|
||||||
Ed Leafe <ed@leafe.com>
|
Ed Leafe <ed@leafe.com>
|
||||||
Eldar Nugaev <enugaev@griddynamics.com>
|
Eldar Nugaev <reldan@oscloud.ru>
|
||||||
Eric Day <eday@oddments.org>
|
Eric Day <eday@oddments.org>
|
||||||
Eric Windisch <eric@cloudscaling.com>
|
Eric Windisch <eric@cloudscaling.com>
|
||||||
Ewan Mellor <ewan.mellor@citrix.com>
|
Ewan Mellor <ewan.mellor@citrix.com>
|
||||||
Gabe Westmaas <gabe.westmaas@rackspace.com>
|
Gabe Westmaas <gabe.westmaas@rackspace.com>
|
||||||
Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
|
Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
|
||||||
Hisaki Ohara <hisaki.ohara@intel.com>
|
Hisaki Ohara <hisaki.ohara@intel.com>
|
||||||
Ilya Alekseyev <ialekseev@griddynamics.com>
|
Ilya Alekseyev <ilyaalekseyev@acm.org>
|
||||||
Isaku Yamahata <yamahata@valinux.co.jp>
|
Isaku Yamahata <yamahata@valinux.co.jp>
|
||||||
Jason Cannavale <jason.cannavale@rackspace.com>
|
Jason Cannavale <jason.cannavale@rackspace.com>
|
||||||
Jason Koelker <jason@koelker.net>
|
Jason Koelker <jason@koelker.net>
|
||||||
@@ -53,6 +53,7 @@ Kei Masumoto <masumotok@nttdata.co.jp>
|
|||||||
Ken Pepple <ken.pepple@gmail.com>
|
Ken Pepple <ken.pepple@gmail.com>
|
||||||
Kevin Bringard <kbringard@attinteractive.com>
|
Kevin Bringard <kbringard@attinteractive.com>
|
||||||
Kevin L. Mitchell <kevin.mitchell@rackspace.com>
|
Kevin L. Mitchell <kevin.mitchell@rackspace.com>
|
||||||
|
Kirill Shileev <kshileev@gmail.com>
|
||||||
Koji Iida <iida.koji@lab.ntt.co.jp>
|
Koji Iida <iida.koji@lab.ntt.co.jp>
|
||||||
Lorin Hochstein <lorin@isi.edu>
|
Lorin Hochstein <lorin@isi.edu>
|
||||||
Lvov Maxim <usrleon@gmail.com>
|
Lvov Maxim <usrleon@gmail.com>
|
||||||
|
116
bin/instance-usage-audit
Executable file
116
bin/instance-usage-audit
Executable file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright (c) 2011 Openstack, LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Cron script to generate usage notifications for instances neither created
|
||||||
|
nor destroyed in a given time period.
|
||||||
|
|
||||||
|
Together with the notifications generated by compute on instance
|
||||||
|
create/delete/resize, over that ime period, this allows an external
|
||||||
|
system consuming usage notification feeds to calculate instance usage
|
||||||
|
for each tenant.
|
||||||
|
|
||||||
|
Time periods are specified like so:
|
||||||
|
<number>[mdy]
|
||||||
|
|
||||||
|
1m = previous month. If the script is run April 1, it will generate usages
|
||||||
|
for March 1 thry March 31.
|
||||||
|
3m = 3 previous months.
|
||||||
|
90d = previous 90 days.
|
||||||
|
1y = previous year. If run on Jan 1, it generates usages for
|
||||||
|
Jan 1 thru Dec 31 of the previous year.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import gettext
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
# If ../nova/__init__.py exists, add ../ to Python search path, so that
|
||||||
|
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||||
|
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||||
|
os.pardir,
|
||||||
|
os.pardir))
|
||||||
|
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')):
|
||||||
|
sys.path.insert(0, POSSIBLE_TOPDIR)
|
||||||
|
|
||||||
|
gettext.install('nova', unicode=1)
|
||||||
|
|
||||||
|
|
||||||
|
from nova import context
|
||||||
|
from nova import db
|
||||||
|
from nova import exception
|
||||||
|
from nova import flags
|
||||||
|
from nova import log as logging
|
||||||
|
from nova import utils
|
||||||
|
|
||||||
|
from nova.notifier import api as notifier_api
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
flags.DEFINE_string('instance_usage_audit_period', '1m',
|
||||||
|
'time period to generate instance usages for.')
|
||||||
|
|
||||||
|
|
||||||
|
def time_period(period):
|
||||||
|
today = datetime.date.today()
|
||||||
|
unit = period[-1]
|
||||||
|
if unit not in 'mdy':
|
||||||
|
raise ValueError('Time period must be m, d, or y')
|
||||||
|
n = int(period[:-1])
|
||||||
|
if unit == 'm':
|
||||||
|
year = today.year - (n // 12)
|
||||||
|
n = n % 12
|
||||||
|
if n >= today.month:
|
||||||
|
year -= 1
|
||||||
|
month = 12 + (today.month - n)
|
||||||
|
else:
|
||||||
|
month = today.month - n
|
||||||
|
begin = datetime.datetime(day=1, month=month, year=year)
|
||||||
|
end = datetime.datetime(day=1, month=today.month, year=today.year)
|
||||||
|
|
||||||
|
elif unit == 'y':
|
||||||
|
begin = datetime.datetime(day=1, month=1, year=today.year - n)
|
||||||
|
end = datetime.datetime(day=1, month=1, year=today.year)
|
||||||
|
|
||||||
|
elif unit == 'd':
|
||||||
|
b = today - datetime.timedelta(days=n)
|
||||||
|
begin = datetime.datetime(day=b.day, month=b.month, year=b.year)
|
||||||
|
end = datetime.datetime(day=today.day,
|
||||||
|
month=today.month,
|
||||||
|
year=today.year)
|
||||||
|
|
||||||
|
return (begin, end)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
utils.default_flagfile()
|
||||||
|
flags.FLAGS(sys.argv)
|
||||||
|
logging.setup()
|
||||||
|
begin, end = time_period(FLAGS.instance_usage_audit_period)
|
||||||
|
print "Creating usages for %s until %s" % (str(begin), str(end))
|
||||||
|
instances = db.instance_get_active_by_window(context.get_admin_context(),
|
||||||
|
begin,
|
||||||
|
end)
|
||||||
|
print "%s instances" % len(instances)
|
||||||
|
for instance_ref in instances:
|
||||||
|
usage_info = utils.usage_from_instance(instance_ref,
|
||||||
|
audit_period_begining=str(begin),
|
||||||
|
audit_period_ending=str(end))
|
||||||
|
notifier_api.notify('compute.%s' % FLAGS.host,
|
||||||
|
'compute.instance.exists',
|
||||||
|
notifier_api.INFO,
|
||||||
|
usage_info)
|
@@ -137,8 +137,9 @@ if __name__ == '__main__':
|
|||||||
utils.default_flagfile()
|
utils.default_flagfile()
|
||||||
FLAGS(sys.argv)
|
FLAGS(sys.argv)
|
||||||
logging.setup()
|
logging.setup()
|
||||||
server = wsgi.Server()
|
acp_port = FLAGS.ajax_console_proxy_port
|
||||||
acp = AjaxConsoleProxy()
|
acp = AjaxConsoleProxy()
|
||||||
acp.register_listeners()
|
acp.register_listeners()
|
||||||
server.start(acp, FLAGS.ajax_console_proxy_port, host='0.0.0.0')
|
server = wsgi.Server("AJAX Console Proxy", acp, port=acp_port)
|
||||||
|
server.start()
|
||||||
server.wait()
|
server.wait()
|
||||||
|
57
bin/nova-api
57
bin/nova-api
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# pylint: disable=C0103
|
|
||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
# Copyright 2010 United States Government as represented by the
|
# Copyright 2010 United States Government as represented by the
|
||||||
@@ -18,44 +17,40 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Starter script for Nova API."""
|
"""Starter script for Nova API.
|
||||||
|
|
||||||
|
Starts both the EC2 and OpenStack APIs in separate processes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
import gettext
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# If ../nova/__init__.py exists, add ../ to Python search path, so that
|
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(
|
||||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
sys.argv[0]), os.pardir, os.pardir))
|
||||||
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")):
|
||||||
os.pardir,
|
|
||||||
os.pardir))
|
|
||||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
|
||||||
sys.path.insert(0, possible_topdir)
|
sys.path.insert(0, possible_topdir)
|
||||||
|
|
||||||
gettext.install('nova', unicode=1)
|
import nova.service
|
||||||
|
import nova.utils
|
||||||
from nova import flags
|
|
||||||
from nova import log as logging
|
|
||||||
from nova import service
|
|
||||||
from nova import utils
|
|
||||||
from nova import version
|
|
||||||
from nova import wsgi
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger('nova.api')
|
def main():
|
||||||
|
"""Launch EC2 and OSAPI services."""
|
||||||
|
nova.utils.Bootstrapper.bootstrap_binary(sys.argv)
|
||||||
|
|
||||||
|
ec2 = nova.service.WSGIService("ec2")
|
||||||
|
osapi = nova.service.WSGIService("osapi")
|
||||||
|
|
||||||
|
launcher = nova.service.Launcher()
|
||||||
|
launcher.launch_service(ec2)
|
||||||
|
launcher.launch_service(osapi)
|
||||||
|
|
||||||
|
try:
|
||||||
|
launcher.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
launcher.stop()
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
utils.default_flagfile()
|
sys.exit(main())
|
||||||
FLAGS(sys.argv)
|
|
||||||
logging.setup()
|
|
||||||
LOG.audit(_("Starting nova-api node (version %s)"),
|
|
||||||
version.version_string_with_vcs())
|
|
||||||
LOG.debug(_("Full set of FLAGS:"))
|
|
||||||
for flag in FLAGS:
|
|
||||||
flag_get = FLAGS.get(flag, None)
|
|
||||||
LOG.debug("%(flag)s : %(flag_get)s" % locals())
|
|
||||||
|
|
||||||
service = service.serve_wsgi(service.ApiService)
|
|
||||||
service.wait()
|
|
||||||
|
@@ -93,6 +93,9 @@ if __name__ == '__main__':
|
|||||||
with_req = direct.PostParamsMiddleware(with_json)
|
with_req = direct.PostParamsMiddleware(with_json)
|
||||||
with_auth = direct.DelegatedAuthMiddleware(with_req)
|
with_auth = direct.DelegatedAuthMiddleware(with_req)
|
||||||
|
|
||||||
server = wsgi.Server()
|
server = wsgi.Server("Direct API",
|
||||||
server.start(with_auth, FLAGS.direct_port, host=FLAGS.direct_host)
|
with_auth,
|
||||||
|
host=FLAGS.direct_host,
|
||||||
|
port=FLAGS.direct_port)
|
||||||
|
server.start()
|
||||||
server.wait()
|
server.wait()
|
||||||
|
@@ -56,11 +56,11 @@
|
|||||||
import gettext
|
import gettext
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
|
import netaddr
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import IPy
|
|
||||||
|
|
||||||
# If ../nova/__init__.py exists, add ../ to Python search path, so that
|
# If ../nova/__init__.py exists, add ../ to Python search path, so that
|
||||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||||
@@ -257,6 +257,11 @@ class RoleCommands(object):
|
|||||||
"""adds role to user
|
"""adds role to user
|
||||||
if project is specified, adds project specific role
|
if project is specified, adds project specific role
|
||||||
arguments: user, role [project]"""
|
arguments: user, role [project]"""
|
||||||
|
if project:
|
||||||
|
projobj = self.manager.get_project(project)
|
||||||
|
if not projobj.has_member(user):
|
||||||
|
print "%s not a member of %s" % (user, project)
|
||||||
|
return
|
||||||
self.manager.add_role(user, role, project)
|
self.manager.add_role(user, role, project)
|
||||||
|
|
||||||
def has(self, user, role, project=None):
|
def has(self, user, role, project=None):
|
||||||
@@ -513,7 +518,7 @@ class FloatingIpCommands(object):
|
|||||||
def create(self, host, range):
|
def create(self, host, range):
|
||||||
"""Creates floating ips for host by range
|
"""Creates floating ips for host by range
|
||||||
arguments: host ip_range"""
|
arguments: host ip_range"""
|
||||||
for address in IPy.IP(range):
|
for address in netaddr.IPNetwork(range):
|
||||||
db.floating_ip_create(context.get_admin_context(),
|
db.floating_ip_create(context.get_admin_context(),
|
||||||
{'address': str(address),
|
{'address': str(address),
|
||||||
'host': host})
|
'host': host})
|
||||||
@@ -521,7 +526,7 @@ class FloatingIpCommands(object):
|
|||||||
def delete(self, ip_range):
|
def delete(self, ip_range):
|
||||||
"""Deletes floating ips by range
|
"""Deletes floating ips by range
|
||||||
arguments: range"""
|
arguments: range"""
|
||||||
for address in IPy.IP(ip_range):
|
for address in netaddr.IPNetwork(ip_range):
|
||||||
db.floating_ip_destroy(context.get_admin_context(),
|
db.floating_ip_destroy(context.get_admin_context(),
|
||||||
str(address))
|
str(address))
|
||||||
|
|
||||||
@@ -612,7 +617,7 @@ class VmCommands(object):
|
|||||||
:param host: show all instance on specified host.
|
:param host: show all instance on specified host.
|
||||||
:param instance: show specificed instance.
|
:param instance: show specificed instance.
|
||||||
"""
|
"""
|
||||||
print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
|
print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \
|
||||||
" %-10s %-10s %-10s %-5s" % (
|
" %-10s %-10s %-10s %-5s" % (
|
||||||
_('instance'),
|
_('instance'),
|
||||||
_('node'),
|
_('node'),
|
||||||
@@ -634,14 +639,14 @@ class VmCommands(object):
|
|||||||
context.get_admin_context(), host)
|
context.get_admin_context(), host)
|
||||||
|
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
print "%-10s %-15s %-10s %-10s %-19s %-12s %-12s %-12s" \
|
print "%-10s %-15s %-10s %-10s %-26s %-9s %-9s %-9s" \
|
||||||
" %-10s %-10s %-10s %-5d" % (
|
" %-10s %-10s %-10s %-5d" % (
|
||||||
instance['hostname'],
|
instance['hostname'],
|
||||||
instance['host'],
|
instance['host'],
|
||||||
instance['instance_type'],
|
instance['instance_type'].name,
|
||||||
instance['state_description'],
|
instance['state_description'],
|
||||||
instance['launched_at'],
|
instance['launched_at'],
|
||||||
instance['image_id'],
|
instance['image_ref'],
|
||||||
instance['kernel_id'],
|
instance['kernel_id'],
|
||||||
instance['ramdisk_id'],
|
instance['ramdisk_id'],
|
||||||
instance['project_id'],
|
instance['project_id'],
|
||||||
@@ -903,7 +908,7 @@ class InstanceTypeCommands(object):
|
|||||||
try:
|
try:
|
||||||
instance_types.create(name, memory, vcpus, local_gb,
|
instance_types.create(name, memory, vcpus, local_gb,
|
||||||
flavorid, swap, rxtx_quota, rxtx_cap)
|
flavorid, swap, rxtx_quota, rxtx_cap)
|
||||||
except exception.InvalidInputException:
|
except exception.InvalidInput, e:
|
||||||
print "Must supply valid parameters to create instance_type"
|
print "Must supply valid parameters to create instance_type"
|
||||||
print e
|
print e
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1101,6 +1106,70 @@ class ImageCommands(object):
|
|||||||
self._convert_images(machine_images)
|
self._convert_images(machine_images)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentBuildCommands(object):
|
||||||
|
"""Class for managing agent builds."""
|
||||||
|
|
||||||
|
def create(self, os, architecture, version, url, md5hash,
|
||||||
|
hypervisor='xen'):
|
||||||
|
"""Creates a new agent build.
|
||||||
|
arguments: os architecture version url md5hash [hypervisor='xen']"""
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
agent_build = db.agent_build_create(ctxt,
|
||||||
|
{'hypervisor': hypervisor,
|
||||||
|
'os': os,
|
||||||
|
'architecture': architecture,
|
||||||
|
'version': version,
|
||||||
|
'url': url,
|
||||||
|
'md5hash': md5hash})
|
||||||
|
|
||||||
|
def delete(self, os, architecture, hypervisor='xen'):
|
||||||
|
"""Deletes an existing agent build.
|
||||||
|
arguments: os architecture [hypervisor='xen']"""
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
agent_build_ref = db.agent_build_get_by_triple(ctxt,
|
||||||
|
hypervisor, os, architecture)
|
||||||
|
db.agent_build_destroy(ctxt, agent_build_ref['id'])
|
||||||
|
|
||||||
|
def list(self, hypervisor=None):
|
||||||
|
"""Lists all agent builds.
|
||||||
|
arguments: <none>"""
|
||||||
|
fmt = "%-10s %-8s %12s %s"
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
by_hypervisor = {}
|
||||||
|
for agent_build in db.agent_build_get_all(ctxt):
|
||||||
|
buildlist = by_hypervisor.get(agent_build.hypervisor)
|
||||||
|
if not buildlist:
|
||||||
|
buildlist = by_hypervisor[agent_build.hypervisor] = []
|
||||||
|
|
||||||
|
buildlist.append(agent_build)
|
||||||
|
|
||||||
|
for key, buildlist in by_hypervisor.iteritems():
|
||||||
|
if hypervisor and key != hypervisor:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print "Hypervisor: %s" % key
|
||||||
|
print fmt % ('-' * 10, '-' * 8, '-' * 12, '-' * 32)
|
||||||
|
for agent_build in buildlist:
|
||||||
|
print fmt % (agent_build.os, agent_build.architecture,
|
||||||
|
agent_build.version, agent_build.md5hash)
|
||||||
|
print ' %s' % agent_build.url
|
||||||
|
|
||||||
|
print
|
||||||
|
|
||||||
|
def modify(self, os, architecture, version, url, md5hash,
|
||||||
|
hypervisor='xen'):
|
||||||
|
"""Update an existing agent build.
|
||||||
|
arguments: os architecture version url md5hash [hypervisor='xen']
|
||||||
|
"""
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
agent_build_ref = db.agent_build_get_by_triple(ctxt,
|
||||||
|
hypervisor, os, architecture)
|
||||||
|
db.agent_build_update(ctxt, agent_build_ref['id'],
|
||||||
|
{'version': version,
|
||||||
|
'url': url,
|
||||||
|
'md5hash': md5hash})
|
||||||
|
|
||||||
|
|
||||||
class ConfigCommands(object):
|
class ConfigCommands(object):
|
||||||
"""Class for exposing the flags defined by flag_file(s)."""
|
"""Class for exposing the flags defined by flag_file(s)."""
|
||||||
|
|
||||||
@@ -1113,6 +1182,7 @@ class ConfigCommands(object):
|
|||||||
|
|
||||||
CATEGORIES = [
|
CATEGORIES = [
|
||||||
('account', AccountCommands),
|
('account', AccountCommands),
|
||||||
|
('agent', AgentBuildCommands),
|
||||||
('config', ConfigCommands),
|
('config', ConfigCommands),
|
||||||
('db', DbCommands),
|
('db', DbCommands),
|
||||||
('fixed', FixedIpCommands),
|
('fixed', FixedIpCommands),
|
||||||
|
@@ -50,6 +50,9 @@ if __name__ == '__main__':
|
|||||||
FLAGS(sys.argv)
|
FLAGS(sys.argv)
|
||||||
logging.setup()
|
logging.setup()
|
||||||
router = s3server.S3Application(FLAGS.buckets_path)
|
router = s3server.S3Application(FLAGS.buckets_path)
|
||||||
server = wsgi.Server()
|
server = wsgi.Server("S3 Objectstore",
|
||||||
server.start(router, FLAGS.s3_port, host=FLAGS.s3_host)
|
router,
|
||||||
|
port=FLAGS.s3_port,
|
||||||
|
host=FLAGS.s3_host)
|
||||||
|
server.start()
|
||||||
server.wait()
|
server.wait()
|
||||||
|
@@ -96,6 +96,9 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
service.serve()
|
service.serve()
|
||||||
|
|
||||||
server = wsgi.Server()
|
server = wsgi.Server("VNC Proxy",
|
||||||
server.start(with_auth, FLAGS.vncproxy_port, host=FLAGS.vncproxy_host)
|
with_auth,
|
||||||
|
host=FLAGS.vncproxy_host,
|
||||||
|
port=FLAGS.vncproxy_port)
|
||||||
|
server.start()
|
||||||
server.wait()
|
server.wait()
|
||||||
|
@@ -100,6 +100,11 @@ class OBJECT_CLASS_VIOLATION(Exception): # pylint: disable=C0103
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SERVER_DOWN(Exception): # pylint: disable=C0103
|
||||||
|
"""Duplicate exception class from real LDAP module."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def initialize(_uri):
|
def initialize(_uri):
|
||||||
"""Opens a fake connection with an LDAP server."""
|
"""Opens a fake connection with an LDAP server."""
|
||||||
return FakeLDAP()
|
return FakeLDAP()
|
||||||
@@ -202,25 +207,38 @@ def _to_json(unencoded):
|
|||||||
return json.dumps(list(unencoded))
|
return json.dumps(list(unencoded))
|
||||||
|
|
||||||
|
|
||||||
|
server_fail = False
|
||||||
|
|
||||||
|
|
||||||
class FakeLDAP(object):
|
class FakeLDAP(object):
|
||||||
"""Fake LDAP connection."""
|
"""Fake LDAP connection."""
|
||||||
|
|
||||||
def simple_bind_s(self, dn, password):
|
def simple_bind_s(self, dn, password):
|
||||||
"""This method is ignored, but provided for compatibility."""
|
"""This method is ignored, but provided for compatibility."""
|
||||||
|
if server_fail:
|
||||||
|
raise SERVER_DOWN
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def unbind_s(self):
|
def unbind_s(self):
|
||||||
"""This method is ignored, but provided for compatibility."""
|
"""This method is ignored, but provided for compatibility."""
|
||||||
|
if server_fail:
|
||||||
|
raise SERVER_DOWN
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_s(self, dn, attr):
|
def add_s(self, dn, attr):
|
||||||
"""Add an object with the specified attributes at dn."""
|
"""Add an object with the specified attributes at dn."""
|
||||||
|
if server_fail:
|
||||||
|
raise SERVER_DOWN
|
||||||
|
|
||||||
key = "%s%s" % (self.__prefix, dn)
|
key = "%s%s" % (self.__prefix, dn)
|
||||||
value_dict = dict([(k, _to_json(v)) for k, v in attr])
|
value_dict = dict([(k, _to_json(v)) for k, v in attr])
|
||||||
Store.instance().hmset(key, value_dict)
|
Store.instance().hmset(key, value_dict)
|
||||||
|
|
||||||
def delete_s(self, dn):
|
def delete_s(self, dn):
|
||||||
"""Remove the ldap object at specified dn."""
|
"""Remove the ldap object at specified dn."""
|
||||||
|
if server_fail:
|
||||||
|
raise SERVER_DOWN
|
||||||
|
|
||||||
Store.instance().delete("%s%s" % (self.__prefix, dn))
|
Store.instance().delete("%s%s" % (self.__prefix, dn))
|
||||||
|
|
||||||
def modify_s(self, dn, attrs):
|
def modify_s(self, dn, attrs):
|
||||||
@@ -232,6 +250,9 @@ class FakeLDAP(object):
|
|||||||
([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value)
|
([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if server_fail:
|
||||||
|
raise SERVER_DOWN
|
||||||
|
|
||||||
store = Store.instance()
|
store = Store.instance()
|
||||||
key = "%s%s" % (self.__prefix, dn)
|
key = "%s%s" % (self.__prefix, dn)
|
||||||
|
|
||||||
@@ -255,6 +276,9 @@ class FakeLDAP(object):
|
|||||||
fields -- fields to return. Returns all fields if not specified
|
fields -- fields to return. Returns all fields if not specified
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if server_fail:
|
||||||
|
raise SERVER_DOWN
|
||||||
|
|
||||||
if scope != SCOPE_BASE and scope != SCOPE_SUBTREE:
|
if scope != SCOPE_BASE and scope != SCOPE_SUBTREE:
|
||||||
raise NotImplementedError(str(scope))
|
raise NotImplementedError(str(scope))
|
||||||
store = Store.instance()
|
store = Store.instance()
|
||||||
|
@@ -101,6 +101,41 @@ def sanitize(fn):
|
|||||||
return _wrapped
|
return _wrapped
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPWrapper(object):
|
||||||
|
def __init__(self, ldap, url, user, password):
|
||||||
|
self.ldap = ldap
|
||||||
|
self.url = url
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.conn = None
|
||||||
|
|
||||||
|
def __wrap_reconnect(f):
|
||||||
|
def inner(self, *args, **kwargs):
|
||||||
|
if self.conn is None:
|
||||||
|
self.connect()
|
||||||
|
return f(self.conn)(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return f(self.conn)(*args, **kwargs)
|
||||||
|
except self.ldap.SERVER_DOWN:
|
||||||
|
self.connect()
|
||||||
|
return f(self.conn)(*args, **kwargs)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
try:
|
||||||
|
self.conn = self.ldap.initialize(self.url)
|
||||||
|
self.conn.simple_bind_s(self.user, self.password)
|
||||||
|
except self.ldap.SERVER_DOWN:
|
||||||
|
self.conn = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
search_s = __wrap_reconnect(lambda conn: conn.search_s)
|
||||||
|
add_s = __wrap_reconnect(lambda conn: conn.add_s)
|
||||||
|
delete_s = __wrap_reconnect(lambda conn: conn.delete_s)
|
||||||
|
modify_s = __wrap_reconnect(lambda conn: conn.modify_s)
|
||||||
|
|
||||||
|
|
||||||
class LdapDriver(object):
|
class LdapDriver(object):
|
||||||
"""Ldap Auth driver
|
"""Ldap Auth driver
|
||||||
|
|
||||||
@@ -124,8 +159,8 @@ class LdapDriver(object):
|
|||||||
LdapDriver.project_objectclass = 'novaProject'
|
LdapDriver.project_objectclass = 'novaProject'
|
||||||
self.__cache = None
|
self.__cache = None
|
||||||
if LdapDriver.conn is None:
|
if LdapDriver.conn is None:
|
||||||
LdapDriver.conn = self.ldap.initialize(FLAGS.ldap_url)
|
LdapDriver.conn = LDAPWrapper(self.ldap, FLAGS.ldap_url,
|
||||||
LdapDriver.conn.simple_bind_s(FLAGS.ldap_user_dn,
|
FLAGS.ldap_user_dn,
|
||||||
FLAGS.ldap_password)
|
FLAGS.ldap_password)
|
||||||
if LdapDriver.mc is None:
|
if LdapDriver.mc is None:
|
||||||
LdapDriver.mc = memcache.Client(FLAGS.memcached_servers, debug=0)
|
LdapDriver.mc = memcache.Client(FLAGS.memcached_servers, debug=0)
|
||||||
|
11
nova/log.py
11
nova/log.py
@@ -314,3 +314,14 @@ logging.setLoggerClass(NovaLogger)
|
|||||||
def audit(msg, *args, **kwargs):
|
def audit(msg, *args, **kwargs):
|
||||||
"""Shortcut for logging to root log with sevrity 'AUDIT'."""
|
"""Shortcut for logging to root log with sevrity 'AUDIT'."""
|
||||||
logging.root.log(AUDIT, msg, *args, **kwargs)
|
logging.root.log(AUDIT, msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class WritableLogger(object):
|
||||||
|
"""A thin wrapper that responds to `write` and logs."""
|
||||||
|
|
||||||
|
def __init__(self, logger, level=logging.INFO):
|
||||||
|
self.logger = logger
|
||||||
|
self.level = level
|
||||||
|
|
||||||
|
def write(self, msg):
|
||||||
|
self.logger.log(self.level, msg)
|
||||||
|
28
nova/notifier/test_notifier.py
Normal file
28
nova/notifier/test_notifier.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from nova import flags
|
||||||
|
from nova import log as logging
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
|
||||||
|
NOTIFICATIONS = []
|
||||||
|
|
||||||
|
|
||||||
|
def notify(message):
|
||||||
|
"""Test notifier, stores notifications in memory for unittests."""
|
||||||
|
NOTIFICATIONS.append(message)
|
@@ -275,6 +275,11 @@ class FanoutAdapterConsumer(AdapterConsumer):
|
|||||||
unique = uuid.uuid4().hex
|
unique = uuid.uuid4().hex
|
||||||
self.queue = '%s_fanout_%s' % (topic, unique)
|
self.queue = '%s_fanout_%s' % (topic, unique)
|
||||||
self.durable = False
|
self.durable = False
|
||||||
|
# Fanout creates unique queue names, so we should auto-remove
|
||||||
|
# them when done, so they're not left around on restart.
|
||||||
|
# Also, we're the only one that should be consuming. exclusive
|
||||||
|
# implies auto_delete, so we'll just set that..
|
||||||
|
self.exclusive = True
|
||||||
LOG.info(_('Created "%(exchange)s" fanout exchange '
|
LOG.info(_('Created "%(exchange)s" fanout exchange '
|
||||||
'with "%(key)s" routing key'),
|
'with "%(key)s" routing key'),
|
||||||
dict(exchange=self.exchange, key=self.routing_key))
|
dict(exchange=self.exchange, key=self.routing_key))
|
||||||
|
@@ -24,6 +24,7 @@ from nova import exception
|
|||||||
from nova import flags
|
from nova import flags
|
||||||
from nova import log as logging
|
from nova import log as logging
|
||||||
from nova import rpc
|
from nova import rpc
|
||||||
|
from nova import utils
|
||||||
|
|
||||||
from eventlet import greenpool
|
from eventlet import greenpool
|
||||||
|
|
||||||
@@ -161,32 +162,53 @@ def child_zone_helper(zone_list, func):
|
|||||||
_wrap_method(_process, func), zone_list)]
|
_wrap_method(_process, func), zone_list)]
|
||||||
|
|
||||||
|
|
||||||
def _issue_novaclient_command(nova, zone, collection, method_name, item_id):
|
def _issue_novaclient_command(nova, zone, collection,
|
||||||
|
method_name, *args, **kwargs):
|
||||||
"""Use novaclient to issue command to a single child zone.
|
"""Use novaclient to issue command to a single child zone.
|
||||||
One of these will be run in parallel for each child zone."""
|
One of these will be run in parallel for each child zone.
|
||||||
|
"""
|
||||||
manager = getattr(nova, collection)
|
manager = getattr(nova, collection)
|
||||||
result = None
|
|
||||||
try:
|
# NOTE(comstud): This is not ideal, but we have to do this based on
|
||||||
|
# how novaclient is implemented right now.
|
||||||
|
# 'find' is special cased as novaclient requires kwargs for it to
|
||||||
|
# filter on a 'get_all'.
|
||||||
|
# Every other method first needs to do a 'get' on the first argument
|
||||||
|
# passed, which should be a UUID. If it's 'get' itself that we want,
|
||||||
|
# we just return the result. Otherwise, we next call the real method
|
||||||
|
# that's wanted... passing other arguments that may or may not exist.
|
||||||
|
if method_name in ['find', 'findall']:
|
||||||
try:
|
try:
|
||||||
result = manager.get(int(item_id))
|
return getattr(manager, method_name)(**kwargs)
|
||||||
except ValueError, e:
|
except novaclient.NotFound:
|
||||||
result = manager.find(name=item_id)
|
url = zone.api_url
|
||||||
|
LOG.debug(_("%(collection)s.%(method_name)s didn't find "
|
||||||
|
"anything matching '%(kwargs)s' on '%(url)s'" %
|
||||||
|
locals()))
|
||||||
|
return None
|
||||||
|
|
||||||
|
args = list(args)
|
||||||
|
# pop off the UUID to look up
|
||||||
|
item = args.pop(0)
|
||||||
|
try:
|
||||||
|
result = manager.get(item)
|
||||||
except novaclient.NotFound:
|
except novaclient.NotFound:
|
||||||
url = zone.api_url
|
url = zone.api_url
|
||||||
LOG.debug(_("%(collection)s '%(item_id)s' not found on '%(url)s'" %
|
LOG.debug(_("%(collection)s '%(item)s' not found on '%(url)s'" %
|
||||||
locals()))
|
locals()))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if method_name.lower() not in ['get', 'find']:
|
if method_name.lower() != 'get':
|
||||||
result = getattr(result, method_name)()
|
# if we're doing something other than 'get', call it passing args.
|
||||||
|
result = getattr(result, method_name)(*args, **kwargs)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def wrap_novaclient_function(f, collection, method_name, item_id):
|
def wrap_novaclient_function(f, collection, method_name, *args, **kwargs):
|
||||||
"""Appends collection, method_name and item_id to the incoming
|
"""Appends collection, method_name and arguments to the incoming
|
||||||
(nova, zone) call from child_zone_helper."""
|
(nova, zone) call from child_zone_helper."""
|
||||||
def inner(nova, zone):
|
def inner(nova, zone):
|
||||||
return f(nova, zone, collection, method_name, item_id)
|
return f(nova, zone, collection, method_name, *args, **kwargs)
|
||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
@@ -201,38 +223,78 @@ class RedirectResult(exception.Error):
|
|||||||
|
|
||||||
|
|
||||||
class reroute_compute(object):
|
class reroute_compute(object):
|
||||||
"""Decorator used to indicate that the method should
|
"""
|
||||||
delegate the call the child zones if the db query
|
reroute_compute is responsible for trying to lookup a resource in the
|
||||||
can't find anything."""
|
current zone and if it's not found there, delegating the call to the
|
||||||
|
child zones.
|
||||||
|
|
||||||
|
Since reroute_compute will be making 'cross-zone' calls, the ID for the
|
||||||
|
object must come in as a UUID-- if we receive an integer ID, we bail.
|
||||||
|
|
||||||
|
The steps involved are:
|
||||||
|
|
||||||
|
1. Validate that item_id is UUID like
|
||||||
|
|
||||||
|
2. Lookup item by UUID in the zone local database
|
||||||
|
|
||||||
|
3. If the item was found, then extract integer ID, and pass that to
|
||||||
|
the wrapped method. (This ensures that zone-local code can
|
||||||
|
continue to use integer IDs).
|
||||||
|
|
||||||
|
4. If the item was not found, we delegate the call to a child zone
|
||||||
|
using the UUID.
|
||||||
|
"""
|
||||||
def __init__(self, method_name):
|
def __init__(self, method_name):
|
||||||
self.method_name = method_name
|
self.method_name = method_name
|
||||||
|
|
||||||
|
def _route_to_child_zones(self, context, collection, item_uuid):
|
||||||
|
if not FLAGS.enable_zone_routing:
|
||||||
|
raise exception.InstanceNotFound(instance_id=item_uuid)
|
||||||
|
|
||||||
|
zones = db.zone_get_all(context)
|
||||||
|
if not zones:
|
||||||
|
raise exception.InstanceNotFound(instance_id=item_uuid)
|
||||||
|
|
||||||
|
# Ask the children to provide an answer ...
|
||||||
|
LOG.debug(_("Asking child zones ..."))
|
||||||
|
result = self._call_child_zones(zones,
|
||||||
|
wrap_novaclient_function(_issue_novaclient_command,
|
||||||
|
collection, self.method_name, item_uuid))
|
||||||
|
# Scrub the results and raise another exception
|
||||||
|
# so the API layers can bail out gracefully ...
|
||||||
|
raise RedirectResult(self.unmarshall_result(result))
|
||||||
|
|
||||||
def __call__(self, f):
|
def __call__(self, f):
|
||||||
def wrapped_f(*args, **kwargs):
|
def wrapped_f(*args, **kwargs):
|
||||||
collection, context, item_id = \
|
collection, context, item_id_or_uuid = \
|
||||||
self.get_collection_context_and_id(args, kwargs)
|
self.get_collection_context_and_id(args, kwargs)
|
||||||
try:
|
|
||||||
# Call the original function ...
|
attempt_reroute = False
|
||||||
|
if utils.is_uuid_like(item_id_or_uuid):
|
||||||
|
item_uuid = item_id_or_uuid
|
||||||
|
try:
|
||||||
|
instance = db.instance_get_by_uuid(context, item_uuid)
|
||||||
|
except exception.InstanceNotFound, e:
|
||||||
|
# NOTE(sirp): since a UUID was passed in, we can attempt
|
||||||
|
# to reroute to a child zone
|
||||||
|
attempt_reroute = True
|
||||||
|
LOG.debug(_("Instance %(item_uuid)s not found "
|
||||||
|
"locally: '%(e)s'" % locals()))
|
||||||
|
else:
|
||||||
|
# NOTE(sirp): since we're not re-routing in this case, and
|
||||||
|
# we we were passed a UUID, we need to replace that UUID
|
||||||
|
# with an integer ID in the argument list so that the
|
||||||
|
# zone-local code can continue to use integer IDs.
|
||||||
|
item_id = instance['id']
|
||||||
|
args = list(args) # needs to be mutable to replace
|
||||||
|
self.replace_uuid_with_id(args, kwargs, item_id)
|
||||||
|
|
||||||
|
if attempt_reroute:
|
||||||
|
return self._route_to_child_zones(context, collection,
|
||||||
|
item_uuid)
|
||||||
|
else:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
except exception.InstanceNotFound, e:
|
|
||||||
LOG.debug(_("Instance %(item_id)s not found "
|
|
||||||
"locally: '%(e)s'" % locals()))
|
|
||||||
|
|
||||||
if not FLAGS.enable_zone_routing:
|
|
||||||
raise
|
|
||||||
|
|
||||||
zones = db.zone_get_all(context)
|
|
||||||
if not zones:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Ask the children to provide an answer ...
|
|
||||||
LOG.debug(_("Asking child zones ..."))
|
|
||||||
result = self._call_child_zones(zones,
|
|
||||||
wrap_novaclient_function(_issue_novaclient_command,
|
|
||||||
collection, self.method_name, item_id))
|
|
||||||
# Scrub the results and raise another exception
|
|
||||||
# so the API layers can bail out gracefully ...
|
|
||||||
raise RedirectResult(self.unmarshall_result(result))
|
|
||||||
return wrapped_f
|
return wrapped_f
|
||||||
|
|
||||||
def _call_child_zones(self, zones, function):
|
def _call_child_zones(self, zones, function):
|
||||||
@@ -251,6 +313,18 @@ class reroute_compute(object):
|
|||||||
instance_id = args[2]
|
instance_id = args[2]
|
||||||
return ("servers", context, instance_id)
|
return ("servers", context, instance_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def replace_uuid_with_id(args, kwargs, replacement_id):
|
||||||
|
"""
|
||||||
|
Extracts the UUID parameter from the arg or kwarg list and replaces
|
||||||
|
it with an integer ID.
|
||||||
|
"""
|
||||||
|
if 'instance_id' in kwargs:
|
||||||
|
kwargs['instance_id'] = replacement_id
|
||||||
|
elif len(args) > 1:
|
||||||
|
args.pop(2)
|
||||||
|
args.insert(2, replacement_id)
|
||||||
|
|
||||||
def unmarshall_result(self, zone_responses):
|
def unmarshall_result(self, zone_responses):
|
||||||
"""Result is a list of responses from each child zone.
|
"""Result is a list of responses from each child zone.
|
||||||
Each decorator derivation is responsible to turning this
|
Each decorator derivation is responsible to turning this
|
||||||
|
@@ -93,6 +93,26 @@ class InstanceTypeFilter(HostFilter):
|
|||||||
"""Use instance_type to filter hosts."""
|
"""Use instance_type to filter hosts."""
|
||||||
return (self._full_name(), instance_type)
|
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):
|
def filter_hosts(self, zone_manager, query):
|
||||||
"""Return a list of hosts that can create instance_type."""
|
"""Return a list of hosts that can create instance_type."""
|
||||||
instance_type = query
|
instance_type = query
|
||||||
@@ -103,7 +123,11 @@ class InstanceTypeFilter(HostFilter):
|
|||||||
disk_bytes = capabilities['disk_available']
|
disk_bytes = capabilities['disk_available']
|
||||||
spec_ram = instance_type['memory_mb']
|
spec_ram = instance_type['memory_mb']
|
||||||
spec_disk = instance_type['local_gb']
|
spec_disk = instance_type['local_gb']
|
||||||
if host_ram_mb >= spec_ram and disk_bytes >= spec_disk:
|
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))
|
selected_hosts.append((host, capabilities))
|
||||||
return selected_hosts
|
return selected_hosts
|
||||||
|
|
||||||
@@ -305,8 +329,9 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler):
|
|||||||
'instance_type': <InstanceType dict>}
|
'instance_type': <InstanceType dict>}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def filter_hosts(self, num, request_spec):
|
def filter_hosts(self, topic, request_spec, hosts=None):
|
||||||
"""Filter the full host list (from the ZoneManager)"""
|
"""Filter the full host list (from the ZoneManager)"""
|
||||||
|
|
||||||
filter_name = request_spec.get('filter', None)
|
filter_name = request_spec.get('filter', None)
|
||||||
host_filter = choose_host_filter(filter_name)
|
host_filter = choose_host_filter(filter_name)
|
||||||
|
|
||||||
@@ -317,8 +342,9 @@ class HostFilterScheduler(zone_aware_scheduler.ZoneAwareScheduler):
|
|||||||
name, query = host_filter.instance_type_to_filter(instance_type)
|
name, query = host_filter.instance_type_to_filter(instance_type)
|
||||||
return host_filter.filter_hosts(self.zone_manager, query)
|
return host_filter.filter_hosts(self.zone_manager, query)
|
||||||
|
|
||||||
def weigh_hosts(self, num, request_spec, hosts):
|
def weigh_hosts(self, topic, request_spec, hosts):
|
||||||
"""Derived classes must override this method and return
|
"""Derived classes must override this method and return
|
||||||
a lists of hosts in [{weight, hostname}] format.
|
a lists of hosts in [{weight, hostname}] format.
|
||||||
"""
|
"""
|
||||||
return [dict(weight=1, hostname=host) for host, caps in hosts]
|
return [dict(weight=1, hostname=hostname, capabilities=caps)
|
||||||
|
for hostname, caps in hosts]
|
||||||
|
@@ -48,25 +48,43 @@ def noop_cost_fn(host):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
flags.DEFINE_integer('fill_first_cost_fn_weight', 1,
|
flags.DEFINE_integer('compute_fill_first_cost_fn_weight', 1,
|
||||||
'How much weight to give the fill-first cost function')
|
'How much weight to give the fill-first cost function')
|
||||||
|
|
||||||
|
|
||||||
def fill_first_cost_fn(host):
|
def compute_fill_first_cost_fn(host):
|
||||||
"""Prefer hosts that have less ram available, filter_hosts will exclude
|
"""Prefer hosts that have less ram available, filter_hosts will exclude
|
||||||
hosts that don't have enough ram"""
|
hosts that don't have enough ram"""
|
||||||
hostname, caps = host
|
hostname, caps = host
|
||||||
free_mem = caps['compute']['host_memory_free']
|
free_mem = caps['host_memory_free']
|
||||||
return free_mem
|
return free_mem
|
||||||
|
|
||||||
|
|
||||||
class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler):
|
class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler):
|
||||||
def get_cost_fns(self):
|
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
|
"""Returns a list of tuples containing weights and cost functions to
|
||||||
use for weighing hosts
|
use for weighing hosts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if topic in self.cost_fns_cache:
|
||||||
|
return self.cost_fns_cache[topic]
|
||||||
|
|
||||||
cost_fns = []
|
cost_fns = []
|
||||||
for cost_fn_str in FLAGS.least_cost_scheduler_cost_functions:
|
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:
|
try:
|
||||||
# NOTE(sirp): import_class is somewhat misnamed since it can
|
# NOTE(sirp): import_class is somewhat misnamed since it can
|
||||||
@@ -84,23 +102,23 @@ class LeastCostScheduler(zone_aware_scheduler.ZoneAwareScheduler):
|
|||||||
|
|
||||||
cost_fns.append((weight, cost_fn))
|
cost_fns.append((weight, cost_fn))
|
||||||
|
|
||||||
|
self.cost_fns_cache[topic] = cost_fns
|
||||||
return cost_fns
|
return cost_fns
|
||||||
|
|
||||||
def weigh_hosts(self, num, request_spec, hosts):
|
def weigh_hosts(self, topic, request_spec, hosts):
|
||||||
"""Returns a list of dictionaries of form:
|
"""Returns a list of dictionaries of form:
|
||||||
[ {weight: weight, hostname: hostname} ]"""
|
[ {weight: weight, hostname: hostname, capabilities: capabs} ]
|
||||||
|
"""
|
||||||
|
|
||||||
# FIXME(sirp): weigh_hosts should handle more than just instances
|
cost_fns = self.get_cost_fns(topic)
|
||||||
hostnames = [hostname for hostname, caps in hosts]
|
|
||||||
|
|
||||||
cost_fns = self.get_cost_fns()
|
|
||||||
costs = weighted_sum(domain=hosts, weighted_fns=cost_fns)
|
costs = weighted_sum(domain=hosts, weighted_fns=cost_fns)
|
||||||
|
|
||||||
weighted = []
|
weighted = []
|
||||||
weight_log = []
|
weight_log = []
|
||||||
for cost, hostname in zip(costs, hostnames):
|
for cost, (hostname, caps) in zip(costs, hosts):
|
||||||
weight_log.append("%s: %s" % (hostname, "%.2f" % cost))
|
weight_log.append("%s: %s" % (hostname, "%.2f" % cost))
|
||||||
weight_dict = dict(weight=cost, hostname=hostname)
|
weight_dict = dict(weight=cost, hostname=hostname,
|
||||||
|
capabilities=caps)
|
||||||
weighted.append(weight_dict)
|
weighted.append(weight_dict)
|
||||||
|
|
||||||
LOG.debug(_("Weighted Costs => %s") % weight_log)
|
LOG.debug(_("Weighted Costs => %s") % weight_log)
|
||||||
@@ -127,7 +145,8 @@ def weighted_sum(domain, weighted_fns, normalize=True):
|
|||||||
weighted_fns - list of weights and functions like:
|
weighted_fns - list of weights and functions like:
|
||||||
[(weight, objective-functions)]
|
[(weight, objective-functions)]
|
||||||
|
|
||||||
Returns an unsorted of scores. To pair with hosts do: zip(scores, hosts)
|
Returns an unsorted list of scores. To pair with hosts do:
|
||||||
|
zip(scores, hosts)
|
||||||
"""
|
"""
|
||||||
# Table of form:
|
# Table of form:
|
||||||
# { domain1: [score1, score2, ..., scoreM]
|
# { domain1: [score1, score2, ..., scoreM]
|
||||||
@@ -150,7 +169,6 @@ def weighted_sum(domain, weighted_fns, normalize=True):
|
|||||||
domain_scores = []
|
domain_scores = []
|
||||||
for idx in sorted(score_table):
|
for idx in sorted(score_table):
|
||||||
elem_score = sum(score_table[idx])
|
elem_score = sum(score_table[idx])
|
||||||
elem = domain[idx]
|
|
||||||
domain_scores.append(elem_score)
|
domain_scores.append(elem_score)
|
||||||
|
|
||||||
return domain_scores
|
return domain_scores
|
||||||
|
@@ -180,18 +180,22 @@ class ZoneAwareScheduler(driver.Scheduler):
|
|||||||
request_spec, kwargs)
|
request_spec, kwargs)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
num_instances = request_spec.get('num_instances', 1)
|
||||||
|
LOG.debug(_("Attempting to build %(num_instances)d instance(s)") %
|
||||||
|
locals())
|
||||||
|
|
||||||
# Create build plan and provision ...
|
# Create build plan and provision ...
|
||||||
build_plan = self.select(context, request_spec)
|
build_plan = self.select(context, request_spec)
|
||||||
if not build_plan:
|
if not build_plan:
|
||||||
raise driver.NoValidHost(_('No hosts were available'))
|
raise driver.NoValidHost(_('No hosts were available'))
|
||||||
|
|
||||||
for num in xrange(request_spec['num_instances']):
|
for num in xrange(num_instances):
|
||||||
if not build_plan:
|
if not build_plan:
|
||||||
break
|
break
|
||||||
|
|
||||||
item = build_plan.pop(0)
|
build_plan_item = build_plan.pop(0)
|
||||||
self._provision_resource(context, item, instance_id, request_spec,
|
self._provision_resource(context, build_plan_item, instance_id,
|
||||||
kwargs)
|
request_spec, kwargs)
|
||||||
|
|
||||||
# Returning None short-circuits the routing to Compute (since
|
# Returning None short-circuits the routing to Compute (since
|
||||||
# we've already done it here)
|
# we've already done it here)
|
||||||
@@ -224,18 +228,36 @@ class ZoneAwareScheduler(driver.Scheduler):
|
|||||||
raise NotImplemented(_("Zone Aware Scheduler only understands "
|
raise NotImplemented(_("Zone Aware Scheduler only understands "
|
||||||
"Compute nodes (for now)"))
|
"Compute nodes (for now)"))
|
||||||
|
|
||||||
#TODO(sandy): how to infer this from OS API params?
|
num_instances = request_spec.get('num_instances', 1)
|
||||||
num_instances = 1
|
instance_type = request_spec['instance_type']
|
||||||
|
|
||||||
# Filter local hosts based on requirements ...
|
weighted = []
|
||||||
host_list = self.filter_hosts(num_instances, request_spec)
|
host_list = None
|
||||||
|
|
||||||
# TODO(sirp): weigh_hosts should also be a function of 'topic' or
|
for i in xrange(num_instances):
|
||||||
# resources, so that we can apply different objective functions to it
|
# 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.
|
# then weigh the selected hosts.
|
||||||
# weighted = [{weight=weight, name=hostname}, ...]
|
# weighted = [{weight=weight, hostname=hostname,
|
||||||
weighted = self.weigh_hosts(num_instances, request_spec, host_list)
|
# 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 ...
|
# Next, tack on the best weights from the child zones ...
|
||||||
json_spec = json.dumps(request_spec)
|
json_spec = json.dumps(request_spec)
|
||||||
@@ -254,18 +276,65 @@ class ZoneAwareScheduler(driver.Scheduler):
|
|||||||
weighted.sort(key=operator.itemgetter('weight'))
|
weighted.sort(key=operator.itemgetter('weight'))
|
||||||
return weighted
|
return weighted
|
||||||
|
|
||||||
def filter_hosts(self, num, request_spec):
|
def compute_filter(self, hostname, capabilities, request_spec):
|
||||||
"""Derived classes must override this method and return
|
"""Return whether or not we can schedule to this compute node.
|
||||||
a list of hosts in [(hostname, capability_dict)] format.
|
Derived classes should override this and return True if the host
|
||||||
|
is acceptable for scheduling.
|
||||||
"""
|
"""
|
||||||
# NOTE(sirp): The default logic is the equivalent to AllHostsFilter
|
instance_type = request_spec['instance_type']
|
||||||
service_states = self.zone_manager.service_states
|
requested_mem = instance_type['memory_mb'] * 1024 * 1024
|
||||||
return [(host, services)
|
return capabilities['host_memory_free'] >= requested_mem
|
||||||
for host, services in service_states.iteritems()]
|
|
||||||
|
|
||||||
def weigh_hosts(self, num, request_spec, hosts):
|
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
|
||||||
|
|
||||||
|
def weigh_hosts(self, topic, request_spec, hosts):
|
||||||
"""Derived classes may override this to provide more sophisticated
|
"""Derived classes may override this to provide more sophisticated
|
||||||
scheduling objectives
|
scheduling objectives
|
||||||
"""
|
"""
|
||||||
# NOTE(sirp): The default logic is the same as the NoopCostFunction
|
# NOTE(sirp): The default logic is the same as the NoopCostFunction
|
||||||
return [dict(weight=1, hostname=host) for host, caps in hosts]
|
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)
|
||||||
|
@@ -67,7 +67,18 @@ class HostFilterTestCase(test.TestCase):
|
|||||||
flavorid=1,
|
flavorid=1,
|
||||||
swap=500,
|
swap=500,
|
||||||
rxtx_quota=30000,
|
rxtx_quota=30000,
|
||||||
rxtx_cap=200)
|
rxtx_cap=200,
|
||||||
|
extra_specs={})
|
||||||
|
self.gpu_instance_type = dict(name='tiny.gpu',
|
||||||
|
memory_mb=50,
|
||||||
|
vcpus=10,
|
||||||
|
local_gb=500,
|
||||||
|
flavorid=2,
|
||||||
|
swap=500,
|
||||||
|
rxtx_quota=30000,
|
||||||
|
rxtx_cap=200,
|
||||||
|
extra_specs={'xpu_arch': 'fermi',
|
||||||
|
'xpu_info': 'Tesla 2050'})
|
||||||
|
|
||||||
self.zone_manager = FakeZoneManager()
|
self.zone_manager = FakeZoneManager()
|
||||||
states = {}
|
states = {}
|
||||||
@@ -75,6 +86,18 @@ class HostFilterTestCase(test.TestCase):
|
|||||||
states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)}
|
states['host%02d' % (x + 1)] = {'compute': self._host_caps(x)}
|
||||||
self.zone_manager.service_states = states
|
self.zone_manager.service_states = states
|
||||||
|
|
||||||
|
# Add some extra capabilities to some hosts
|
||||||
|
host07 = self.zone_manager.service_states['host07']['compute']
|
||||||
|
host07['xpu_arch'] = 'fermi'
|
||||||
|
host07['xpu_info'] = 'Tesla 2050'
|
||||||
|
|
||||||
|
host08 = self.zone_manager.service_states['host08']['compute']
|
||||||
|
host08['xpu_arch'] = 'radeon'
|
||||||
|
|
||||||
|
host09 = self.zone_manager.service_states['host09']['compute']
|
||||||
|
host09['xpu_arch'] = 'fermi'
|
||||||
|
host09['xpu_info'] = 'Tesla 2150'
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
FLAGS.default_host_filter = self.old_flag
|
FLAGS.default_host_filter = self.old_flag
|
||||||
|
|
||||||
@@ -116,6 +139,17 @@ class HostFilterTestCase(test.TestCase):
|
|||||||
self.assertEquals('host05', just_hosts[0])
|
self.assertEquals('host05', just_hosts[0])
|
||||||
self.assertEquals('host10', just_hosts[5])
|
self.assertEquals('host10', just_hosts[5])
|
||||||
|
|
||||||
|
def test_instance_type_filter_extra_specs(self):
|
||||||
|
hf = host_filter.InstanceTypeFilter()
|
||||||
|
# filter all hosts that can support 50 ram and 500 disk
|
||||||
|
name, cooked = hf.instance_type_to_filter(self.gpu_instance_type)
|
||||||
|
self.assertEquals('nova.scheduler.host_filter.InstanceTypeFilter',
|
||||||
|
name)
|
||||||
|
hosts = hf.filter_hosts(self.zone_manager, cooked)
|
||||||
|
self.assertEquals(1, len(hosts))
|
||||||
|
just_hosts = [host for host, caps in hosts]
|
||||||
|
self.assertEquals('host07', just_hosts[0])
|
||||||
|
|
||||||
def test_json_filter(self):
|
def test_json_filter(self):
|
||||||
hf = host_filter.JsonFilter()
|
hf = host_filter.JsonFilter()
|
||||||
# filter all hosts that can support 50 ram and 500 disk
|
# filter all hosts that can support 50 ram and 500 disk
|
||||||
|
@@ -122,15 +122,16 @@ class LeastCostSchedulerTestCase(test.TestCase):
|
|||||||
for hostname, caps in hosts]
|
for hostname, caps in hosts]
|
||||||
self.assertWeights(expected, num, request_spec, hosts)
|
self.assertWeights(expected, num, request_spec, hosts)
|
||||||
|
|
||||||
def test_fill_first_cost_fn(self):
|
def test_compute_fill_first_cost_fn(self):
|
||||||
FLAGS.least_cost_scheduler_cost_functions = [
|
FLAGS.least_cost_scheduler_cost_functions = [
|
||||||
'nova.scheduler.least_cost.fill_first_cost_fn',
|
'nova.scheduler.least_cost.compute_fill_first_cost_fn',
|
||||||
]
|
]
|
||||||
FLAGS.fill_first_cost_fn_weight = 1
|
FLAGS.compute_fill_first_cost_fn_weight = 1
|
||||||
|
|
||||||
num = 1
|
num = 1
|
||||||
request_spec = {}
|
instance_type = {'memory_mb': 1024}
|
||||||
hosts = self.sched.filter_hosts(num, request_spec)
|
request_spec = {'instance_type': instance_type}
|
||||||
|
hosts = self.sched.filter_hosts('compute', request_spec, None)
|
||||||
|
|
||||||
expected = []
|
expected = []
|
||||||
for idx, (hostname, caps) in enumerate(hosts):
|
for idx, (hostname, caps) in enumerate(hosts):
|
||||||
|
@@ -55,29 +55,21 @@ def fake_zone_manager_service_states(num_hosts):
|
|||||||
|
|
||||||
|
|
||||||
class FakeZoneAwareScheduler(zone_aware_scheduler.ZoneAwareScheduler):
|
class FakeZoneAwareScheduler(zone_aware_scheduler.ZoneAwareScheduler):
|
||||||
def filter_hosts(self, num, specs):
|
# No need to stub anything at the moment
|
||||||
# NOTE(sirp): this is returning [(hostname, services)]
|
pass
|
||||||
return self.zone_manager.service_states.items()
|
|
||||||
|
|
||||||
def weigh_hosts(self, num, specs, hosts):
|
|
||||||
fake_weight = 99
|
|
||||||
weighted = []
|
|
||||||
for hostname, caps in hosts:
|
|
||||||
weighted.append(dict(weight=fake_weight, name=hostname))
|
|
||||||
return weighted
|
|
||||||
|
|
||||||
|
|
||||||
class FakeZoneManager(zone_manager.ZoneManager):
|
class FakeZoneManager(zone_manager.ZoneManager):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.service_states = {
|
self.service_states = {
|
||||||
'host1': {
|
'host1': {
|
||||||
'compute': {'ram': 1000},
|
'compute': {'host_memory_free': 1073741824},
|
||||||
},
|
},
|
||||||
'host2': {
|
'host2': {
|
||||||
'compute': {'ram': 2000},
|
'compute': {'host_memory_free': 2147483648},
|
||||||
},
|
},
|
||||||
'host3': {
|
'host3': {
|
||||||
'compute': {'ram': 3000},
|
'compute': {'host_memory_free': 3221225472},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,8 +146,8 @@ class ZoneAwareSchedulerTestCase(test.TestCase):
|
|||||||
|
|
||||||
def test_zone_aware_scheduler(self):
|
def test_zone_aware_scheduler(self):
|
||||||
"""
|
"""
|
||||||
Create a nested set of FakeZones, ensure that a select call returns the
|
Create a nested set of FakeZones, try to build multiple instances
|
||||||
appropriate build plan.
|
and ensure that a select call returns the appropriate build plan.
|
||||||
"""
|
"""
|
||||||
sched = FakeZoneAwareScheduler()
|
sched = FakeZoneAwareScheduler()
|
||||||
self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method)
|
self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method)
|
||||||
@@ -164,13 +156,17 @@ class ZoneAwareSchedulerTestCase(test.TestCase):
|
|||||||
sched.set_zone_manager(zm)
|
sched.set_zone_manager(zm)
|
||||||
|
|
||||||
fake_context = {}
|
fake_context = {}
|
||||||
build_plan = sched.select(fake_context, {})
|
build_plan = sched.select(fake_context,
|
||||||
|
{'instance_type': {'memory_mb': 512},
|
||||||
|
'num_instances': 4})
|
||||||
|
|
||||||
self.assertEqual(15, len(build_plan))
|
# 4 from local zones, 12 from remotes
|
||||||
|
self.assertEqual(16, len(build_plan))
|
||||||
|
|
||||||
hostnames = [plan_item['name']
|
hostnames = [plan_item['hostname']
|
||||||
for plan_item in build_plan if 'name' in plan_item]
|
for plan_item in build_plan if 'hostname' in plan_item]
|
||||||
self.assertEqual(3, len(hostnames))
|
# 4 local hosts
|
||||||
|
self.assertEqual(4, len(hostnames))
|
||||||
|
|
||||||
def test_empty_zone_aware_scheduler(self):
|
def test_empty_zone_aware_scheduler(self):
|
||||||
"""
|
"""
|
||||||
@@ -185,8 +181,7 @@ class ZoneAwareSchedulerTestCase(test.TestCase):
|
|||||||
fake_context = {}
|
fake_context = {}
|
||||||
self.assertRaises(driver.NoValidHost, sched.schedule_run_instance,
|
self.assertRaises(driver.NoValidHost, sched.schedule_run_instance,
|
||||||
fake_context, 1,
|
fake_context, 1,
|
||||||
dict(host_filter=None,
|
dict(host_filter=None, instance_type={}))
|
||||||
request_spec={'instance_type': {}}))
|
|
||||||
|
|
||||||
def test_schedule_do_not_schedule_with_hint(self):
|
def test_schedule_do_not_schedule_with_hint(self):
|
||||||
"""
|
"""
|
||||||
|
111
nova/tests/test_adminapi.py
Normal file
111
nova/tests/test_adminapi.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from eventlet import greenthread
|
||||||
|
|
||||||
|
from nova import context
|
||||||
|
from nova import db
|
||||||
|
from nova import flags
|
||||||
|
from nova import log as logging
|
||||||
|
from nova import rpc
|
||||||
|
from nova import test
|
||||||
|
from nova import utils
|
||||||
|
from nova.auth import manager
|
||||||
|
from nova.api.ec2 import admin
|
||||||
|
from nova.image import fake
|
||||||
|
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
LOG = logging.getLogger('nova.tests.adminapi')
|
||||||
|
|
||||||
|
|
||||||
|
class AdminApiTestCase(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(AdminApiTestCase, self).setUp()
|
||||||
|
self.flags(connection_type='fake')
|
||||||
|
|
||||||
|
self.conn = rpc.Connection.instance()
|
||||||
|
|
||||||
|
# set up our cloud
|
||||||
|
self.api = admin.AdminController()
|
||||||
|
|
||||||
|
# set up services
|
||||||
|
self.compute = self.start_service('compute')
|
||||||
|
self.scheduter = self.start_service('scheduler')
|
||||||
|
self.network = self.start_service('network')
|
||||||
|
self.volume = self.start_service('volume')
|
||||||
|
self.image_service = utils.import_object(FLAGS.image_service)
|
||||||
|
|
||||||
|
self.manager = manager.AuthManager()
|
||||||
|
self.user = self.manager.create_user('admin', 'admin', 'admin', True)
|
||||||
|
self.project = self.manager.create_project('proj', 'admin', 'proj')
|
||||||
|
self.context = context.RequestContext(user=self.user,
|
||||||
|
project=self.project)
|
||||||
|
host = self.network.get_network_host(self.context.elevated())
|
||||||
|
|
||||||
|
def fake_show(meh, context, id):
|
||||||
|
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1,
|
||||||
|
'type': 'machine', 'image_state': 'available'}}
|
||||||
|
|
||||||
|
self.stubs.Set(fake._FakeImageService, 'show', fake_show)
|
||||||
|
self.stubs.Set(fake._FakeImageService, 'show_by_name', fake_show)
|
||||||
|
|
||||||
|
# NOTE(vish): set up a manual wait so rpc.cast has a chance to finish
|
||||||
|
rpc_cast = rpc.cast
|
||||||
|
|
||||||
|
def finish_cast(*args, **kwargs):
|
||||||
|
rpc_cast(*args, **kwargs)
|
||||||
|
greenthread.sleep(0.2)
|
||||||
|
|
||||||
|
self.stubs.Set(rpc, 'cast', finish_cast)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
network_ref = db.project_get_network(self.context,
|
||||||
|
self.project.id)
|
||||||
|
db.network_disassociate(self.context, network_ref['id'])
|
||||||
|
self.manager.delete_project(self.project)
|
||||||
|
self.manager.delete_user(self.user)
|
||||||
|
super(AdminApiTestCase, self).tearDown()
|
||||||
|
|
||||||
|
def test_block_external_ips(self):
|
||||||
|
"""Make sure provider firewall rules are created."""
|
||||||
|
result = self.api.block_external_addresses(self.context, '1.1.1.1/32')
|
||||||
|
self.api.remove_external_address_block(self.context, '1.1.1.1/32')
|
||||||
|
self.assertEqual('OK', result['status'])
|
||||||
|
self.assertEqual('Added 3 rules', result['message'])
|
||||||
|
|
||||||
|
def test_list_blocked_ips(self):
|
||||||
|
"""Make sure we can see the external blocks that exist."""
|
||||||
|
self.api.block_external_addresses(self.context, '1.1.1.2/32')
|
||||||
|
result = self.api.describe_external_address_blocks(self.context)
|
||||||
|
num = len(db.provider_fw_rule_get_all(self.context))
|
||||||
|
self.api.remove_external_address_block(self.context, '1.1.1.2/32')
|
||||||
|
# we only list IP, not tcp/udp/icmp rules
|
||||||
|
self.assertEqual(num / 3, len(result['externalIpBlockInfo']))
|
||||||
|
|
||||||
|
def test_remove_ip_block(self):
|
||||||
|
"""Remove ip blocks."""
|
||||||
|
result = self.api.block_external_addresses(self.context, '1.1.1.3/32')
|
||||||
|
self.assertEqual('OK', result['status'])
|
||||||
|
num0 = len(db.provider_fw_rule_get_all(self.context))
|
||||||
|
result = self.api.remove_external_address_block(self.context,
|
||||||
|
'1.1.1.3/32')
|
||||||
|
self.assertEqual('OK', result['status'])
|
||||||
|
self.assertEqual('Deleted 3 rules', result['message'])
|
||||||
|
num1 = len(db.provider_fw_rule_get_all(self.context))
|
||||||
|
self.assert_(num1 < num0)
|
@@ -25,6 +25,7 @@ from nova import log as logging
|
|||||||
from nova import test
|
from nova import test
|
||||||
from nova.auth import manager
|
from nova.auth import manager
|
||||||
from nova.api.ec2 import cloud
|
from nova.api.ec2 import cloud
|
||||||
|
from nova.auth import fakeldap
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
LOG = logging.getLogger('nova.tests.auth_unittest')
|
LOG = logging.getLogger('nova.tests.auth_unittest')
|
||||||
@@ -369,6 +370,15 @@ class _AuthManagerBaseTestCase(test.TestCase):
|
|||||||
class AuthManagerLdapTestCase(_AuthManagerBaseTestCase):
|
class AuthManagerLdapTestCase(_AuthManagerBaseTestCase):
|
||||||
auth_driver = 'nova.auth.ldapdriver.FakeLdapDriver'
|
auth_driver = 'nova.auth.ldapdriver.FakeLdapDriver'
|
||||||
|
|
||||||
|
def test_reconnect_on_server_failure(self):
|
||||||
|
self.manager.get_users()
|
||||||
|
fakeldap.server_fail = True
|
||||||
|
try:
|
||||||
|
self.assertRaises(fakeldap.SERVER_DOWN, self.manager.get_users)
|
||||||
|
finally:
|
||||||
|
fakeldap.server_fail = False
|
||||||
|
self.manager.get_users()
|
||||||
|
|
||||||
|
|
||||||
class AuthManagerDbTestCase(_AuthManagerBaseTestCase):
|
class AuthManagerDbTestCase(_AuthManagerBaseTestCase):
|
||||||
auth_driver = 'nova.auth.dbdriver.DbDriver'
|
auth_driver = 'nova.auth.dbdriver.DbDriver'
|
||||||
|
@@ -37,6 +37,7 @@ from nova import log as logging
|
|||||||
from nova import rpc
|
from nova import rpc
|
||||||
from nova import test
|
from nova import test
|
||||||
from nova import utils
|
from nova import utils
|
||||||
|
from nova.notifier import test_notifier
|
||||||
|
|
||||||
LOG = logging.getLogger('nova.tests.compute')
|
LOG = logging.getLogger('nova.tests.compute')
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
@@ -62,6 +63,7 @@ class ComputeTestCase(test.TestCase):
|
|||||||
super(ComputeTestCase, self).setUp()
|
super(ComputeTestCase, self).setUp()
|
||||||
self.flags(connection_type='fake',
|
self.flags(connection_type='fake',
|
||||||
stub_network=True,
|
stub_network=True,
|
||||||
|
notification_driver='nova.notifier.test_notifier',
|
||||||
network_manager='nova.network.manager.FlatManager')
|
network_manager='nova.network.manager.FlatManager')
|
||||||
self.compute = utils.import_object(FLAGS.compute_manager)
|
self.compute = utils.import_object(FLAGS.compute_manager)
|
||||||
self.compute_api = compute.API()
|
self.compute_api = compute.API()
|
||||||
@@ -69,6 +71,7 @@ class ComputeTestCase(test.TestCase):
|
|||||||
self.user = self.manager.create_user('fake', 'fake', 'fake')
|
self.user = self.manager.create_user('fake', 'fake', 'fake')
|
||||||
self.project = self.manager.create_project('fake', 'fake', 'fake')
|
self.project = self.manager.create_project('fake', 'fake', 'fake')
|
||||||
self.context = context.RequestContext('fake', 'fake', False)
|
self.context = context.RequestContext('fake', 'fake', False)
|
||||||
|
test_notifier.NOTIFICATIONS = []
|
||||||
|
|
||||||
def fake_show(meh, context, id):
|
def fake_show(meh, context, id):
|
||||||
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}}
|
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}}
|
||||||
@@ -281,6 +284,14 @@ class ComputeTestCase(test.TestCase):
|
|||||||
"File Contents")
|
"File Contents")
|
||||||
self.compute.terminate_instance(self.context, instance_id)
|
self.compute.terminate_instance(self.context, instance_id)
|
||||||
|
|
||||||
|
def test_agent_update(self):
|
||||||
|
"""Ensure instance can have its agent updated"""
|
||||||
|
instance_id = self._create_instance()
|
||||||
|
self.compute.run_instance(self.context, instance_id)
|
||||||
|
self.compute.agent_update(self.context, instance_id,
|
||||||
|
'http://127.0.0.1/agent', '00112233445566778899aabbccddeeff')
|
||||||
|
self.compute.terminate_instance(self.context, instance_id)
|
||||||
|
|
||||||
def test_snapshot(self):
|
def test_snapshot(self):
|
||||||
"""Ensure instance can be snapshotted"""
|
"""Ensure instance can be snapshotted"""
|
||||||
instance_id = self._create_instance()
|
instance_id = self._create_instance()
|
||||||
@@ -319,6 +330,50 @@ class ComputeTestCase(test.TestCase):
|
|||||||
self.assert_(console)
|
self.assert_(console)
|
||||||
self.compute.terminate_instance(self.context, instance_id)
|
self.compute.terminate_instance(self.context, instance_id)
|
||||||
|
|
||||||
|
def test_run_instance_usage_notification(self):
|
||||||
|
"""Ensure run instance generates apropriate usage notification"""
|
||||||
|
instance_id = self._create_instance()
|
||||||
|
self.compute.run_instance(self.context, instance_id)
|
||||||
|
self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
|
||||||
|
msg = test_notifier.NOTIFICATIONS[0]
|
||||||
|
self.assertEquals(msg['priority'], 'INFO')
|
||||||
|
self.assertEquals(msg['event_type'], 'compute.instance.create')
|
||||||
|
payload = msg['payload']
|
||||||
|
self.assertEquals(payload['tenant_id'], self.project.id)
|
||||||
|
self.assertEquals(payload['user_id'], self.user.id)
|
||||||
|
self.assertEquals(payload['instance_id'], instance_id)
|
||||||
|
self.assertEquals(payload['instance_type'], 'm1.tiny')
|
||||||
|
type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
|
||||||
|
self.assertEquals(str(payload['instance_type_id']), str(type_id))
|
||||||
|
self.assertTrue('display_name' in payload)
|
||||||
|
self.assertTrue('created_at' in payload)
|
||||||
|
self.assertTrue('launched_at' in payload)
|
||||||
|
self.assertEquals(payload['image_ref'], '1')
|
||||||
|
self.compute.terminate_instance(self.context, instance_id)
|
||||||
|
|
||||||
|
def test_terminate_usage_notification(self):
|
||||||
|
"""Ensure terminate_instance generates apropriate usage notification"""
|
||||||
|
instance_id = self._create_instance()
|
||||||
|
self.compute.run_instance(self.context, instance_id)
|
||||||
|
test_notifier.NOTIFICATIONS = []
|
||||||
|
self.compute.terminate_instance(self.context, instance_id)
|
||||||
|
|
||||||
|
self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
|
||||||
|
msg = test_notifier.NOTIFICATIONS[0]
|
||||||
|
self.assertEquals(msg['priority'], 'INFO')
|
||||||
|
self.assertEquals(msg['event_type'], 'compute.instance.delete')
|
||||||
|
payload = msg['payload']
|
||||||
|
self.assertEquals(payload['tenant_id'], self.project.id)
|
||||||
|
self.assertEquals(payload['user_id'], self.user.id)
|
||||||
|
self.assertEquals(payload['instance_id'], instance_id)
|
||||||
|
self.assertEquals(payload['instance_type'], 'm1.tiny')
|
||||||
|
type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
|
||||||
|
self.assertEquals(str(payload['instance_type_id']), str(type_id))
|
||||||
|
self.assertTrue('display_name' in payload)
|
||||||
|
self.assertTrue('created_at' in payload)
|
||||||
|
self.assertTrue('launched_at' in payload)
|
||||||
|
self.assertEquals(payload['image_ref'], '1')
|
||||||
|
|
||||||
def test_run_instance_existing(self):
|
def test_run_instance_existing(self):
|
||||||
"""Ensure failure when running an instance that already exists"""
|
"""Ensure failure when running an instance that already exists"""
|
||||||
instance_id = self._create_instance()
|
instance_id = self._create_instance()
|
||||||
@@ -370,6 +425,36 @@ class ComputeTestCase(test.TestCase):
|
|||||||
|
|
||||||
self.compute.terminate_instance(self.context, instance_id)
|
self.compute.terminate_instance(self.context, instance_id)
|
||||||
|
|
||||||
|
def test_resize_instance_notification(self):
|
||||||
|
"""Ensure notifications on instance migrate/resize"""
|
||||||
|
instance_id = self._create_instance()
|
||||||
|
context = self.context.elevated()
|
||||||
|
|
||||||
|
self.compute.run_instance(self.context, instance_id)
|
||||||
|
test_notifier.NOTIFICATIONS = []
|
||||||
|
|
||||||
|
db.instance_update(self.context, instance_id, {'host': 'foo'})
|
||||||
|
self.compute.prep_resize(context, instance_id, 1)
|
||||||
|
migration_ref = db.migration_get_by_instance_and_status(context,
|
||||||
|
instance_id, 'pre-migrating')
|
||||||
|
|
||||||
|
self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
|
||||||
|
msg = test_notifier.NOTIFICATIONS[0]
|
||||||
|
self.assertEquals(msg['priority'], 'INFO')
|
||||||
|
self.assertEquals(msg['event_type'], 'compute.instance.resize.prep')
|
||||||
|
payload = msg['payload']
|
||||||
|
self.assertEquals(payload['tenant_id'], self.project.id)
|
||||||
|
self.assertEquals(payload['user_id'], self.user.id)
|
||||||
|
self.assertEquals(payload['instance_id'], instance_id)
|
||||||
|
self.assertEquals(payload['instance_type'], 'm1.tiny')
|
||||||
|
type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
|
||||||
|
self.assertEquals(str(payload['instance_type_id']), str(type_id))
|
||||||
|
self.assertTrue('display_name' in payload)
|
||||||
|
self.assertTrue('created_at' in payload)
|
||||||
|
self.assertTrue('launched_at' in payload)
|
||||||
|
self.assertEquals(payload['image_ref'], '1')
|
||||||
|
self.compute.terminate_instance(context, instance_id)
|
||||||
|
|
||||||
def test_resize_instance(self):
|
def test_resize_instance(self):
|
||||||
"""Ensure instance can be migrated/resized"""
|
"""Ensure instance can be migrated/resized"""
|
||||||
instance_id = self._create_instance()
|
instance_id = self._create_instance()
|
||||||
@@ -563,7 +648,7 @@ class ComputeTestCase(test.TestCase):
|
|||||||
self.mox.StubOutWithMock(self.compute.driver, 'live_migration')
|
self.mox.StubOutWithMock(self.compute.driver, 'live_migration')
|
||||||
self.compute.driver.live_migration(c, i_ref, i_ref['host'],
|
self.compute.driver.live_migration(c, i_ref, i_ref['host'],
|
||||||
self.compute.post_live_migration,
|
self.compute.post_live_migration,
|
||||||
self.compute.recover_live_migration,
|
self.compute.rollback_live_migration,
|
||||||
False)
|
False)
|
||||||
|
|
||||||
self.compute.db = dbmock
|
self.compute.db = dbmock
|
||||||
@@ -594,6 +679,9 @@ class ComputeTestCase(test.TestCase):
|
|||||||
'host': i_ref['host']})
|
'host': i_ref['host']})
|
||||||
for v in i_ref['volumes']:
|
for v in i_ref['volumes']:
|
||||||
dbmock.volume_update(c, v['id'], {'status': 'in-use'})
|
dbmock.volume_update(c, v['id'], {'status': 'in-use'})
|
||||||
|
# mock for volume_api.remove_from_compute
|
||||||
|
rpc.call(c, topic, {"method": "remove_volume",
|
||||||
|
"args": {'volume_id': v['id']}})
|
||||||
|
|
||||||
self.compute.db = dbmock
|
self.compute.db = dbmock
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
@@ -647,7 +735,7 @@ class ComputeTestCase(test.TestCase):
|
|||||||
self.mox.StubOutWithMock(self.compute.driver, 'live_migration')
|
self.mox.StubOutWithMock(self.compute.driver, 'live_migration')
|
||||||
self.compute.driver.live_migration(c, i_ref, i_ref['host'],
|
self.compute.driver.live_migration(c, i_ref, i_ref['host'],
|
||||||
self.compute.post_live_migration,
|
self.compute.post_live_migration,
|
||||||
self.compute.recover_live_migration,
|
self.compute.rollback_live_migration,
|
||||||
False)
|
False)
|
||||||
|
|
||||||
self.compute.db = dbmock
|
self.compute.db = dbmock
|
||||||
@@ -682,6 +770,10 @@ class ComputeTestCase(test.TestCase):
|
|||||||
self.compute.volume_manager.remove_compute_volume(c, v['id'])
|
self.compute.volume_manager.remove_compute_volume(c, v['id'])
|
||||||
self.mox.StubOutWithMock(self.compute.driver, 'unfilter_instance')
|
self.mox.StubOutWithMock(self.compute.driver, 'unfilter_instance')
|
||||||
self.compute.driver.unfilter_instance(i_ref)
|
self.compute.driver.unfilter_instance(i_ref)
|
||||||
|
self.mox.StubOutWithMock(rpc, 'call')
|
||||||
|
rpc.call(c, db.queue_get_for(c, FLAGS.compute_topic, dest),
|
||||||
|
{"method": "post_live_migration_at_destination",
|
||||||
|
"args": {'instance_id': i_ref['id'], 'block_migration': False}})
|
||||||
|
|
||||||
# executing
|
# executing
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
"""
|
"""
|
||||||
Unit Tests for flat network code
|
Unit Tests for flat network code
|
||||||
"""
|
"""
|
||||||
import IPy
|
import netaddr
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -45,8 +45,8 @@ class FlatNetworkTestCase(base.NetworkTestCase):
|
|||||||
|
|
||||||
self.context._project = self.projects[0]
|
self.context._project = self.projects[0]
|
||||||
self.context.project_id = self.projects[0].id
|
self.context.project_id = self.projects[0].id
|
||||||
pubnet = IPy.IP(flags.FLAGS.floating_range)
|
pubnet = netaddr.IPRange(flags.FLAGS.floating_range)
|
||||||
address = str(pubnet[0])
|
address = str(list(pubnet)[0])
|
||||||
try:
|
try:
|
||||||
db.floating_ip_get_by_address(context.get_admin_context(), address)
|
db.floating_ip_get_by_address(context.get_admin_context(), address)
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
|
@@ -67,7 +67,8 @@ class HostFilterTestCase(test.TestCase):
|
|||||||
flavorid=1,
|
flavorid=1,
|
||||||
swap=500,
|
swap=500,
|
||||||
rxtx_quota=30000,
|
rxtx_quota=30000,
|
||||||
rxtx_cap=200)
|
rxtx_cap=200,
|
||||||
|
extra_specs={})
|
||||||
|
|
||||||
self.zone_manager = FakeZoneManager()
|
self.zone_manager = FakeZoneManager()
|
||||||
states = {}
|
states = {}
|
||||||
|
@@ -702,7 +702,7 @@ class LibvirtConnTestCase(test.TestCase):
|
|||||||
self.assertRaises(libvirt.libvirtError,
|
self.assertRaises(libvirt.libvirtError,
|
||||||
conn._live_migration,
|
conn._live_migration,
|
||||||
self.context, instance_ref, 'dest', '',
|
self.context, instance_ref, 'dest', '',
|
||||||
self.compute.recover_live_migration)
|
self.compute.rollback_live_migration)
|
||||||
|
|
||||||
instance_ref = db.instance_get(self.context, instance_ref['id'])
|
instance_ref = db.instance_get(self.context, instance_ref['id'])
|
||||||
self.assertTrue(instance_ref['state_description'] == 'running')
|
self.assertTrue(instance_ref['state_description'] == 'running')
|
||||||
@@ -741,7 +741,7 @@ class LibvirtConnTestCase(test.TestCase):
|
|||||||
conn.pre_block_migration(self.context, instance_ref,
|
conn.pre_block_migration(self.context, instance_ref,
|
||||||
dummyjson % tmpdir)
|
dummyjson % tmpdir)
|
||||||
|
|
||||||
self.assertTrue(os.path.exists('%s/%s/libvirt.xml' %
|
self.assertTrue(os.path.exists('%s/%s/' %
|
||||||
(tmpdir, instance_ref.name)))
|
(tmpdir, instance_ref.name)))
|
||||||
|
|
||||||
shutil.rmtree(tmpdir)
|
shutil.rmtree(tmpdir)
|
||||||
@@ -889,7 +889,9 @@ class IptablesFirewallTestCase(test.TestCase):
|
|||||||
self.network = utils.import_object(FLAGS.network_manager)
|
self.network = utils.import_object(FLAGS.network_manager)
|
||||||
|
|
||||||
class FakeLibvirtConnection(object):
|
class FakeLibvirtConnection(object):
|
||||||
pass
|
def nwfilterDefineXML(*args, **kwargs):
|
||||||
|
"""setup_basic_rules in nwfilter calls this."""
|
||||||
|
pass
|
||||||
self.fake_libvirt_connection = FakeLibvirtConnection()
|
self.fake_libvirt_connection = FakeLibvirtConnection()
|
||||||
self.fw = firewall.IptablesFirewallDriver(
|
self.fw = firewall.IptablesFirewallDriver(
|
||||||
get_connection=lambda: self.fake_libvirt_connection)
|
get_connection=lambda: self.fake_libvirt_connection)
|
||||||
@@ -1125,7 +1127,6 @@ class IptablesFirewallTestCase(test.TestCase):
|
|||||||
fakefilter.filterDefineXMLMock
|
fakefilter.filterDefineXMLMock
|
||||||
self.fw.nwfilter._conn.nwfilterLookupByName =\
|
self.fw.nwfilter._conn.nwfilterLookupByName =\
|
||||||
fakefilter.nwfilterLookupByName
|
fakefilter.nwfilterLookupByName
|
||||||
|
|
||||||
instance_ref = self._create_instance_ref()
|
instance_ref = self._create_instance_ref()
|
||||||
inst_id = instance_ref['id']
|
inst_id = instance_ref['id']
|
||||||
instance = db.instance_get(self.context, inst_id)
|
instance = db.instance_get(self.context, inst_id)
|
||||||
@@ -1147,6 +1148,70 @@ class IptablesFirewallTestCase(test.TestCase):
|
|||||||
|
|
||||||
db.instance_destroy(admin_ctxt, instance_ref['id'])
|
db.instance_destroy(admin_ctxt, instance_ref['id'])
|
||||||
|
|
||||||
|
def test_provider_firewall_rules(self):
|
||||||
|
# setup basic instance data
|
||||||
|
instance_ref = self._create_instance_ref()
|
||||||
|
nw_info = _create_network_info(1)
|
||||||
|
ip = '10.11.12.13'
|
||||||
|
network_ref = db.project_get_network(self.context, 'fake')
|
||||||
|
admin_ctxt = context.get_admin_context()
|
||||||
|
fixed_ip = {'address': ip, 'network_id': network_ref['id']}
|
||||||
|
db.fixed_ip_create(admin_ctxt, fixed_ip)
|
||||||
|
db.fixed_ip_update(admin_ctxt, ip, {'allocated': True,
|
||||||
|
'instance_id': instance_ref['id']})
|
||||||
|
# FRAGILE: peeks at how the firewall names chains
|
||||||
|
chain_name = 'inst-%s' % instance_ref['id']
|
||||||
|
|
||||||
|
# create a firewall via setup_basic_filtering like libvirt_conn.spawn
|
||||||
|
# should have a chain with 0 rules
|
||||||
|
self.fw.setup_basic_filtering(instance_ref, network_info=nw_info)
|
||||||
|
self.assertTrue('provider' in self.fw.iptables.ipv4['filter'].chains)
|
||||||
|
rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
|
||||||
|
if rule.chain == 'provider']
|
||||||
|
self.assertEqual(0, len(rules))
|
||||||
|
|
||||||
|
# add a rule and send the update message, check for 1 rule
|
||||||
|
provider_fw0 = db.provider_fw_rule_create(admin_ctxt,
|
||||||
|
{'protocol': 'tcp',
|
||||||
|
'cidr': '10.99.99.99/32',
|
||||||
|
'from_port': 1,
|
||||||
|
'to_port': 65535})
|
||||||
|
self.fw.refresh_provider_fw_rules()
|
||||||
|
rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
|
||||||
|
if rule.chain == 'provider']
|
||||||
|
self.assertEqual(1, len(rules))
|
||||||
|
|
||||||
|
# Add another, refresh, and make sure number of rules goes to two
|
||||||
|
provider_fw1 = db.provider_fw_rule_create(admin_ctxt,
|
||||||
|
{'protocol': 'udp',
|
||||||
|
'cidr': '10.99.99.99/32',
|
||||||
|
'from_port': 1,
|
||||||
|
'to_port': 65535})
|
||||||
|
self.fw.refresh_provider_fw_rules()
|
||||||
|
rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
|
||||||
|
if rule.chain == 'provider']
|
||||||
|
self.assertEqual(2, len(rules))
|
||||||
|
|
||||||
|
# create the instance filter and make sure it has a jump rule
|
||||||
|
self.fw.prepare_instance_filter(instance_ref, network_info=nw_info)
|
||||||
|
self.fw.apply_instance_filter(instance_ref)
|
||||||
|
inst_rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
|
||||||
|
if rule.chain == chain_name]
|
||||||
|
jump_rules = [rule for rule in inst_rules if '-j' in rule.rule]
|
||||||
|
provjump_rules = []
|
||||||
|
# IptablesTable doesn't make rules unique internally
|
||||||
|
for rule in jump_rules:
|
||||||
|
if 'provider' in rule.rule and rule not in provjump_rules:
|
||||||
|
provjump_rules.append(rule)
|
||||||
|
self.assertEqual(1, len(provjump_rules))
|
||||||
|
|
||||||
|
# remove a rule from the db, cast to compute to refresh rule
|
||||||
|
db.provider_fw_rule_destroy(admin_ctxt, provider_fw1['id'])
|
||||||
|
self.fw.refresh_provider_fw_rules()
|
||||||
|
rules = [rule for rule in self.fw.iptables.ipv4['filter'].rules
|
||||||
|
if rule.chain == 'provider']
|
||||||
|
self.assertEqual(1, len(rules))
|
||||||
|
|
||||||
|
|
||||||
class NWFilterTestCase(test.TestCase):
|
class NWFilterTestCase(test.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
"""
|
"""
|
||||||
Unit Tests for network code
|
Unit Tests for network code
|
||||||
"""
|
"""
|
||||||
import IPy
|
import netaddr
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from nova import test
|
from nova import test
|
||||||
@@ -164,3 +164,33 @@ class IptablesManagerTestCase(test.TestCase):
|
|||||||
self.assertTrue('-A %s -j run_tests.py-%s' \
|
self.assertTrue('-A %s -j run_tests.py-%s' \
|
||||||
% (chain, chain) in new_lines,
|
% (chain, chain) in new_lines,
|
||||||
"Built-in chain %s not wrapped" % (chain,))
|
"Built-in chain %s not wrapped" % (chain,))
|
||||||
|
|
||||||
|
def test_will_empty_chain(self):
|
||||||
|
self.manager.ipv4['filter'].add_chain('test-chain')
|
||||||
|
self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP')
|
||||||
|
old_count = len(self.manager.ipv4['filter'].rules)
|
||||||
|
self.manager.ipv4['filter'].empty_chain('test-chain')
|
||||||
|
self.assertEqual(old_count - 1, len(self.manager.ipv4['filter'].rules))
|
||||||
|
|
||||||
|
def test_will_empty_unwrapped_chain(self):
|
||||||
|
self.manager.ipv4['filter'].add_chain('test-chain', wrap=False)
|
||||||
|
self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP',
|
||||||
|
wrap=False)
|
||||||
|
old_count = len(self.manager.ipv4['filter'].rules)
|
||||||
|
self.manager.ipv4['filter'].empty_chain('test-chain', wrap=False)
|
||||||
|
self.assertEqual(old_count - 1, len(self.manager.ipv4['filter'].rules))
|
||||||
|
|
||||||
|
def test_will_not_empty_wrapped_when_unwrapped(self):
|
||||||
|
self.manager.ipv4['filter'].add_chain('test-chain')
|
||||||
|
self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP')
|
||||||
|
old_count = len(self.manager.ipv4['filter'].rules)
|
||||||
|
self.manager.ipv4['filter'].empty_chain('test-chain', wrap=False)
|
||||||
|
self.assertEqual(old_count, len(self.manager.ipv4['filter'].rules))
|
||||||
|
|
||||||
|
def test_will_not_empty_unwrapped_when_wrapped(self):
|
||||||
|
self.manager.ipv4['filter'].add_chain('test-chain', wrap=False)
|
||||||
|
self.manager.ipv4['filter'].add_rule('test-chain', '-j DROP',
|
||||||
|
wrap=False)
|
||||||
|
old_count = len(self.manager.ipv4['filter'].rules)
|
||||||
|
self.manager.ipv4['filter'].empty_chain('test-chain')
|
||||||
|
self.assertEqual(old_count, len(self.manager.ipv4['filter'].rules))
|
||||||
|
@@ -18,7 +18,7 @@
|
|||||||
"""
|
"""
|
||||||
Unit Tests for vlan network code
|
Unit Tests for vlan network code
|
||||||
"""
|
"""
|
||||||
import IPy
|
import netaddr
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from nova import context
|
from nova import context
|
||||||
@@ -44,8 +44,8 @@ class VlanNetworkTestCase(base.NetworkTestCase):
|
|||||||
# TODO(vish): better way of adding floating ips
|
# TODO(vish): better way of adding floating ips
|
||||||
self.context._project = self.projects[0]
|
self.context._project = self.projects[0]
|
||||||
self.context.project_id = self.projects[0].id
|
self.context.project_id = self.projects[0].id
|
||||||
pubnet = IPy.IP(flags.FLAGS.floating_range)
|
pubnet = netaddr.IPNetwork(flags.FLAGS.floating_range)
|
||||||
address = str(pubnet[0])
|
address = str(list(pubnet)[0])
|
||||||
try:
|
try:
|
||||||
db.floating_ip_get_by_address(context.get_admin_context(), address)
|
db.floating_ip_get_by_address(context.get_admin_context(), address)
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
|
@@ -37,9 +37,8 @@ from nova import exception
|
|||||||
from nova.virt import xenapi_conn
|
from nova.virt import xenapi_conn
|
||||||
from nova.virt.xenapi import fake as xenapi_fake
|
from nova.virt.xenapi import fake as xenapi_fake
|
||||||
from nova.virt.xenapi import volume_utils
|
from nova.virt.xenapi import volume_utils
|
||||||
|
from nova.virt.xenapi import vmops
|
||||||
from nova.virt.xenapi import vm_utils
|
from nova.virt.xenapi import vm_utils
|
||||||
from nova.virt.xenapi.vmops import SimpleDH
|
|
||||||
from nova.virt.xenapi.vmops import VMOps
|
|
||||||
from nova.tests.db import fakes as db_fakes
|
from nova.tests.db import fakes as db_fakes
|
||||||
from nova.tests.xenapi import stubs
|
from nova.tests.xenapi import stubs
|
||||||
from nova.tests.glance import stubs as glance_stubs
|
from nova.tests.glance import stubs as glance_stubs
|
||||||
@@ -85,7 +84,8 @@ class XenAPIVolumeTestCase(test.TestCase):
|
|||||||
'ramdisk_id': 3,
|
'ramdisk_id': 3,
|
||||||
'instance_type_id': '3', # m1.large
|
'instance_type_id': '3', # m1.large
|
||||||
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
'os_type': 'linux'}
|
'os_type': 'linux',
|
||||||
|
'architecture': 'x86-64'}
|
||||||
|
|
||||||
def _create_volume(self, size='0'):
|
def _create_volume(self, size='0'):
|
||||||
"""Create a volume object."""
|
"""Create a volume object."""
|
||||||
@@ -192,7 +192,7 @@ class XenAPIVMTestCase(test.TestCase):
|
|||||||
stubs.stubout_get_this_vm_uuid(self.stubs)
|
stubs.stubout_get_this_vm_uuid(self.stubs)
|
||||||
stubs.stubout_stream_disk(self.stubs)
|
stubs.stubout_stream_disk(self.stubs)
|
||||||
stubs.stubout_is_vdi_pv(self.stubs)
|
stubs.stubout_is_vdi_pv(self.stubs)
|
||||||
self.stubs.Set(VMOps, 'reset_network', reset_network)
|
self.stubs.Set(vmops.VMOps, 'reset_network', reset_network)
|
||||||
stubs.stub_out_vm_methods(self.stubs)
|
stubs.stub_out_vm_methods(self.stubs)
|
||||||
glance_stubs.stubout_glance_client(self.stubs)
|
glance_stubs.stubout_glance_client(self.stubs)
|
||||||
fake_utils.stub_out_utils_execute(self.stubs)
|
fake_utils.stub_out_utils_execute(self.stubs)
|
||||||
@@ -212,7 +212,8 @@ class XenAPIVMTestCase(test.TestCase):
|
|||||||
'ramdisk_id': 3,
|
'ramdisk_id': 3,
|
||||||
'instance_type_id': '3', # m1.large
|
'instance_type_id': '3', # m1.large
|
||||||
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
'os_type': 'linux'}
|
'os_type': 'linux',
|
||||||
|
'architecture': 'x86-64'}
|
||||||
instance = db.instance_create(self.context, values)
|
instance = db.instance_create(self.context, values)
|
||||||
self.conn.spawn(instance)
|
self.conn.spawn(instance)
|
||||||
|
|
||||||
@@ -370,7 +371,8 @@ class XenAPIVMTestCase(test.TestCase):
|
|||||||
|
|
||||||
def _test_spawn(self, image_ref, kernel_id, ramdisk_id,
|
def _test_spawn(self, image_ref, kernel_id, ramdisk_id,
|
||||||
instance_type_id="3", os_type="linux",
|
instance_type_id="3", os_type="linux",
|
||||||
instance_id=1, check_injection=False):
|
architecture="x86-64", instance_id=1,
|
||||||
|
check_injection=False):
|
||||||
stubs.stubout_loopingcall_start(self.stubs)
|
stubs.stubout_loopingcall_start(self.stubs)
|
||||||
values = {'id': instance_id,
|
values = {'id': instance_id,
|
||||||
'project_id': self.project.id,
|
'project_id': self.project.id,
|
||||||
@@ -380,11 +382,14 @@ class XenAPIVMTestCase(test.TestCase):
|
|||||||
'ramdisk_id': ramdisk_id,
|
'ramdisk_id': ramdisk_id,
|
||||||
'instance_type_id': instance_type_id,
|
'instance_type_id': instance_type_id,
|
||||||
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
'os_type': os_type}
|
'os_type': os_type,
|
||||||
|
'architecture': architecture}
|
||||||
instance = db.instance_create(self.context, values)
|
instance = db.instance_create(self.context, values)
|
||||||
self.conn.spawn(instance)
|
self.conn.spawn(instance)
|
||||||
self.create_vm_record(self.conn, os_type, instance_id)
|
self.create_vm_record(self.conn, os_type, instance_id)
|
||||||
self.check_vm_record(self.conn, check_injection)
|
self.check_vm_record(self.conn, check_injection)
|
||||||
|
self.assertTrue(instance.os_type)
|
||||||
|
self.assertTrue(instance.architecture)
|
||||||
|
|
||||||
def test_spawn_not_enough_memory(self):
|
def test_spawn_not_enough_memory(self):
|
||||||
FLAGS.xenapi_image_service = 'glance'
|
FLAGS.xenapi_image_service = 'glance'
|
||||||
@@ -409,7 +414,7 @@ class XenAPIVMTestCase(test.TestCase):
|
|||||||
def test_spawn_vhd_glance_linux(self):
|
def test_spawn_vhd_glance_linux(self):
|
||||||
FLAGS.xenapi_image_service = 'glance'
|
FLAGS.xenapi_image_service = 'glance'
|
||||||
self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
|
self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
|
||||||
os_type="linux")
|
os_type="linux", architecture="x86-64")
|
||||||
self.check_vm_params_for_linux()
|
self.check_vm_params_for_linux()
|
||||||
|
|
||||||
def test_spawn_vhd_glance_swapdisk(self):
|
def test_spawn_vhd_glance_swapdisk(self):
|
||||||
@@ -438,7 +443,7 @@ class XenAPIVMTestCase(test.TestCase):
|
|||||||
def test_spawn_vhd_glance_windows(self):
|
def test_spawn_vhd_glance_windows(self):
|
||||||
FLAGS.xenapi_image_service = 'glance'
|
FLAGS.xenapi_image_service = 'glance'
|
||||||
self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
|
self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
|
||||||
os_type="windows")
|
os_type="windows", architecture="i386")
|
||||||
self.check_vm_params_for_windows()
|
self.check_vm_params_for_windows()
|
||||||
|
|
||||||
def test_spawn_glance(self):
|
def test_spawn_glance(self):
|
||||||
@@ -589,7 +594,8 @@ class XenAPIVMTestCase(test.TestCase):
|
|||||||
'ramdisk_id': 3,
|
'ramdisk_id': 3,
|
||||||
'instance_type_id': '3', # m1.large
|
'instance_type_id': '3', # m1.large
|
||||||
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
'os_type': 'linux'}
|
'os_type': 'linux',
|
||||||
|
'architecture': 'x86-64'}
|
||||||
instance = db.instance_create(self.context, values)
|
instance = db.instance_create(self.context, values)
|
||||||
self.conn.spawn(instance)
|
self.conn.spawn(instance)
|
||||||
return instance
|
return instance
|
||||||
@@ -599,8 +605,8 @@ class XenAPIDiffieHellmanTestCase(test.TestCase):
|
|||||||
"""Unit tests for Diffie-Hellman code."""
|
"""Unit tests for Diffie-Hellman code."""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(XenAPIDiffieHellmanTestCase, self).setUp()
|
super(XenAPIDiffieHellmanTestCase, self).setUp()
|
||||||
self.alice = SimpleDH()
|
self.alice = vmops.SimpleDH()
|
||||||
self.bob = SimpleDH()
|
self.bob = vmops.SimpleDH()
|
||||||
|
|
||||||
def test_shared(self):
|
def test_shared(self):
|
||||||
alice_pub = self.alice.get_public()
|
alice_pub = self.alice.get_public()
|
||||||
@@ -664,7 +670,8 @@ class XenAPIMigrateInstance(test.TestCase):
|
|||||||
'local_gb': 5,
|
'local_gb': 5,
|
||||||
'instance_type_id': '3', # m1.large
|
'instance_type_id': '3', # m1.large
|
||||||
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
||||||
'os_type': 'linux'}
|
'os_type': 'linux',
|
||||||
|
'architecture': 'x86-64'}
|
||||||
|
|
||||||
fake_utils.stub_out_utils_execute(self.stubs)
|
fake_utils.stub_out_utils_execute(self.stubs)
|
||||||
stubs.stub_out_migration_methods(self.stubs)
|
stubs.stub_out_migration_methods(self.stubs)
|
||||||
@@ -703,6 +710,7 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase):
|
|||||||
self.fake_instance = FakeInstance()
|
self.fake_instance = FakeInstance()
|
||||||
self.fake_instance.id = 42
|
self.fake_instance.id = 42
|
||||||
self.fake_instance.os_type = 'linux'
|
self.fake_instance.os_type = 'linux'
|
||||||
|
self.fake_instance.architecture = 'x86-64'
|
||||||
|
|
||||||
def assert_disk_type(self, disk_type):
|
def assert_disk_type(self, disk_type):
|
||||||
dt = vm_utils.VMHelper.determine_disk_image_type(
|
dt = vm_utils.VMHelper.determine_disk_image_type(
|
||||||
@@ -747,6 +755,28 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase):
|
|||||||
self.assert_disk_type(vm_utils.ImageType.DISK_VHD)
|
self.assert_disk_type(vm_utils.ImageType.DISK_VHD)
|
||||||
|
|
||||||
|
|
||||||
|
class CompareVersionTestCase(test.TestCase):
|
||||||
|
def test_less_than(self):
|
||||||
|
"""Test that cmp_version compares a as less than b"""
|
||||||
|
self.assertTrue(vmops.cmp_version('1.2.3.4', '1.2.3.5') < 0)
|
||||||
|
|
||||||
|
def test_greater_than(self):
|
||||||
|
"""Test that cmp_version compares a as greater than b"""
|
||||||
|
self.assertTrue(vmops.cmp_version('1.2.3.5', '1.2.3.4') > 0)
|
||||||
|
|
||||||
|
def test_equal(self):
|
||||||
|
"""Test that cmp_version compares a as equal to b"""
|
||||||
|
self.assertTrue(vmops.cmp_version('1.2.3.4', '1.2.3.4') == 0)
|
||||||
|
|
||||||
|
def test_non_lexical(self):
|
||||||
|
"""Test that cmp_version compares non-lexically"""
|
||||||
|
self.assertTrue(vmops.cmp_version('1.2.3.10', '1.2.3.4') > 0)
|
||||||
|
|
||||||
|
def test_length(self):
|
||||||
|
"""Test that cmp_version compares by length as last resort"""
|
||||||
|
self.assertTrue(vmops.cmp_version('1.2.3', '1.2.3.4') < 0)
|
||||||
|
|
||||||
|
|
||||||
class FakeXenApi(object):
|
class FakeXenApi(object):
|
||||||
"""Fake XenApi for testing HostState."""
|
"""Fake XenApi for testing HostState."""
|
||||||
|
|
||||||
|
@@ -69,7 +69,6 @@ from nose import core
|
|||||||
from nose import result
|
from nose import result
|
||||||
|
|
||||||
from nova import log as logging
|
from nova import log as logging
|
||||||
from nova.tests import fake_flags
|
|
||||||
|
|
||||||
|
|
||||||
class _AnsiColorizer(object):
|
class _AnsiColorizer(object):
|
||||||
@@ -211,11 +210,11 @@ class NovaTestResult(result.TextTestResult):
|
|||||||
break
|
break
|
||||||
sys.stdout = stdout
|
sys.stdout = stdout
|
||||||
|
|
||||||
# NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate
|
# NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate
|
||||||
# error results in it failing to be initialized later. Otherwise,
|
# error results in it failing to be initialized later. Otherwise,
|
||||||
# _handleElapsedTime will fail, causing the wrong error message to
|
# _handleElapsedTime will fail, causing the wrong error message to
|
||||||
# be outputted.
|
# be outputted.
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
|
|
||||||
def getDescription(self, test):
|
def getDescription(self, test):
|
||||||
return str(test)
|
return str(test)
|
||||||
|
Reference in New Issue
Block a user