From: Frank Brehm Date: Fri, 4 Aug 2017 14:01:58 +0000 (+0200) Subject: Finishing pp_lib/pidfile.py and using it X-Git-Tag: 0.1.2~141 X-Git-Url: https://git.uhu-banane.de/?a=commitdiff_plain;h=278e807f9027e27f77cfe343c1a031c85d9abdde;p=pixelpark%2Fadmin-tools.git Finishing pp_lib/pidfile.py and using it --- diff --git a/pp_lib/config_named_app.py b/pp_lib/config_named_app.py index 4626787..b339779 100644 --- a/pp_lib/config_named_app.py +++ b/pp_lib/config_named_app.py @@ -32,7 +32,9 @@ from .common import pp, to_bool, to_bytes from .cfg_app import PpCfgAppError, PpConfigApplication -__version__ = '0.4.1' +from .pidfile import PidFileError, InvalidPidFileError, PidFileInUseError, PidFile + +__version__ = '0.4.2' LOG = logging.getLogger(__name__) @@ -99,7 +101,7 @@ class PpConfigNamedApp(PpConfigApplication): self._show_simulate_opt = True - self.pidfile = self.default_pidfile + self.pidfile_name = self.default_pidfile self.pdns_api_host = self.default_pdns_api_host self.pdns_api_port = self.default_pdns_api_port @@ -150,6 +152,7 @@ class PpConfigNamedApp(PpConfigApplication): self.zone_masters = copy.copy(self.default_zone_masters) self.zones = [] + self.pidfile = None description = textwrap.dedent('''\ Generation of configuration of named (the BIND 9 name daemon). @@ -247,7 +250,7 @@ class PpConfigNamedApp(PpConfigApplication): section = self.cfg[section_name] if section_name.lower() == 'app': - self._check_path_config(section, section_name, 'pidfile', 'pidfile', True) + self._check_path_config(section, section_name, 'pidfile', 'pidfile_name', True) if section_name.lower() in ( 'powerdns-api', 'powerdns_api', 'powerdnsapi', @@ -475,6 +478,10 @@ class PpConfigNamedApp(PpConfigApplication): if not cred_ok: self.exit(1) + self.pidfile = PidFile( + filename=self.pidfile_name, appname=self.appname, verbose=self.verbose, + base_dir=self.base_dir, simulate=self.simulate) + self.initialized = True # ------------------------------------------------------------------------- @@ -484,7 +491,17 @@ class PpConfigNamedApp(PpConfigApplication): LOG.error("You must be root to execute this script.") self.exit(1) - self.get_api_zones() + try: + self.pidfile.create() + except PidFileError as e: + LOG.error("Could not occupy pidfile: {}".format(e)) + self.exit(7) + return + + try: + self.get_api_zones() + finally: + self.pidfile = None # ------------------------------------------------------------------------- def get_api_zones(self): diff --git a/pp_lib/pidfile.py b/pp_lib/pidfile.py index 466ba14..ddb67ea 100644 --- a/pp_lib/pidfile.py +++ b/pp_lib/pidfile.py @@ -32,7 +32,7 @@ from .obj import PpBaseObject from .common import to_utf8 -__version__ = '0.1.1' +__version__ = '0.2.2' LOG = logging.getLogger(__name__) @@ -105,6 +105,411 @@ class PidFileInUseError(PidFileError): return msg +# ============================================================================= +class PidFile(PpBaseObject): + """ + Base class for a pidfile object. + """ + + open_args = {} + if six.PY3: + open_args = { + 'encoding': 'utf-8', + 'errors': 'surrogateescape', + } + + # ------------------------------------------------------------------------- + def __init__( + self, filename, auto_remove=True, appname=None, verbose=0, + version=__version__, base_dir=None, + initialized=False, simulate=False, timeout=10): + """ + Initialisation of the pidfile object. + + @raise ValueError: no filename was given + @raise PidFileError: on some errors. + + @param filename: the filename of the pidfile + @type filename: str + @param auto_remove: Remove the self created pidfile on destroying + the current object + @type auto_remove: bool + @param appname: name of the current running application + @type appname: str + @param verbose: verbose level + @type verbose: int + @param version: the version string of the current object or application + @type version: str + @param base_dir: the base directory of all operations + @type base_dir: str + @param initialized: initialisation is complete after __init__() + of this object + @type initialized: bool + @param simulate: simulation mode + @type simulate: bool + @param timeout: timeout in seconds for IO operations on pidfile + @type timeout: int + + @return: None + """ + + self._created = False + """ + @ivar: the pidfile was created by this current object + @type: bool + """ + + super(PidFile, self).__init__( + appname=appname, + verbose=verbose, + version=version, + base_dir=base_dir, + initialized=False, + ) + + if not filename: + raise ValueError(_('No filename given on initializing PidFile object.')) + + self._filename = os.path.abspath(str(filename)) + """ + @ivar: The filename of the pidfile + @type: str + """ + + self._auto_remove = bool(auto_remove) + """ + @ivar: Remove the self created pidfile on destroying the current object + @type: bool + """ + + self._simulate = bool(simulate) + """ + @ivar: Simulation mode + @type: bool + """ + + self._timeout = int(timeout) + """ + @ivar: timeout in seconds for IO operations on pidfile + @type: int + """ + + # ----------------------------------------------------------- + @property + def filename(self): + """The filename of the pidfile.""" + return self._filename + + # ----------------------------------------------------------- + @property + def auto_remove(self): + """Remove the self created pidfile on destroying the current object.""" + return self._auto_remove + + @auto_remove.setter + def auto_remove(self, value): + self._auto_remove = bool(value) + + # ----------------------------------------------------------- + @property + def simulate(self): + """Simulation mode.""" + return self._simulate + + # ----------------------------------------------------------- + @property + def created(self): + """The pidfile was created by this current object.""" + return self._created + + # ----------------------------------------------------------- + @property + def timeout(self): + """The timeout in seconds for IO operations on pidfile.""" + return self._timeout + + # ----------------------------------------------------------- + @property + def parent_dir(self): + """The directory containing the pidfile.""" + return os.path.dirname(self.filename) + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(PidFile, self).as_dict(short=short) + res['filename'] = self.filename + res['auto_remove'] = self.auto_remove + res['simulate'] = self.simulate + res['created'] = self.created + res['timeout'] = self.timeout + res['parent_dir'] = self.parent_dir + + return res + + # ------------------------------------------------------------------------- + def __repr__(self): + """Typecasting into a string for reproduction.""" + + out = "<%s(" % (self.__class__.__name__) + + fields = [] + fields.append("filename=%r" % (self.filename)) + fields.append("auto_remove=%r" % (self.auto_remove)) + fields.append("appname=%r" % (self.appname)) + fields.append("verbose=%r" % (self.verbose)) + fields.append("base_dir=%r" % (self.base_dir)) + fields.append("initialized=%r" % (self.initialized)) + fields.append("simulate=%r" % (self.simulate)) + fields.append("timeout=%r" % (self.timeout)) + + out += ", ".join(fields) + ")>" + return out + + # ------------------------------------------------------------------------- + def __del__(self): + """Destructor. Removes the pidfile, if it was created by ourselfes.""" + + if not self.created: + return + + if not os.path.exists(self.filename): + if self.verbose > 3: + LOG.debug("Pidfile {!r} doesn't exists, not removing.".format(self.filename)) + return + + if not self.auto_remove: + if self.verbose > 3: + LOG.debug("Auto removing disabled, don't deleting {!r}.".format(self.filename)) + return + + if self.verbose > 1: + LOG.debug("Removing pidfile {!r} ...".format(self.filename)) + if self.simulate: + if self.verbose > 1: + LOG.debug("Just kidding ..") + return + try: + os.remove(self.filename) + except OSError as e: + LOG.err("Could not delete pidfile {!r}: {}".format(self.filename, e)) + except Exception as e: + self.handle_error(str(e), e.__class__.__name__, True) + + # ------------------------------------------------------------------------- + def create(self, pid=None): + """ + The main method of this class. It tries to write the PID of the process + into the pidfile. + + @param pid: the pid to write into the pidfile. If not given, the PID of + the current process will taken. + @type pid: int + + """ + + if pid: + pid = int(pid) + if pid <= 0: + msg = "Invalid PID {} for creating pidfile {!r} given.".format(pid, self.filename) + raise PidFileError(msg) + else: + pid = os.getpid() + + if self.check(): + + LOG.info("Deleting pidfile {!r} ...".format(self.filename)) + if self.simulate: + LOG.debug("Just kidding ..") + else: + try: + os.remove(self.filename) + except OSError as e: + raise InvalidPidFileError(self.filename, str(e)) + + if self.verbose > 1: + LOG.debug("Trying opening {!r} exclusive ...".format(self.filename)) + + if self.simulate: + LOG.debug("Simulation mode - don't real writing in {!r}.".format(self.filename)) + self._created = True + return + + fd = None + try: + fd = os.open( + self.filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644) + except OSError as e: + error_tuple = sys.exc_info() + msg = "Error on creating pidfile {!r}: {}".format(self.filename, e) + reraise(PidFileError, msg, error_tuple[2]) + + if self.verbose > 2: + LOG.debug("Writing {} into {!r} ...".format(pid, self.filename)) + + out = to_utf8("%d\n" % (pid)) + try: + os.write(fd, out) + finally: + os.close(fd) + + self._created = True + + # ------------------------------------------------------------------------- + def recreate(self, pid=None): + """ + Rewrites an even created pidfile with the current PID. + + @param pid: the pid to write into the pidfile. If not given, the PID of + the current process will taken. + @type pid: int + + """ + + if not self.created: + msg = "Calling recreate() on a not self created pidfile." + raise PidFileError(msg) + + if pid: + pid = int(pid) + if pid <= 0: + msg = "Invalid PID {} for creating pidfile {!r} given.".format(pid, self.filename) + raise PidFileError(msg) + else: + pid = os.getpid() + + if self.verbose > 1: + LOG.debug("Trying opening {!r} for recreate ...".format(self.filename)) + + if self.simulate: + LOG.debug("Simulation mode - don't real writing in {!r}.".format(self.filename)) + return + + fh = None + try: + fh = open(self.filename, 'w', self.open_args) + except OSError as e: + error_tuple = sys.exc_info() + msg = "Error on recreating pidfile {!r}: {}".format(self.filename, e) + reraise(PidFileError, msg, error_tuple[2]) + + if self.verbose > 2: + LOG.debug("Writing {} into {!r} ...".format(pid, self.filename)) + + try: + fh.write("%d\n" % (pid)) + finally: + fh.close() + + # ------------------------------------------------------------------------- + def check(self): + """ + This methods checks the usability of the pidfile. + If the method doesn't raise an exception, the pidfile is usable. + + It returns, whether the pidfile exist and can be deleted or not. + + @raise InvalidPidFileError: if the pidfile is unusable + @raise PidFileInUseError: if the pidfile is in use by another application + @raise PbReadTimeoutError: on timeout reading an existing pidfile + @raise OSError: on some other reasons, why the existing pidfile + couldn't be read + + @return: the pidfile exists, but can be deleted - or it doesn't + exists. + @rtype: bool + + """ + + if not os.path.exists(self.filename): + if not os.path.exists(self.parent_dir): + reason = "Pidfile parent directory {!r} doesn't exists.".format(self.parent_dir) + raise InvalidPidFileError(self.filename, reason) + if not os.path.isdir(self.parent_dir): + reason = "Pidfile parent directory {!r} is not a directory.".format(self.parent_dir) + raise InvalidPidFileError(self.filename, reason) + if not os.access(self.parent_dir, os.X_OK): + reason = "No write access to pidfile parent directory {!r}.".format(self.parent_dir) + raise InvalidPidFileError(self.filename, reason) + + return False + + if not os.path.isfile(self.filename): + reason = "It is not a regular file." + raise InvalidPidFileError(self.filename, self.parent_dir) + + # --------- + def pidfile_read_alarm_caller(signum, sigframe): + """ + This nested function will be called in event of a timeout. + + @param signum: the signal number (POSIX) which happend + @type signum: int + @param sigframe: the frame of the signal + @type sigframe: object + """ + + return PbReadTimeoutError(self.timeout, self.filename) + + if self.verbose > 1: + LOG.debug("Reading content of pidfile {!r} ...".format(self.filename)) + + signal.signal(signal.SIGALRM, pidfile_read_alarm_caller) + signal.alarm(self.timeout) + + content = '' + fh = None + + try: + fh = open(self.filename, 'r') + for line in fh.readlines(): + content += line + finally: + if fh: + fh.close() + signal.alarm(0) + + # Performing content of pidfile + + pid = None + line = content.strip() + match = re.search(r'^\s*(\d+)\s*$', line) + if match: + pid = int(match.group(1)) + else: + msg = "No useful information found in pidfile {!r}: {!r}".format(self.filename, line) + return True + + if self.verbose > 1: + LOG.debug("Trying check for process with PID {} ...".format(pid)) + + try: + os.kill(pid, 0) + except OSError as err: + if err.errno == errno.ESRCH: + LOG.info("Process with PID {} anonymous died.".format(pid)) + return True + elif err.errno == errno.EPERM: + error_tuple = sys.exc_info() + msg = "No permission to signal the process {} ...".format(pid) + reraise(PidFileError, msg, error_tuple[2]) + else: + error_tuple = sys.exc_info() + msg = "Got a {}: {}.".format(err.__class__.__name__, err) + reraise(PidFileError, msg, error_tuple[2]) + else: + raise PidFileInUseError(self.filename, pid) + + return False # =============================================================================