]> Frank Brehm's Git Trees - pixelpark/admin-tools.git/commitdiff
Finishing pp_lib/pidfile.py and using it
authorFrank Brehm <frank.brehm@pixelpark.com>
Fri, 4 Aug 2017 14:01:58 +0000 (16:01 +0200)
committerFrank Brehm <frank.brehm@pixelpark.com>
Fri, 4 Aug 2017 14:01:58 +0000 (16:01 +0200)
pp_lib/config_named_app.py
pp_lib/pidfile.py

index 4626787c5268b308c753a5a5a14381c6c9e96678..b339779dcb8069ccb90735db8896f0e4e54545b4 100644 (file)
@@ -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):
index 466ba14bccbe18652174524479137fa76601f448..ddb67ea66fcaca294e4f67fed3a4c01eaa80d1d0 100644 (file)
@@ -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
 
 
 # =============================================================================