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
This commit is contained in:
parent
c3739a2b3d
commit
3abf2080f1
25
LICENSE
25
LICENSE
|
@ -173,3 +173,28 @@
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
of your accepting any such warranty or additional liability.
|
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.
|
||||||
|
|
253
README.rst
253
README.rst
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
=======
|
=======
|
||||||
Freezer
|
Freezer
|
||||||
=======
|
=======
|
||||||
|
@ -47,7 +46,7 @@ Windows Requirements
|
||||||
====================
|
====================
|
||||||
|
|
||||||
- Python 2.7
|
- 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)
|
- [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)
|
- [Sync](https://technet.microsoft.com/en-us/sysinternals/bb897438.aspx)
|
||||||
- [Microsoft Visual C++ Compiler for Python 2.7](http://aka.ms/vcpython27)
|
- [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:\OpenSSL-Win64\bin
|
||||||
- ;C:\Python27;C:\Python27\Lib\site-packages\;C:\Python27\Scripts\
|
- ;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-agent
|
||||||
- freezer-scheduler
|
- freezer-scheduler
|
||||||
|
|
||||||
|
|
||||||
Installation & Env Setup
|
Installation & Env Setup
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
@ -236,6 +236,7 @@ and assumes that making backup of that image will be sufficient to restore your
|
||||||
data in future.
|
data in future.
|
||||||
|
|
||||||
Execute a cinder backup::
|
Execute a cinder backup::
|
||||||
|
|
||||||
$ freezerc --cinder-vol-id 3ad7a62f-217a-48cd-a861-43ec0a04a78b
|
$ freezerc --cinder-vol-id 3ad7a62f-217a-48cd-a861-43ec0a04a78b
|
||||||
|
|
||||||
Execute a mysql backup with cinder::
|
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.
|
of that instance will be sufficient to restore your data in future.
|
||||||
|
|
||||||
Execute a nova backup::
|
Execute a nova backup::
|
||||||
|
|
||||||
$ freezerc --nova-inst-id 3ad7a62f-217a-48cd-a861-43ec0a04a78b
|
$ freezerc --nova-inst-id 3ad7a62f-217a-48cd-a861-43ec0a04a78b
|
||||||
|
|
||||||
Execute a mysql backup with nova::
|
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.
|
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
|
The freezer-scheduler process can be started/stopped in daemon mode using the usual
|
||||||
positional arguments
|
positional arguments::
|
||||||
::
|
|
||||||
|
|
||||||
freezer-scheduler start|stop
|
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
|
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.
|
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
|
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"
|
"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
|
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
|
freezer-scheduler -c node12 job-list
|
||||||
|
|
||||||
|
@ -681,13 +678,11 @@ The newly created job can be found with:
|
||||||
| 07999ea33a494ccf84590191d6fe850c | schedule_backups 6 | 1 | | | | |
|
| 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
|
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
|
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.
|
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
|
Please check the FAQ to: FAQ.rst
|
||||||
|
|
||||||
Available options::
|
Available options::
|
||||||
|
|
||||||
usage: freezerc [-h] [--config CONFIG]
|
usage: freezerc [-h] [--config CONFIG]
|
||||||
[--action {backup,restore,info,admin,exec}]
|
[--action {backup,restore,info,admin,exec}]
|
||||||
[-F PATH_TO_BACKUP] [-N BACKUP_NAME] [-m MODE] [-C CONTAINER]
|
[-F PATH_TO_BACKUP] [-N BACKUP_NAME] [-m MODE] [-C CONTAINER]
|
||||||
[-L] [-l] [-o GET_OBJECT] [-d DST_FILE] [-s]
|
[-L] [-l] [-o GET_OBJECT] [-d DST_FILE] [-s]
|
||||||
[--lvm-auto-snap LVM_AUTO_SNAP] [--lvm-srcvol LVM_SRCVOL]
|
[--lvm-auto-snap LVM_AUTO_SNAP] [--lvm-srcvol LVM_SRCVOL]
|
||||||
[--lvm-snapname LVM_SNAPNAME] [--lvm-snap-perm {ro,rw}]
|
[--lvm-snapname LVM_SNAPNAME] [--lvm-snap-perm {ro,rw}]
|
||||||
[--lvm-snapsize LVM_SNAPSIZE] [--lvm-dirmount LVM_DIRMOUNT]
|
[--lvm-snapsize LVM_SNAPSIZE] [--lvm-dirmount LVM_DIRMOUNT]
|
||||||
[--lvm-volgroup LVM_VOLGROUP] [--max-level MAX_LEVEL]
|
[--lvm-volgroup LVM_VOLGROUP] [--max-level MAX_LEVEL]
|
||||||
[--always-level ALWAYS_LEVEL]
|
[--always-level ALWAYS_LEVEL]
|
||||||
[--restart-always-level RESTART_ALWAYS_LEVEL]
|
[--restart-always-level RESTART_ALWAYS_LEVEL]
|
||||||
[-R REMOVE_OLDER_THAN] [--remove-from-date REMOVE_FROM_DATE]
|
[-R REMOVE_OLDER_THAN] [--remove-from-date REMOVE_FROM_DATE]
|
||||||
[--no-incremental] [--hostname HOSTNAME]
|
[--no-incremental] [--hostname HOSTNAME]
|
||||||
[--mysql-conf MYSQL_CONF] [--metadata-out METADATA_OUT]
|
[--mysql-conf MYSQL_CONF] [--metadata-out METADATA_OUT]
|
||||||
[--log-file LOG_FILE] [--exclude EXCLUDE]
|
[--log-file LOG_FILE] [--exclude EXCLUDE]
|
||||||
[--dereference-symlink {none,soft,hard,all}] [-U]
|
[--dereference-symlink {none,soft,hard,all}] [-U]
|
||||||
[--encrypt-pass-file ENCRYPT_PASS_FILE] [-M MAX_SEGMENT_SIZE]
|
[--encrypt-pass-file ENCRYPT_PASS_FILE] [-M MAX_SEGMENT_SIZE]
|
||||||
[--restore-abs-path RESTORE_ABS_PATH]
|
[--restore-abs-path RESTORE_ABS_PATH]
|
||||||
[--restore-from-host HOSTNAME]
|
[--restore-from-host HOSTNAME]
|
||||||
[--restore-from-date RESTORE_FROM_DATE] [--max-priority] [-V]
|
[--restore-from-date RESTORE_FROM_DATE] [--max-priority] [-V]
|
||||||
[-q] [--insecure] [--os-auth-ver {1,2,2.0,3}] [--proxy PROXY]
|
[-q] [--insecure] [--os-auth-ver {1,2,2.0,3}] [--proxy PROXY]
|
||||||
[--dry-run] [--upload-limit UPLOAD_LIMIT]
|
[--dry-run] [--upload-limit UPLOAD_LIMIT]
|
||||||
[--cinder-vol-id CINDER_VOL_ID] [--nova-inst-id NOVA_INST_ID]
|
[--cinder-vol-id CINDER_VOL_ID] [--nova-inst-id NOVA_INST_ID]
|
||||||
[--cindernative-vol-id CINDERNATIVE_VOL_ID]
|
[--cindernative-vol-id CINDERNATIVE_VOL_ID]
|
||||||
[--download-limit DOWNLOAD_LIMIT]
|
[--download-limit DOWNLOAD_LIMIT]
|
||||||
[--sql-server-conf SQL_SERVER_CONF] [--vssadmin VSSADMIN]
|
[--sql-server-conf SQL_SERVER_CONF] [--vssadmin VSSADMIN]
|
||||||
[--command COMMAND] [--compression {gzip,bzip2,xz}]
|
[--command COMMAND] [--compression {gzip,bzip2,xz}]
|
||||||
[--storage {local,swift,ssh}] [--ssh-key SSH_KEY]
|
[--storage {local,swift,ssh}] [--ssh-key SSH_KEY]
|
||||||
[--ssh-username SSH_USERNAME] [--ssh-host SSH_HOST]
|
[--ssh-username SSH_USERNAME] [--ssh-host SSH_HOST]
|
||||||
[--ssh-port SSH_PORT]
|
[--ssh-port SSH_PORT]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--config CONFIG Config file abs path. Option arguments are provided
|
--config CONFIG Config file abs path. Option arguments are provided
|
||||||
from config file. When config file is used any option
|
from config file. When config file is used any option
|
||||||
from command line provided take precedence.
|
from command line provided take precedence.
|
||||||
--action {backup,restore,info,admin,exec}
|
--action {backup,restore,info,admin,exec}
|
||||||
Set the action to be taken. backup and restore are
|
Set the action to be taken. backup and restore are
|
||||||
self explanatory, info is used to retrieve info from
|
self explanatory, info is used to retrieve info from
|
||||||
the storage media, exec is used to execute a script,
|
the storage media, exec is used to execute a script,
|
||||||
while admin is used to delete old backups and other
|
while admin is used to delete old backups and other
|
||||||
admin actions. Default backup.
|
admin actions. Default backup.
|
||||||
-F PATH_TO_BACKUP, --path-to-backup PATH_TO_BACKUP, --file-to-backup PATH_TO_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
|
The file or directory you want to back up to Swift
|
||||||
-N BACKUP_NAME, --backup-name BACKUP_NAME
|
-N BACKUP_NAME, --backup-name BACKUP_NAME
|
||||||
The backup name you want to use to identify your
|
The backup name you want to use to identify your
|
||||||
backup on Swift
|
backup on Swift
|
||||||
-m MODE, --mode MODE Set the technology to back from. Options are, fs
|
-m MODE, --mode MODE Set the technology to back from. Options are, fs
|
||||||
(filesystem), mongo (MongoDB), mysql (MySQL),
|
(filesystem), mongo (MongoDB), mysql (MySQL),
|
||||||
sqlserver (SQL Server) Default set to fs
|
sqlserver (SQL Server) Default set to fs
|
||||||
-C CONTAINER, --container CONTAINER
|
-C CONTAINER, --container CONTAINER
|
||||||
The Swift container (or path to local storage) used to
|
The Swift container (or path to local storage) used to
|
||||||
upload files to
|
upload files to
|
||||||
-L, --list-containers
|
-L, --list-containers
|
||||||
List the Swift containers on remote Object Storage
|
List the Swift containers on remote Object Storage
|
||||||
Server
|
Server
|
||||||
-l, --list-objects List the Swift objects stored in a container on remote
|
-l, --list-objects List the Swift objects stored in a container on remote
|
||||||
Object Storage Server.
|
Object Storage Server.
|
||||||
-o GET_OBJECT, --get-object GET_OBJECT
|
-o GET_OBJECT, --get-object GET_OBJECT
|
||||||
The Object name you want to download on the local file
|
The Object name you want to download on the local file
|
||||||
system.
|
system.
|
||||||
-d DST_FILE, --dst-file DST_FILE
|
-d DST_FILE, --dst-file DST_FILE
|
||||||
The file name used to save the object on your local
|
The file name used to save the object on your local
|
||||||
disk and upload file in swift
|
disk and upload file in swift
|
||||||
-s, --snapshot Create a snapshot of the fs containing the resource to
|
-s, --snapshot Create a snapshot of the fs containing the resource to
|
||||||
backup. When used, the lvm parameters will be guessed
|
backup. When used, the lvm parameters will be guessed
|
||||||
and/or the default values will be used
|
and/or the default values will be used
|
||||||
--lvm-auto-snap LVM_AUTO_SNAP
|
--lvm-auto-snap LVM_AUTO_SNAP
|
||||||
Automatically guess the volume group and volume name
|
Automatically guess the volume group and volume name
|
||||||
for given PATH.
|
for given PATH.
|
||||||
--lvm-srcvol LVM_SRCVOL
|
--lvm-srcvol LVM_SRCVOL
|
||||||
Set the lvm volume you want to take a snaphost from.
|
Set the lvm volume you want to take a snaphost from.
|
||||||
Default no volume
|
Default no volume
|
||||||
--lvm-snapname LVM_SNAPNAME
|
--lvm-snapname LVM_SNAPNAME
|
||||||
Set the lvm snapshot name to use. If the snapshot name
|
Set the lvm snapshot name to use. If the snapshot name
|
||||||
already exists, the old one will be used a no new one
|
already exists, the old one will be used a no new one
|
||||||
will be created. Default freezer_backup_snap.
|
will be created. Default freezer_backup_snap.
|
||||||
--lvm-snap-perm {ro,rw}
|
--lvm-snap-perm {ro,rw}
|
||||||
Set the lvm snapshot permission to use. If the
|
Set the lvm snapshot permission to use. If the
|
||||||
permission is set to ro The snapshot will be immutable
|
permission is set to ro The snapshot will be immutable
|
||||||
- read only -. If the permission is set to rw it will
|
- read only -. If the permission is set to rw it will
|
||||||
be mutable
|
be mutable
|
||||||
--lvm-snapsize LVM_SNAPSIZE
|
--lvm-snapsize LVM_SNAPSIZE
|
||||||
Set the lvm snapshot size when creating a new
|
Set the lvm snapshot size when creating a new
|
||||||
snapshot. Please add G for Gigabytes or M for
|
snapshot. Please add G for Gigabytes or M for
|
||||||
Megabytes, i.e. 500M or 8G. Default 1G.
|
Megabytes, i.e. 500M or 8G. Default 1G.
|
||||||
--lvm-dirmount LVM_DIRMOUNT
|
--lvm-dirmount LVM_DIRMOUNT
|
||||||
Set the directory you want to mount the lvm snapshot
|
Set the directory you want to mount the lvm snapshot
|
||||||
to. Default to /var/lib/freezer
|
to. Default to /var/lib/freezer
|
||||||
--lvm-volgroup LVM_VOLGROUP
|
--lvm-volgroup LVM_VOLGROUP
|
||||||
Specify the volume group of your logical volume. This
|
Specify the volume group of your logical volume. This
|
||||||
is important to mount your snapshot volume. Default
|
is important to mount your snapshot volume. Default
|
||||||
not set
|
not set
|
||||||
--max-level MAX_LEVEL
|
--max-level MAX_LEVEL
|
||||||
Set the backup level used with tar to implement
|
Set the backup level used with tar to implement
|
||||||
incremental backup. If a level 1 is specified but no
|
incremental backup. If a level 1 is specified but no
|
||||||
level 0 is already available, a level 0 will be done
|
level 0 is already available, a level 0 will be done
|
||||||
and subsequently backs to level 1. Default 0 (No
|
and subsequently backs to level 1. Default 0 (No
|
||||||
Incremental)
|
Incremental)
|
||||||
--always-level ALWAYS_LEVEL
|
--always-level ALWAYS_LEVEL
|
||||||
Set backup maximum level used with tar to implement
|
Set backup maximum level used with tar to implement
|
||||||
incremental backup. If a level 3 is specified, the
|
incremental backup. If a level 3 is specified, the
|
||||||
backup will be executed from level 0 to level 3 and to
|
backup will be executed from level 0 to level 3 and to
|
||||||
that point always a backup level 3 will be executed.
|
that point always a backup level 3 will be executed.
|
||||||
It will not restart from level 0. This option has
|
It will not restart from level 0. This option has
|
||||||
precedence over --max-backup-level. Default False
|
precedence over --max-backup-level. Default False
|
||||||
(Disabled)
|
(Disabled)
|
||||||
--restart-always-level RESTART_ALWAYS_LEVEL
|
--restart-always-level RESTART_ALWAYS_LEVEL
|
||||||
Restart the backup from level 0 after n days. Valid
|
Restart the backup from level 0 after n days. Valid
|
||||||
only if --always-level option if set. If --always-
|
only if --always-level option if set. If --always-
|
||||||
|
|
|
@ -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",
|
||||||
|
]
|
|
@ -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))
|
|
@ -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
|
|
@ -24,7 +24,7 @@ from tempfile import gettempdir
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
from pep3143daemon import DaemonContext, PidFile
|
from freezer.lib.pep3143daemon import DaemonContext, PidFile
|
||||||
from freezer.utils import create_dir
|
from freezer.utils import create_dir
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,3 @@ paramiko>=1.13.0
|
||||||
|
|
||||||
# Not in global-requirements
|
# Not in global-requirements
|
||||||
apscheduler
|
apscheduler
|
||||||
|
|
||||||
# Platform specific
|
|
||||||
pywin32; sys_platform == 'win32'
|
|
||||||
pep3143daemon; sys_platform != 'win32'
|
|
||||||
|
|
Loading…
Reference in New Issue