diff options
-rw-r--r-- | CHANGELOG | 3 | ||||
-rw-r--r-- | cobbler/action_litesync.py | 8 | ||||
-rw-r--r-- | cobbler/action_sync.py | 7 | ||||
-rw-r--r-- | cobbler/api.py | 19 | ||||
-rw-r--r-- | cobbler/collection.py | 46 | ||||
-rw-r--r-- | cobbler/collection_distros.py | 13 | ||||
-rw-r--r-- | cobbler/collection_profiles.py | 7 | ||||
-rw-r--r-- | cobbler/collection_repos.py | 2 | ||||
-rw-r--r-- | cobbler/collection_systems.py | 7 | ||||
-rw-r--r-- | cobbler/commands.py | 7 | ||||
-rw-r--r-- | cobbler/modules/cli_distro.py | 6 | ||||
-rw-r--r-- | cobbler/modules/cli_profile.py | 8 | ||||
-rw-r--r-- | cobbler/modules/cli_repo.py | 8 | ||||
-rw-r--r-- | cobbler/modules/cli_system.py | 10 | ||||
-rw-r--r-- | cobbler/remote.py | 11 | ||||
-rw-r--r-- | cobbler/serializer.py | 47 | ||||
-rw-r--r-- | tests/performance.py | 75 |
17 files changed, 229 insertions, 55 deletions
@@ -10,6 +10,9 @@ Cobbler CHANGELOG - Implemented fully pluggable authn/authz system - WebUI is now mod_python based - Greatly enhanced logging (goes to /var/log/cobbler/cobbler.log) +- New --no-triggers and --no-sync on "adds" for performance and other reasons +- pxe_just_once is now much faster. +- performance testing scripts (in source checkout) - ... * Wed Nov 14 2007 - 0.6.4 diff --git a/cobbler/action_litesync.py b/cobbler/action_litesync.py index 0cd4318..37d5bd5 100644 --- a/cobbler/action_litesync.py +++ b/cobbler/action_litesync.py @@ -92,7 +92,13 @@ class BootLiteSync: self.sync.rmfile(os.path.join(self.settings.webdir, "profiles", name)) # delete contents on kickstarts/$name directory in webdir self.sync.rmtree(os.path.join(self.settings.webdir, "kickstarts", name)) - + + def update_system_netboot_status(self,name): + system = self.systems.find(name=name) + if system is None: + raise CX(_("error in system lookup for %s") % name) + self.sync.write_all_system_files(system,True) + def add_single_system(self, name): # get the system object: system = self.systems.find(name=name) diff --git a/cobbler/action_sync.py b/cobbler/action_sync.py index 44f1c6b..86bfc0d 100644 --- a/cobbler/action_sync.py +++ b/cobbler/action_sync.py @@ -730,7 +730,7 @@ class BootSync: self.apply_template(infile_data, blended, outfile) - def write_all_system_files(self,system): + def write_all_system_files(self,system,just_edit_pxe=False): profile = system.get_conceptual_parent() if profile is None: @@ -773,7 +773,10 @@ class BootSync: # ensure the file doesn't exist self.rmfile(f2) - self.write_system_file(f3,system) + if not just_edit_pxe: + # allows netboot-disable to be highly performant + # by not invoking the Cheetah engine + self.write_system_file(f3,system) counter = counter + 1 diff --git a/cobbler/api.py b/cobbler/api.py index 0830342..d44fc73 100644 --- a/cobbler/api.py +++ b/cobbler/api.py @@ -25,23 +25,32 @@ import action_validate import sub_process import module_loader import logging +import os +import fcntl ERROR = 100 INFO = 10 DEBUG = 5 +# notes on locking: +# BootAPI is a singleton object +# the XMLRPC variants allow 1 simultaneous request +# therefore we flock on /var/lib/cobbler/settings for now +# on a request by request basis. + class BootAPI: + __shared_state = {} - has_loaded = False + __has_loaded = False def __init__(self): """ Constructor """ - self.__dict__ = self.__shared_state - if not BootAPI.has_loaded: + self.__dict__ = BootAPI.__shared_state + if not BootAPI.__has_loaded: # NOTE: we do not log all API actions, because # a simple CLI invocation may call adds and such @@ -52,7 +61,7 @@ class BootAPI: self.logger = self.__setup_logger("api") self.logger_remote = self.__setup_logger("remote") - BootAPI.has_loaded = True + BootAPI.__has_loaded = True module_loader.load_modules() self._config = config.Config(self) self.deserialize() @@ -202,7 +211,7 @@ class BootAPI: cobbler_repo.set_mirror(url) cobbler_repo.set_name(auto_name) print "auto adding: %s (%s)" % (auto_name, url) - self._config.repos().add(cobbler_repo,with_copy=True) + self._config.repos().add(cobbler_repo,save=True) print "run cobbler reposync to apply changes" return True diff --git a/cobbler/collection.py b/cobbler/collection.py index 41154ab..2fe3967 100644 --- a/cobbler/collection.py +++ b/cobbler/collection.py @@ -36,6 +36,7 @@ class Collection(serializable.Serializable): self.config = config self.clear() self.log_func = self.config.api.log + self.lite_sync = None def factory_produce(self,config,seed_data): """ @@ -97,7 +98,7 @@ class Collection(serializable.Serializable): item = self.factory_produce(self.config,seed_data) self.add(item) - def add(self,ref,with_copy=False,with_triggers=True): + def add(self,ref,save=False,with_copy=False,with_triggers=True,with_sync=True,quick_pxe_update=False): """ Add an object to the collection, if it's valid. Returns True if the object was added to the collection. Returns False if the @@ -113,8 +114,18 @@ class Collection(serializable.Serializable): So, in that case, don't run any triggers and don't deal with any actual files. """ + if self.lite_sync is None: + self.lite_sync = action_litesync.BootLiteSync(self.config) - + # migration path for old API parameter that I've renamed. + if with_copy and not save: + save = with_copy + + if not save: + # for people that aren't quite aware of the API + # if not saving the object, you can't run these features + with_triggers = False + with_sync = False if ref is None or not ref.is_valid(): raise CX(_("insufficient or invalid arguments supplied")) @@ -122,13 +133,13 @@ class Collection(serializable.Serializable): if ref.COLLECTION_TYPE != self.collection_type(): raise CX(_("API error: storing wrong data type in collection")) - if not with_copy: + if not save: # don't need to run triggers, so add it already ... self.listing[ref.name.lower()] = ref # perform filesystem operations - if with_copy: + if save: self.log_func("saving %s %s" % (self.collection_type(), ref.name)) # failure of a pre trigger will prevent the object from being added if with_triggers: @@ -139,18 +150,21 @@ class Collection(serializable.Serializable): # the whole collection self.config.serialize_item(self, ref) - lite_sync = action_litesync.BootLiteSync(self.config) - if isinstance(ref, item_system.System): - lite_sync.add_single_system(ref.name) - elif isinstance(ref, item_profile.Profile): - lite_sync.add_single_profile(ref.name) - elif isinstance(ref, item_distro.Distro): - lite_sync.add_single_distro(ref.name) - elif isinstance(ref, item_repo.Repo): - pass - else: - print _("Internal error. Object type not recognized: %s") % type(ref) - + if with_sync: + if isinstance(ref, item_system.System): + self.lite_sync.add_single_system(ref.name) + elif isinstance(ref, item_profile.Profile): + self.lite_sync.add_single_profile(ref.name) + elif isinstance(ref, item_distro.Distro): + self.lite_sync.add_single_distro(ref.name) + elif isinstance(ref, item_repo.Repo): + pass + else: + print _("Internal error. Object type not recognized: %s") % type(ref) + if not with_sync and quick_pxe_update: + if isinstance(ref, item_system.System): + self.lite_sync.update_system_netboot_status(ref.name) + # save the tree, so if neccessary, scripts can examine it. if with_triggers: self._run_triggers(ref,"/var/lib/cobbler/triggers/add/%s/post/*" % self.collection_type()) diff --git a/cobbler/collection_distros.py b/cobbler/collection_distros.py index a01e876..a2a0464 100644 --- a/cobbler/collection_distros.py +++ b/cobbler/collection_distros.py @@ -31,7 +31,7 @@ class Distros(collection.Collection): """ return distro.Distro(config).from_datastruct(seed_data) - def remove(self,name,with_delete=True,with_triggers=True): + def remove(self,name,with_delete=True,with_sync=True,with_triggers=True): """ Remove element named 'name' from the collection """ @@ -43,14 +43,17 @@ class Distros(collection.Collection): obj = self.find(name=name) if obj is not None: if with_delete: - if with_triggers: self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/distro/pre/*") - lite_sync = action_litesync.BootLiteSync(self.config) - lite_sync.remove_single_profile(name) + if with_triggers: + self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/distro/pre/*") + if with_sync: + lite_sync = action_litesync.BootLiteSync(self.config) + lite_sync.remove_single_profile(name) del self.listing[name] self.config.serialize_delete(self, obj) if with_delete: self.log_func("deleted distro %s" % name) - if with_triggers: self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/distro/post/*") + if with_triggers: + self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/distro/post/*") return True raise CX(_("cannot delete object that does not exist")) diff --git a/cobbler/collection_profiles.py b/cobbler/collection_profiles.py index 78dd62f..dcfd701 100644 --- a/cobbler/collection_profiles.py +++ b/cobbler/collection_profiles.py @@ -32,7 +32,7 @@ class Profiles(collection.Collection): def factory_produce(self,config,seed_data): return profile.Profile(config).from_datastruct(seed_data) - def remove(self,name,with_delete=True,with_triggers=True): + def remove(self,name,with_delete=True,with_sync=True,with_triggers=True): """ Remove element named 'name' from the collection """ @@ -45,8 +45,9 @@ class Profiles(collection.Collection): if with_delete: if with_triggers: self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/profile/pre/*") - lite_sync = action_litesync.BootLiteSync(self.config) - lite_sync.remove_single_profile(name) + if with_sync: + lite_sync = action_litesync.BootLiteSync(self.config) + lite_sync.remove_single_profile(name) del self.listing[name] self.config.serialize_delete(self, obj) if with_delete: diff --git a/cobbler/collection_repos.py b/cobbler/collection_repos.py index 720f469..6e24983 100644 --- a/cobbler/collection_repos.py +++ b/cobbler/collection_repos.py @@ -36,7 +36,7 @@ class Repos(collection.Collection): """ return repo.Repo(config).from_datastruct(seed_data) - def remove(self,name,with_delete=True,with_triggers=True): + def remove(self,name,with_delete=True,with_sync=True,with_triggers=True): """ Remove element named 'name' from the collection """ diff --git a/cobbler/collection_systems.py b/cobbler/collection_systems.py index b89048b..bdc8228 100644 --- a/cobbler/collection_systems.py +++ b/cobbler/collection_systems.py @@ -33,7 +33,7 @@ class Systems(collection.Collection): """ return system.System(config).from_datastruct(seed_data) - def remove(self,name,with_delete=True,with_triggers=True): + def remove(self,name,with_delete=True,with_sync=True,with_triggers=True): """ Remove element named 'name' from the collection """ @@ -45,8 +45,9 @@ class Systems(collection.Collection): if with_delete: if with_triggers: self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/system/pre/*") - lite_sync = action_litesync.BootLiteSync(self.config) - lite_sync.remove_single_system(name) + if with_sync: + lite_sync = action_litesync.BootLiteSync(self.config) + lite_sync.remove_single_system(name) del self.listing[name] self.config.serialize_delete(self, obj) if with_delete: diff --git a/cobbler/commands.py b/cobbler/commands.py index 5760b26..4bffe1f 100644 --- a/cobbler/commands.py +++ b/cobbler/commands.py @@ -229,7 +229,7 @@ class CobblerFunction: return obj - def object_manipulator_finish(self,obj,collect_fn): + def object_manipulator_finish(self,obj,collect_fn, options): """ Boilerplate for objects that offer add/edit/delete/remove/copy functionality. """ @@ -241,7 +241,10 @@ class CobblerFunction: else: raise CX(_("--newname is required")) - rc = collect_fn().add(obj, with_copy=True) + opt_sync = not options.nosync + opt_triggers = not options.notriggers + + rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers) if "rename" in self.args: return collect_fn().remove(self.options.name, with_delete=True) diff --git a/cobbler/modules/cli_distro.py b/cobbler/modules/cli_distro.py index 3cfa716..d3b32f8 100644 --- a/cobbler/modules/cli_distro.py +++ b/cobbler/modules/cli_distro.py @@ -49,6 +49,10 @@ class DistroFunction(commands.CobblerFunction): if self.matches_args(args,["copy","rename"]): p.add_option("--newname", dest="newname", help="for copy/rename commands") + if not self.matches_args(args,["remove","report","list"]): + p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") + if not self.matches_args(args,["report","list"]): + p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") def run(self): @@ -67,7 +71,7 @@ class DistroFunction(commands.CobblerFunction): if self.options.breed: obj.set_breed(self.options.breed) - return self.object_manipulator_finish(obj, self.api.distros) + return self.object_manipulator_finish(obj, self.api.distros, self.options) diff --git a/cobbler/modules/cli_profile.py b/cobbler/modules/cli_profile.py index 3026c3c..19d9ab4 100644 --- a/cobbler/modules/cli_profile.py +++ b/cobbler/modules/cli_profile.py @@ -49,6 +49,12 @@ class ProfileFunction(commands.CobblerFunction): if "copy" in args or "rename" in args: p.add_option("--newname", dest="newname") + if not self.matches_args(args,["remove","report", "list"]): + p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") + if not self.matches_args(args,["report", "list"]): + p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") + + if not self.matches_args(args,["remove","report","list"]): p.add_option("--repos", dest="repos", help="names of cobbler repos") p.add_option("--server-override", dest="server_override", help="overrides value in settings file") @@ -85,7 +91,7 @@ class ProfileFunction(commands.CobblerFunction): if self.options.dhcp_tag: obj.set_dhcp_tag(self.options.dhcp_tag) if self.options.server_override: obj.set_server(self.options.server) - return self.object_manipulator_finish(obj, self.api.profiles) + return self.object_manipulator_finish(obj, self.api.profiles, self.options) diff --git a/cobbler/modules/cli_repo.py b/cobbler/modules/cli_repo.py index 88de685..8bfeb8e 100644 --- a/cobbler/modules/cli_repo.py +++ b/cobbler/modules/cli_repo.py @@ -52,6 +52,12 @@ class RepoFunction(commands.CobblerFunction): p.add_option("--newname", dest="newname", help="used for copy/edit") + if not self.matches_args(args,["remove","report","list"]): + p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") + if not self.matches_args(args,["report","list"]): + p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") + + def run(self): obj = self.object_manipulator_start(self.api.new_repo,self.api.repos) @@ -65,7 +71,7 @@ class RepoFunction(commands.CobblerFunction): if self.options.priority: obj.set_priority(self.options.priority) if self.options.mirror: obj.set_mirror(self.options.mirror) - return self.object_manipulator_finish(obj, self.api.repos) + return self.object_manipulator_finish(obj, self.api.repos, self.options) diff --git a/cobbler/modules/cli_system.py b/cobbler/modules/cli_system.py index 4056359..3e50a64 100644 --- a/cobbler/modules/cli_system.py +++ b/cobbler/modules/cli_system.py @@ -56,12 +56,18 @@ class SystemFunction(commands.CobblerFunction): p.add_option("--newname", dest="newname", help="for use with copy/edit") if not self.matches_args(args,["remove","report","list"]): + p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed") + if not self.matches_args(args,["report","list"]): + p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution") + + + if not self.matches_args(args,["remove","report","list"]): p.add_option("--profile", dest="profile", help="name of cobbler profile (REQUIRED)") p.add_option("--server-override", dest="server_override", help="overrides server value in settings file") p.add_option("--subnet", dest="subnet", help="for static IP / templating usage") p.add_option("--virt-bridge", dest="virt_bridge", help="ex: virbr0") p.add_option("--virt-path", dest="virt_path", help="path, partition, or volume") - p.add_option("--virt-type", dest="virt_type", help="ex: xenpv, qemu") + p.add_option("--virt-type", dest="virt_type", help="ex: xenpv, qemu, xenfv") def run(self): @@ -90,7 +96,7 @@ class SystemFunction(commands.CobblerFunction): if self.options.gateway: obj.set_gateway(self.options.gateway, my_interface) if self.options.dhcp_tag: obj.set_dhcp_tag(self.options.dhcp_tag, my_interface) - return self.object_manipulator_finish(obj, self.api.systems) + return self.object_manipulator_finish(obj, self.api.systems, self.options) diff --git a/cobbler/remote.py b/cobbler/remote.py index efa8326..d74b002 100644 --- a/cobbler/remote.py +++ b/cobbler/remote.py @@ -183,7 +183,8 @@ class CobblerXMLRPCInterface: # system not found! return False obj.set_netboot_enabled(0) - systems.add(obj,with_copy=True) + # disabling triggers and sync to make this extremely fast. + systems.add(obj,save=True,with_triggers=False,with_sync=False,quick_pxe_update=True) return True def run_post_install_triggers(self,name,token=None): @@ -710,7 +711,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): self.log("save_distro",object_id=object_id,token=token) self.check_access(token,"save_distro") obj = self.__get_object(object_id) - return self.api.distros().add(obj,with_copy=True) + return self.api.distros().add(obj,save=True) def save_profile(self,object_id,token): """ @@ -719,7 +720,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): self.log("save_profile",token=token,object_id=object_id) self.check_access(token,"save_profile") obj = self.__get_object(object_id) - return self.api.profiles().add(obj,with_copy=True) + return self.api.profiles().add(obj,save=True) def save_system(self,object_id,token): """ @@ -728,7 +729,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): self.log("save_system",token=token,object_id=object_id) self.check_access(token,"save_system") obj = self.__get_object(object_id) - return self.api.systems().add(obj,with_copy=True) + return self.api.systems().add(obj,save=True) def save_repo(self,object_id,token=None): """ @@ -737,7 +738,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): self.log("save_repo",object_id=object_id,token=token) self.check_access(token,"save_repo") obj = self.__get_object(object_id) - return self.api.repos().add(obj,with_copy=True) + return self.api.repos().add(obj,save=True) def __call_method(self, obj, attribute, arg): """ diff --git a/cobbler/serializer.py b/cobbler/serializer.py index 8593aad..ae9f18c 100644 --- a/cobbler/serializer.py +++ b/cobbler/serializer.py @@ -16,52 +16,82 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import errno import os from rhpl.translate import _, N_, textdomain, utf8 +import fcntl from cexceptions import * import utils import api as cobbler_api +LOCK_ENABLED = True +LOCK_HANDLE = None + +def __grab_lock(): + if not LOCK_ENABLED: + return + if not os.path.exists("/var/lib/cobbler/lock"): + fd = open("/var/lib/cobbler/lock","w+") + fd.close() + LOCK_HANDLE = open("/var/lib/cobbler/lock","r") + fcntl.flock(LOCK_HANDLE.fileno(), fcntl.LOCK_EX) + +def __release_lock(): + if not LOCK_ENABLED: + return + LOCK_HANDLE = open("/var/lib/cobbler/lock","r") + fcntl.flock(LOCK_HANDLE.fileno(), fcntl.LOCK_UN) + LOCK_HANDLE.close() + def serialize(obj): """ Save a collection to disk or other storage. """ + __grab_lock() storage_module = __get_storage_module(obj.collection_type()) storage_module.serialize(obj) + __release_lock() return True def serialize_item(collection, item): """ Save an item. """ + __grab_lock() storage_module = __get_storage_module(collection.collection_type()) save_fn = getattr(storage_module, "serialize_item", None) if save_fn is None: # print "DEBUG: WARNING: full serializer" - return storage_module.serialize(collection) + rc = storage_module.serialize(collection) else: # print "DEBUG: partial serializer" - return save_fn(collection,item) + rc = save_fn(collection,item) + __release_lock() + return rc def serialize_delete(collection, item): """ Delete an object from a saved state. """ + __grab_lock() storage_module = __get_storage_module(collection.collection_type()) delete_fn = getattr(storage_module, "serialize_delete", None) if delete_fn is None: # print "DEBUG: full delete" - return storage_module.serialize(collection) + rc = storage_module.serialize(collection) else: # print "DEBUG: partial delete" - return delete_fn(collection,item) - + rc = delete_fn(collection,item) + __release_lock() + return rc def deserialize(obj,topological=False): """ Fill in an empty collection from disk or other storage """ + __grab_lock() storage_module = __get_storage_module(obj.collection_type()) - return storage_module.deserialize(obj,topological) + rc = storage_module.deserialize(obj,topological) + __release_lock() + return rc def deserialize_raw(collection_type): """ @@ -69,8 +99,11 @@ def deserialize_raw(collection_type): disk state, without going through the Cobbler object system. Much faster, when you don't need the objects. """ + __grab_lock() storage_module = __get_storage_module(collection_type) - return storage_module.deserialize_raw(collection_type) + rc = storage_module.deserialize_raw(collection_type) + __release_lock() + return rc def __get_storage_module(collection_type): """ diff --git a/tests/performance.py b/tests/performance.py new file mode 100644 index 0000000..15b9bad --- /dev/null +++ b/tests/performance.py @@ -0,0 +1,75 @@ +# test script to evaluate Cobbler API performance +# +# Michael DeHaan <mdehaan@redhat.com> + +import os +import cobbler.api as capi +import time +import sys +import random + +N = 200 +print "sample size is %s" % N + +api = capi.BootAPI() + +# part one ... create our test systems for benchmarking purposes if +# they do not seem to exist. + +if not api.profiles().find("foo"): + print "CREATE A PROFILE NAMED 'foo' to be able to run this test" + sys.exit(0) + +def random_mac(): + mac = [ 0x00, 0x16, 0x3e, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff) ] + return ':'.join(map(lambda x: "%02x" % x, mac)) + +print "Deleting autotest entries from a previous run" +time1 = time.time() +for x in xrange(0,N): + try: + sys = api.systems().remove("autotest-%s" % x,with_delete=True) + except: + pass +time2 = time.time() +print "ELAPSED: %s seconds" % (time2 - time1) + +print "Creating test systems from scratch" +time1 = time.time() +for x in xrange(0,N): + sys = api.new_system() + sys.set_name("autotest-%s" % x) + sys.set_mac_address(random_mac()) + sys.set_profile("foo") # assumes there is already a foo + # print "... adding: %s" % sys.name + api.systems().add(sys,save=True,with_sync=False,with_triggers=False) +time2 = time.time() +print "ELAPSED %s seconds" % (time2 - time1) + +for mode2 in [ "fast", "normal", "full" ]: + for mode in [ "on", "off" ]: + + print "Running netboot edit benchmarks (turn %s, %s)" % (mode, mode2) + time1 = time.time() + for x in xrange(0,N): + sys = api.systems().find("autotest-%s" % x) + if mode == "off": + sys.set_netboot_enabled(0) + else: + sys.set_netboot_enabled(1) + # print "... editing: %s" % sys.name + if mode2 == "fast": + api.systems().add(sys, save=True, with_sync=False, with_triggers=False, quick_pxe_update=True) + if mode2 == "normal": + api.systems().add(sys, save=True, with_sync=False, with_triggers=False) + if mode2 == "full": + api.systems().add(sys, save=True, with_sync=True, with_triggers=True) + + time2 = time.time() + print "ELAPSED: %s seconds" % (time2 - time1) + + + |