#!/usr/bin/env python # Copyright (C) 2012 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Manage jobs in Jenkins server import errno import io import logging import os import re import tempfile import fasteners import yaml from jenkins_jobs import errors logger = logging.getLogger(__name__) class JobCache(object): # ensure each instance of the class has a reference to the required # modules so that they are available to be used when the destructor # is being called since python will not guarantee that it won't have # removed global module references during teardown. _logger = logger _os = os _tempfile = tempfile _yaml = yaml def __init__(self, jenkins_url, flush=False): cache_dir = self.get_cache_dir() # One cache per remote Jenkins URL: host_vary = re.sub(r"[^A-Za-z0-9\-\~]", "_", jenkins_url) self.cachefilename = os.path.join( cache_dir, "cache-host-jobs-" + host_vary + ".yml" ) # generate named lockfile if none exists, and lock it self._locked = self._lock() if not self._locked: raise errors.JenkinsJobsException( "Unable to lock cache for '%s'" % jenkins_url ) if flush or not os.path.isfile(self.cachefilename): self.data = {} else: with io.open(self.cachefilename, "r", encoding="utf-8") as yfile: self.data = yaml.safe_load(yfile) logger.debug("Using cache: '{0}'".format(self.cachefilename)) def _lock(self): self._fastener = fasteners.InterProcessLock("%s.lock" % self.cachefilename) return self._fastener.acquire(delay=1, max_delay=2, timeout=60) def _unlock(self): if getattr(self, "_locked", False): if getattr(self, "_fastener", None) is not None: self._fastener.release() self._locked = None @staticmethod def get_cache_dir(): home = os.path.expanduser("~") if home == "~": raise OSError("Could not locate home folder") xdg_cache_home = os.environ.get("XDG_CACHE_HOME") or os.path.join( home, ".cache" ) path = os.path.join(xdg_cache_home, "jenkins_jobs") if not os.path.isdir(path): try: os.makedirs(path) except OSError as ose: # it could happen that two jjb instances are running at the # same time and that the other instance created the directory # after we made the check, in which case there is no error if ose.errno != errno.EEXIST: raise return path def set(self, job, md5): self.data[job] = md5 def clear(self): self.data.clear() def is_cached(self, job): if job in self.data: return True return False def has_changed(self, job, md5): if job in self.data and self.data[job] == md5: return False return True def save(self): # use self references to required modules in case called via __del__ # write to tempfile under same directory and then replace to avoid # issues around corruption such the process be killed tfile = self._tempfile.NamedTemporaryFile( dir=self.get_cache_dir(), delete=False ) tfile.write(self._yaml.dump(self.data).encode("utf-8")) # force contents to be synced on disk before overwriting cachefile tfile.flush() self._os.fsync(tfile.fileno()) tfile.close() try: self._os.rename(tfile.name, self.cachefilename) except OSError: # On Windows, if dst already exists, OSError will be raised even if # it is a file. Remove the file first in that case and try again. self._os.remove(self.cachefilename) self._os.rename(tfile.name, self.cachefilename) self._logger.debug("Cache written out to '%s'" % self.cachefilename) def __del__(self): # check we initialized sufficiently in case called # due to an exception occurring in the __init__ if getattr(self, "data", None) is not None: try: self.save() except Exception as e: self._logger.error( "Failed to write to cache file '%s' on " "exit: %s" % (self.cachefilename, e) ) self._unlock()