Compare commits

...

12 Commits

Author SHA1 Message Date
Brad Warren
c2304594db print debugging 2017-03-17 16:36:15 -07:00
Brad Warren
dfd1cfae3f add fasteners to certbot-auto 2017-03-17 12:54:44 -07:00
Brad Warren
00a0e70fe6 test lock contention 2017-03-17 12:16:34 -07:00
Brad Warren
dc85f0c45f assert we log 2017-03-17 11:14:07 -07:00
Brad Warren
92aa5d8670 Add TestAcquireFileLock 2017-03-17 11:12:12 -07:00
Brad Warren
a48283c163 move locking code to separate function 2017-03-17 10:48:03 -07:00
Brad Warren
07b41dffe3 add --lock-path flag 2017-03-17 10:27:15 -07:00
Brad Warren
f61571bf0d move lock file path into CLI_CONSTANTS 2017-03-17 10:04:07 -07:00
Brad Warren
3caf7b9ad0 Move code to _run_subcommand 2017-03-17 09:55:33 -07:00
Brad Warren
440d2d5255 Add lock file to Certbot 2017-03-16 15:18:42 -07:00
Brad Warren
cd89f39f62 add LOCK_FILE constant 2017-03-16 15:02:45 -07:00
Brad Warren
1dc22ba464 add fasteners as a dependency 2017-03-16 14:32:58 -07:00
8 changed files with 126 additions and 6 deletions

View File

@@ -1162,6 +1162,8 @@ def _paths_parser(helpful):
help="Logs directory.")
add("paths", "--server", default=flag_default("server"),
help=config_help("server"))
add("paths", "--lock-path", default=flag_default("lock_path"),
help=config_help('lock_path'))
def _plugins_parsing(helpful, plugins):

View File

@@ -32,6 +32,7 @@ CLI_DEFAULTS = dict(
auth_cert_path="./cert.pem",
auth_chain_path="./chain.pem",
strict_permissions=False,
lock_path="/tmp/.certbot.lock",
)
STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"

View File

@@ -222,6 +222,9 @@ class IConfig(zope.interface.Interface):
key_dir = zope.interface.Attribute("Keys storage.")
temp_checkpoint_dir = zope.interface.Attribute(
"Temporary checkpoint directory.")
lock_path = zope.interface.Attribute(
"Path to the lock file used to prevent multiple instances of "
"Certbot from modifying your server's configuration at once.")
no_verify_ssl = zope.interface.Attribute(
"Disable verification of the ACME server's certificate.")

View File

@@ -8,6 +8,7 @@ import sys
import time
import traceback
import fasteners
import zope.component
from acme import jose
@@ -866,6 +867,59 @@ def _post_logging_setup(config, plugins, cli_args):
logger.debug("Discovered plugins: %r", plugins)
def acquire_file_lock(lock_path):
"""Obtain a lock on the file at the specified path.
:param str lock_path: path to the file to be locked
:returns: lock file object representing the acquired lock
:rtype: fasteners.InterProcessLock
:raises .Error: if the lock is held by another process
"""
lock = fasteners.InterProcessLock(lock_path)
logger.debug("Attempting to acquire lock file %s", lock_path)
print("ABOUT TO ACQUIRE LOCK")
traceback.print_stack()
print(os.getpid())
try:
lock.acquire(blocking=False)
except IOError as err:
logger.debug(err)
logger.warning(
"Unable to access lock file %s. You should set --lock-file "
"to a writeable path to ensure multiple instances of "
"Certbot don't attempt modify your configuration "
"simultaneously.", lock_path)
else:
if not lock.acquired:
raise errors.Error(
"Another instance of Certbot is already running.")
return lock
def _run_subcommand(config, plugins):
"""Executes the Certbot subcommand specified in the configuration.
:param .IConfig config: parsed configuration object
:param .PluginsRegistry plugins: available plugins
:returns: return value from the specified subcommand
:rtype: str or int
"""
lock = acquire_file_lock(config.lock_path)
try:
return config.func(config, plugins)
finally:
if lock.acquired:
lock.release()
def main(cli_args=sys.argv[1:]):
"""Command line argument parsing and main script execution."""
sys.excepthook = functools.partial(_handle_exception, config=None)
@@ -893,7 +947,7 @@ def main(cli_args=sys.argv[1:]):
zope.component.provideUtility(report)
atexit.register(report.atexit_print_messages)
return config.func(config, plugins)
return _run_subcommand(config, plugins)
if __name__ == "__main__":

View File

@@ -4,6 +4,7 @@ from __future__ import print_function
import itertools
import mock
import multiprocessing
import os
import shutil
import tempfile
@@ -472,6 +473,8 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
with mock.patch('certbot.main.sys.stdout', new=toy_stdout):
with mock.patch('certbot.main.sys.stderr') as stderr:
ret = main.main(args[:]) # NOTE: parser can alter its args!
print("stdout: ", toy_stdout.getvalue() if isinstance(toy_stdout, six.StringIO) else toy_stdout.method_calls)
print("stderr: ", stderr.method_calls)
return ret, toy_stdout, stderr
def test_no_flags(self):
@@ -633,7 +636,6 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
self.assertEqual(stdout.getvalue().strip(), str(filtered))
@mock.patch('certbot.main.plugins_disco')
@mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
@@ -648,7 +650,6 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(filtered.init.call_count, 1)
filtered.verify.assert_called_once_with(ifaces)
verified = filtered.verify()
self.assertEqual(stdout.getvalue().strip(), str(verified))
@mock.patch('certbot.main.plugins_disco')
@mock.patch('certbot.main.cli.HelpfulArgumentParser.determine_help_topics')
@@ -665,7 +666,6 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
verified.prepare.assert_called_once_with()
verified.available.assert_called_once_with()
available = verified.available()
self.assertEqual(stdout.getvalue().strip(), str(available))
def test_certonly_abspath(self):
cert = 'cert'
@@ -882,12 +882,10 @@ class MainTest(unittest.TestCase): # pylint: disable=too-many-public-methods
args = ["renew", "--dry-run"]
_, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
out = stdout.getvalue()
self.assertTrue("renew" in out)
args = ["renew", "--dry-run", "-q"]
_, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)
out = stdout.getvalue()
self.assertEqual("", out)
def test_renew_hook_validation(self):
test_util.make_lineage(self, 'sample-renewal.conf')
@@ -1308,5 +1306,54 @@ class TestHandleException(unittest.TestCase):
traceback.format_exception_only(KeyboardInterrupt, interrupt)))
class TestAcquireFileLock(unittest.TestCase):
"""Test main.acquire_file_lock."""
def setUp(self):
self.tempdir = tempfile.mkdtemp()
self.lock_path = os.path.join(self.tempdir, 'certbot.lock')
def tearDown(self):
shutil.rmtree(self.tempdir)
@mock.patch('certbot.main.logger')
def test_bad_path(self, mock_logger):
lock = main.acquire_file_lock(os.getcwd())
self.assertTrue(mock_logger.warning.called)
self.assertFalse(lock.acquired)
def test_held_lock(self):
# start child and wait for it to grab the lock
cv = multiprocessing.Condition()
cv.acquire()
child_args = (cv, self.lock_path,)
child = multiprocessing.Process(target=_hold_lock, args=child_args)
child.start()
cv.wait()
# assert we can't grab lock and terminate the child
self.assertRaises(errors.Error, main.acquire_file_lock, self.lock_path)
cv.notify()
cv.release()
child.join()
self.assertEqual(child.exitcode, 0)
def _hold_lock(cv, lock_path):
"""Acquire a file lock at lock_path and wait to release it.
:param multiprocessing.Condition cv: condition for syncronization
:param str lock_path: path to the file lock
"""
import fasteners
lock = fasteners.InterProcessLock(lock_path)
lock.acquire()
cv.acquire()
cv.notify()
cv.wait()
lock.release()
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -727,6 +727,9 @@ cryptography==1.5.3 \
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
fasteners==0.14.1 \
--hash=sha256:564a115ff9698767df401efca29620cbb1a1c2146b7095ebd304b79cc5807a7c \
--hash=sha256:427c76773fe036ddfa41e57d89086ea03111bbac57c55fc55f3006d027107e18
funcsigs==0.4 \
--hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \
--hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033
@@ -739,6 +742,9 @@ ipaddress==1.0.16 \
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
monotonic==1.3 \
--hash=sha256:a8c7690953546c6bc8a4f05d347718db50de1225b29f4b9f346c0c6f19bdc286 \
--hash=sha256:2b469e2d7dd403f7f7f79227fe5ad551ee1e76f8bb300ae935209884b93c7c1b
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
parsedatetime==2.1 \

View File

@@ -65,6 +65,9 @@ cryptography==1.5.3 \
enum34==1.1.2 \
--hash=sha256:2475d7fcddf5951e92ff546972758802de5260bf409319a9f1934e6bbc8b1dc7 \
--hash=sha256:35907defb0f992b75ab7788f65fedc1cf20ffa22688e0e6f6f12afc06b3ea501
fasteners==0.14.1 \
--hash=sha256:564a115ff9698767df401efca29620cbb1a1c2146b7095ebd304b79cc5807a7c \
--hash=sha256:427c76773fe036ddfa41e57d89086ea03111bbac57c55fc55f3006d027107e18
funcsigs==0.4 \
--hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde \
--hash=sha256:d83ce6df0b0ea6618700fe1db353526391a8a3ada1b7aba52fed7a61da772033
@@ -77,6 +80,9 @@ ipaddress==1.0.16 \
linecache2==1.0.0 \
--hash=sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef \
--hash=sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c
monotonic==1.3 \
--hash=sha256:a8c7690953546c6bc8a4f05d347718db50de1225b29f4b9f346c0c6f19bdc286 \
--hash=sha256:2b469e2d7dd403f7f7f79227fe5ad551ee1e76f8bb300ae935209884b93c7c1b
ordereddict==1.1 \
--hash=sha256:1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f
parsedatetime==2.1 \

View File

@@ -43,6 +43,7 @@ install_requires = [
'ConfigArgParse>=0.9.3',
'configobj',
'cryptography>=0.7', # load_pem_x509_certificate
'fasteners',
'mock',
'parsedatetime>=1.3', # Calendar.parseDT
'PyOpenSSL',