From 3abf2080f1ed0ec733e246af9ff877ac84159c52 Mon Sep 17 00:00:00 2001 From: Fausto Marzi Date: Fri, 20 Nov 2015 13:20:30 +0000 Subject: [PATCH] Import pep3134daemon as local module pep3134daemon is now imported from local path rather than pip. This generated many issues as the package is not in the global-requirements.txt of kilo and liberty. Also pbr in the kilo release does not support env markers which further complitated the installation. This change needs to be backported to stable/kilo also. Also some minor fix for RST format. Change-Id: I34db28ffc928703b01e6430a661214d66d81c519 --- LICENSE | 25 ++ README.rst | 253 ++++++++------- freezer/lib/__init__.py | 0 freezer/lib/pep3143daemon/__init__.py | 20 ++ freezer/lib/pep3143daemon/daemon.py | 449 ++++++++++++++++++++++++++ freezer/lib/pep3143daemon/pidfile.py | 105 ++++++ freezer/scheduler/daemon.py | 2 +- requirements.txt | 4 - 8 files changed, 729 insertions(+), 129 deletions(-) create mode 100644 freezer/lib/__init__.py create mode 100644 freezer/lib/pep3143daemon/__init__.py create mode 100644 freezer/lib/pep3143daemon/daemon.py create mode 100644 freezer/lib/pep3143daemon/pidfile.py diff --git a/LICENSE b/LICENSE index e97bf01c..820e95e0 100644 --- a/LICENSE +++ b/LICENSE @@ -173,3 +173,28 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +As per https://wiki.openstack.org/wiki/LegalIssuesFAQ#Incorporating_BSD.2FMIT_Licensed_Code +the code located in freezer/lib/pep3143daemon contains code released under +the MIT License: + +The MIT License (MIT) + +Copyright (c) 2014 Stephan Schultchen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.rst b/README.rst index d9bad8aa..93c3f905 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,3 @@ - ======= Freezer ======= @@ -47,7 +46,7 @@ Windows Requirements ==================== - Python 2.7 - - GNU Tar (we recommend to follow [this guide](https://github.com/memogarcia/freezer-windows-binaries#windows-binaries-for-freezer) to install them) + - GNU Tar binaries (we recommend to follow [this guide](https://github.com/memogarcia/freezer-windows-binaries#windows-binaries-for-freezer) to install them) - [OpenSSL pre-compiled for windows](https://wiki.openssl.org/index.php/Binaries) or [direct download](https://indy.fulgan.com/SSL/openssl-1.0.1-i386-win32.zip) - [Sync](https://technet.microsoft.com/en-us/sysinternals/bb897438.aspx) - [Microsoft Visual C++ Compiler for Python 2.7](http://aka.ms/vcpython27) @@ -60,11 +59,12 @@ Go to **Control Panel\System and Security\System** and then **Advanced System Se - ;C:\OpenSSL-Win64\bin - ;C:\Python27;C:\Python27\Lib\site-packages\;C:\Python27\Scripts\ -The following components support Windowd OS Platform: +The following components support Windows OS Platform: - freezer-agent - freezer-scheduler + Installation & Env Setup ======================== @@ -236,6 +236,7 @@ and assumes that making backup of that image will be sufficient to restore your data in future. Execute a cinder backup:: + $ freezerc --cinder-vol-id 3ad7a62f-217a-48cd-a861-43ec0a04a78b Execute a mysql backup with cinder:: @@ -252,6 +253,7 @@ Freezer doesn't do any additional checks and assumes that making backup of that instance will be sufficient to restore your data in future. Execute a nova backup:: + $ freezerc --nova-inst-id 3ad7a62f-217a-48cd-a861-43ec0a04a78b Execute a mysql backup with nova:: @@ -593,13 +595,11 @@ It has a double role: it is used both to start the scheduler process, and as a cli-tool which allows the user to interact with the api. The freezer-scheduler process can be started/stopped in daemon mode using the usual -positional arguments -:: +positional arguments:: freezer-scheduler start|stop -It can be also be started as a foreground process using the --no-daemon flag: -:: +It can be also be started as a foreground process using the --no-daemon flag:: freezer-scheduler --no-daemon start @@ -638,8 +638,7 @@ which is composed from the tenant-is and the hostname of the machine it is running on. -The first step to use the scheduler is creating a document with the job: -:: +The first step to use the scheduler is creating a document with the job:: cat test_job.json @@ -665,13 +664,11 @@ The first step to use the scheduler is creating a document with the job: "description": "my scheduled backup 6" } -Then upload that job into the api: -:: +Then upload that job into the api:: freezer-scheduler -c node12 job-create --file test_job.json -The newly created job can be found with: -:: +The newly created job can be found with:: freezer-scheduler -c node12 job-list @@ -681,13 +678,11 @@ The newly created job can be found with: | 07999ea33a494ccf84590191d6fe850c | schedule_backups 6 | 1 | | | | | +----------------------------------+--------------------+-----------+--------+-------+--------+------------+ -Its content can be read with: -:: +Its content can be read with:: freezer-scheduler -c node12 job-get -j 07999ea33a494ccf84590191d6fe850c -The scheduler can be started on the target node with: -:: +The scheduler can be started on the target node with:: freezer-scheduler -c node12 -i 15 -f ~/job_dir start @@ -695,119 +690,129 @@ The scheduler could have already been started. As soon as the freezer-scheduler it fetches the job and schedules it. -Miscellanea ------------ +Misc +==== + +Dependencies notes +------------------ +In stable/kilo and stable/liberty the module peppep3134daemon is imported +from local path +rather than pip. This generated many issues +as the package is not in the global-requirements.txt +of kilo and liberty. Also pbr in the kilo release +does not support env markers which further complitated +the installation Please check the FAQ to: FAQ.rst Available options:: -usage: freezerc [-h] [--config CONFIG] - [--action {backup,restore,info,admin,exec}] - [-F PATH_TO_BACKUP] [-N BACKUP_NAME] [-m MODE] [-C CONTAINER] - [-L] [-l] [-o GET_OBJECT] [-d DST_FILE] [-s] - [--lvm-auto-snap LVM_AUTO_SNAP] [--lvm-srcvol LVM_SRCVOL] - [--lvm-snapname LVM_SNAPNAME] [--lvm-snap-perm {ro,rw}] - [--lvm-snapsize LVM_SNAPSIZE] [--lvm-dirmount LVM_DIRMOUNT] - [--lvm-volgroup LVM_VOLGROUP] [--max-level MAX_LEVEL] - [--always-level ALWAYS_LEVEL] - [--restart-always-level RESTART_ALWAYS_LEVEL] - [-R REMOVE_OLDER_THAN] [--remove-from-date REMOVE_FROM_DATE] - [--no-incremental] [--hostname HOSTNAME] - [--mysql-conf MYSQL_CONF] [--metadata-out METADATA_OUT] - [--log-file LOG_FILE] [--exclude EXCLUDE] - [--dereference-symlink {none,soft,hard,all}] [-U] - [--encrypt-pass-file ENCRYPT_PASS_FILE] [-M MAX_SEGMENT_SIZE] - [--restore-abs-path RESTORE_ABS_PATH] - [--restore-from-host HOSTNAME] - [--restore-from-date RESTORE_FROM_DATE] [--max-priority] [-V] - [-q] [--insecure] [--os-auth-ver {1,2,2.0,3}] [--proxy PROXY] - [--dry-run] [--upload-limit UPLOAD_LIMIT] - [--cinder-vol-id CINDER_VOL_ID] [--nova-inst-id NOVA_INST_ID] - [--cindernative-vol-id CINDERNATIVE_VOL_ID] - [--download-limit DOWNLOAD_LIMIT] - [--sql-server-conf SQL_SERVER_CONF] [--vssadmin VSSADMIN] - [--command COMMAND] [--compression {gzip,bzip2,xz}] - [--storage {local,swift,ssh}] [--ssh-key SSH_KEY] - [--ssh-username SSH_USERNAME] [--ssh-host SSH_HOST] - [--ssh-port SSH_PORT] + usage: freezerc [-h] [--config CONFIG] + [--action {backup,restore,info,admin,exec}] + [-F PATH_TO_BACKUP] [-N BACKUP_NAME] [-m MODE] [-C CONTAINER] + [-L] [-l] [-o GET_OBJECT] [-d DST_FILE] [-s] + [--lvm-auto-snap LVM_AUTO_SNAP] [--lvm-srcvol LVM_SRCVOL] + [--lvm-snapname LVM_SNAPNAME] [--lvm-snap-perm {ro,rw}] + [--lvm-snapsize LVM_SNAPSIZE] [--lvm-dirmount LVM_DIRMOUNT] + [--lvm-volgroup LVM_VOLGROUP] [--max-level MAX_LEVEL] + [--always-level ALWAYS_LEVEL] + [--restart-always-level RESTART_ALWAYS_LEVEL] + [-R REMOVE_OLDER_THAN] [--remove-from-date REMOVE_FROM_DATE] + [--no-incremental] [--hostname HOSTNAME] + [--mysql-conf MYSQL_CONF] [--metadata-out METADATA_OUT] + [--log-file LOG_FILE] [--exclude EXCLUDE] + [--dereference-symlink {none,soft,hard,all}] [-U] + [--encrypt-pass-file ENCRYPT_PASS_FILE] [-M MAX_SEGMENT_SIZE] + [--restore-abs-path RESTORE_ABS_PATH] + [--restore-from-host HOSTNAME] + [--restore-from-date RESTORE_FROM_DATE] [--max-priority] [-V] + [-q] [--insecure] [--os-auth-ver {1,2,2.0,3}] [--proxy PROXY] + [--dry-run] [--upload-limit UPLOAD_LIMIT] + [--cinder-vol-id CINDER_VOL_ID] [--nova-inst-id NOVA_INST_ID] + [--cindernative-vol-id CINDERNATIVE_VOL_ID] + [--download-limit DOWNLOAD_LIMIT] + [--sql-server-conf SQL_SERVER_CONF] [--vssadmin VSSADMIN] + [--command COMMAND] [--compression {gzip,bzip2,xz}] + [--storage {local,swift,ssh}] [--ssh-key SSH_KEY] + [--ssh-username SSH_USERNAME] [--ssh-host SSH_HOST] + [--ssh-port SSH_PORT] - optional arguments: - -h, --help show this help message and exit - --config CONFIG Config file abs path. Option arguments are provided - from config file. When config file is used any option - from command line provided take precedence. - --action {backup,restore,info,admin,exec} - Set the action to be taken. backup and restore are - self explanatory, info is used to retrieve info from - the storage media, exec is used to execute a script, - while admin is used to delete old backups and other - admin actions. Default backup. - -F PATH_TO_BACKUP, --path-to-backup PATH_TO_BACKUP, --file-to-backup PATH_TO_BACKUP - The file or directory you want to back up to Swift - -N BACKUP_NAME, --backup-name BACKUP_NAME - The backup name you want to use to identify your - backup on Swift - -m MODE, --mode MODE Set the technology to back from. Options are, fs - (filesystem), mongo (MongoDB), mysql (MySQL), - sqlserver (SQL Server) Default set to fs - -C CONTAINER, --container CONTAINER - The Swift container (or path to local storage) used to - upload files to - -L, --list-containers - List the Swift containers on remote Object Storage - Server - -l, --list-objects List the Swift objects stored in a container on remote - Object Storage Server. - -o GET_OBJECT, --get-object GET_OBJECT - The Object name you want to download on the local file - system. - -d DST_FILE, --dst-file DST_FILE - The file name used to save the object on your local - disk and upload file in swift - -s, --snapshot Create a snapshot of the fs containing the resource to - backup. When used, the lvm parameters will be guessed - and/or the default values will be used - --lvm-auto-snap LVM_AUTO_SNAP - Automatically guess the volume group and volume name - for given PATH. - --lvm-srcvol LVM_SRCVOL - Set the lvm volume you want to take a snaphost from. - Default no volume - --lvm-snapname LVM_SNAPNAME - Set the lvm snapshot name to use. If the snapshot name - already exists, the old one will be used a no new one - will be created. Default freezer_backup_snap. - --lvm-snap-perm {ro,rw} - Set the lvm snapshot permission to use. If the - permission is set to ro The snapshot will be immutable - - read only -. If the permission is set to rw it will - be mutable - --lvm-snapsize LVM_SNAPSIZE - Set the lvm snapshot size when creating a new - snapshot. Please add G for Gigabytes or M for - Megabytes, i.e. 500M or 8G. Default 1G. - --lvm-dirmount LVM_DIRMOUNT - Set the directory you want to mount the lvm snapshot - to. Default to /var/lib/freezer - --lvm-volgroup LVM_VOLGROUP - Specify the volume group of your logical volume. This - is important to mount your snapshot volume. Default - not set - --max-level MAX_LEVEL - Set the backup level used with tar to implement - incremental backup. If a level 1 is specified but no - level 0 is already available, a level 0 will be done - and subsequently backs to level 1. Default 0 (No - Incremental) - --always-level ALWAYS_LEVEL - Set backup maximum level used with tar to implement - incremental backup. If a level 3 is specified, the - backup will be executed from level 0 to level 3 and to - that point always a backup level 3 will be executed. - It will not restart from level 0. This option has - precedence over --max-backup-level. Default False - (Disabled) + optional arguments: + -h, --help show this help message and exit + --config CONFIG Config file abs path. Option arguments are provided + from config file. When config file is used any option + from command line provided take precedence. + --action {backup,restore,info,admin,exec} + Set the action to be taken. backup and restore are + self explanatory, info is used to retrieve info from + the storage media, exec is used to execute a script, + while admin is used to delete old backups and other + admin actions. Default backup. + -F PATH_TO_BACKUP, --path-to-backup PATH_TO_BACKUP, --file-to-backup PATH_TO_BACKUP + The file or directory you want to back up to Swift + -N BACKUP_NAME, --backup-name BACKUP_NAME + The backup name you want to use to identify your + backup on Swift + -m MODE, --mode MODE Set the technology to back from. Options are, fs + (filesystem), mongo (MongoDB), mysql (MySQL), + sqlserver (SQL Server) Default set to fs + -C CONTAINER, --container CONTAINER + The Swift container (or path to local storage) used to + upload files to + -L, --list-containers + List the Swift containers on remote Object Storage + Server + -l, --list-objects List the Swift objects stored in a container on remote + Object Storage Server. + -o GET_OBJECT, --get-object GET_OBJECT + The Object name you want to download on the local file + system. + -d DST_FILE, --dst-file DST_FILE + The file name used to save the object on your local + disk and upload file in swift + -s, --snapshot Create a snapshot of the fs containing the resource to + backup. When used, the lvm parameters will be guessed + and/or the default values will be used + --lvm-auto-snap LVM_AUTO_SNAP + Automatically guess the volume group and volume name + for given PATH. + --lvm-srcvol LVM_SRCVOL + Set the lvm volume you want to take a snaphost from. + Default no volume + --lvm-snapname LVM_SNAPNAME + Set the lvm snapshot name to use. If the snapshot name + already exists, the old one will be used a no new one + will be created. Default freezer_backup_snap. + --lvm-snap-perm {ro,rw} + Set the lvm snapshot permission to use. If the + permission is set to ro The snapshot will be immutable + - read only -. If the permission is set to rw it will + be mutable + --lvm-snapsize LVM_SNAPSIZE + Set the lvm snapshot size when creating a new + snapshot. Please add G for Gigabytes or M for + Megabytes, i.e. 500M or 8G. Default 1G. + --lvm-dirmount LVM_DIRMOUNT + Set the directory you want to mount the lvm snapshot + to. Default to /var/lib/freezer + --lvm-volgroup LVM_VOLGROUP + Specify the volume group of your logical volume. This + is important to mount your snapshot volume. Default + not set + --max-level MAX_LEVEL + Set the backup level used with tar to implement + incremental backup. If a level 1 is specified but no + level 0 is already available, a level 0 will be done + and subsequently backs to level 1. Default 0 (No + Incremental) + --always-level ALWAYS_LEVEL + Set backup maximum level used with tar to implement + incremental backup. If a level 3 is specified, the + backup will be executed from level 0 to level 3 and to + that point always a backup level 3 will be executed. + It will not restart from level 0. This option has + precedence over --max-backup-level. Default False + (Disabled) --restart-always-level RESTART_ALWAYS_LEVEL Restart the backup from level 0 after n days. Valid only if --always-level option if set. If --always- diff --git a/freezer/lib/__init__.py b/freezer/lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/freezer/lib/pep3143daemon/__init__.py b/freezer/lib/pep3143daemon/__init__.py new file mode 100644 index 00000000..d1f0f2f4 --- /dev/null +++ b/freezer/lib/pep3143daemon/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +""" +pep3143daemon is a implementation of the PEP 3143, describing a well behaving +Unix daemon, as documented in Stevens 'Unix Network Programming' + +Copyright (c) 2014, Stephan Schultchen. + +License: MIT (see LICENSE for details) +""" + +from freezer.lib.pep3143daemon.daemon import DaemonContext, DaemonError +from freezer.lib.pep3143daemon.pidfile import PidFile + + +__all__ = [ + "DaemonContext", + "DaemonError", + "PidFile", +] diff --git a/freezer/lib/pep3143daemon/daemon.py b/freezer/lib/pep3143daemon/daemon.py new file mode 100644 index 00000000..de4c5c4e --- /dev/null +++ b/freezer/lib/pep3143daemon/daemon.py @@ -0,0 +1,449 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +# The MIT License (MIT) +# +# Copyright (c) 2014 Stephan Schultchen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Implementation of PEP 3143 DaemonContext""" +__author__ = 'schlitzer' + + +import errno +import os +import resource +import signal +import socket +import sys + +# PY2 / PY3 gap +PY3 = sys.version_info[0] == 3 +if PY3: + string_types = str, +else: + string_types = basestring, + + +class DaemonError(Exception): + """ Exception raised by DaemonContext""" + pass + + +class DaemonContext(object): + """ Implementation of PEP 3143 DaemonContext class + + This class should be instantiated only once in every program that + has to become a Unix Daemon. Typically you should call its open method + after you have done everything that may require root privileges. + For example opening port <= 1024. + + Each option can be passed as a keyword argument to the constructor, but + can also be changed by assigning a new value to the corresponding attribute + on the instance. + + Altering attributes after open() is called, will have no effect. + In future versions, trying to do so, will may raise a DaemonError. + + :param chroot_directory: + Full path to the directory that should be set as effective root + directory. If None, the root directory is not changed. + :type chroot_directory: str + + :param working_directory: + Full Path to the working directory to which to change to. + If chroot_directory is not None, and working_directory is not + starting with chroot_directory, working directory is prefixed + with chroot_directory. + :type working_directory: str. + + :param umask: + File access creation mask for this daemon after start + :type umask: int. + + :param uid: + Effective user id after daemon start. + :type uid: int. + + :param gid: + Effective group id after daemon start. + :type gid: int. + + :param prevent_core: + Prevent core file generation. + :type prevent_core: bool. + + :param detach_process: + If True, do the double fork magic. If the process was started + by inet or an init like program, you may donĀ“t need to detach. + If not set, we try to figure out if forking is needed. + :type detach_process: bool. + + :param files_preserve: + List of integers, or objects with a fileno method, that + represent files that should not be closed while daemoninzing. + :type files_preserve: list + + :param pidfile: + Instance that implements a pidfile, while daemonizing its + acquire method will be called. + :type pidfile: Instance of Class that implements a pidfile behaviour + + :param stdin: + Redirect stdin to this file, if None, redirect to /dev/null. + :type stdin: file object. + + :param stdout: + Redirect stdout to this file, if None, redirect to /dev/null. + :type stdout: file object. + + :param stderr: + Redirect stderr to this file, if None, redirect to /dev/null. + :type stderr: file object. + + :param signal_map: + Mapping from operating system signal to callback actions. + :type signal_map: instance of dict + """ + def __init__( + self, chroot_directory=None, working_directory='/', + umask=0, uid=None, gid=None, prevent_core=True, + detach_process=None, files_preserve=None, pidfile=None, + stdin=None, stdout=None, stderr=None, signal_map=None): + """ Initialize a new Instance + + """ + self._is_open = False + self._working_directory = None + self.chroot_directory = chroot_directory + self.umask = umask + self.uid = uid if uid else os.getuid() + self.gid = gid if gid else os.getgid() + if detach_process is None: + self.detach_process = detach_required() + else: + self.detach_process = detach_process + self.signal_map = signal_map if signal_map else default_signal_map() + self.files_preserve = files_preserve + self.pidfile = pidfile + self.prevent_core = prevent_core + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.working_directory = working_directory + + def __enter__(self): + """ Context Handler, wrapping self.open() + + :return: self + """ + self.open() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ Context Handler, wrapping self.close() + + :return: None + """ + self.close() + + def _get_signal_handler(self, handler): + """ get the callback function for handler + + If the handler is None, returns signal.SIG_IGN. + If the handler is a string, return the matching attribute of this + instance if possible. + Else return the handler itself. + + :param handler: + :type handler: str, None, function + :return: function + """ + if not handler: + result = signal.SIG_IGN + elif isinstance(handler, string_types): + result = getattr(self, handler) + else: + result = handler + return result + + @property + def _files_preserve(self): + """ create a set of protected files + + create a set of files, based on self.files_preserve and + self.stdin, self,stdout and self.stderr, that should not get + closed while daemonizing. + + :return: set + """ + result = set() + files = [] if not self.files_preserve else self.files_preserve + files.extend([self.stdin, self.stdout, self.stderr]) + for item in files: + if hasattr(item, 'fileno'): + result.add(item.fileno()) + if isinstance(item, int): + result.add(item) + return result + + @property + def _signal_handler_map(self): + """ Create the signal handler map + + create a dictionary with signal:handler mapping based on + self.signal_map + + :return: dict + """ + result = {} + for signum, handler in self.signal_map.items(): + result[signum] = self._get_signal_handler(handler) + return result + + @property + def working_directory(self): + """ The working_directory property + + :return: str + """ + if self.chroot_directory and not \ + self._working_directory.startswith(self.chroot_directory): + return self.chroot_directory + self._working_directory + else: + return self._working_directory + + @working_directory.setter + def working_directory(self, value): + """ Set working directory + + New value is ignored if already daemonized. + + :param value: str + :return: + """ + self._working_directory = value + + @property + def is_open(self): + """ True when this instances open method was called + + :return: bool + """ + return self._is_open + + def close(self): + """ Dummy function""" + pass + + def open(self): + """ Daemonize this process + + Do everything that is needed to become a Unix daemon. + + :return: None + :raise: DaemonError + """ + if self.is_open: + return + try: + os.chdir(self.working_directory) + if self.chroot_directory: + os.chroot(self.chroot_directory) + os.setgid(self.gid) + os.setuid(self.uid) + os.umask(self.umask) + except OSError as err: + raise DaemonError('Setting up Environment failed: {0}' + .format(err)) + + if self.prevent_core: + try: + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + except Exception as err: + raise DaemonError('Could not disable core files: {0}' + .format(err)) + + if self.detach_process: + try: + if os.fork() > 0: + os._exit(0) + except OSError as err: + raise DaemonError('First fork failed: {0}'.format(err)) + os.setsid() + try: + if os.fork() > 0: + os._exit(0) + except OSError as err: + raise DaemonError('Second fork failed: {0}'.format(err)) + + for (signal_number, handler) in self._signal_handler_map.items(): + signal.signal(signal_number, handler) + + close_filenos(self._files_preserve) + + redirect_stream(sys.stdin, self.stdin) + redirect_stream(sys.stdout, self.stdout) + redirect_stream(sys.stderr, self.stderr) + + if self.pidfile: + self.pidfile.acquire() + + self._is_open = True + + def terminate(self, signal_number, stack_frame): + """ Terminate this process + + Simply terminate this process by raising SystemExit. + This method is called if signal.SIGTERM was received. + + Check carefully if this really is what you want! + + Most likely it is not! + + You should implement a function/method that is able to cleanly + shutdown you daemon. Like gracefully terminating child processes, + threads. or closing files. + + You can create a custom handler by overriding this method, ot + setting a custom handler via the signal_map. It is also possible + to set the signal handlers directly via signal.signal(). + + :return: None + :raise: SystemExit + """ + raise SystemExit('Terminating on signal {0}'.format(signal_number)) + + +def close_filenos(preserve): + """ Close unprotected file descriptors + + Close all open file descriptors that are not in preserve. + + If ulimit -nofile is "unlimited", all is defined filenos <= 4096, + else all is <= the output of resource.getrlimit(). + + :param preserve: set with protected files + :type preserve: set + + :return: None + """ + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if maxfd == resource.RLIM_INFINITY: + maxfd = 4096 + for fileno in range(maxfd): + if fileno not in preserve: + try: + os.close(fileno) + except OSError as err: + if not err.errno == errno.EBADF: + raise DaemonError( + 'Failed to close file descriptor {0}: {1}' + .format(fileno, err)) + + +def default_signal_map(): + """ Create the default signal map for this system. + + :return: dict + """ + name_map = { + 'SIGTSTP': None, + 'SIGTTIN': None, + 'SIGTTOU': None, + 'SIGTERM': 'terminate'} + signal_map = {} + for name, target in name_map.items(): + if hasattr(signal, name): + signal_map[getattr(signal, name)] = target + return signal_map + + +def parent_is_init(): + """ Check if parent is Init + + Check if the parent process is init, or something else that + owns PID 1. + + :return: bool + """ + if os.getppid() == 1: + return True + return False + + +def parent_is_inet(): + """ Check if parent is inet + + Check if our parent seems ot be a superserver, aka inetd/xinetd. + + This is done by checking if sys.__stdin__ is a network socket. + + :return: bool + """ + result = False + sock = socket.fromfd( + sys.__stdin__.fileno(), + socket.AF_INET, + socket.SOCK_RAW) + try: + sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) + result = True + except (OSError, socket.error) as err: + if not err.args[0] == errno.ENOTSOCK: + result = True + return result + + +def detach_required(): + """ Check if detaching is required + + This is done by collecting the results of parent_is_inet and + parent_is_init. If one of them is True, detaching, aka the daemoninzing, + aka the double fork magic, is not required, and can be skipped. + + :return: bool + """ + if parent_is_inet() or parent_is_init(): + return False + return True + + +def redirect_stream(system, target): + """ Redirect Unix streams + + If None, redirect Stream to /dev/null, else redirect to target. + + :param system: ether sys.stdin, sys.stdout, or sys.stderr + :type system: file object + + :param target: File like object, or None + :type target: None, File Object + + :return: None + :raise: DaemonError + """ + if target is None: + target_fd = os.open(os.devnull, os.O_RDWR) + else: + target_fd = target.fileno() + try: + os.dup2(target_fd, system.fileno()) + except OSError as err: + raise DaemonError('Could not redirect {0} to {1}: {2}' + .format(system, target, err)) diff --git a/freezer/lib/pep3143daemon/pidfile.py b/freezer/lib/pep3143daemon/pidfile.py new file mode 100644 index 00000000..625117cf --- /dev/null +++ b/freezer/lib/pep3143daemon/pidfile.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +# The MIT License (MIT) +# +# Copyright (c) 2014 Stephan Schultchen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# -*- coding: utf-8 -*- +""" +Simple PidFile Module for a pep3143 daemon implementation. + +""" +__author__ = 'schlitzer' + + +import atexit +import fcntl +import os + + +class PidFile(object): + """ + PidFile implementation for PEP 3143 Daemon. + + This Class can also be used with pythons 'with' + statement. + + :param pidfile: + filename to be used as pidfile, including path + :type pidfile: str + """ + + def __init__(self, pidfile): + """ + Create a new instance + """ + self._pidfile = pidfile + self.pidfile = None + + def __enter__(self): + self.acquire() + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + if exc_type is not None: + self.release() + return False + self.release() + return True + + def acquire(self): + """Acquire the pidfile. + + Create the pidfile, lock it, write the pid into it + and register the release with atexit. + + + :return: None + :raise: SystemExit + """ + try: + pidfile = open(self._pidfile, "a") + except IOError as err: + raise SystemExit(err) + try: + fcntl.flock(pidfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError: + raise SystemExit('Already running according to ' + self._pidfile) + pidfile.seek(0) + pidfile.truncate() + pidfile.write(str(os.getpid()) + '\n') + pidfile.flush() + self.pidfile = pidfile + atexit.register(self.release) + + def release(self): + """Release the pidfile. + + Close and delete the Pidfile. + + + :return: None + """ + try: + self.pidfile.close() + os.remove(self._pidfile) + except OSError as err: + if err.errno != 2: + raise diff --git a/freezer/scheduler/daemon.py b/freezer/scheduler/daemon.py index a6eeed8e..3bdfdbc3 100644 --- a/freezer/scheduler/daemon.py +++ b/freezer/scheduler/daemon.py @@ -24,7 +24,7 @@ from tempfile import gettempdir from time import sleep -from pep3143daemon import DaemonContext, PidFile +from freezer.lib.pep3143daemon import DaemonContext, PidFile from freezer.utils import create_dir diff --git a/requirements.txt b/requirements.txt index 91fb32f4..2c7d6b18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,3 @@ paramiko>=1.13.0 # Not in global-requirements apscheduler - -# Platform specific -pywin32; sys_platform == 'win32' -pep3143daemon; sys_platform != 'win32'