def threadsafe_generate_install_media(self, guest): # Oz caching of install media and modified install media is not thread safe # Make it safe here using some locks # We can only have one active generate_install_media() call for each unique tuple: # (OS, update, architecture, installtype) tdl = guest.tdl queue_name = "%s-%s-%s-%s" % (tdl.distro, tdl.update, tdl.arch, tdl.installtype) res_mgr = ReservationManager() res_mgr.get_named_lock(queue_name) try: guest.generate_install_media(force_download=False) finally: res_mgr.release_named_lock(queue_name)
class TinMan(object): def activity(self, activity): # Simple helper function # Activity should be a one line human-readable string indicating the task in progress # We log it at DEBUG and also set it as the status_detail on our active image self.log.debug(activity) self.active_image.status_detail['activity'] = activity ## INTERFACE METHOD def create_target_image(self, builder, target, base_image, parameters): self.log.info( 'create_target_image() called for TinMan plugin - creating a TargetImage' ) self.active_image = builder.target_image self.target = target self.base_image = builder.base_image # populate our target_image bodyfile with the original base image # which we do not want to modify in place self.activity("Copying BaseImage to modifiable TargetImage") self.log.debug( "Copying base_image file (%s) to new target_image file (%s)" % (builder.base_image.data, builder.target_image.data)) oz.ozutil.copyfile_sparse(builder.base_image.data, builder.target_image.data) self.image = builder.target_image.data # Merge together any TDL-style customizations requested via our plugin-to-plugin interface # with any target specific packages, repos and commands and then run a second Oz customization # step. self.tdlobj = oz.TDL.TDL( xmlstring=builder.base_image.template, rootpw_required=self.app_config["tdl_require_root_pw"]) # We remove any packages, commands and files from the original TDL - these have already been # installed/executed. We leave the repos in place, as it is possible that the target # specific packages or commands may require them. self.tdlobj.packages = [] self.tdlobj.commands = {} self.tdlobj.files = {} # This is user-defined target-specific packages and repos in a local config file self.add_target_content() # This is content deposited by cloud plugins - typically commands to run to prep the image further self.merge_cloud_plugin_content() # If there are no new commands, packages or files, we can stop here - there is no need to run Oz again if (len(self.tdlobj.packages) + len(self.tdlobj.commands) + len(self.tdlobj.files)) == 0: self.log.debug( "No further modification of the TargetImage to perform in the OS Plugin - returning" ) return # We have some additional work to do - create a new Oz guest object that we can use to run the guest # customization a second time self._init_oz() self.guest.diskimage = builder.target_image.data libvirt_xml = self.guest._generate_xml("hd", None) # One last step is required here - The persistent net rules in some Fedora and RHEL versions # Will cause our new incarnation of the image to fail to get network - fix that here # We unfortunately end up having to duplicate this a second time in the cloud plugins # when we are done with our second stage customizations # TODO: Consider moving all of that back here guestfs_handle = launch_inspect_and_mount(builder.target_image.data) remove_net_persist(guestfs_handle) shutdown_and_close(guestfs_handle) try: self.log.debug( "Doing second-stage target_image customization and ICICLE generation" ) #self.percent_complete = 30 builder.target_image.icicle = self.guest.customize_and_generate_icicle( libvirt_xml) self.log.debug("Customization and ICICLE generation complete") #self.percent_complete = 50 finally: self.activity("Cleaning up install artifacts") self.guest.cleanup_install() def add_cloud_plugin_content(self, content): # This is a method that cloud plugins can call to deposit content/commands to be run # during the OS-specific first stage of the Target Image creation. # The expected input is a dict containing commands and files # No support for repos at the moment as these introduce external deps that we may not be able to count on # Add this to an array which will later be merged into the TDL object used to drive Oz self.cloud_plugin_content.append(content) def merge_cloud_plugin_content(self): for content in self.cloud_plugin_content: if 'files' in content: for fileentry in content['files']: if not 'name' in fileentry: raise ImageFactoryException( "File given without a name") if not 'type' in fileentry: raise ImageFactoryException( "File given without a type") if not 'file' in fileentry: raise ImageFactoryException( "File given without any content") if fileentry['type'] == 'raw': self.tdlobj.files[ fileentry['name']] = fileentry['file'] elif fileentry['type'] == 'base64': if len(fileentry['file']) == 0: self.tdlobj.files[fileentry['name']] = "" else: self.tdlobj.files[ fileentry['name']] = base64.b64decode( fileentry['file']) else: raise ImageFactoryException( "File given with invalid type (%s)" % (fileentry['type'])) if 'commands' in content: for command in content['commands']: if not 'name' in command: raise ImageFactoryException( "Command given without a name") if not 'type' in command: raise ImageFactoryException( "Command given without a type") if not 'command' in command: raise ImageFactoryException( "Command given without any content") if command['type'] == 'raw': self.tdlobj.commands[ command['name']] = command['command'] elif command['type'] == 'base64': if len(command['command']) == 0: self.log.warning("Command with zero length given") self.tdlobj.commands[command['name']] = "" else: self.tdlobj.commands[ command['name']] = base64.b64decode( command['command']) else: raise ImageFactoryException( "Command given with invalid type (%s)" % (command['type'])) def add_target_content(self): """Merge in target specific package and repo content. TDL object must already exist as self.tdlobj""" doc = None if isfile("/etc/imagefactory/target_content.xml"): doc = libxml2.parseFile("/etc/imagefactory/target_content.xml") else: self.log.debug( "Found neither a call-time config nor a config file - doing nothing" ) return # Purely to make the xpath statements below a tiny bit shorter target = self.target os = self.tdlobj.distro version = self.tdlobj.update arch = self.tdlobj.arch # We go from most to least specific in this order: # arch -> version -> os-> target # Note that at the moment we even allow an include statment that covers absolutely everything. # That is, one that doesn't even specify a target - this is to support a very simple call-time syntax include = doc.xpathEval( "/template_includes/include[@target='%s' and @os='%s' and @version='%s' and @arch='%s']" % (target, os, version, arch)) if len(include) == 0: include = doc.xpathEval( "/template_includes/include[@target='%s' and @os='%s' and @version='%s' and not(@arch)]" % (target, os, version)) if len(include) == 0: include = doc.xpathEval( "/template_includes/include[@target='%s' and @os='%s' and not(@version) and not(@arch)]" % (target, os)) if len(include) == 0: include = doc.xpathEval( "/template_includes/include[@target='%s' and not(@os) and not(@version) and not(@arch)]" % (target)) if len(include) == 0: include = doc.xpathEval( "/template_includes/include[not(@target) and not(@os) and not(@version) and not(@arch)]" ) if len(include) == 0: self.log.debug( "cannot find a config section that matches our build details - doing nothing" ) return # OK - We have at least one config block that matches our build - take the first one, merge it and be done # TODO: Merge all of them? Err out if there is more than one? Warn? include = include[0] packages = include.xpathEval("packages") if len(packages) > 0: self.tdlobj.merge_packages(str(packages[0])) repositories = include.xpathEval("repositories") if len(repositories) > 0: self.tdlobj.merge_repositories(str(repositories[0])) def __init__(self): super(TinMan, self).__init__() self.cloud_plugin_content = [] config_obj = ApplicationConfiguration() self.app_config = config_obj.configuration self.res_mgr = ReservationManager() self.log = logging.getLogger('%s.%s' % (__name__, self.__class__.__name__)) self.parameters = None self.install_script_object = None self.guest = None def abort(self): self.log.debug("ABORT called in TinMan plugin") # If we have an active Oz VM destroy it - if not do nothing but log why we did nothing if not self.guest: self.log.debug("No Oz guest object present - nothing to do") return try: # Oz doesn't keep the active domain object as an instance variable so we have to look it up guest_dom = self.guest.libvirt_conn.lookupByName(self.tdlobj.name) except Exception as e: self.log.exception(e) self.log.debug("No Oz VM found with name (%s) - nothing to do" % (self.tdlobj.name)) self.log.debug( "This likely means the local VM has already been destroyed or never started" ) return try: self.log.debug("Attempting to destroy local guest/domain (%s)" % (self.tdlobj.name)) guest_dom.destroy() except Exception as e: self.log.exception(e) self.log.warning( "Exception encountered while destroying domain - it may still exist" ) def _init_oz(self): # TODO: This is a convenience variable for refactoring - rename self.new_image_id = self.active_image.identifier # Create a name combining the TDL name and the UUID for use when tagging EC2 AMIs self.longname = self.tdlobj.name + "-" + self.new_image_id # Oz assumes unique names - TDL built for multiple backends guarantees they are not unique # We don't really care about the name so just force uniqueness # 18-Jul-2011 - Moved to constructor and modified to change TDL object name itself # Oz now uses the tdlobject name property directly in several places so we must change it self.tdlobj.name = "factory-build-" + self.new_image_id # populate a config object to pass to OZ; this allows us to specify our # own output dir but inherit other Oz behavior self.oz_config = configparser.SafeConfigParser() if self.oz_config.read("/etc/oz/oz.cfg") != []: if self.parameters.get("oz_overrides", None) != None: oz_overrides = json.loads( self.parameters.get("oz_overrides", None).replace("'", "\"")) for i in oz_overrides: for key, val in oz_overrides[i].items(): self.oz_config.set(i, key, str(val)) self.oz_config.set('paths', 'output_dir', self.app_config["imgdir"]) if "oz_data_dir" in self.app_config: self.oz_config.set('paths', 'data_dir', self.app_config["oz_data_dir"]) if "oz_screenshot_dir" in self.app_config: self.oz_config.set('paths', 'screenshot_dir', self.app_config["oz_screenshot_dir"]) print("=============== Final Oz Config ================") for section in self.oz_config.sections(): print("[ {0} ]".format(section)) for option in self.oz_config.options(section): print(" {0} = {1}".format( option, self.oz_config.get(section, option))) else: raise ImageFactoryException( "No Oz config file found. Can't continue.") # make this a property to enable quick cleanup on abort self.instance = None # Here we are always dealing with a local install self.init_guest() ## INTERFACE METHOD def create_base_image(self, builder, template, parameters): self.log.info( 'create_base_image() called for TinMan plugin - creating a BaseImage' ) self.tdlobj = oz.TDL.TDL( xmlstring=template.xml, rootpw_required=self.app_config["tdl_require_root_pw"]) if parameters: self.parameters = parameters else: self.parameters = {} # TODO: Standardize reference scheme for the persistent image objects in our builder # Having local short-name copies like this may well be a good idea though they # obscure the fact that these objects are in a container "upstream" of our plugin object self.base_image = builder.base_image # Set to the image object that is actively being created or modified # Used in the logging helper function above self.active_image = self.base_image try: self._init_oz() self.guest.diskimage = self.base_image.data self.activity("Cleaning up any old Oz guest") self.guest.cleanup_old_guest() self.activity("Generating JEOS install media") self.threadsafe_generate_install_media(self.guest) self.percent_complete = 10 # We want to save this later for use by RHEV-M and Condor clouds libvirt_xml = "" gfs = None try: self.activity("Generating JEOS disk image") # Newer Oz versions introduce a configurable disk size in TDL # We must still detect that it is present and pass it in this call try: disksize = getattr(self.guest, "disksize") except AttributeError: disksize = 10 self.guest.generate_diskimage(size=disksize) # TODO: If we already have a base install reuse it # subject to some rules about updates to underlying repo self.activity("Execute JEOS install") libvirt_xml = self.guest.install(self.app_config["timeout"]) self.base_image.parameters['libvirt_xml'] = libvirt_xml self.image = self.guest.diskimage self.log.debug( "Base install complete - Doing customization and ICICLE generation" ) self.percent_complete = 30 # Power users may wish to avoid ever booting the guest after the installer is finished # They can do so by passing in a { "generate_icicle": False } KV pair in the parameters dict if parameter_cast_to_bool( self.parameters.get("generate_icicle", True)): if parameter_cast_to_bool( self.parameters.get("offline_icicle", False)): self.guest.customize(libvirt_xml) gfs = launch_inspect_and_mount(self.image, readonly=True) # Monkey-patching is bad # TODO: Work with Chris to incorporate a more elegant version of this into Oz itself def libguestfs_execute_command(gfs, cmd, timeout): stdout = gfs.sh(cmd) return (stdout, None, 0) self.guest.guest_execute_command = libguestfs_execute_command builder.base_image.icicle = self.guest.do_icicle(gfs) else: builder.base_image.icicle = self.guest.customize_and_generate_icicle( libvirt_xml) else: # koji errs out if this value is None - set to an empty ICICLE instead builder.base_image.icicle = "<icicle></icicle>" self.guest.customize(libvirt_xml) self.log.debug("Customization and ICICLE generation complete") self.percent_complete = 50 finally: self.activity("Cleaning up install artifacts") if self.guest: self.guest.cleanup_install() if self.install_script_object: # NamedTemporaryFile - removed on close self.install_script_object.close() if gfs: shutdown_and_close(gfs) self.log.debug("Generated disk image (%s)" % (self.guest.diskimage)) # OK great, we now have a customized KVM image finally: pass # TODO: Create the base_image object representing this # TODO: Create the base_image object at the beginning and then set the diskimage accordingly def init_guest(self): # Use the factory function from Oz directly # This raises an exception if the TDL contains an unsupported distro or version # Cloud plugins that use KVM directly, such as RHEV-M and openstack-kvm can accept # any arbitrary guest that Oz is capable of producing install_script_name = None install_script = self.parameters.get("install_script", None) if install_script: self.install_script_object = NamedTemporaryFile(mode='w') self.install_script_object.write(install_script) self.install_script_object.flush() install_script_name = self.install_script_object.name try: self.guest = oz.GuestFactory.guest_factory(self.tdlobj, self.oz_config, install_script_name) # Oz just selects a random port here - This could potentially collide if we are unlucky self.guest.listen_port = self.res_mgr.get_next_listen_port() except libvirtError as e: raise ImageFactoryException( "Cannot connect to libvirt. Make sure libvirt is running. [Original message: %s]" % e.message) except OzException as e: if "Unsupported" in e.message: raise ImageFactoryException( "TinMan plugin does not support distro (%s) update (%s) in TDL" % (self.tdlobj.distro, self.tdlobj.update)) else: raise e def log_exc(self): self.log.debug("Exception caught in ImageFactory") self.log.debug(traceback.format_exc()) self.active_image.status_detal['error'] = traceback.format_exc() def threadsafe_generate_install_media(self, guest): # Oz caching of install media and modified install media is not thread safe # Make it safe here using some locks # We can only have one active generate_install_media() call for each unique tuple: # (OS, update, architecture, installtype) tdl = guest.tdl queue_name = "%s-%s-%s-%s" % (tdl.distro, tdl.update, tdl.arch, tdl.installtype) self.res_mgr.get_named_lock(queue_name) try: guest.generate_install_media(force_download=False) finally: self.res_mgr.release_named_lock(queue_name)
class SecondaryDispatcher(Singleton): def _singleton_init(self): self.log = logging.getLogger('%s.%s' % (__name__, self.__class__.__name__)) self.pending_uploads = dict() self.pending_uploads_lock = BoundedSemaphore() self.pim = PersistentImageManager.default_manager() self.res = ReservationManager() self.secondaries = { } if os.path.isfile(SECONDARIES): try: secs = open(SECONDARIES, "r") self.secondaries = json.load(secs) secs.close() except: self.log.warning("Unable to load JSON for secondaries from %s", SECONDARIES) if not 'targets' in self.secondaries: self.secondaries['targets'] = { } if not 'providers' in self.secondaries: self.secondaries['providers'] = { } def queue_pending_upload(self, target_image_uuid): # Create a UUID - map it to the target_image_uuid and return it # TODO: Expire these somehow upload_uuid = str(uuid.uuid4()) self.pending_uploads_lock.acquire() try: self.pending_uploads[upload_uuid] = target_image_uuid finally: self.pending_uploads_lock.release() return upload_uuid def target_image_for_upload_uuid(self, upload_uuid): # Return the target_image UUID for a given upload UUID if it exists # and remove it from the dict. Return None if the UUID is not in the dict. self.pending_uploads_lock.acquire() target_image_uuid = None try: if upload_uuid in self.pending_uploads: target_image_uuid = self.pending_uploads[upload_uuid] del self.pending_uploads[upload_uuid] finally: self.pending_uploads_lock.release() return target_image_uuid def update_target_image_body(self, target_image, new_body): # Called during the clone process - we background the actual copy # and update the target_image in question to COMPLETED when the copy is finished # This allows the HTTP transaction to complete - in testing some upload clients # timed out waiting for the local copy to complete # (bottle.py internally processes all uploaded files to completion, storing them as unnamed temporary # files) target_update_thread = Thread(target=self._update_target_image_body, args=(target_image, new_body)) target_update_thread.start() def _update_target_image_body(self, target_image, new_body): try: self.log.debug("Copying incoming file to %s" % (target_image.data)) dest = open(target_image.data,"w") shutil.copyfileobj(new_body, dest, 16384) self.log.debug("Finished copying incoming file to %s" % (target_image.data)) target_image.status="COMPLETE" except Exception as e: self.log.debug("Exception encountered when attempting to update target_image body") self.log.exception(e) target_image.status="FAILED" finally: self.pim.save_image(target_image) def prep_target_image_clone(self, request_data, target_image_id): # Request data should contain all target_image metadata to be cloned # If target_image with this ID doest not exist, create it and establish an # upload UUID and return both. # If taget_image already exists, return just the existing target_image upload_id = None self.res.get_named_lock(target_image_id) # At this point no other thread, either remote or local, can operate on this ID # The image either already exists or it doesn't. If it doesn't we can safely create # it without worrying about concurrency problems. try: target_image = self.pim.image_with_id(target_image_id) if not target_image: upload_id = self.queue_pending_upload(target_image_id) target_image = TargetImage(image_id=target_image_id) metadata_keys = target_image.metadata() for data_element in request_data.keys(): if not (data_element in metadata_keys): self.log.warning("Metadata field (%s) in incoming target_image clone request is non-standard - skipping" % (data_element)) else: setattr(target_image, data_element, request_data[data_element]) # The posted image will, of course, be complete, so fix status here target_image.status = "PENDING" target_image.percent_complete = 0 target_image.status_detail = { 'activity': 'Image being cloned from primary factory - initial metadata set', 'error':None } self.pim.add_image(target_image) self.pim.save_image(target_image) self.log.debug("Completed save of target_image (%s)" % target_image.identifier) finally: self.res.release_named_lock(target_image_id) return (target_image, upload_id) def get_secondary(self, target, provider): if provider in self.secondaries['providers']: return self.secondaries['providers'][provider] elif target in self.secondaries['targets']: return self.secondaries['targets'][target] else: return None def get_helper(self, secondary): try: helper = SecondaryHelper(**secondary) return helper except Exception as e: self.log.error("Exception encountered when trying to create secondary helper object") self.log.error("Secondary details: %s" % (secondary)) self.log.exception(e)
class FedoraOS(object): zope.interface.implements(OSDelegate) def activity(self, activity): # Simple helper function # Activity should be a one line human-readable string indicating the task in progress # We log it at DEBUG and also set it as the status_detail on our active image self.log.debug(activity) self.active_image.status_detail['activity'] = activity ## INTERFACE METHOD def create_target_image(self, builder, target, base_image, parameters): self.log.info('create_target_image() called for FedoraOS plugin - creating a TargetImage') self.active_image = builder.target_image self.target = target self.base_image = builder.base_image # populate our target_image bodyfile with the original base image # which we do not want to modify in place self.activity("Copying BaseImage to modifiable TargetImage") self.log.debug("Copying base_image file (%s) to new target_image file (%s)" % (builder.base_image.data, builder.target_image.data)) oz.ozutil.copyfile_sparse(builder.base_image.data, builder.target_image.data) self.image = builder.target_image.data # Merge together any TDL-style customizations requested via our plugin-to-plugin interface # with any target specific packages, repos and commands and then run a second Oz customization # step. self.tdlobj = oz.TDL.TDL(xmlstring=builder.base_image.template, rootpw_required=self.app_config["tdl_require_root_pw"]) # We remove any packages, commands and files from the original TDL - these have already been # installed/executed. We leave the repos in place, as it is possible that the target # specific packages or commands may require them. self.tdlobj.packages = [ ] self.tdlobj.commands = { } self.tdlobj.files = { } # This is user-defined target-specific packages and repos in a local config file self.add_target_content() # This is content deposited by cloud plugins - typically commands to run to prep the image further self.merge_cloud_plugin_content() # If there are no new commands, packages or files, we can stop here - there is no need to run Oz again if (len(self.tdlobj.packages) + len(self.tdlobj.commands) + len(self.tdlobj.files)) == 0: self.log.debug("No further modification of the TargetImage to perform in the OS Plugin - returning") return # We have some additional work to do - create a new Oz guest object that we can use to run the guest # customization a second time self._init_oz() self.guest.diskimage = builder.target_image.data libvirt_xml = self.guest._generate_xml("hd", None) # One last step is required here - The persistent net rules in some Fedora and RHEL versions # Will cause our new incarnation of the image to fail to get network - fix that here # We unfortunately end up having to duplicate this a second time in the cloud plugins # when we are done with our second stage customizations # TODO: Consider moving all of that back here guestfs_handle = launch_inspect_and_mount(builder.target_image.data) remove_net_persist(guestfs_handle) shutdown_and_close(guestfs_handle) try: self.log.debug("Doing second-stage target_image customization and ICICLE generation") #self.percent_complete = 30 self.output_descriptor = self.guest.customize_and_generate_icicle(libvirt_xml) self.log.debug("Customization and ICICLE generation complete") #self.percent_complete = 50 finally: self.activity("Cleaning up install artifacts") self.guest.cleanup_install() def add_cloud_plugin_content(self, content): # This is a method that cloud plugins can call to deposit content/commands to be run # during the OS-specific first stage of the Target Image creation. # The expected input is a dict containing commands and files # No support for repos at the moment as these introduce external deps that we may not be able to count on # Add this to an array which will later be merged into the TDL object used to drive Oz self.cloud_plugin_content.append(content) def merge_cloud_plugin_content(self): for content in self.cloud_plugin_content: if 'files' in content: for fileentry in content['files']: if not 'name' in fileentry: raise ImageFactoryException("File given without a name") if not 'type' in fileentry: raise ImageFactoryException("File given without a type") if not 'file' in fileentry: raise ImageFactoryException("File given without any content") if fileentry['type'] == 'raw': self.tdlobj.files[fileentry['name']] = fileentry['file'] elif fileentry['type'] == 'base64': if len(fileentry['file']) == 0: self.tdlobj.files[fileentry['name']] = "" else: self.tdlobj.files[fileentry['name']] = base64.b64decode(fileentry['file']) else: raise ImageFactoryException("File given with invalid type (%s)" % (file['type'])) if 'commands' in content: for command in content['commands']: if not 'name' in command: raise ImageFactoryException("Command given without a name") if not 'type' in command: raise ImageFactoryException("Command given without a type") if not 'command' in command: raise ImageFactoryException("Command given without any content") if command['type'] == 'raw': self.tdlobj.commands[command['name']] = command['command'] elif command['type'] == 'base64': if len(command['command']) == 0: self.log.warning("Command with zero length given") self.tdlobj.commands[command['name']] = "" else: self.tdlobj.commandss[command['name']] = base64.b64decode(command['command']) else: raise ImageFactoryException("Command given with invalid type (%s)" % (command['type'])) def add_target_content(self): """Merge in target specific package and repo content. TDL object must already exist as self.tdlobj""" doc = None if isfile("/etc/imagefactory/target_content.xml"): doc = libxml2.parseFile("/etc/imagefactory/target_content.xml") else: self.log.debug("Found neither a call-time config nor a config file - doing nothing") return # Purely to make the xpath statements below a tiny bit shorter target = self.target os=self.tdlobj.distro version=self.tdlobj.update arch=self.tdlobj.arch # We go from most to least specific in this order: # arch -> version -> os-> target # Note that at the moment we even allow an include statment that covers absolutely everything. # That is, one that doesn't even specify a target - this is to support a very simple call-time syntax include = doc.xpathEval("/template_includes/include[@target='%s' and @os='%s' and @version='%s' and @arch='%s']" % (target, os, version, arch)) if len(include) == 0: include = doc.xpathEval("/template_includes/include[@target='%s' and @os='%s' and @version='%s' and not(@arch)]" % (target, os, version)) if len(include) == 0: include = doc.xpathEval("/template_includes/include[@target='%s' and @os='%s' and not(@version) and not(@arch)]" % (target, os)) if len(include) == 0: include = doc.xpathEval("/template_includes/include[@target='%s' and not(@os) and not(@version) and not(@arch)]" % (target)) if len(include) == 0: include = doc.xpathEval("/template_includes/include[not(@target) and not(@os) and not(@version) and not(@arch)]") if len(include) == 0: self.log.debug("cannot find a config section that matches our build details - doing nothing") return # OK - We have at least one config block that matches our build - take the first one, merge it and be done # TODO: Merge all of them? Err out if there is more than one? Warn? include = include[0] packages = include.xpathEval("packages") if len(packages) > 0: self.tdlobj.merge_packages(str(packages[0])) repositories = include.xpathEval("repositories") if len(repositories) > 0: self.tdlobj.merge_repositories(str(repositories[0])) def __init__(self): super(FedoraOS, self).__init__() self.cloud_plugin_content = [ ] config_obj = ApplicationConfiguration() self.app_config = config_obj.configuration self.res_mgr = ReservationManager() self.log = logging.getLogger('%s.%s' % (__name__, self.__class__.__name__)) def _init_oz(self): # TODO: This is a convenience variable for refactoring - rename self.new_image_id = self.active_image.identifier # Create a name combining the TDL name and the UUID for use when tagging EC2 AMIs self.longname = self.tdlobj.name + "-" + self.new_image_id # Oz assumes unique names - TDL built for multiple backends guarantees they are not unique # We don't really care about the name so just force uniqueness # 18-Jul-2011 - Moved to constructor and modified to change TDL object name itself # Oz now uses the tdlobject name property directly in several places so we must change it self.tdlobj.name = "factory-build-" + self.new_image_id # populate a config object to pass to OZ; this allows us to specify our # own output dir but inherit other Oz behavior self.oz_config = ConfigParser.SafeConfigParser() if self.oz_config.read("/etc/oz/oz.cfg") != []: self.oz_config.set('paths', 'output_dir', self.app_config["imgdir"]) else: raise ImageFactoryException("No Oz config file found. Can't continue.") # make this a property to enable quick cleanup on abort self.instance = None # Here we are always dealing with a local install self.init_guest() ## INTERFACE METHOD def create_base_image(self, builder, template, parameters): self.log.info('create_base_image() called for FedoraOS plugin - creating a BaseImage') self.tdlobj = oz.TDL.TDL(xmlstring=template.xml, rootpw_required=self.app_config["tdl_require_root_pw"]) # TODO: Standardize reference scheme for the persistent image objects in our builder # Having local short-name copies like this may well be a good idea though they # obscure the fact that these objects are in a container "upstream" of our plugin object self.base_image = builder.base_image # Set to the image object that is actively being created or modified # Used in the logging helper function above self.active_image = self.base_image self._init_oz() self.guest.diskimage = self.base_image.data # The remainder comes from the original build_upload(self, build_id) self.status="BUILDING" try: self.activity("Cleaning up any old Oz guest") self.guest.cleanup_old_guest() self.activity("Generating JEOS install media") self.threadsafe_generate_install_media(self.guest) self.percent_complete=10 # We want to save this later for use by RHEV-M and Condor clouds libvirt_xml="" try: self.activity("Generating JEOS disk image") self.guest.generate_diskimage() # TODO: If we already have a base install reuse it # subject to some rules about updates to underlying repo self.activity("Execute JEOS install") libvirt_xml = self.guest.install(self.app_config["timeout"]) self.base_image.parameters['libvirt_xml'] = libvirt_xml self.image = self.guest.diskimage self.log.debug("Base install complete - Doing customization and ICICLE generation") self.percent_complete = 30 self.output_descriptor = self.guest.customize_and_generate_icicle(libvirt_xml) self.log.debug("Customization and ICICLE generation complete") self.percent_complete = 50 finally: self.activity("Cleaning up install artifacts") self.guest.cleanup_install() self.log.debug("Generated disk image (%s)" % (self.guest.diskimage)) # OK great, we now have a customized KVM image finally: pass # TODO: Create the base_image object representing this # TODO: Create the base_image object at the beginning and then set the diskimage accordingly def init_guest(self): # Use the factory function from Oz directly # This raises an exception if the TDL contains an unsupported distro or version # Cloud plugins that use KVM directly, such as RHEV-M and openstack-kvm can accept # any arbitrary guest that Oz is capable of producing try: self.guest = oz.GuestFactory.guest_factory(self.tdlobj, self.oz_config, None) # Oz just selects a random port here - This could potentially collide if we are unlucky self.guest.listen_port = self.res_mgr.get_next_listen_port() except: raise ImageFactoryException("OS plugin does not support distro (%s) update (%s) in TDL" % (self.tdlobj.distro, self.tdlobj.update) ) def log_exc(self): self.log.debug("Exception caught in ImageFactory") self.log.debug(traceback.format_exc()) self.active_image.status_detal['error'] = traceback.format_exc() def threadsafe_generate_install_media(self, guest): # Oz caching of install media and modified install media is not thread safe # Make it safe here using some locks # We can only have one active generate_install_media() call for each unique tuple: # (OS, update, architecture, installtype) tdl = guest.tdl queue_name = "%s-%s-%s-%s" % (tdl.distro, tdl.update, tdl.arch, tdl.installtype) self.res_mgr.get_named_lock(queue_name) try: guest.generate_install_media(force_download=False) finally: self.res_mgr.release_named_lock(queue_name)
class SecondaryDispatcher(Singleton): def _singleton_init(self): self.log = logging.getLogger('%s.%s' % (__name__, self.__class__.__name__)) self.pending_uploads = dict() self.pending_uploads_lock = BoundedSemaphore() self.pim = PersistentImageManager.default_manager() self.res = ReservationManager() self.secondaries = { } if os.path.isfile(SECONDARIES): try: secs = open(SECONDARIES, "r") self.secondaries = json.load(secs) secs.close() except: self.log.warning("Unable to load JSON for secondaries from %s", SECONDARIES) if not 'targets' in self.secondaries: self.secondaries['targets'] = { } if not 'providers' in self.secondaries: self.secondaries['providers'] = { } def queue_pending_upload(self, target_image_uuid): # Create a UUID - map it to the target_image_uuid and return it # TODO: Expire these somehow upload_uuid = str(uuid.uuid4()) self.pending_uploads_lock.acquire() try: self.pending_uploads[upload_uuid] = target_image_uuid finally: self.pending_uploads_lock.release() return upload_uuid def target_image_for_upload_uuid(self, upload_uuid): # Return the target_image UUID for a given upload UUID if it exists # and remove it from the dict. Return None if the UUID is not in the dict. self.pending_uploads_lock.acquire() target_image_uuid = None try: if upload_uuid in self.pending_uploads: target_image_uuid = self.pending_uploads[upload_uuid] del self.pending_uploads[upload_uuid] finally: self.pending_uploads_lock.release() return target_image_uuid def update_target_image_body(self, target_image, new_body): # Called during the clone process - we background the actual copy # and update the target_image in question to COMPLETED when the copy is finished # This allows the HTTP transaction to complete - in testing some upload clients # timed out waiting for the local copy to complete # (bottle.py internally processes all uploaded files to completion, storing them as unnamed temporary # files) target_update_thread = Thread(target=self._update_target_image_body, args=(target_image, new_body)) target_update_thread.start() def _update_target_image_body(self, target_image, new_body): try: self.log.debug("Copying incoming file to %s" % (target_image.data)) dest = open(target_image.data,"w") shutil.copyfileobj(new_body, dest, 16384) self.log.debug("Finished copying incoming file to %s" % (target_image.data)) target_image.status="COMPLETE" except Exception as e: self.log.debug("Exception encountered when attempting to update target_image body") self.log.exception(e) target_image.status_detail = {'activity': 'Failed to update image.', 'error': e.message} target_image.status="FAILED" finally: self.pim.save_image(target_image) def prep_target_image_clone(self, request_data, target_image_id): # Request data should contain all target_image metadata to be cloned # If target_image with this ID doest not exist, create it and establish an # upload UUID and return both. # If taget_image already exists, return just the existing target_image upload_id = None self.res.get_named_lock(target_image_id) # At this point no other thread, either remote or local, can operate on this ID # The image either already exists or it doesn't. If it doesn't we can safely create # it without worrying about concurrency problems. try: target_image = self.pim.image_with_id(target_image_id) if not target_image: upload_id = self.queue_pending_upload(target_image_id) target_image = TargetImage(image_id=target_image_id) metadata_keys = target_image.metadata() for data_element in request_data.keys(): if not (data_element in metadata_keys): self.log.warning("Metadata field (%s) in incoming target_image clone request is non-standard - skipping" % (data_element)) else: setattr(target_image, data_element, request_data[data_element]) # The posted image will, of course, be complete, so fix status here target_image.status = "PENDING" target_image.percent_complete = 0 target_image.status_detail = { 'activity': 'Image being cloned from primary factory - initial metadata set', 'error':None } self.pim.add_image(target_image) self.pim.save_image(target_image) self.log.debug("Completed save of target_image (%s)" % target_image.identifier) finally: self.res.release_named_lock(target_image_id) return (target_image, upload_id) def get_secondary(self, target, provider): if provider in self.secondaries['providers']: return self.secondaries['providers'][provider] elif target in self.secondaries['targets']: return self.secondaries['targets'][target] else: return None def get_helper(self, secondary): try: helper = SecondaryHelper(**secondary) return helper except Exception as e: self.log.error("Exception encountered when trying to create secondary helper object") self.log.error("Secondary details: %s" % (secondary)) self.log.exception(e)