Module: check_mk
Branch: master
Commit: 712b6a52d80bec4ee2f73913b1fe014ab56f47fa
URL: http://git.mathias-kettner.de/git/?p=check_mk.git;a=commit;h=712b6a52d80bec…
Author: Lars Michelsen <lm(a)mathias-kettner.de>
Date: Fri Jan 4 20:40:46 2019 +0100
7018 Livestatus can now be configured to connect via IPv6
In previous versions it was not possible to connect the GUI to a remote
site via Livestatus using IPv6. This is now possible and can be
configured from the "Distributed Monitoring" configuration.
Technically this was prevented by several smaller things.
The internal Livestatus xinetd configuration now allows ::/0 besides
0.0.0.0 by default. In case you have modified this setting and want to
use IPv6, you may have to add the IPv6 addresses of your choice to this
option.
The site configuration GUI is now able to handle IPv6 addresses
properly.
The internally used livestatus.py Livestatus client implementation
supports IPv6 now.
Livestatus proxy can now connect to Livestatus via IPv6. Added tests for
livestatus proxy connection initiation using the different kind of
sockets.
The cascading livestatus proxy feature, which is used to make the local
unix socket of a site available via the network, can now be used with
IPv6 in addition to the already existing IPv4 support.
Change-Id: I6c9cada5e21cd7d43a9133256a172d36d7db4497
---
.werks/7018 | 26 +++++++++++++++
cmk/gui/watolib.py | 15 ++++++---
livestatus/api/python/livestatus.py | 12 ++++---
.../mk-livestatus/LIVESTATUS_TCP_ONLY_FROM | 6 ++--
tests/unit/livestatus/test_livestatus_unit.py | 38 ++++++++++++++++++++++
5 files changed, 84 insertions(+), 13 deletions(-)
diff --git a/.werks/7018 b/.werks/7018
new file mode 100644
index 0000000..47b5b15
--- /dev/null
+++ b/.werks/7018
@@ -0,0 +1,26 @@
+Title: Livestatus can now be configured to connect via IPv6
+Level: 2
+Component: multisite
+Compatible: compat
+Edition: cre
+Version: 1.6.0i1
+Date: 1546113685
+Class: feature
+
+In previous versions it was not possible to connect the GUI to a remote site
+via Livestatus using IPv6. This is now possible and can be configured from the
+"Distributed Monitoring" configuration.
+
+Technically this was prevented by several smaller things.
+
+The internal Livestatus xinetd configuration now allows ::/0 besides 0.0.0.0 by
+default. In case you have modified this setting and want to use IPv6, you may
+have to add the IPv6 addresses of your choice to this option.
+
+The site configuration GUI is now able to handle IPv6 addresses properly.
+
+The internally used livestatus.py Livestatus client implementation supports
+IPv6 now. Livestatus proxy can now connect to Livestatus via IPv6 and also the
+cascading proxy feature, which is used to make the local unix socket of a site
+available via the network, can now be used with IPv6 in addition to the already
+existing IPv4 support.
diff --git a/cmk/gui/watolib.py b/cmk/gui/watolib.py
index 745738b..4d02163 100644
--- a/cmk/gui/watolib.py
+++ b/cmk/gui/watolib.py
@@ -102,8 +102,9 @@ from cmk.gui.exceptions import MKGeneralException, MKAuthException, MKUserError,
from cmk.gui.valuespec import (
Dictionary,
Integer,
+ HostAddress,
ListOfStrings,
- IPv4Network,
+ IPNetwork,
Checkbox,
Transform,
DropdownChoice,
@@ -4028,7 +4029,8 @@ class SiteManagement(object):
None,
totext="",
)),
- ("tcp", _("Connect via TCP (IPv4)"), cls._tcp_socket_valuespec()),
+ ("tcp", _("Connect via TCP (IPv4)"), cls._tcp_socket_valuespec(ipv6=False)),
+ ("tcp6", _("Connect via TCP (IPv6)"), cls._tcp_socket_valuespec(ipv6=True)),
("unix", _("Connect via UNIX socket"),
Dictionary(
elements=[
@@ -4044,7 +4046,7 @@ class SiteManagement(object):
return conn_choices
@classmethod
- def _tcp_socket_valuespec(cls):
+ def _tcp_socket_valuespec(cls, ipv6):
return Dictionary(
elements=[
("address",
@@ -4052,10 +4054,12 @@ class SiteManagement(object):
title=_("TCP address to connect to"),
orientation="float",
elements=[
- TextAscii(
+ HostAddress(
label=_("Host:"),
allow_empty=False,
size=15,
+ allow_ipv4_address=not ipv6,
+ allow_ipv6_address=ipv6,
),
Integer(
label=_("Port:"),
@@ -10789,9 +10793,10 @@ class LivestatusViaTCP(Dictionary):
help=_("The access to Livestatus via TCP will only be allowed from the "
"configured source IP addresses. You can either configure specific "
"IP addresses or networks in the syntax <tt>10.3.3.0/24</tt>."),
- valuespec=IPv4Network(),
+ valuespec=IPNetwork(),
orientation="horizontal",
allow_empty=False,
+ default_value=["0.0.0.0", "::/0"],
)),
]
kwargs["optional_keys"] = ["only_from"]
diff --git a/livestatus/api/python/livestatus.py b/livestatus/api/python/livestatus.py
index f458bbc..1b0a70a 100644
--- a/livestatus/api/python/livestatus.py
+++ b/livestatus/api/python/livestatus.py
@@ -317,17 +317,19 @@ class SingleSiteConnection(Helpers):
if family_txt == "unix":
return socket.AF_UNIX, url
- elif family_txt == "tcp":
+ elif family_txt in ["tcp", "tcp6"]:
try:
host, port_txt = url.rsplit(":", 1)
port = int(port_txt)
except ValueError:
- raise MKLivestatusConfigError("Invalid livestatus tcp URL '%s'. "
- "Correct example is 'tcp:somehost:6557'" % url)
- return socket.AF_INET, (host, port)
+ raise MKLivestatusConfigError(
+ "Invalid livestatus tcp URL '%s'. "
+ "Correct example is 'tcp:somehost:6557' or 'tcp6:somehost:6557'" % url)
+ address_family = socket.AF_INET if family_txt == "tcp" else socket.AF_INET6
+ return address_family, (host, port)
raise MKLivestatusConfigError("Invalid livestatus URL '%s'. "
- "Must begin with 'tcp:' or 'unix:'" % url)
+ "Must begin with 'tcp:', 'tcp6:' or 'unix:'" % url)
def disconnect(self):
self.socket = None
diff --git a/omd/packages/mk-livestatus/LIVESTATUS_TCP_ONLY_FROM b/omd/packages/mk-livestatus/LIVESTATUS_TCP_ONLY_FROM
index 7efa9ba..ed77503 100755
--- a/omd/packages/mk-livestatus/LIVESTATUS_TCP_ONLY_FROM
+++ b/omd/packages/mk-livestatus/LIVESTATUS_TCP_ONLY_FROM
@@ -5,12 +5,12 @@
# Description:
# If Livestatus is configured to listen on a TCP port, you
# can configure the IP addresses that are allowed to
-# connect to livestatus here. The setting 0.0.0.0 makes the
-# port available to all clients.
+# connect to livestatus here. The setting "0.0.0.0 ::/0" makes the
+# port available to all IPv4 and IPv6 clients,
case "$1" in
default)
- echo "0.0.0.0"
+ echo "0.0.0.0 ::/0"
;;
choices)
echo "(?:(?:[\d]{1,3})\.(?:[\d]{1,3})\.(?:[\d]{1,3})\.(?:[\d]{1,3})(/[0-9]{1,2})?\s?)+"
diff --git a/tests/unit/livestatus/test_livestatus_unit.py b/tests/unit/livestatus/test_livestatus_unit.py
index 49fafec..08347c4 100644
--- a/tests/unit/livestatus/test_livestatus_unit.py
+++ b/tests/unit/livestatus/test_livestatus_unit.py
@@ -1,4 +1,5 @@
import socket
+from contextlib import closing
from pathlib2 import Path
import pytest # type: ignore
@@ -54,11 +55,48 @@ def test_livestatus_local_connection(sock_path):
assert isinstance(live, livestatus.SingleSiteConnection)
+def test_livestatus_ipv4_connection():
+ with closing(socket.socket(socket.AF_INET)) as sock:
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # pylint: disable=no-member
+
+ # Pick a random port
+ sock.bind(("127.0.0.1", 0)) # pylint: disable=no-member
+ port = sock.getsockname()[1] # pylint: disable=no-member
+
+ sock.listen(1) # pylint: disable=no-member
+
+ live = livestatus.SingleSiteConnection("tcp:127.0.0.1:%d" % port)
+ live.connect()
+
+
+def test_livestatus_ipv6_connection():
+ with closing(socket.socket(socket.AF_INET6)) as sock:
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # pylint: disable=no-member
+
+ # Pick a random port
+ try:
+ sock.bind(("::1", 0)) # pylint: disable=no-member
+ except socket.error as e:
+ # Skip this test in case ::1 can not be bound to
+ # (happened in docker container with IPv6 disabled)
+ if e.errno == 99: # Cannot assign requested address
+ pytest.skip("Unable to bind to ::1 (%s)" % e)
+
+ port = sock.getsockname()[1] # pylint: disable=no-member
+
+ sock.listen(1) # pylint: disable=no-member
+
+ live = livestatus.SingleSiteConnection("tcp6:::1:%d" % port)
+ live.connect()
+
+
@pytest.mark.parametrize("socket_url,result", [
("unix:/omd/sites/heute/tmp/run/live", (socket.AF_UNIX, "/omd/sites/heute/tmp/run/live")),
("unix:/omd/sites/heute/tmp/run/li:ve", (socket.AF_UNIX, "/omd/sites/heute/tmp/run/li:ve")),
("tcp:127.0.0.1:1234", (socket.AF_INET, ("127.0.0.1", 1234))),
("tcp:126.0.0.1:abc", None),
+ ("tcp6:::1:1234", (socket.AF_INET6, ("::1", 1234))),
+ ("tcp6:::1:abc", None),
("xyz:bla", None),
])
def test_single_site_connection_socketurl(socket_url, result, monkeypatch):
Module: check_mk
Branch: master
Commit: 2cc032a4cf04f97ab65b3e2df692a8103f249b9c
URL: http://git.mathias-kettner.de/git/?p=check_mk.git;a=commit;h=2cc032a4cf04f9…
Author: Sven Panne <sp(a)mathias-kettner.de>
Date: Thu Jan 10 08:48:25 2019 +0100
Make WATO's exclusive lock a context manager.
There are a few very obscure unbalanced lock/unlock call sites left, we
should *really* fix these: Locking must be done in a balanced way locally,
otherwise hard-to-find bugs are basically guaranteed....
Change-Id: Ia6a90eee5ba329e0b49512d2fd5ab2d1c710357b
---
cmk/gui/wato/__init__.py | 22 ++++-----
cmk/gui/wato/pages/bulk_discovery.py | 12 ++---
cmk/gui/wato/pages/parentscan.py | 9 ++--
cmk/gui/watolib.py | 86 ++++++++++++++++++++----------------
4 files changed, 62 insertions(+), 67 deletions(-)
diff --git a/cmk/gui/wato/__init__.py b/cmk/gui/wato/__init__.py
index 7bd898a..77ef646 100644
--- a/cmk/gui/wato/__init__.py
+++ b/cmk/gui/wato/__init__.py
@@ -868,23 +868,19 @@ def add_scanned_hosts_to_folder(folder, found):
if not watolib.Host.host_exists(host_name):
entries.append((host_name, attrs, None))
- watolib.lock_exclusive()
- folder.create_hosts(entries)
- folder.save()
- watolib.unlock_exclusive()
+ with watolib.exclusive_lock():
+ folder.create_hosts(entries)
+ folder.save()
def save_network_scan_result(folder, result):
# Reload the folder, lock WATO before to protect against concurrency problems.
- watolib.lock_exclusive()
-
- # A user might have changed the folder somehow since starting the scan. Load the
- # folder again to get the current state.
- write_folder = watolib.Folder.folder(folder.path())
- write_folder.set_attribute("network_scan_result", result)
- write_folder.save()
-
- watolib.unlock_exclusive()
+ with watolib.exclusive_lock():
+ # A user might have changed the folder somehow since starting the scan. Load the
+ # folder again to get the current state.
+ write_folder = watolib.Folder.folder(folder.path())
+ write_folder.set_attribute("network_scan_result", result)
+ write_folder.save()
#.
diff --git a/cmk/gui/wato/pages/bulk_discovery.py b/cmk/gui/wato/pages/bulk_discovery.py
index edf971f..8064f01 100644
--- a/cmk/gui/wato/pages/bulk_discovery.py
+++ b/cmk/gui/wato/pages/bulk_discovery.py
@@ -132,23 +132,17 @@ class BulkDiscoveryBackgroundJob(WatoBackgroundJob):
return counts, failed_hosts
def _process_discovery_results(self, task, job_interface, counts, failed_hosts):
- try:
- # The following code updates the host config. The progress from loading the WATO folder
- # until it has been saved needs to be locked.
- watolib.lock_exclusive()
-
+ # The following code updates the host config. The progress from loading the WATO folder
+ # until it has been saved needs to be locked.
+ with watolib.exclusive_lock():
watolib.Folder.invalidate_caches()
folder = watolib.Folder.folder(task.folder_path)
-
for hostname in task.host_names:
self._process_service_counts_for_host(counts[hostname])
msg = self._process_discovery_result_for_host(
folder.host(hostname), failed_hosts.get(hostname, False), counts[hostname])
job_interface.send_progress_update("%s: %s" % (hostname, msg))
- finally:
- watolib.unlock_exclusive()
-
def _process_service_counts_for_host(self, host_counts):
self._num_services_added += host_counts[0]
self._num_services_removed += host_counts[1]
diff --git a/cmk/gui/wato/pages/parentscan.py b/cmk/gui/wato/pages/parentscan.py
index 93940bf..e7d5466 100644
--- a/cmk/gui/wato/pages/parentscan.py
+++ b/cmk/gui/wato/pages/parentscan.py
@@ -127,13 +127,10 @@ class ParentScanBackgroundJob(WatoBackgroundJob):
state, skipped_gateways, error = gateways[0][1:]
if state in ["direct", "root", "gateway"]:
- try:
- # The following code updates the host config. The progress from loading the WATO folder
- # until it has been saved needs to be locked.
- watolib.lock_exclusive()
+ # The following code updates the host config. The progress from loading the WATO folder
+ # until it has been saved needs to be locked.
+ with watolib.exclusive_lock():
self._configure_host_and_gateway(task, settings, state, gateway)
- finally:
- watolib.unlock_exclusive()
else:
self._logger.error(error)
diff --git a/cmk/gui/watolib.py b/cmk/gui/watolib.py
index fa67d43..455f03c 100644
--- a/cmk/gui/watolib.py
+++ b/cmk/gui/watolib.py
@@ -46,6 +46,7 @@ import abc
import ast
import base64
import cStringIO
+from contextlib import contextmanager
import copy
import glob
from hashlib import sha256
@@ -236,16 +237,16 @@ def init_wato_datastructures(with_wato_lock=False):
not _need_to_create_sample_config():
return
- if with_wato_lock:
- lock_exclusive()
-
- if not os.path.exists(ConfigDomainCACertificates.trusted_cas_file):
- ConfigDomainCACertificates().activate()
-
- _create_sample_config()
+ def init():
+ if not os.path.exists(ConfigDomainCACertificates.trusted_cas_file):
+ ConfigDomainCACertificates().activate()
+ _create_sample_config()
if with_wato_lock:
- unlock_exclusive()
+ with exclusive_lock():
+ init()
+ else:
+ init()
# TODO: Create a hook here and move CEE and other specific things away
@@ -5459,35 +5460,33 @@ class ActivateChangesManager(ActivateChanges):
# Lock WATO modifications during snapshot creation
def _create_snapshots(self):
- lock_exclusive()
-
- if not self._changes:
- raise MKUserError(None, _("Currently there are no changes to activate."))
+ with exclusive_lock():
+ if not self._changes:
+ raise MKUserError(None, _("Currently there are no changes to activate."))
- if self._get_last_change_id() != self._activate_until:
- raise MKUserError(
- None,
- _("Another change has been made in the meantime. Please review it "
- "to ensure you also want to activate it now and start the "
- "activation again."))
-
- # Create (legacy) WATO config snapshot
- start = time.time()
- logger.debug("Snapshot creation started")
- # TODO: Remove/Refactor once new changes mechanism has been implemented
- # This single function is responsible for the slow activate changes (python tar packaging..)
- create_snapshot(self._comment)
-
- work_dir = os.path.join(self.activation_tmp_base_dir, self._activation_id)
- if cmk.is_managed_edition():
- import cmk.gui.cme.managed_snapshots as managed_snapshots
- managed_snapshots.CMESnapshotManager(
- work_dir, self._get_site_configurations()).generate_snapshots()
- else:
- self._generate_snapshots(work_dir)
+ if self._get_last_change_id() != self._activate_until:
+ raise MKUserError(
+ None,
+ _("Another change has been made in the meantime. Please review it "
+ "to ensure you also want to activate it now and start the "
+ "activation again."))
+
+ # Create (legacy) WATO config snapshot
+ start = time.time()
+ logger.debug("Snapshot creation started")
+ # TODO: Remove/Refactor once new changes mechanism has been implemented
+ # This single function is responsible for the slow activate changes (python tar packaging..)
+ create_snapshot(self._comment)
+
+ work_dir = os.path.join(self.activation_tmp_base_dir, self._activation_id)
+ if cmk.is_managed_edition():
+ import cmk.gui.cme.managed_snapshots as managed_snapshots
+ managed_snapshots.CMESnapshotManager(
+ work_dir, self._get_site_configurations()).generate_snapshots()
+ else:
+ self._generate_snapshots(work_dir)
- logger.debug("Snapshot creation took %.4f" % (time.time() - start))
- unlock_exclusive()
+ logger.debug("Snapshot creation took %.4f" % (time.time() - start))
def _get_site_configurations(self):
site_configurations = {}
@@ -5664,8 +5663,7 @@ class ActivateChangesManager(ActivateChanges):
# Cleanup stale activations?
def _do_housekeeping(self):
- lock_exclusive()
- try:
+ with exclusive_lock():
for activation_id in self._existing_activation_ids():
# skip the current activation_id
if self._activation_id == activation_id:
@@ -5690,8 +5688,6 @@ class ActivateChangesManager(ActivateChanges):
if delete:
shutil.rmtree("%s/%s" % (ActivateChangesManager.activation_tmp_base_dir,
activation_id))
- finally:
- unlock_exclusive()
def _existing_activation_ids(self):
ids = []
@@ -10536,10 +10532,22 @@ def make_action_link(vars_):
return folder_preserving_link(vars_ + [("_transid", html.transaction_manager.get())])
+@contextmanager
+def exclusive_lock():
+ path = cmk.utils.paths.default_config_dir + "/multisite.mk"
+ store.aquire_lock(path)
+ try:
+ yield
+ finally:
+ store.release_lock(path)
+
+
+# TODO: Use exclusive_lock() and nuke this!
def lock_exclusive():
store.aquire_lock(cmk.utils.paths.default_config_dir + "/multisite.mk")
+# TODO: Use exclusive_lock() and nuke this!
def unlock_exclusive():
store.release_lock(cmk.utils.paths.default_config_dir + "/multisite.mk")