Blob Blame History Raw
From db8643c4489274faee0bba008846a63c2ab63f46 Mon Sep 17 00:00:00 2001
From: Tomas Jelinek <tojeline@redhat.com>
Date: Wed, 15 Jun 2016 14:52:39 +0200
Subject: [PATCH] bz1158805-01-add support for qdevice-qnetd provided by
 corosync

---
 pcs/cli/common/lib_wrapper.py                     |   10 +
 pcs/cluster.py                                    |  119 +-
 pcs/common/report_codes.py                        |   31 +-
 pcs/lib/commands/qdevice.py                       |   88 +-
 pcs/lib/commands/quorum.py                        |  217 +-
 pcs/lib/corosync/config_facade.py                 |   98 +-
 pcs/lib/corosync/live.py                          |   15 +
 pcs/lib/corosync/qdevice_client.py                |   93 +
 pcs/lib/corosync/qdevice_net.py                   |  314 ++-
 pcs/lib/env.py                                    |   11 +-
 pcs/lib/errors.py                                 |    6 +-
 pcs/lib/external.py                               |   44 +-
 pcs/lib/nodes_task.py                             |   69 +-
 pcs/lib/reports.py                                |  225 +-
 pcs/pcs.8                                         |   27 +-
 pcs/qdevice.py                                    |   71 +
 pcs/quorum.py                                     |   34 +-
 pcs/settings_default.py                           |    6 +-
 pcs/test/resources/qdevice-certs/qnetd-cacert.crt |    1 +
 pcs/test/test_lib_commands_qdevice.py             |  255 ++
 pcs/test/test_lib_commands_quorum.py              | 1109 ++++++++-
 pcs/test/test_lib_corosync_config_facade.py       |  367 ++-
 pcs/test/test_lib_corosync_live.py                |   62 +-
 pcs/test/test_lib_corosync_qdevice_client.py      |   60 +
 pcs/test/test_lib_corosync_qdevice_net.py         |  965 +++++++-
 pcs/test/test_lib_env.py                          |  142 +-
 pcs/test/test_lib_external.py                     |  126 +-
 pcs/test/test_lib_nodes_task.py                   |  168 +-
 pcs/test/test_quorum.py                           |    9 +-
 pcs/test/test_utils.py                            | 2628 +++++++++++----------
 pcs/usage.py                                      |   53 +-
 pcs/utils.py                                      |  147 +-
 pcsd/pcs.rb                                       |   17 +
 pcsd/remote.rb                                    |  163 +-
 pcsd/settings.rb                                  |    6 +
 pcsd/settings.rb.debian                           |   10 +-
 36 files changed, 6170 insertions(+), 1596 deletions(-)
 create mode 100644 pcs/lib/corosync/qdevice_client.py
 create mode 100644 pcs/test/resources/qdevice-certs/qnetd-cacert.crt
 create mode 100644 pcs/test/test_lib_corosync_qdevice_client.py

diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
index 2ba5602..2dd5810 100644
--- a/pcs/cli/common/lib_wrapper.py
+++ b/pcs/cli/common/lib_wrapper.py
@@ -117,6 +117,8 @@ def load_module(env, middleware_factory, name):
                 "get_config": quorum.get_config,
                 "remove_device": quorum.remove_device,
                 "set_options": quorum.set_options,
+                "status": quorum.status_text,
+                "status_device": quorum.status_device_text,
                 "update_device": quorum.update_device,
             }
         )
@@ -125,6 +127,7 @@ def load_module(env, middleware_factory, name):
             env,
             middleware.build(),
             {
+                "status": qdevice.qdevice_status_text,
                 "setup": qdevice.qdevice_setup,
                 "destroy": qdevice.qdevice_destroy,
                 "start": qdevice.qdevice_start,
@@ -132,6 +135,13 @@ def load_module(env, middleware_factory, name):
                 "kill": qdevice.qdevice_kill,
                 "enable": qdevice.qdevice_enable,
                 "disable": qdevice.qdevice_disable,
+                # following commands are internal use only, called from pcsd
+                "client_net_setup": qdevice.client_net_setup,
+                "client_net_import_certificate":
+                    qdevice.client_net_import_certificate,
+                "client_net_destroy": qdevice.client_net_destroy,
+                "sign_net_cert_request":
+                    qdevice.qdevice_net_sign_certificate_request,
             }
         )
     if name == "sbd":
diff --git a/pcs/cluster.py b/pcs/cluster.py
index 002b5c5..988ab75 100644
--- a/pcs/cluster.py
+++ b/pcs/cluster.py
@@ -36,23 +36,29 @@ from pcs import (
 )
 from pcs.utils import parallel_for_nodes
 from pcs.common import report_codes
+from pcs.cli.common.reports import process_library_reports
 from pcs.lib import (
     pacemaker as lib_pacemaker,
     sbd as lib_sbd,
     reports as lib_reports,
 )
-from pcs.lib.tools import environment_file_to_dict
+from pcs.lib.commands.quorum import _add_device_model_net
+from pcs.lib.corosync import (
+    config_parser as corosync_conf_utils,
+    qdevice_net,
+)
+from pcs.lib.corosync.config_facade import ConfigFacade as corosync_conf_facade
+from pcs.lib.errors import (
+    LibraryError,
+    ReportItemSeverity,
+)
 from pcs.lib.external import (
     disable_service,
     NodeCommunicationException,
     node_communicator_exception_to_report_item,
 )
 from pcs.lib.node import NodeAddresses
-from pcs.lib.errors import (
-    LibraryError,
-    ReportItemSeverity,
-)
-from pcs.lib.corosync import config_parser as corosync_conf_utils
+from pcs.lib.tools import environment_file_to_dict
 
 def cluster_cmd(argv):
     if len(argv) == 0:
@@ -288,7 +294,7 @@ def cluster_setup(argv):
         )
     if udpu_rrp and "rrp_mode" not in options["transport_options"]:
         options["transport_options"]["rrp_mode"] = "passive"
-    utils.process_library_reports(messages)
+    process_library_reports(messages)
 
     # prepare config file
     if is_rhel6:
@@ -306,7 +312,7 @@ def cluster_setup(argv):
             options["totem_options"],
             options["quorum_options"]
         )
-    utils.process_library_reports(messages)
+    process_library_reports(messages)
 
     # setup on the local node
     if "--local" in utils.pcs_options:
@@ -870,6 +876,7 @@ def start_cluster(argv):
         return
 
     print("Starting Cluster...")
+    service_list = []
     if utils.is_rhel6():
 #   Verify that CMAN_QUORUM_TIMEOUT is set, if not, then we set it to 0
         retval, output = getstatusoutput('source /etc/sysconfig/cman ; [ -z "$CMAN_QUORUM_TIMEOUT" ]')
@@ -882,14 +889,15 @@ def start_cluster(argv):
             print(output)
             utils.err("unable to start cman")
     else:
-        output, retval = utils.run(["service", "corosync","start"])
+        service_list.append("corosync")
+        if utils.need_to_handle_qdevice_service():
+            service_list.append("corosync-qdevice")
+    service_list.append("pacemaker")
+    for service in service_list:
+        output, retval = utils.run(["service", service, "start"])
         if retval != 0:
             print(output)
-            utils.err("unable to start corosync")
-    output, retval = utils.run(["service", "pacemaker", "start"])
-    if retval != 0:
-        print(output)
-        utils.err("unable to start pacemaker")
+            utils.err("unable to start {0}".format(service))
     if wait:
         wait_for_nodes_started([], wait_timeout)
 
@@ -1035,14 +1043,20 @@ def enable_cluster(argv):
         enable_cluster_nodes(argv)
         return
 
-    utils.enableServices()
+    try:
+        utils.enableServices()
+    except LibraryError as e:
+        process_library_reports(e.args)
 
 def disable_cluster(argv):
     if len(argv) > 0:
         disable_cluster_nodes(argv)
         return
 
-    utils.disableServices()
+    try:
+        utils.disableServices()
+    except LibraryError as e:
+        process_library_reports(e.args)
 
 def enable_cluster_all():
     enable_cluster_nodes(utils.getNodesFromCorosyncConf())
@@ -1132,13 +1146,18 @@ def stop_cluster_corosync():
             utils.err("unable to stop cman")
     else:
         print("Stopping Cluster (corosync)...")
-        output, retval = utils.run(["service", "corosync","stop"])
-        if retval != 0:
-            print(output)
-            utils.err("unable to stop corosync")
+        service_list = []
+        if utils.need_to_handle_qdevice_service():
+            service_list.append("corosync-qdevice")
+        service_list.append("corosync")
+        for service in service_list:
+            output, retval = utils.run(["service", service, "stop"])
+            if retval != 0:
+                print(output)
+                utils.err("unable to stop {0}".format(service))
 
 def kill_cluster(argv):
-    daemons = ["crmd", "pengine", "attrd", "lrmd", "stonithd", "cib", "pacemakerd", "corosync"]
+    daemons = ["crmd", "pengine", "attrd", "lrmd", "stonithd", "cib", "pacemakerd", "corosync-qdevice", "corosync"]
     dummy_output, dummy_retval = utils.run(["killall", "-9"] + daemons)
 #    if dummy_retval != 0:
 #        print "Error: unable to execute killall -9"
@@ -1321,19 +1340,16 @@ def cluster_node(argv):
                 "cluster is not configured for RRP, "
                 "you must not specify ring 1 address for the node"
             )
-        utils.check_qdevice_algorithm_and_running_cluster(
-            utils.getCorosyncConf(), add=True
-        )
         corosync_conf = None
         (canAdd, error) =  utils.canAddNodeToCluster(node0)
         if not canAdd:
             utils.err("Unable to add '%s' to cluster: %s" % (node0, error))
 
+        lib_env = utils.get_lib_env()
+        report_processor = lib_env.report_processor
+        node_communicator = lib_env.node_communicator()
+        node_addr = NodeAddresses(node0, node1)
         try:
-            node_addr = NodeAddresses(node0, node1)
-            lib_env = utils.get_lib_env()
-            report_processor = lib_env.report_processor
-            node_communicator = lib_env.node_communicator()
             if lib_sbd.is_sbd_enabled(utils.cmd_runner()):
                 if "--watchdog" not in utils.pcs_options:
                     watchdog = settings.sbd_watchdog_default
@@ -1367,9 +1383,9 @@ def cluster_node(argv):
                     report_processor, node_communicator, node_addr
                 )
         except LibraryError as e:
-            utils.process_library_reports(e.args)
+            process_library_reports(e.args)
         except NodeCommunicationException as e:
-            utils.process_library_reports(
+            process_library_reports(
                 [node_communicator_exception_to_report_item(e)]
             )
 
@@ -1383,6 +1399,8 @@ def cluster_node(argv):
             else:
                 print("%s: Corosync updated" % my_node)
                 corosync_conf = output
+        # corosync.conf must be reloaded before the new node is started
+        output, retval = utils.reloadCorosync()
         if corosync_conf != None:
             # send local cluster pcsd configs to the new node
             # may be used for sending corosync config as well in future
@@ -1406,6 +1424,25 @@ def cluster_node(argv):
                 except:
                     utils.err('Unable to communicate with pcsd')
 
+            # set qdevice-net certificates if needed
+            if not utils.is_rhel6():
+                try:
+                    conf_facade = corosync_conf_facade.from_string(
+                        corosync_conf
+                    )
+                    qdevice_model, qdevice_model_options, _ = conf_facade.get_quorum_device_settings()
+                    if qdevice_model == "net":
+                        _add_device_model_net(
+                            lib_env,
+                            qdevice_model_options["host"],
+                            conf_facade.get_cluster_name(),
+                            [node_addr],
+                            skip_offline_nodes=False
+                        )
+                except LibraryError as e:
+                    process_library_reports(e.args)
+
+            print("Setting up corosync...")
             utils.setCorosyncConfig(node0, corosync_conf)
             if "--enable" in utils.pcs_options:
                 retval, err = utils.enableCluster(node0)
@@ -1421,7 +1458,6 @@ def cluster_node(argv):
             pcsd.pcsd_sync_certs([node0], exit_after_error=False)
         else:
             utils.err("Unable to update any nodes")
-        output, retval = utils.reloadCorosync()
         if utils.is_cman_with_udpu_transport():
             print("Warning: Using udpu transport on a CMAN cluster, "
                 + "cluster restart is required to apply node addition")
@@ -1433,9 +1469,6 @@ def cluster_node(argv):
             utils.err(
                 "node '%s' does not appear to exist in configuration" % node0
             )
-        utils.check_qdevice_algorithm_and_running_cluster(
-            utils.getCorosyncConf(), add=False
-        )
         if "--force" not in utils.pcs_options:
             retval, data = utils.get_remote_quorumtool_output(node0)
             if retval != 0:
@@ -1697,10 +1730,18 @@ def cluster_destroy(argv):
     else:
         print("Shutting down pacemaker/corosync services...")
         os.system("service pacemaker stop")
+        # returns error if qdevice is not running, it is safe to ignore it
+        # since we want it not to be running
+        os.system("service corosync-qdevice stop")
         os.system("service corosync stop")
         print("Killing any remaining services...")
-        os.system("killall -q -9 corosync aisexec heartbeat pacemakerd ccm stonithd ha_logd lrmd crmd pengine attrd pingd mgmtd cib fenced dlm_controld gfs_controld")
-        utils.disableServices()
+        os.system("killall -q -9 corosync corosync-qdevice aisexec heartbeat pacemakerd ccm stonithd ha_logd lrmd crmd pengine attrd pingd mgmtd cib fenced dlm_controld gfs_controld")
+        try:
+            utils.disableServices()
+        except:
+            # previously errors were suppressed in here, let's keep it that way
+            # for now
+            pass
         try:
             disable_service(utils.cmd_runner(), "sbd")
         except:
@@ -1716,6 +1757,12 @@ def cluster_destroy(argv):
                 "pe*.bz2","cib.*"]
         for name in state_files:
             os.system("find /var/lib -name '"+name+"' -exec rm -f \{\} \;")
+        try:
+            qdevice_net.client_destroy()
+        except:
+            # errors from deleting other files are suppressed as well
+            # we do not want to fail if qdevice was not set up
+            pass
 
 def cluster_verify(argv):
     nofilename = True
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
index bda982a..afe0554 100644
--- a/pcs/common/report_codes.py
+++ b/pcs/common/report_codes.py
@@ -45,6 +45,8 @@ COROSYNC_CONFIG_RELOAD_ERROR = "COROSYNC_CONFIG_RELOAD_ERROR"
 COROSYNC_NOT_RUNNING_CHECK_STARTED = "COROSYNC_NOT_RUNNING_CHECK_STARTED"
 COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR = "COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR"
 COROSYNC_NOT_RUNNING_ON_NODE = "COROSYNC_NOT_RUNNING_ON_NODE"
+COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE = "COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE"
+COROSYNC_QUORUM_GET_STATUS_ERROR = "COROSYNC_QUORUM_GET_STATUS_ERROR"
 COROSYNC_RUNNING_ON_NODE = "COROSYNC_RUNNING_ON_NODE"
 CRM_MON_ERROR = "CRM_MON_ERROR"
 DUPLICATE_CONSTRAINTS_EXIST = "DUPLICATE_CONSTRAINTS_EXIST"
@@ -62,11 +64,11 @@ INVALID_SCORE = "INVALID_SCORE"
 INVALID_TIMEOUT_VALUE = "INVALID_TIMEOUT_VALUE"
 MULTIPLE_SCORE_OPTIONS = "MULTIPLE_SCORE_OPTIONS"
 NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL = "NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL"
-NODE_COMMUNICATION_ERROR = "NODE_COMMUNICATION_ERROR",
-NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED = "NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED",
-NODE_COMMUNICATION_ERROR_PERMISSION_DENIED = "NODE_COMMUNICATION_ERROR_PERMISSION_DENIED",
-NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT = "NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT",
-NODE_COMMUNICATION_ERROR_UNSUPPORTED_COMMAND = "NODE_COMMUNICATION_ERROR_UNSUPPORTED_COMMAND",
+NODE_COMMUNICATION_ERROR = "NODE_COMMUNICATION_ERROR"
+NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED = "NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED"
+NODE_COMMUNICATION_ERROR_PERMISSION_DENIED = "NODE_COMMUNICATION_ERROR_PERMISSION_DENIED"
+NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT = "NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT"
+NODE_COMMUNICATION_ERROR_UNSUPPORTED_COMMAND = "NODE_COMMUNICATION_ERROR_UNSUPPORTED_COMMAND"
 NODE_COMMUNICATION_FINISHED = "NODE_COMMUNICATION_FINISHED"
 NODE_COMMUNICATION_NOT_CONNECTED = "NODE_COMMUNICATION_NOT_CONNECTED"
 NODE_COMMUNICATION_STARTED = "NODE_COMMUNICATION_STARTED"
@@ -74,16 +76,25 @@ NODE_NOT_FOUND = "NODE_NOT_FOUND"
 NON_UDP_TRANSPORT_ADDR_MISMATCH = 'NON_UDP_TRANSPORT_ADDR_MISMATCH'
 OMITTING_NODE = "OMITTING_NODE"
 PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND = "PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND"
-PARSE_ERROR_COROSYNC_CONF_MISSING_CLOSING_BRACE = "PARSE_ERROR_COROSYNC_CONF_MISSING_CLOSING_BRACE",
-PARSE_ERROR_COROSYNC_CONF = "PARSE_ERROR_COROSYNC_CONF",
-PARSE_ERROR_COROSYNC_CONF_UNEXPECTED_CLOSING_BRACE = "PARSE_ERROR_COROSYNC_CONF_UNEXPECTED_CLOSING_BRACE",
+PARSE_ERROR_COROSYNC_CONF_MISSING_CLOSING_BRACE = "PARSE_ERROR_COROSYNC_CONF_MISSING_CLOSING_BRACE"
+PARSE_ERROR_COROSYNC_CONF = "PARSE_ERROR_COROSYNC_CONF"
+PARSE_ERROR_COROSYNC_CONF_UNEXPECTED_CLOSING_BRACE = "PARSE_ERROR_COROSYNC_CONF_UNEXPECTED_CLOSING_BRACE"
 QDEVICE_ALREADY_DEFINED = "QDEVICE_ALREADY_DEFINED"
 QDEVICE_ALREADY_INITIALIZED = "QDEVICE_ALREADY_INITIALIZED"
+QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE = "QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE"
+QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED = "QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED"
+QDEVICE_CERTIFICATE_REMOVAL_STARTED = "QDEVICE_CERTIFICATE_REMOVAL_STARTED"
+QDEVICE_CERTIFICATE_REMOVED_FROM_NODE = "QDEVICE_CERTIFICATE_REMOVED_FROM_NODE"
+QDEVICE_CERTIFICATE_IMPORT_ERROR = "QDEVICE_CERTIFICATE_IMPORT_ERROR"
+QDEVICE_CERTIFICATE_SIGN_ERROR = "QDEVICE_CERTIFICATE_SIGN_ERROR"
 QDEVICE_DESTROY_ERROR = "QDEVICE_DESTROY_ERROR"
 QDEVICE_DESTROY_SUCCESS = "QDEVICE_DESTROY_SUCCESS"
+QDEVICE_GET_STATUS_ERROR = "QDEVICE_GET_STATUS_ERROR"
 QDEVICE_INITIALIZATION_ERROR = "QDEVICE_INITIALIZATION_ERROR"
 QDEVICE_INITIALIZATION_SUCCESS = "QDEVICE_INITIALIZATION_SUCCESS"
 QDEVICE_NOT_DEFINED = "QDEVICE_NOT_DEFINED"
+QDEVICE_NOT_INITIALIZED = "QDEVICE_NOT_INITIALIZED"
+QDEVICE_CLIENT_RELOAD_STARTED = "QDEVICE_CLIENT_RELOAD_STARTED"
 QDEVICE_REMOVE_OR_CLUSTER_STOP_NEEDED = "QDEVICE_REMOVE_OR_CLUSTER_STOP_NEEDED"
 REQUIRED_OPTION_IS_MISSING = "REQUIRED_OPTION_IS_MISSING"
 RESOURCE_CLEANUP_ERROR = "RESOURCE_CLEANUP_ERROR"
@@ -106,12 +117,16 @@ SBD_ENABLING_STARTED = "SBD_ENABLING_STARTED"
 SBD_NOT_INSTALLED = "SBD_NOT_INSTALLED"
 SBD_NOT_ENABLED = "SBD_NOT_ENABLED"
 SERVICE_DISABLE_ERROR = "SERVICE_DISABLE_ERROR"
+SERVICE_DISABLE_STARTED = "SERVICE_DISABLE_STARTED"
 SERVICE_DISABLE_SUCCESS = "SERVICE_DISABLE_SUCCESS"
 SERVICE_ENABLE_ERROR = "SERVICE_ENABLE_ERROR"
+SERVICE_ENABLE_STARTED = "SERVICE_ENABLE_STARTED"
+SERVICE_ENABLE_SKIPPED = "SERVICE_ENABLE_SKIPPED"
 SERVICE_ENABLE_SUCCESS = "SERVICE_ENABLE_SUCCESS"
 SERVICE_KILL_ERROR = "SERVICE_KILL_ERROR"
 SERVICE_KILL_SUCCESS = "SERVICE_KILL_SUCCESS"
 SERVICE_START_ERROR = "SERVICE_START_ERROR"
+SERVICE_START_SKIPPED = "SERVICE_START_SKIPPED"
 SERVICE_START_STARTED = "SERVICE_START_STARTED"
 SERVICE_START_SUCCESS = "SERVICE_START_SUCCESS"
 SERVICE_STOP_ERROR = "SERVICE_STOP_ERROR"
diff --git a/pcs/lib/commands/qdevice.py b/pcs/lib/commands/qdevice.py
index c300a4c..1d1d85f 100644
--- a/pcs/lib/commands/qdevice.py
+++ b/pcs/lib/commands/qdevice.py
@@ -5,6 +5,9 @@ from __future__ import (
     unicode_literals,
 )
 
+import base64
+import binascii
+
 from pcs.lib import external, reports
 from pcs.lib.corosync import qdevice_net
 from pcs.lib.errors import LibraryError
@@ -31,7 +34,7 @@ def qdevice_setup(lib_env, model, enable, start):
 def qdevice_destroy(lib_env, model):
     """
     Stop and disable qdevice on local host and remove its configuration
-    string model qdevice model to initialize
+    string model qdevice model to destroy
     """
     _ensure_not_cman(lib_env)
     _check_model(model)
@@ -40,6 +43,22 @@ def qdevice_destroy(lib_env, model):
     qdevice_net.qdevice_destroy()
     lib_env.report_processor.process(reports.qdevice_destroy_success(model))
 
+def qdevice_status_text(lib_env, model, verbose=False, cluster=None):
+    """
+    Get runtime status of a quorum device in plain text
+    string model qdevice model to query
+    bool verbose get more detailed output
+    string cluster show information only about specified cluster
+    """
+    _ensure_not_cman(lib_env)
+    _check_model(model)
+    runner = lib_env.cmd_runner()
+    return (
+        qdevice_net.qdevice_status_generic_text(runner, verbose)
+        +
+        qdevice_net.qdevice_status_cluster_text(runner, cluster, verbose)
+    )
+
 def qdevice_enable(lib_env, model):
     """
     make qdevice start automatically on boot on local host
@@ -80,6 +99,73 @@ def qdevice_kill(lib_env, model):
     _check_model(model)
     _service_kill(lib_env, qdevice_net.qdevice_kill)
 
+def qdevice_net_sign_certificate_request(
+    lib_env, certificate_request, cluster_name
+):
+    """
+    Sign node certificate request by qnetd CA
+    string certificate_request base64 encoded certificate request
+    string cluster_name name of the cluster to which qdevice is being added
+    """
+    _ensure_not_cman(lib_env)
+    try:
+        certificate_request_data = base64.b64decode(certificate_request)
+    except (TypeError, binascii.Error):
+        raise LibraryError(reports.invalid_option_value(
+            "qnetd certificate request",
+            certificate_request,
+            ["base64 encoded certificate"]
+        ))
+    return base64.b64encode(
+        qdevice_net.qdevice_sign_certificate_request(
+            lib_env.cmd_runner(),
+            certificate_request_data,
+            cluster_name
+        )
+    )
+
+def client_net_setup(lib_env, ca_certificate):
+    """
+    Intialize qdevice net client on local host
+    ca_certificate base64 encoded qnetd CA certificate
+    """
+    _ensure_not_cman(lib_env)
+    try:
+        ca_certificate_data = base64.b64decode(ca_certificate)
+    except (TypeError, binascii.Error):
+        raise LibraryError(reports.invalid_option_value(
+            "qnetd CA certificate",
+            ca_certificate,
+            ["base64 encoded certificate"]
+        ))
+    qdevice_net.client_setup(lib_env.cmd_runner(), ca_certificate_data)
+
+def client_net_import_certificate(lib_env, certificate):
+    """
+    Import qnetd client certificate to local node certificate storage
+    certificate base64 encoded qnetd client certificate
+    """
+    _ensure_not_cman(lib_env)
+    try:
+        certificate_data = base64.b64decode(certificate)
+    except (TypeError, binascii.Error):
+        raise LibraryError(reports.invalid_option_value(
+            "qnetd client certificate",
+            certificate,
+            ["base64 encoded certificate"]
+        ))
+    qdevice_net.client_import_certificate_and_key(
+        lib_env.cmd_runner(),
+        certificate_data
+    )
+
+def client_net_destroy(lib_env):
+    """
+    delete qdevice client config files on local host
+    """
+    _ensure_not_cman(lib_env)
+    qdevice_net.client_destroy()
+
 def _ensure_not_cman(lib_env):
     if lib_env.is_cman_cluster:
         raise LibraryError(reports.cman_unsupported_command())
diff --git a/pcs/lib/commands/quorum.py b/pcs/lib/commands/quorum.py
index 1ee5411..aa00bbd 100644
--- a/pcs/lib/commands/quorum.py
+++ b/pcs/lib/commands/quorum.py
@@ -5,9 +5,18 @@ from __future__ import (
     unicode_literals,
 )
 
-
 from pcs.lib import reports
 from pcs.lib.errors import LibraryError
+from pcs.lib.corosync import (
+    live as corosync_live,
+    qdevice_net,
+    qdevice_client
+)
+from pcs.lib.external import (
+    NodeCommunicationException,
+    node_communicator_exception_to_report_item,
+    parallel_nodes_communication_helper,
+)
 
 
 def get_config(lib_env):
@@ -42,6 +51,21 @@ def set_options(lib_env, options, skip_offline_nodes=False):
     cfg.set_quorum_options(lib_env.report_processor, options)
     lib_env.push_corosync_conf(cfg, skip_offline_nodes)
 
+def status_text(lib_env):
+    """
+    Get quorum runtime status in plain text
+    """
+    __ensure_not_cman(lib_env)
+    return corosync_live.get_quorum_status_text(lib_env.cmd_runner())
+
+def status_device_text(lib_env, verbose=False):
+    """
+    Get quorum device client runtime status in plain text
+    bool verbose get more detailed output
+    """
+    __ensure_not_cman(lib_env)
+    return qdevice_client.get_status_text(lib_env.cmd_runner(), verbose)
+
 def add_device(
     lib_env, model, model_options, generic_options, force_model=False,
     force_options=False, skip_offline_nodes=False
@@ -58,6 +82,8 @@ def add_device(
     __ensure_not_cman(lib_env)
 
     cfg = lib_env.get_corosync_conf()
+    # Try adding qdevice to corosync.conf. This validates all the options and
+    # makes sure qdevice is not defined in corosync.conf yet.
     cfg.add_quorum_device(
         lib_env.report_processor,
         model,
@@ -66,9 +92,131 @@ def add_device(
         force_model,
         force_options
     )
-    # TODO validation, verification, certificates, etc.
+
+    # First setup certificates for qdevice, then send corosync.conf to nodes.
+    # If anything fails, nodes will not have corosync.conf with qdevice in it,
+    # so there is no effect on the cluster.
+    if lib_env.is_corosync_conf_live:
+        # do model specific configuration
+        # if model is not known to pcs and was forced, do not configure antyhing
+        # else but corosync.conf, as we do not know what to do anyways
+        if model == "net":
+            _add_device_model_net(
+                lib_env,
+                # we are sure it's there, it was validated in add_quorum_device
+                model_options["host"],
+                cfg.get_cluster_name(),
+                cfg.get_nodes(),
+                skip_offline_nodes
+            )
+
+        lib_env.report_processor.process(
+            reports.service_enable_started("corosync-qdevice")
+        )
+        communicator = lib_env.node_communicator()
+        parallel_nodes_communication_helper(
+            qdevice_client.remote_client_enable,
+            [
+                [(lib_env.report_processor, communicator, node), {}]
+                for node in cfg.get_nodes()
+            ],
+            lib_env.report_processor,
+            skip_offline_nodes
+        )
+
+    # everything set up, it's safe to tell the nodes to use qdevice
     lib_env.push_corosync_conf(cfg, skip_offline_nodes)
 
+    # Now, when corosync.conf has been reloaded, we can start qdevice service.
+    if lib_env.is_corosync_conf_live:
+        lib_env.report_processor.process(
+            reports.service_start_started("corosync-qdevice")
+        )
+        communicator = lib_env.node_communicator()
+        parallel_nodes_communication_helper(
+            qdevice_client.remote_client_start,
+            [
+                [(lib_env.report_processor, communicator, node), {}]
+                for node in cfg.get_nodes()
+            ],
+            lib_env.report_processor,
+            skip_offline_nodes
+        )
+
+def _add_device_model_net(
+    lib_env, qnetd_host, cluster_name, cluster_nodes, skip_offline_nodes
+):
+    """
+    setup cluster nodes for using qdevice model net
+    string qnetd_host address of qdevice provider (qnetd host)
+    string cluster_name name of the cluster to which qdevice is being added
+    NodeAddressesList cluster_nodes list of cluster nodes addresses
+    bool skip_offline_nodes continue even if not all nodes are accessible
+    """
+    communicator = lib_env.node_communicator()
+    runner = lib_env.cmd_runner()
+    reporter = lib_env.report_processor
+
+    reporter.process(
+        reports.qdevice_certificate_distribution_started()
+    )
+    # get qnetd CA certificate
+    try:
+        qnetd_ca_cert = qdevice_net.remote_qdevice_get_ca_certificate(
+            communicator,
+            qnetd_host
+        )
+    except NodeCommunicationException as e:
+        raise LibraryError(
+            node_communicator_exception_to_report_item(e)
+        )
+    # init certificate storage on all nodes
+    parallel_nodes_communication_helper(
+        qdevice_net.remote_client_setup,
+        [
+            ((communicator, node, qnetd_ca_cert), {})
+            for node in cluster_nodes
+        ],
+        reporter,
+        skip_offline_nodes
+    )
+    # create client certificate request
+    cert_request = qdevice_net.client_generate_certificate_request(
+        runner,
+        cluster_name
+    )
+    # sign the request on qnetd host
+    try:
+        signed_certificate = qdevice_net.remote_sign_certificate_request(
+            communicator,
+            qnetd_host,
+            cert_request,
+            cluster_name
+        )
+    except NodeCommunicationException as e:
+        raise LibraryError(
+            node_communicator_exception_to_report_item(e)
+        )
+    # transform the signed certificate to pk12 format which can sent to nodes
+    pk12 = qdevice_net.client_cert_request_to_pk12(runner, signed_certificate)
+    # distribute final certificate to nodes
+    def do_and_report(reporter, communicator, node, pk12):
+        qdevice_net.remote_client_import_certificate_and_key(
+            communicator, node, pk12
+        )
+        reporter.process(
+            reports.qdevice_certificate_accepted_by_node(node.label)
+        )
+    parallel_nodes_communication_helper(
+        do_and_report,
+        [
+            ((reporter, communicator, node, pk12), {})
+            for node in cluster_nodes
+        ],
+        reporter,
+        skip_offline_nodes
+    )
+
 def update_device(
     lib_env, model_options, generic_options, force_options=False,
     skip_offline_nodes=False
@@ -98,9 +246,74 @@ def remove_device(lib_env, skip_offline_nodes=False):
     __ensure_not_cman(lib_env)
 
     cfg = lib_env.get_corosync_conf()
+    model, dummy_options, dummy_options = cfg.get_quorum_device_settings()
     cfg.remove_quorum_device()
     lib_env.push_corosync_conf(cfg, skip_offline_nodes)
 
+    if lib_env.is_corosync_conf_live:
+        # disable qdevice
+        lib_env.report_processor.process(
+            reports.service_disable_started("corosync-qdevice")
+        )
+        communicator = lib_env.node_communicator()
+        parallel_nodes_communication_helper(
+            qdevice_client.remote_client_disable,
+            [
+                [(lib_env.report_processor, communicator, node), {}]
+                for node in cfg.get_nodes()
+            ],
+            lib_env.report_processor,
+            skip_offline_nodes
+        )
+        # stop qdevice
+        lib_env.report_processor.process(
+            reports.service_stop_started("corosync-qdevice")
+        )
+        communicator = lib_env.node_communicator()
+        parallel_nodes_communication_helper(
+            qdevice_client.remote_client_stop,
+            [
+                [(lib_env.report_processor, communicator, node), {}]
+                for node in cfg.get_nodes()
+            ],
+            lib_env.report_processor,
+            skip_offline_nodes
+        )
+        # handle model specific configuration
+        if model == "net":
+            _remove_device_model_net(
+                lib_env,
+                cfg.get_nodes(),
+                skip_offline_nodes
+            )
+
+def _remove_device_model_net(lib_env, cluster_nodes, skip_offline_nodes):
+    """
+    remove configuration used by qdevice model net
+    NodeAddressesList cluster_nodes list of cluster nodes addresses
+    bool skip_offline_nodes continue even if not all nodes are accessible
+    """
+    reporter = lib_env.report_processor
+    communicator = lib_env.node_communicator()
+
+    reporter.process(
+        reports.qdevice_certificate_removal_started()
+    )
+    def do_and_report(reporter, communicator, node):
+        qdevice_net.remote_client_destroy(communicator, node)
+        reporter.process(
+            reports.qdevice_certificate_removed_from_node(node.label)
+        )
+    parallel_nodes_communication_helper(
+        do_and_report,
+        [
+            [(reporter, communicator, node), {}]
+            for node in cluster_nodes
+        ],
+        lib_env.report_processor,
+        skip_offline_nodes
+    )
+
 def __ensure_not_cman(lib_env):
     if lib_env.is_corosync_conf_live and lib_env.is_cman_cluster:
         raise LibraryError(reports.cman_unsupported_command())
diff --git a/pcs/lib/corosync/config_facade.py b/pcs/lib/corosync/config_facade.py
index 5a486ca..600a89b 100644
--- a/pcs/lib/corosync/config_facade.py
+++ b/pcs/lib/corosync/config_facade.py
@@ -22,6 +22,12 @@ class ConfigFacade(object):
         "last_man_standing_window",
         "wait_for_all",
     )
+    QUORUM_OPTIONS_INCOMPATIBLE_WITH_QDEVICE = (
+        "auto_tie_breaker",
+        "last_man_standing",
+        "last_man_standing_window",
+    )
+
 
     @classmethod
     def from_string(cls, config_string):
@@ -52,6 +58,8 @@ class ConfigFacade(object):
         self._config = parsed_config
         # set to True if changes cannot be applied on running cluster
         self._need_stopped_cluster = False
+        # set to True if qdevice reload is required to apply changes
+        self._need_qdevice_reload = False
 
     @property
     def config(self):
@@ -61,6 +69,17 @@ class ConfigFacade(object):
     def need_stopped_cluster(self):
         return self._need_stopped_cluster
 
+    @property
+    def need_qdevice_reload(self):
+        return self._need_qdevice_reload
+
+    def get_cluster_name(self):
+        cluster_name = ""
+        for totem in self.config.get_sections("totem"):
+            for attrs in totem.get_attributes("cluster_name"):
+                cluster_name = attrs[1]
+        return cluster_name
+
     def get_nodes(self):
         """
         Get all defined nodes
@@ -112,8 +131,9 @@ class ConfigFacade(object):
 
     def __validate_quorum_options(self, options):
         report_items = []
+        has_qdevice = self.has_quorum_device()
+        qdevice_incompatible_options = []
         for name, value in sorted(options.items()):
-
             allowed_names = self.__class__.QUORUM_OPTIONS
             if name not in allowed_names:
                 report_items.append(
@@ -124,6 +144,13 @@ class ConfigFacade(object):
             if value == "":
                 continue
 
+            if (
+                has_qdevice
+                and
+                name in self.__class__.QUORUM_OPTIONS_INCOMPATIBLE_WITH_QDEVICE
+            ):
+                qdevice_incompatible_options.append(name)
+
             if name == "last_man_standing_window":
                 if not value.isdigit():
                     report_items.append(reports.invalid_option_value(
@@ -137,6 +164,13 @@ class ConfigFacade(object):
                         name, value, allowed_values
                     ))
 
+        if qdevice_incompatible_options:
+            report_items.append(
+                reports.corosync_options_incompatible_with_qdevice(
+                    qdevice_incompatible_options
+                )
+            )
+
         return report_items
 
     def has_quorum_device(self):
@@ -201,13 +235,13 @@ class ConfigFacade(object):
                 force=force_options
             )
         )
+
         # configuration cleanup
-        remove_need_stopped_cluster = {
-            "auto_tie_breaker": "",
-            "last_man_standing": "",
-            "last_man_standing_window": "",
-        }
-        need_stopped_cluster = False
+        remove_need_stopped_cluster = dict([
+            (name, "")
+            for name in self.__class__.QUORUM_OPTIONS_INCOMPATIBLE_WITH_QDEVICE
+        ])
+        # remove old device settings
         quorum_section_list = self.__ensure_section(self.config, "quorum")
         for quorum in quorum_section_list:
             for device in quorum.get_sections("device"):
@@ -218,13 +252,19 @@ class ConfigFacade(object):
                     and
                     value not in ["", "0"]
                 ):
-                    need_stopped_cluster = True
+                    self._need_stopped_cluster = True
+        # remove conflicting quorum options
         attrs_to_remove = {
             "allow_downscale": "",
             "two_node": "",
         }
         attrs_to_remove.update(remove_need_stopped_cluster)
         self.__set_section_options(quorum_section_list, attrs_to_remove)
+        # remove nodes' votes
+        for nodelist in self.config.get_sections("nodelist"):
+            for node in nodelist.get_sections("node"):
+                node.del_attributes_by_name("quorum_votes")
+
         # add new configuration
         quorum = quorum_section_list[-1]
         new_device = config_parser.Section("device")
@@ -234,12 +274,9 @@ class ConfigFacade(object):
         new_model = config_parser.Section(model)
         self.__set_section_options([new_model], model_options)
         new_device.add_section(new_model)
+        self.__update_qdevice_votes()
         self.__update_two_node()
         self.__remove_empty_sections(self.config)
-        # update_two_node sets self._need_stopped_cluster when changing an
-        # algorithm lms <-> 2nodelms. We don't care about that, it's not really
-        # a change, as there was no qdevice before. So we override it.
-        self._need_stopped_cluster = need_stopped_cluster
 
     def update_quorum_device(
         self, report_processor, model_options, generic_options,
@@ -281,9 +318,10 @@ class ConfigFacade(object):
                 model_sections.extend(device.get_sections(model))
         self.__set_section_options(device_sections, generic_options)
         self.__set_section_options(model_sections, model_options)
+        self.__update_qdevice_votes()
         self.__update_two_node()
         self.__remove_empty_sections(self.config)
-        self._need_stopped_cluster = True
+        self._need_qdevice_reload = True
 
     def remove_quorum_device(self):
         """
@@ -369,7 +407,7 @@ class ConfigFacade(object):
                     continue
 
             if name == "algorithm":
-                allowed_values = ("2nodelms", "ffsplit", "lms")
+                allowed_values = ("ffsplit", "lms")
                 if value not in allowed_values:
                     report_items.append(reports.invalid_option_value(
                         name, value, allowed_values, severity, forceable
@@ -461,19 +499,29 @@ class ConfigFacade(object):
         else:
             for quorum in self.config.get_sections("quorum"):
                 quorum.del_attributes_by_name("two_node")
-        # update qdevice algorithm "lms" vs "2nodelms"
+
+    def __update_qdevice_votes(self):
+        # ffsplit won't start if votes is missing or not set to 1
+        # for other algorithms it's required not to put votes at all
+        model = None
+        algorithm = None
+        device_sections = []
         for quorum in self.config.get_sections("quorum"):
             for device in quorum.get_sections("device"):
-                for net in device.get_sections("net"):
-                    algorithm = None
-                    for dummy_name, value in net.get_attributes("algorithm"):
-                        algorithm = value
-                    if algorithm == "lms" and has_two_nodes:
-                        net.set_attribute("algorithm", "2nodelms")
-                        self._need_stopped_cluster = True
-                    elif algorithm == "2nodelms" and not has_two_nodes:
-                        net.set_attribute("algorithm", "lms")
-                        self._need_stopped_cluster = True
+                device_sections.append(device)
+                for dummy_name, value in device.get_attributes("model"):
+                    model = value
+        for device in device_sections:
+            for model_section in device.get_sections(model):
+                for dummy_name, value in model_section.get_attributes(
+                    "algorithm"
+                ):
+                    algorithm = value
+        if model == "net":
+            if algorithm == "ffsplit":
+                self.__set_section_options(device_sections, {"votes": "1"})
+            else:
+                self.__set_section_options(device_sections, {"votes": ""})
 
     def __set_section_options(self, section_list, options):
         for section in section_list[:-1]:
diff --git a/pcs/lib/corosync/live.py b/pcs/lib/corosync/live.py
index 2446a46..4129aeb 100644
--- a/pcs/lib/corosync/live.py
+++ b/pcs/lib/corosync/live.py
@@ -47,3 +47,18 @@ def reload_config(runner):
             reports.corosync_config_reload_error(output.rstrip())
         )
 
+def get_quorum_status_text(runner):
+    """
+    Get runtime quorum status from the local node
+    """
+    output, retval = runner.run([
+        os.path.join(settings.corosync_binaries, "corosync-quorumtool"),
+        "-p"
+    ])
+    # retval is 0 on success if node is not in partition with quorum
+    # retval is 1 on error OR on success if node has quorum
+    if retval not in [0, 1]:
+        raise LibraryError(
+            reports.corosync_quorum_get_status_error(output)
+        )
+    return output
diff --git a/pcs/lib/corosync/qdevice_client.py b/pcs/lib/corosync/qdevice_client.py
new file mode 100644
index 0000000..98fbb0e
--- /dev/null
+++ b/pcs/lib/corosync/qdevice_client.py
@@ -0,0 +1,93 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+import os.path
+
+from pcs import settings
+from pcs.lib import reports
+from pcs.lib.errors import LibraryError
+
+
+def get_status_text(runner, verbose=False):
+    """
+    Get quorum device client runtime status in plain text
+    bool verbose get more detailed output
+    """
+    cmd = [
+        os.path.join(settings.corosync_binaries, "corosync-qdevice-tool"),
+        "-s"
+    ]
+    if verbose:
+        cmd.append("-v")
+    output, retval = runner.run(cmd)
+    if retval != 0:
+        raise LibraryError(
+            reports.corosync_quorum_get_status_error(output)
+        )
+    return output
+
+def remote_client_enable(reporter, node_communicator, node):
+    """
+    enable qdevice client service (corosync-qdevice) on a remote node
+    """
+    response = node_communicator.call_node(
+        node,
+        "remote/qdevice_client_enable",
+        None
+    )
+    if response == "corosync is not enabled, skipping":
+        reporter.process(
+            reports.service_enable_skipped(
+                "corosync-qdevice",
+                "corosync is not enabled",
+                node.label
+            )
+        )
+    else:
+        reporter.process(
+            reports.service_enable_success("corosync-qdevice", node.label)
+        )
+
+def remote_client_disable(reporter, node_communicator, node):
+    """
+    disable qdevice client service (corosync-qdevice) on a remote node
+    """
+    node_communicator.call_node(node, "remote/qdevice_client_disable", None)
+    reporter.process(
+        reports.service_disable_success("corosync-qdevice", node.label)
+    )
+
+def remote_client_start(reporter, node_communicator, node):
+    """
+    start qdevice client service (corosync-qdevice) on a remote node
+    """
+    response = node_communicator.call_node(
+        node,
+        "remote/qdevice_client_start",
+        None
+    )
+    if response == "corosync is not running, skipping":
+        reporter.process(
+            reports.service_start_skipped(
+                "corosync-qdevice",
+                "corosync is not running",
+                node.label
+            )
+        )
+    else:
+        reporter.process(
+            reports.service_start_success("corosync-qdevice", node.label)
+        )
+
+def remote_client_stop(reporter, node_communicator, node):
+    """
+    stop qdevice client service (corosync-qdevice) on a remote node
+    """
+    node_communicator.call_node(node, "remote/qdevice_client_stop", None)
+    reporter.process(
+        reports.service_stop_success("corosync-qdevice", node.label)
+    )
diff --git a/pcs/lib/corosync/qdevice_net.py b/pcs/lib/corosync/qdevice_net.py
index 7479257..4054592 100644
--- a/pcs/lib/corosync/qdevice_net.py
+++ b/pcs/lib/corosync/qdevice_net.py
@@ -5,8 +5,14 @@ from __future__ import (
     unicode_literals,
 )
 
+import base64
+import binascii
+import functools
+import os
 import os.path
+import re
 import shutil
+import tempfile
 
 from pcs import settings
 from pcs.lib import external, reports
@@ -15,6 +21,18 @@ from pcs.lib.errors import LibraryError
 
 __model = "net"
 __service_name = "corosync-qnetd"
+__qnetd_certutil = os.path.join(
+    settings.corosync_qnet_binaries,
+    "corosync-qnetd-certutil"
+)
+__qnetd_tool = os.path.join(
+    settings.corosync_qnet_binaries,
+    "corosync-qnetd-tool"
+)
+__qdevice_certutil = os.path.join(
+    settings.corosync_binaries,
+    "corosync-qdevice-net-certutil"
+)
 
 def qdevice_setup(runner):
     """
@@ -24,25 +42,63 @@ def qdevice_setup(runner):
         raise LibraryError(reports.qdevice_already_initialized(__model))
 
     output, retval = runner.run([
-        os.path.join(settings.corosync_binaries, "corosync-qnetd-certutil"),
-        "-i"
+        __qnetd_certutil, "-i"
     ])
     if retval != 0:
         raise LibraryError(
             reports.qdevice_initialization_error(__model, output.rstrip())
         )
 
+def qdevice_initialized():
+    """
+    check if qdevice server certificate database has been initialized
+    """
+    return os.path.exists(os.path.join(
+        settings.corosync_qdevice_net_server_certs_dir,
+        "cert8.db"
+    ))
+
 def qdevice_destroy():
     """
     delete qdevice configuration on local host
     """
     try:
-        shutil.rmtree(settings.corosync_qdevice_net_server_certs_dir)
+        if qdevice_initialized():
+            shutil.rmtree(settings.corosync_qdevice_net_server_certs_dir)
     except EnvironmentError as e:
         raise LibraryError(
             reports.qdevice_destroy_error(__model, e.strerror)
         )
 
+def qdevice_status_generic_text(runner, verbose=False):
+    """
+    get qdevice runtime status in plain text
+    bool verbose get more detailed output
+    """
+    cmd = [__qnetd_tool, "-s"]
+    if verbose:
+        cmd.append("-v")
+    output, retval = runner.run(cmd)
+    if retval != 0:
+        raise LibraryError(reports.qdevice_get_status_error(__model, output))
+    return output
+
+def qdevice_status_cluster_text(runner, cluster=None, verbose=False):
+    """
+    get qdevice runtime status in plain text
+    bool verbose get more detailed output
+    string cluster show information only about specified cluster
+    """
+    cmd = [__qnetd_tool, "-l"]
+    if verbose:
+        cmd.append("-v")
+    if cluster:
+        cmd.extend(["-c", cluster])
+    output, retval = runner.run(cmd)
+    if retval != 0:
+        raise LibraryError(reports.qdevice_get_status_error(__model, output))
+    return output
+
 def qdevice_enable(runner):
     """
     make qdevice start automatically on boot on local host
@@ -72,3 +128,255 @@ def qdevice_kill(runner):
     kill qdevice now on local host
     """
     external.kill_services(runner, [__service_name])
+
+def qdevice_sign_certificate_request(runner, cert_request, cluster_name):
+    """
+    sign client certificate request
+    cert_request certificate request data
+    string cluster_name name of the cluster to which qdevice is being added
+    """
+    if not qdevice_initialized():
+        raise LibraryError(reports.qdevice_not_initialized(__model))
+    # save the certificate request, corosync tool only works with files
+    tmpfile = _store_to_tmpfile(
+        cert_request,
+        reports.qdevice_certificate_sign_error
+    )
+    # sign the request
+    output, retval = runner.run([
+        __qnetd_certutil, "-s", "-c", tmpfile.name, "-n", cluster_name
+    ])
+    tmpfile.close() # temp file is deleted on close
+    if retval != 0:
+        raise LibraryError(
+            reports.qdevice_certificate_sign_error(output.strip())
+        )
+    # get signed certificate, corosync tool only works with files
+    return _get_output_certificate(
+        output,
+        reports.qdevice_certificate_sign_error
+    )
+
+def client_setup(runner, ca_certificate):
+    """
+    initialize qdevice client on local host
+    ca_certificate qnetd CA certificate
+    """
+    client_destroy()
+    # save CA certificate, corosync tool only works with files
+    ca_file_path = os.path.join(
+        settings.corosync_qdevice_net_client_certs_dir,
+        settings.corosync_qdevice_net_client_ca_file_name
+    )
+    try:
+        if not os.path.exists(ca_file_path):
+            os.makedirs(
+                settings.corosync_qdevice_net_client_certs_dir,
+                mode=0o700
+            )
+        with open(ca_file_path, "wb") as ca_file:
+            ca_file.write(ca_certificate)
+    except EnvironmentError as e:
+        raise LibraryError(
+            reports.qdevice_initialization_error(__model, e.strerror)
+        )
+    # initialize client's certificate storage
+    output, retval = runner.run([
+        __qdevice_certutil, "-i", "-c", ca_file_path
+    ])
+    if retval != 0:
+        raise LibraryError(
+            reports.qdevice_initialization_error(__model, output.rstrip())
+        )
+
+def client_initialized():
+    """
+    check if qdevice net client certificate database has been initialized
+    """
+    return os.path.exists(os.path.join(
+        settings.corosync_qdevice_net_client_certs_dir,
+        "cert8.db"
+    ))
+
+def client_destroy():
+    """
+    delete qdevice client config files on local host
+    """
+    try:
+        if client_initialized():
+            shutil.rmtree(settings.corosync_qdevice_net_client_certs_dir)
+    except EnvironmentError as e:
+        raise LibraryError(
+            reports.qdevice_destroy_error(__model, e.strerror)
+        )
+
+def client_generate_certificate_request(runner, cluster_name):
+    """
+    create a certificate request which can be signed by qnetd server
+    string cluster_name name of the cluster to which qdevice is being added
+    """
+    if not client_initialized():
+        raise LibraryError(reports.qdevice_not_initialized(__model))
+    output, retval = runner.run([
+        __qdevice_certutil, "-r", "-n", cluster_name
+    ])
+    if retval != 0:
+        raise LibraryError(
+            reports.qdevice_initialization_error(__model, output.rstrip())
+        )
+    return _get_output_certificate(
+        output,
+        functools.partial(reports.qdevice_initialization_error, __model)
+    )
+
+def client_cert_request_to_pk12(runner, cert_request):
+    """
+    transform signed certificate request to pk12 certificate which can be
+    imported to nodes
+    cert_request signed certificate request
+    """
+    if not client_initialized():
+        raise LibraryError(reports.qdevice_not_initialized(__model))
+    # save the signed certificate request, corosync tool only works with files
+    tmpfile = _store_to_tmpfile(
+        cert_request,
+        reports.qdevice_certificate_import_error
+    )
+    # transform it
+    output, retval = runner.run([
+        __qdevice_certutil, "-M", "-c", tmpfile.name
+    ])
+    tmpfile.close() # temp file is deleted on close
+    if retval != 0:
+        raise LibraryError(
+            reports.qdevice_certificate_import_error(output)
+        )
+    # get resulting pk12, corosync tool only works with files
+    return _get_output_certificate(
+        output,
+        reports.qdevice_certificate_import_error
+    )
+
+def client_import_certificate_and_key(runner, pk12_certificate):
+    """
+    import qdevice client certificate to the local node certificate storage
+    """
+    if not client_initialized():
+        raise LibraryError(reports.qdevice_not_initialized(__model))
+    # save the certificate, corosync tool only works with files
+    tmpfile = _store_to_tmpfile(
+        pk12_certificate,
+        reports.qdevice_certificate_import_error
+    )
+    output, retval = runner.run([
+        __qdevice_certutil, "-m", "-c", tmpfile.name
+    ])
+    tmpfile.close() # temp file is deleted on close
+    if retval != 0:
+        raise LibraryError(
+            reports.qdevice_certificate_import_error(output)
+        )
+
+def remote_qdevice_get_ca_certificate(node_communicator, host):
+    """
+    connect to a qnetd host and get qnetd CA certificate
+    string host address of the qnetd host
+    """
+    try:
+        return base64.b64decode(
+            node_communicator.call_host(
+                host,
+                "remote/qdevice_net_get_ca_certificate",
+                None
+            )
+        )
+    except (TypeError, binascii.Error):
+        raise LibraryError(reports.invalid_response_format(host))
+
+def remote_client_setup(node_communicator, node, qnetd_ca_certificate):
+    """
+    connect to a remote node and initialize qdevice there
+    NodeAddresses node target node
+    qnetd_ca_certificate qnetd CA certificate
+    """
+    return node_communicator.call_node(
+        node,
+        "remote/qdevice_net_client_init_certificate_storage",
+        external.NodeCommunicator.format_data_dict([
+            ("ca_certificate", base64.b64encode(qnetd_ca_certificate)),
+        ])
+    )
+
+def remote_sign_certificate_request(
+    node_communicator, host, cert_request, cluster_name
+):
+    """
+    connect to a qdevice host and sign node certificate there
+    string host address of the qnetd host
+    cert_request certificate request to be signed
+    string cluster_name name of the cluster to which qdevice is being added
+    """
+    try:
+        return base64.b64decode(
+            node_communicator.call_host(
+                host,
+                "remote/qdevice_net_sign_node_certificate",
+                external.NodeCommunicator.format_data_dict([
+                    ("certificate_request", base64.b64encode(cert_request)),
+                    ("cluster_name", cluster_name),
+                ])
+            )
+        )
+    except (TypeError, binascii.Error):
+        raise LibraryError(reports.invalid_response_format(host))
+
+def remote_client_import_certificate_and_key(node_communicator, node, pk12):
+    """
+    import pk12 certificate on a remote node
+    NodeAddresses node target node
+    pk12 certificate
+    """
+    return node_communicator.call_node(
+        node,
+        "remote/qdevice_net_client_import_certificate",
+        external.NodeCommunicator.format_data_dict([
+            ("certificate", base64.b64encode(pk12)),
+        ])
+    )
+
+def remote_client_destroy(node_communicator, node):
+    """
+    delete qdevice client config files on a remote node
+    NodeAddresses node target node
+    """
+    return node_communicator.call_node(
+        node,
+        "remote/qdevice_net_client_destroy",
+        None
+    )
+
+def _store_to_tmpfile(data, report_func):
+    try:
+        tmpfile = tempfile.NamedTemporaryFile(mode="wb", suffix=".pcs")
+        tmpfile.write(data)
+        tmpfile.flush()
+        return tmpfile
+    except EnvironmentError as e:
+        raise LibraryError(report_func(e.strerror))
+
+def _get_output_certificate(cert_tool_output, report_func):
+    regexp = re.compile(r"^Certificate( request)? stored in (?P<path>.+)$")
+    filename = None
+    for line in cert_tool_output.splitlines():
+        match = regexp.search(line)
+        if match:
+            filename = match.group("path")
+    if not filename:
+        raise LibraryError(report_func(cert_tool_output))
+    try:
+        with open(filename, "rb") as cert_file:
+            return cert_file.read()
+    except EnvironmentError as e:
+        raise LibraryError(report_func(
+            "{path}: {error}".format(path=filename, error=e.strerror)
+        ))
diff --git a/pcs/lib/env.py b/pcs/lib/env.py
index 1151891..24e4252 100644
--- a/pcs/lib/env.py
+++ b/pcs/lib/env.py
@@ -10,6 +10,7 @@ from lxml import etree
 from pcs.lib import reports
 from pcs.lib.external import (
     is_cman_cluster,
+    is_service_running,
     CommandRunner,
     NodeCommunicator,
 )
@@ -21,6 +22,7 @@ from pcs.lib.corosync.live import (
 from pcs.lib.nodes_task import (
     distribute_corosync_conf,
     check_corosync_offline_on_nodes,
+    qdevice_reload_on_nodes,
 )
 from pcs.lib.pacemaker import (
     get_cib,
@@ -152,11 +154,18 @@ class LibraryEnvironment(object):
                 corosync_conf_data,
                 skip_offline_nodes
             )
-            if not corosync_conf_facade.need_stopped_cluster:
+            if is_service_running(self.cmd_runner(), "corosync"):
                 reload_corosync_config(self.cmd_runner())
                 self.report_processor.process(
                     reports.corosync_config_reloaded()
                 )
+            if corosync_conf_facade.need_qdevice_reload:
+                qdevice_reload_on_nodes(
+                    self.node_communicator(),
+                    self.report_processor,
+                    node_list,
+                    skip_offline_nodes
+                )
         else:
             self._corosync_conf_data = corosync_conf_data
 
diff --git a/pcs/lib/errors.py b/pcs/lib/errors.py
index c0bd3d1..9cab5e9 100644
--- a/pcs/lib/errors.py
+++ b/pcs/lib/errors.py
@@ -42,4 +42,8 @@ class ReportItem(object):
         self.message = self.message_pattern.format(**self.info)
 
     def __repr__(self):
-        return self.code+": "+str(self.info)
+        return "{severity} {code}: {info}".format(
+            severity=self.severity,
+            code=self.code,
+            info=self.info
+        )
diff --git a/pcs/lib/external.py b/pcs/lib/external.py
index 34426f9..c773e5a 100644
--- a/pcs/lib/external.py
+++ b/pcs/lib/external.py
@@ -49,7 +49,11 @@ except ImportError:
 
 from pcs.lib import reports
 from pcs.lib.errors import LibraryError, ReportItemSeverity
-from pcs.common.tools import simple_cache
+from pcs.common import report_codes
+from pcs.common.tools import (
+    simple_cache,
+    run_parallel as tools_run_parallel,
+)
 from pcs import settings
 
 
@@ -521,7 +525,7 @@ class NodeCommunicator(object):
                 # text in response body with HTTP code 400
                 # we need to be backward compatible with that
                 raise NodeCommandUnsuccessfulException(
-                    host, request, response_data
+                    host, request, response_data.rstrip()
                 )
             elif e.code == 401:
                 raise NodeAuthenticationException(
@@ -581,3 +585,39 @@ class NodeCommunicator(object):
                 base64.b64encode(" ".join(self._groups).encode("utf-8"))
             ))
         return cookies
+
+
+def parallel_nodes_communication_helper(
+    func, func_args_kwargs, reporter, skip_offline_nodes=False
+):
+    """
+    Help running node calls in parallel and handle communication exceptions.
+    Raise LibraryError on any failure.
+
+    function func function to be run, should be a function calling a node
+    iterable func_args_kwargs list of tuples: (*args, **kwargs)
+    bool skip_offline_nodes do not raise LibraryError if a node is unreachable
+    """
+    failure_severity = ReportItemSeverity.ERROR
+    failure_forceable = report_codes.SKIP_OFFLINE_NODES
+    if skip_offline_nodes:
+        failure_severity = ReportItemSeverity.WARNING
+        failure_forceable = None
+    report_items = []
+
+    def _parallel(*args, **kwargs):
+        try:
+            func(*args, **kwargs)
+        except NodeCommunicationException as e:
+            report_items.append(
+                node_communicator_exception_to_report_item(
+                    e,
+                    failure_severity,
+                    failure_forceable
+                )
+            )
+        except LibraryError as e:
+            report_items.extend(e.args)
+
+    tools_run_parallel(_parallel, func_args_kwargs)
+    reporter.process_list(report_items)
diff --git a/pcs/lib/nodes_task.py b/pcs/lib/nodes_task.py
index b9a61f6..e94d327 100644
--- a/pcs/lib/nodes_task.py
+++ b/pcs/lib/nodes_task.py
@@ -8,14 +8,19 @@ from __future__ import (
 import json
 
 from pcs.common import report_codes
+from pcs.common.tools import run_parallel as tools_run_parallel
 from pcs.lib import reports
-from pcs.lib.errors import ReportItemSeverity
+from pcs.lib.errors import LibraryError, ReportItemSeverity
 from pcs.lib.external import (
     NodeCommunicator,
     NodeCommunicationException,
     node_communicator_exception_to_report_item,
+    parallel_nodes_communication_helper,
+)
+from pcs.lib.corosync import (
+    live as corosync_live,
+    qdevice_client,
 )
-from pcs.lib.corosync import live as corosync_live
 
 
 def distribute_corosync_conf(
@@ -33,11 +38,9 @@ def distribute_corosync_conf(
     if skip_offline_nodes:
         failure_severity = ReportItemSeverity.WARNING
         failure_forceable = None
-
-    reporter.process(reports.corosync_config_distribution_started())
     report_items = []
-    # TODO use parallel communication
-    for node in node_addr_list:
+
+    def _parallel(node):
         try:
             corosync_live.set_remote_corosync_conf(
                 node_communicator,
@@ -62,6 +65,12 @@ def distribute_corosync_conf(
                     failure_forceable
                 )
             )
+
+    reporter.process(reports.corosync_config_distribution_started())
+    tools_run_parallel(
+        _parallel,
+        [((node, ), {}) for node in node_addr_list]
+    )
     reporter.process_list(report_items)
 
 def check_corosync_offline_on_nodes(
@@ -77,13 +86,11 @@ def check_corosync_offline_on_nodes(
     if skip_offline_nodes:
         failure_severity = ReportItemSeverity.WARNING
         failure_forceable = None
-
-    reporter.process(reports.corosync_not_running_check_started())
     report_items = []
-    # TODO use parallel communication
-    for node in node_addr_list:
+
+    def _parallel(node):
         try:
-            status = node_communicator.call_node(node, "remote/status", "")
+            status = node_communicator.call_node(node, "remote/status", None)
             if not json.loads(status)["corosync"]:
                 reporter.process(
                     reports.corosync_not_running_on_node_ok(node.label)
@@ -115,8 +122,48 @@ def check_corosync_offline_on_nodes(
                     failure_forceable
                 )
             )
+
+    reporter.process(reports.corosync_not_running_check_started())
+    tools_run_parallel(
+        _parallel,
+        [((node, ), {}) for node in node_addr_list]
+    )
     reporter.process_list(report_items)
 
+def qdevice_reload_on_nodes(
+    node_communicator, reporter, node_addr_list, skip_offline_nodes=False
+):
+    """
+    Reload corosync-qdevice configuration on cluster nodes
+    NodeAddressesList node_addr_list nodes to reload config on
+    bool skip_offline_nodes don't raise an error on node communication errors
+    """
+    reporter.process(reports.qdevice_client_reload_started())
+    parallel_params = [
+        [(reporter, node_communicator, node), {}]
+        for node in node_addr_list
+    ]
+    # catch an exception so we try to start qdevice on nodes where we stopped it
+    report_items = []
+    try:
+        parallel_nodes_communication_helper(
+            qdevice_client.remote_client_stop,
+            parallel_params,
+            reporter,
+            skip_offline_nodes
+        )
+    except LibraryError as e:
+        report_items.extend(e.args)
+    try:
+        parallel_nodes_communication_helper(
+            qdevice_client.remote_client_start,
+            parallel_params,
+            reporter,
+            skip_offline_nodes
+        )
+    except LibraryError as e:
+        report_items.extend(e.args)
+    reporter.process_list(report_items)
 
 def node_check_auth(communicator, node):
     """
diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py
index 490b4ff..d8f88cd 100644
--- a/pcs/lib/reports.py
+++ b/pcs/lib/reports.py
@@ -552,6 +552,19 @@ def corosync_running_on_node_fail(node):
         info={"node": node}
     )
 
+def corosync_quorum_get_status_error(reason):
+    """
+    unable to get runtime status of quorum on local node
+    string reason an error message
+    """
+    return ReportItem.error(
+        report_codes.COROSYNC_QUORUM_GET_STATUS_ERROR,
+        "Unable to get quorum status: {reason}",
+        info={
+            "reason": reason,
+        }
+    )
+
 def corosync_config_reloaded():
     """
     corosync configuration has been reloaded
@@ -614,6 +627,21 @@ def corosync_config_parser_other_error():
         "Unable to parse corosync config"
     )
 
+def corosync_options_incompatible_with_qdevice(options):
+    """
+    cannot set specified corosync options when qdevice is in use
+    iterable options incompatible options names
+    """
+    return ReportItem.error(
+        report_codes.COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE,
+        "These options cannot be set when the cluster uses a quorum device: "
+        + "{options_names_str}",
+        info={
+            "options_names": options,
+            "options_names_str": ", ".join(sorted(options)),
+        }
+    )
+
 def qdevice_already_defined():
     """
     qdevice is already set up in a cluster, when it was expected not to be
@@ -641,6 +669,15 @@ def qdevice_remove_or_cluster_stop_needed():
         "You need to stop the cluster or remove qdevice from cluster to continue"
     )
 
+def qdevice_client_reload_started():
+    """
+    qdevice client configuration is about to be reloaded on nodes
+    """
+    return ReportItem.info(
+        report_codes.QDEVICE_CLIENT_RELOAD_STARTED,
+        "Reloading qdevice configuration on nodes..."
+    )
+
 def qdevice_already_initialized(model):
     """
     cannot create qdevice on local host, it has been already created
@@ -654,6 +691,19 @@ def qdevice_already_initialized(model):
         }
     )
 
+def qdevice_not_initialized(model):
+    """
+    cannot work with qdevice on local host, it has not been created yet
+    string model qdevice model
+    """
+    return ReportItem.error(
+        report_codes.QDEVICE_NOT_INITIALIZED,
+        "Quorum device '{model}' has not been initialized yet",
+        info={
+            "model": model,
+        }
+    )
+
 def qdevice_initialization_success(model):
     """
     qdevice was successfully initialized on local host
@@ -682,6 +732,72 @@ def qdevice_initialization_error(model, reason):
         }
     )
 
+def qdevice_certificate_distribution_started():
+    """
+    Qdevice certificates are about to be set up on nodes
+    """
+    return ReportItem.info(
+        report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+        "Setting up qdevice certificates on nodes..."
+    )
+
+def qdevice_certificate_accepted_by_node(node):
+    """
+    Qdevice certificates have been saved to a node
+    string node node on which certificates have been saved
+    """
+    return ReportItem.info(
+        report_codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE,
+        "{node}: Succeeded",
+        info={"node": node}
+    )
+
+def qdevice_certificate_removal_started():
+    """
+    Qdevice certificates are about to be removed from nodes
+    """
+    return ReportItem.info(
+        report_codes.QDEVICE_CERTIFICATE_REMOVAL_STARTED,
+        "Removing qdevice certificates from nodes..."
+    )
+
+def qdevice_certificate_removed_from_node(node):
+    """
+    Qdevice certificates have been removed from a node
+    string node node on which certificates have been deleted
+    """
+    return ReportItem.info(
+        report_codes.QDEVICE_CERTIFICATE_REMOVED_FROM_NODE,
+        "{node}: Succeeded",
+        info={"node": node}
+    )
+
+def qdevice_certificate_import_error(reason):
+    """
+    an error occured when importing qdevice certificate to a node
+    string reason an error message
+    """
+    return ReportItem.error(
+        report_codes.QDEVICE_CERTIFICATE_IMPORT_ERROR,
+        "Unable to import quorum device certificate: {reason}",
+        info={
+            "reason": reason,
+        }
+    )
+
+def qdevice_certificate_sign_error(reason):
+    """
+    an error occured when signing qdevice certificate
+    string reason an error message
+    """
+    return ReportItem.error(
+        report_codes.QDEVICE_CERTIFICATE_SIGN_ERROR,
+        "Unable to sign quorum device certificate: {reason}",
+        info={
+            "reason": reason,
+        }
+    )
+
 def qdevice_destroy_success(model):
     """
     qdevice configuration successfully removed from local host
@@ -710,6 +826,21 @@ def qdevice_destroy_error(model, reason):
         }
     )
 
+def qdevice_get_status_error(model, reason):
+    """
+    unable to get runtime status of qdevice
+    string model qdevice model
+    string reason an error message
+    """
+    return ReportItem.error(
+        report_codes.QDEVICE_GET_STATUS_ERROR,
+        "Unable to get status of quorum device '{model}': {reason}",
+        info={
+            "model": model,
+            "reason": reason,
+        }
+    )
+
 def cman_unsupported_command():
     """
     requested library command is not available as local cluster is CMAN based
@@ -1022,31 +1153,55 @@ def service_start_started(service):
         }
     )
 
-def service_start_error(service, reason):
+def service_start_error(service, reason, node=None):
     """
     system service start failed
     string service service name or description
     string reason error message
+    string node node on which service has been requested to start
     """
+    msg = "Unable to start {service}: {reason}"
     return ReportItem.error(
         report_codes.SERVICE_START_ERROR,
-        "Unable to start {service}: {reason}",
+        msg if node is None else "{node}: " + msg,
         info={
             "service": service,
             "reason": reason,
+            "node": node,
         }
     )
 
-def service_start_success(service):
+def service_start_success(service, node=None):
     """
     system service was started successfully
     string service service name or description
+    string node node on which service has been requested to start
     """
+    msg = "{service} started"
     return ReportItem.info(
         report_codes.SERVICE_START_SUCCESS,
-        "{service} started",
+        msg if node is None else "{node}: " + msg,
         info={
             "service": service,
+            "node": node,
+        }
+    )
+
+def service_start_skipped(service, reason, node=None):
+    """
+    starting system service was skipped, no error occured
+    string service service name or description
+    string reason why the start has been skipped
+    string node node on which service has been requested to start
+    """
+    msg = "not starting {service} - {reason}"
+    return ReportItem.info(
+        report_codes.SERVICE_START_SKIPPED,
+        msg if node is None else "{node}: " + msg,
+        info={
+            "service": service,
+            "reason": reason,
+            "node": node,
         }
     )
 
@@ -1063,31 +1218,37 @@ def service_stop_started(service):
         }
     )
 
-def service_stop_error(service, reason):
+def service_stop_error(service, reason, node=None):
     """
     system service stop failed
     string service service name or description
     string reason error message
+    string node node on which service has been requested to stop
     """
+    msg = "Unable to stop {service}: {reason}"
     return ReportItem.error(
         report_codes.SERVICE_STOP_ERROR,
-        "Unable to stop {service}: {reason}",
+        msg if node is None else "{node}: " + msg,
         info={
             "service": service,
             "reason": reason,
+            "node": node,
         }
     )
 
-def service_stop_success(service):
+def service_stop_success(service, node=None):
     """
     system service was stopped successfully
     string service service name or description
+    string node node on which service has been requested to stop
     """
+    msg = "{service} stopped"
     return ReportItem.info(
         report_codes.SERVICE_STOP_SUCCESS,
-        "{service} stopped",
+        msg if node is None else "{node}: " + msg,
         info={
             "service": service,
+            "node": node,
         }
     )
 
@@ -1121,6 +1282,19 @@ def service_kill_success(services):
         }
     )
 
+def service_enable_started(service):
+    """
+    system service is being enabled
+    string service service name or description
+    """
+    return ReportItem.info(
+        report_codes.SERVICE_ENABLE_STARTED,
+        "Enabling {service}...",
+        info={
+            "service": service,
+        }
+    )
+
 def service_enable_error(service, reason, node=None):
     """
     system service enable failed
@@ -1143,7 +1317,7 @@ def service_enable_success(service, node=None):
     """
     system service was enabled successfully
     string service service name or description
-    string node node on which service was enabled
+    string node node on which service has been enabled
     """
     msg = "{service} enabled"
     return ReportItem.info(
@@ -1155,6 +1329,37 @@ def service_enable_success(service, node=None):
         }
     )
 
+def service_enable_skipped(service, reason, node=None):
+    """
+    enabling system service was skipped, no error occured
+    string service service name or description
+    string reason why the enabling has been skipped
+    string node node on which service has been requested to enable
+    """
+    msg = "not enabling {service} - {reason}"
+    return ReportItem.info(
+        report_codes.SERVICE_ENABLE_SKIPPED,
+        msg if node is None else "{node}: " + msg,
+        info={
+            "service": service,
+            "reason": reason,
+            "node": node,
+        }
+    )
+
+def service_disable_started(service):
+    """
+    system service is being disabled
+    string service service name or description
+    """
+    return ReportItem.info(
+        report_codes.SERVICE_DISABLE_STARTED,
+        "Disabling {service}...",
+        info={
+            "service": service,
+        }
+    )
+
 def service_disable_error(service, reason, node=None):
     """
     system service disable failed
@@ -1189,7 +1394,6 @@ def service_disable_success(service, node=None):
         }
     )
 
-
 def invalid_metadata_format(severity=ReportItemSeverity.ERROR, forceable=None):
     """
     Invalid format of metadata
@@ -1201,7 +1405,6 @@ def invalid_metadata_format(severity=ReportItemSeverity.ERROR, forceable=None):
         forceable=forceable
     )
 
-
 def unable_to_get_agent_metadata(
     agent, reason, severity=ReportItemSeverity.ERROR, forceable=None
 ):
diff --git a/pcs/pcs.8 b/pcs/pcs.8
index 425b613..a72a9bd 100644
--- a/pcs/pcs.8
+++ b/pcs/pcs.8
@@ -518,8 +518,11 @@ rule remove <rule id>
 Remove a rule if a rule id is specified, if rule is last rule in its constraint, the constraint will be removed.
 .SS "qdevice"
 .TP
+status <device model> [\fB\-\-full\fR] [<cluster name>]
+Show runtime status of specified model of quorum device provider.  Using \fB\-\-full\fR will give more detailed output.  If <cluster name> is specified, only information about the specified cluster will be displayed.
+.TP
 setup model <device model> [\fB\-\-enable\fR] [\fB\-\-start\fR]
-Configure specified model of quorum device provider.  Quorum device then may be added to clusters by "pcs quorum device add" command.  \fB\-\-start\fR will also start the provider.  \fB\-\-enable\fR will configure the provider to start on boot.
+Configure specified model of quorum device provider.  Quorum device then can be added to clusters by running "pcs quorum device add" command in a cluster.  \fB\-\-start\fR will also start the provider.  \fB\-\-enable\fR will configure the provider to start on boot.
 .TP
 destroy <device model>
 Disable and stop specified model of quorum device provider and delete its configuration files.
@@ -531,7 +534,7 @@ stop <device model>
 Stop specified model of quorum device provider.
 .TP
 kill <device model>
-Force specified model of quorum device provider to stop (performs kill -9).
+Force specified model of quorum device provider to stop (performs kill \-9).  Note that init system (e.g. systemd) can detect that the qdevice is not running and start it again.  If you want to stop the qdevice, run "pcs qdevice stop" command.
 .TP
 enable <device model>
 Configure specified model of quorum device provider to start on boot.
@@ -543,14 +546,22 @@ Configure specified model of quorum device provider to not start on boot.
 config
 Show quorum configuration.
 .TP
-device add [generic options] model <device model> [model options]
-Add quorum device to cluster.  Quorum device needs to be created first by "pcs qdevice setup" command.
+status
+Show quorum runtime status.
+.TP
+device add [<generic options>] model <device model> [<model options>]
+Add a quorum device to the cluster.  Quorum device needs to be created first by "pcs qdevice setup" command.  It is not possible to use more than one quorum device in a cluster simultaneously.  Generic options, model and model options are all documented in corosync's corosync\-qdevice(8) man page.
 .TP
 device remove
-Remove quorum device from cluster.
+Remove a quorum device from the cluster.
 .TP
-device update [generic options] [model <model options>]
-Add/Change quorum device options.  Requires cluster to be stopped.
+device status [\fB\-\-full\fR]
+Show quorum device runtime status.  Using \fB\-\-full\fR will give more detailed output.
+.TP
+device update [<generic options>] [model <model options>]
+Add/Change quorum device options.  Generic options and model options are all documented in corosync's corosync\-qdevice(8) man page.  Requires the cluster to be stopped.
+
+WARNING: If you want to change "host" option of qdevice model net, use "pcs quorum device remove" and "pcs quorum device add" commands to set up configuration properly unless old and new host is the same machine.
 .TP
 unblock [\fB\-\-force\fR]
 Cancel waiting for all nodes when establishing quorum.  Useful in situations where you know the cluster is inquorate, but you are confident that the cluster should proceed with resource management regardless.  This command should ONLY be used when nodes which the cluster is waiting for have been confirmed to be powered off and to have no access to shared resources.
@@ -558,7 +569,7 @@ Cancel waiting for all nodes when establishing quorum.  Useful in situations whe
 .B WARNING: If the nodes are not actually powered off or they do have access to shared resources, data corruption/cluster failure can occur. To prevent accidental running of this command, \-\-force or interactive user response is required in order to proceed.
 .TP
 update [auto_tie_breaker=[0|1]] [last_man_standing=[0|1]] [last_man_standing_window=[<time in ms>]] [wait_for_all=[0|1]]
-Add/Change quorum options.  At least one option must be specified.  Options are documented in corosync's votequorum(5) man page.  Requires cluster to be stopped.
+Add/Change quorum options.  At least one option must be specified.  Options are documented in corosync's votequorum(5) man page.  Requires the cluster to be stopped.
 .SS "status"
 .TP
 [status] [\fB\-\-full\fR | \fB\-\-hide-inactive\fR]
diff --git a/pcs/qdevice.py b/pcs/qdevice.py
index 1f06709..0037704 100644
--- a/pcs/qdevice.py
+++ b/pcs/qdevice.py
@@ -23,6 +23,8 @@ def qdevice_cmd(lib, argv, modifiers):
     try:
         if sub_cmd == "help":
             usage.qdevice(argv)
+        elif sub_cmd == "status":
+            qdevice_status_cmd(lib, argv_next, modifiers)
         elif sub_cmd == "setup":
             qdevice_setup_cmd(lib, argv_next, modifiers)
         elif sub_cmd == "destroy":
@@ -37,6 +39,11 @@ def qdevice_cmd(lib, argv, modifiers):
             qdevice_enable_cmd(lib, argv_next, modifiers)
         elif sub_cmd == "disable":
             qdevice_disable_cmd(lib, argv_next, modifiers)
+        # following commands are internal use only, called from pcsd
+        elif sub_cmd == "sign-net-cert-request":
+            qdevice_sign_net_cert_request_cmd(lib, argv_next, modifiers)
+        elif sub_cmd == "net-client":
+            qdevice_net_client_cmd(lib, argv_next, modifiers)
         else:
             raise CmdLineInputError()
     except LibraryError as e:
@@ -44,6 +51,35 @@ def qdevice_cmd(lib, argv, modifiers):
     except CmdLineInputError as e:
         utils.exit_on_cmdline_input_errror(e, "qdevice", sub_cmd)
 
+# this is internal use only, called from pcsd
+def qdevice_net_client_cmd(lib, argv, modifiers):
+    if len(argv) < 1:
+        utils.err("invalid command")
+
+    sub_cmd, argv_next = argv[0], argv[1:]
+    try:
+        if sub_cmd == "setup":
+            qdevice_net_client_setup_cmd(lib, argv_next, modifiers)
+        elif sub_cmd == "import-certificate":
+            qdevice_net_client_import_certificate_cmd(lib, argv_next, modifiers)
+        elif sub_cmd == "destroy":
+            qdevice_net_client_destroy(lib, argv_next, modifiers)
+        else:
+            raise CmdLineInputError("invalid command")
+    except LibraryError as e:
+        utils.process_library_reports(e.args)
+    except CmdLineInputError as e:
+        utils.err(e.message)
+
+def qdevice_status_cmd(lib, argv, modifiers):
+    if len(argv) < 1 or len(argv) > 2:
+        raise CmdLineInputError()
+    model = argv[0]
+    cluster = None if len(argv) < 2 else argv[1]
+    print(
+        lib.qdevice.status(model, modifiers["full"], cluster)
+    )
+
 def qdevice_setup_cmd(lib, argv, modifiers):
     if len(argv) != 2:
         raise CmdLineInputError()
@@ -87,3 +123,38 @@ def qdevice_disable_cmd(lib, argv, modifiers):
         raise CmdLineInputError()
     model = argv[0]
     lib.qdevice.disable(model)
+
+# following commands are internal use only, called from pcsd
+
+def qdevice_net_client_setup_cmd(lib, argv, modifiers):
+    ca_certificate = _read_stdin()
+    lib.qdevice.client_net_setup(ca_certificate)
+
+def qdevice_net_client_import_certificate_cmd(lib, argv, modifiers):
+    certificate = _read_stdin()
+    lib.qdevice.client_net_import_certificate(certificate)
+
+def qdevice_net_client_destroy(lib, argv, modifiers):
+    lib.qdevice.client_net_destroy()
+
+def qdevice_sign_net_cert_request_cmd(lib, argv, modifiers):
+    certificate_request = _read_stdin()
+    signed = lib.qdevice.sign_net_cert_request(
+        certificate_request,
+        modifiers["name"]
+    )
+    if sys.version_info.major > 2:
+        # In python3 base64.b64encode returns bytes.
+        # In python2 base64.b64encode returns string.
+        # Bytes is printed like this: b'bytes content'
+        # and we need to get rid of that b'', so we change bytes to string.
+        # Since it's base64encoded, it's safe to use ascii.
+        signed = signed.decode("ascii")
+    print(signed)
+
+def _read_stdin():
+    # in python3 stdin returns str so we need to use buffer
+    if hasattr(sys.stdin, "buffer"):
+        return sys.stdin.buffer.read()
+    else:
+        return sys.stdin.read()
diff --git a/pcs/quorum.py b/pcs/quorum.py
index f793a21..27085ac 100644
--- a/pcs/quorum.py
+++ b/pcs/quorum.py
@@ -28,6 +28,8 @@ def quorum_cmd(lib, argv, modificators):
             usage.quorum(argv)
         elif sub_cmd == "config":
             quorum_config_cmd(lib, argv_next, modificators)
+        elif sub_cmd == "status":
+            quorum_status_cmd(lib, argv_next, modificators)
         elif sub_cmd == "device":
             quorum_device_cmd(lib, argv_next, modificators)
         elif sub_cmd == "unblock":
@@ -51,6 +53,8 @@ def quorum_device_cmd(lib, argv, modificators):
             quorum_device_add_cmd(lib, argv_next, modificators)
         elif sub_cmd == "remove":
             quorum_device_remove_cmd(lib, argv_next, modificators)
+        elif sub_cmd == "status":
+            quorum_device_status_cmd(lib, argv_next, modificators)
         elif sub_cmd == "update":
             quorum_device_update_cmd(lib, argv_next, modificators)
         else:
@@ -97,6 +101,21 @@ def quorum_config_to_str(config):
 
     return lines
 
+def quorum_status_cmd(lib, argv, modificators):
+    if argv:
+        raise CmdLineInputError()
+    print(lib.quorum.status())
+
+def quorum_update_cmd(lib, argv, modificators):
+    options = parse_args.prepare_options(argv)
+    if not options:
+        raise CmdLineInputError()
+
+    lib.quorum.set_options(
+        options,
+        skip_offline_nodes=modificators["skip_offline_nodes"]
+    )
+
 def quorum_device_add_cmd(lib, argv, modificators):
     # we expect "model" keyword once, followed by the actual model value
     options_lists = parse_args.split_list(argv, "model")
@@ -131,6 +150,11 @@ def quorum_device_remove_cmd(lib, argv, modificators):
         skip_offline_nodes=modificators["skip_offline_nodes"]
     )
 
+def quorum_device_status_cmd(lib, argv, modificators):
+    if argv:
+        raise CmdLineInputError()
+    print(lib.quorum.status_device(modificators["full"]))
+
 def quorum_device_update_cmd(lib, argv, modificators):
     # we expect "model" keyword once
     options_lists = parse_args.split_list(argv, "model")
@@ -154,13 +178,3 @@ def quorum_device_update_cmd(lib, argv, modificators):
         force_options=modificators["force"],
         skip_offline_nodes=modificators["skip_offline_nodes"]
     )
-
-def quorum_update_cmd(lib, argv, modificators):
-    options = parse_args.prepare_options(argv)
-    if not options:
-        raise CmdLineInputError()
-
-    lib.quorum.set_options(
-        options,
-        skip_offline_nodes=modificators["skip_offline_nodes"]
-    )
diff --git a/pcs/settings_default.py b/pcs/settings_default.py
index 3acd8e0..9d44918 100644
--- a/pcs/settings_default.py
+++ b/pcs/settings_default.py
@@ -2,18 +2,20 @@ import os.path
 
 pacemaker_binaries = "/usr/sbin/"
 corosync_binaries = "/usr/sbin/"
+corosync_qnet_binaries = "/usr/bin/"
 ccs_binaries = "/usr/sbin/"
 corosync_conf_dir = "/etc/corosync/"
 corosync_conf_file = os.path.join(corosync_conf_dir, "corosync.conf")
 corosync_uidgid_dir = os.path.join(corosync_conf_dir, "uidgid.d/")
 corosync_qdevice_net_server_certs_dir = os.path.join(
     corosync_conf_dir,
-    "qdevice/net/qnetd/nssdb"
+    "qnetd/nssdb"
 )
 corosync_qdevice_net_client_certs_dir = os.path.join(
     corosync_conf_dir,
-    "qdevice/net/node/nssdb"
+    "qdevice/net/nssdb"
 )
+corosync_qdevice_net_client_ca_file_name = "qnetd-cacert.crt"
 cluster_conf_file = "/etc/cluster/cluster.conf"
 fence_agent_binaries = "/usr/sbin/"
 pengine_binary = "/usr/libexec/pacemaker/pengine"
diff --git a/pcs/test/resources/qdevice-certs/qnetd-cacert.crt b/pcs/test/resources/qdevice-certs/qnetd-cacert.crt
new file mode 100644
index 0000000..34dcab0
--- /dev/null
+++ b/pcs/test/resources/qdevice-certs/qnetd-cacert.crt
@@ -0,0 +1 @@
+certificate data
\ No newline at end of file
diff --git a/pcs/test/test_lib_commands_qdevice.py b/pcs/test/test_lib_commands_qdevice.py
index 3900c1d..ff588d5 100644
--- a/pcs/test/test_lib_commands_qdevice.py
+++ b/pcs/test/test_lib_commands_qdevice.py
@@ -6,6 +6,7 @@ from __future__ import (
 )
 
 from unittest import TestCase
+import base64
 import logging
 
 from pcs.test.tools.pcs_mock import mock
@@ -58,6 +59,11 @@ class QdeviceDisabledOnCmanTest(QdeviceTestCase):
             lambda: lib.qdevice_destroy(self.lib_env, "bad model")
         )
 
+    def test_status_text(self):
+        self.base_test(
+            lambda: lib.qdevice_status_text(self.lib_env, "bad model")
+        )
+
     def test_enable(self):
         self.base_test(
             lambda: lib.qdevice_enable(self.lib_env, "bad model")
@@ -83,6 +89,30 @@ class QdeviceDisabledOnCmanTest(QdeviceTestCase):
             lambda: lib.qdevice_kill(self.lib_env, "bad model")
         )
 
+    def test_qdevice_net_sign_certificate_request(self):
+        self.base_test(
+            lambda: lib.qdevice_net_sign_certificate_request(
+                self.lib_env,
+                "certificate request",
+                "cluster name"
+            )
+        )
+
+    def test_client_net_setup(self):
+        self.base_test(
+            lambda: lib.client_net_setup(self.lib_env, "ca certificate")
+        )
+
+    def test_client_net_import_certificate(self):
+        self.base_test(
+            lambda: lib.client_net_import_certificate(self.lib_env, "cert")
+        )
+
+    def test_client_net_destroy(self):
+        self.base_test(
+            lambda: lib.client_net_destroy(self.lib_env)
+        )
+
 
 @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
 class QdeviceBadModelTest(QdeviceTestCase):
@@ -110,6 +140,11 @@ class QdeviceBadModelTest(QdeviceTestCase):
             lambda: lib.qdevice_destroy(self.lib_env, "bad model")
         )
 
+    def test_status_text(self):
+        self.base_test(
+            lambda: lib.qdevice_status_text(self.lib_env, "bad model")
+        )
+
     def test_enable(self):
         self.base_test(
             lambda: lib.qdevice_enable(self.lib_env, "bad model")
@@ -489,6 +524,80 @@ class QdeviceNetDestroyTest(QdeviceTestCase):
         )
 
 
+@mock.patch("pcs.lib.commands.qdevice.qdevice_net.qdevice_status_cluster_text")
+@mock.patch("pcs.lib.commands.qdevice.qdevice_net.qdevice_status_generic_text")
+@mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+class TestQdeviceNetStatusTextTest(QdeviceTestCase):
+    def test_success(self, mock_status_generic, mock_status_cluster):
+        mock_status_generic.return_value = "generic status info\n"
+        mock_status_cluster.return_value = "cluster status info\n"
+
+        self.assertEquals(
+            lib.qdevice_status_text(self.lib_env, "net"),
+             "generic status info\ncluster status info\n"
+        )
+
+        mock_status_generic.assert_called_once_with("mock_runner", False)
+        mock_status_cluster.assert_called_once_with("mock_runner", None, False)
+
+    def test_success_verbose(self, mock_status_generic, mock_status_cluster):
+        mock_status_generic.return_value = "generic status info\n"
+        mock_status_cluster.return_value = "cluster status info\n"
+
+        self.assertEquals(
+            lib.qdevice_status_text(self.lib_env, "net", verbose=True),
+             "generic status info\ncluster status info\n"
+        )
+
+        mock_status_generic.assert_called_once_with("mock_runner", True)
+        mock_status_cluster.assert_called_once_with("mock_runner", None, True)
+
+    def test_success_cluster(self, mock_status_generic, mock_status_cluster):
+        mock_status_generic.return_value = "generic status info\n"
+        mock_status_cluster.return_value = "cluster status info\n"
+
+        self.assertEquals(
+            lib.qdevice_status_text(self.lib_env, "net", cluster="name"),
+             "generic status info\ncluster status info\n"
+        )
+
+        mock_status_generic.assert_called_once_with("mock_runner", False)
+        mock_status_cluster.assert_called_once_with("mock_runner", "name", False)
+
+    def test_error_generic_status(
+        self, mock_status_generic, mock_status_cluster
+    ):
+        mock_status_generic.side_effect = LibraryError("mock_report_item")
+        mock_status_cluster.return_value = "cluster status info\n"
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib.qdevice_status_text(self.lib_env, "net")
+        )
+
+        mock_status_generic.assert_called_once_with("mock_runner", False)
+        mock_status_cluster.assert_not_called()
+
+    def test_error_cluster_status(
+        self, mock_status_generic, mock_status_cluster
+    ):
+        mock_status_generic.return_value = "generic status info\n"
+        mock_status_cluster.side_effect = LibraryError("mock_report_item")
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib.qdevice_status_text(self.lib_env, "net")
+        )
+
+        mock_status_generic.assert_called_once_with("mock_runner", False)
+        mock_status_cluster.assert_called_once_with("mock_runner", None, False)
+
+
 @mock.patch("pcs.lib.external.enable_service")
 @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
 @mock.patch.object(
@@ -757,3 +866,149 @@ class QdeviceNetKillTest(QdeviceTestCase):
             "mock_runner",
             ["corosync-qnetd"]
         )
+
+
+@mock.patch(
+    "pcs.lib.commands.qdevice.qdevice_net.qdevice_sign_certificate_request"
+)
+@mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+class QdeviceNetSignCertificateRequestTest(QdeviceTestCase):
+    def test_success(self, mock_qdevice_func):
+        qdevice_func_input = "certificate request".encode("utf-8")
+        qdevice_func_output = "signed certificate".encode("utf-8")
+        mock_qdevice_func.return_value = qdevice_func_output
+        cluster_name = "clusterName"
+
+        self.assertEqual(
+            base64.b64encode(qdevice_func_output),
+            lib.qdevice_net_sign_certificate_request(
+                self.lib_env,
+                base64.b64encode(qdevice_func_input),
+                cluster_name
+            )
+        )
+
+        mock_qdevice_func.assert_called_once_with(
+            "mock_runner",
+            qdevice_func_input,
+            cluster_name
+        )
+
+    def test_bad_input(self, mock_qdevice_func):
+        qdevice_func_input = "certificate request".encode("utf-8")
+        cluster_name = "clusterName"
+
+        assert_raise_library_error(
+            lambda: lib.qdevice_net_sign_certificate_request(
+                self.lib_env,
+                qdevice_func_input,
+                cluster_name
+            ),
+            (
+                severity.ERROR,
+                report_codes.INVALID_OPTION_VALUE,
+                {
+                    "option_name": "qnetd certificate request",
+                    "option_value": qdevice_func_input,
+                    "allowed_values": ["base64 encoded certificate"],
+                }
+            )
+        )
+
+        mock_qdevice_func.assert_not_called()
+
+
+@mock.patch("pcs.lib.commands.qdevice.qdevice_net.client_setup")
+@mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+class ClientNetSetupTest(QdeviceTestCase):
+    def test_success(self, mock_qdevice_func):
+        qdevice_func_input = "CA certificate".encode("utf-8")
+
+        lib.client_net_setup(self.lib_env, base64.b64encode(qdevice_func_input))
+
+        mock_qdevice_func.assert_called_once_with(
+            "mock_runner",
+            qdevice_func_input
+        )
+
+    def test_bad_input(self, mock_qdevice_func):
+        qdevice_func_input = "CA certificate".encode("utf-8")
+
+        assert_raise_library_error(
+            lambda: lib.client_net_setup(self.lib_env, qdevice_func_input),
+            (
+                severity.ERROR,
+                report_codes.INVALID_OPTION_VALUE,
+                {
+                    "option_name": "qnetd CA certificate",
+                    "option_value": qdevice_func_input,
+                    "allowed_values": ["base64 encoded certificate"],
+                }
+            )
+        )
+
+        mock_qdevice_func.assert_not_called()
+
+
+@mock.patch(
+    "pcs.lib.commands.qdevice.qdevice_net.client_import_certificate_and_key"
+)
+@mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+class ClientNetImportCertificateTest(QdeviceTestCase):
+    def test_success(self, mock_qdevice_func):
+        qdevice_func_input = "client certificate".encode("utf-8")
+
+        lib.client_net_import_certificate(
+            self.lib_env,
+            base64.b64encode(qdevice_func_input)
+        )
+
+        mock_qdevice_func.assert_called_once_with(
+            "mock_runner",
+            qdevice_func_input
+        )
+
+    def test_bad_input(self, mock_qdevice_func):
+        qdevice_func_input = "client certificate".encode("utf-8")
+
+        assert_raise_library_error(
+            lambda: lib.client_net_import_certificate(
+                self.lib_env,
+                qdevice_func_input
+            ),
+            (
+                severity.ERROR,
+                report_codes.INVALID_OPTION_VALUE,
+                {
+                    "option_name": "qnetd client certificate",
+                    "option_value": qdevice_func_input,
+                    "allowed_values": ["base64 encoded certificate"],
+                }
+            )
+        )
+
+        mock_qdevice_func.assert_not_called()
+
+
+@mock.patch("pcs.lib.commands.qdevice.qdevice_net.client_destroy")
+@mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+class ClientNetDestroyTest(QdeviceTestCase):
+    def test_success(self, mock_qdevice_func):
+        lib.client_net_destroy(self.lib_env)
+        mock_qdevice_func.assert_called_once_with()
+
diff --git a/pcs/test/test_lib_commands_quorum.py b/pcs/test/test_lib_commands_quorum.py
index 5725381..e824f37 100644
--- a/pcs/test/test_lib_commands_quorum.py
+++ b/pcs/test/test_lib_commands_quorum.py
@@ -21,7 +21,12 @@ from pcs.test.tools.pcs_mock import mock
 
 from pcs.common import report_codes
 from pcs.lib.env import LibraryEnvironment
-from pcs.lib.errors import ReportItemSeverity as severity
+from pcs.lib.errors import (
+    LibraryError,
+    ReportItemSeverity as severity,
+)
+from pcs.lib.external import NodeCommunicationException
+from pcs.lib.node import NodeAddresses, NodeAddressesList
 
 from pcs.lib.commands import quorum as lib
 
@@ -243,25 +248,102 @@ class SetQuorumOptionsTest(TestCase, CmanMixin):
         mock_push_corosync.assert_not_called()
 
 
+@mock.patch("pcs.lib.commands.quorum.corosync_live.get_quorum_status_text")
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+class StatusTextTest(TestCase, CmanMixin):
+    def setUp(self):
+        self.mock_logger = mock.MagicMock(logging.Logger)
+        self.mock_reporter = MockLibraryReportProcessor()
+        self.lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
+    def test_disabled_on_cman(self, mock_status):
+        self.assert_disabled_on_cman(
+            lambda: lib.status_text(self.lib_env)
+        )
+        mock_status.assert_not_called()
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+    def test_success(self, mock_status):
+        mock_status.return_value = "status text"
+        self.assertEqual(
+            lib.status_text(self.lib_env),
+            "status text"
+        )
+        mock_status.assert_called_once_with("mock_runner")
+
+
+@mock.patch("pcs.lib.commands.quorum.qdevice_client.get_status_text")
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+class StatusDeviceTextTest(TestCase, CmanMixin):
+    def setUp(self):
+        self.mock_logger = mock.MagicMock(logging.Logger)
+        self.mock_reporter = MockLibraryReportProcessor()
+        self.lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
+    def test_disabled_on_cman(self, mock_status):
+        self.assert_disabled_on_cman(
+            lambda: lib.status_device_text(self.lib_env)
+        )
+        mock_status.assert_not_called()
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+    def test_success(self, mock_status):
+        mock_status.return_value = "status text"
+        self.assertEqual(
+            lib.status_device_text(self.lib_env),
+            "status text"
+        )
+        mock_status.assert_called_once_with("mock_runner", False)
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+    def test_success_verbose(self, mock_status):
+        mock_status.return_value = "status text"
+        self.assertEqual(
+            lib.status_device_text(self.lib_env, True),
+            "status text"
+        )
+        mock_status.assert_called_once_with("mock_runner", True)
+
+
 @mock.patch.object(LibraryEnvironment, "push_corosync_conf")
 @mock.patch.object(LibraryEnvironment, "get_corosync_conf_data")
+@mock.patch("pcs.lib.commands.quorum._add_device_model_net")
+@mock.patch("pcs.lib.commands.quorum.qdevice_client.remote_client_enable")
+@mock.patch("pcs.lib.commands.quorum.qdevice_client.remote_client_start")
 class AddDeviceTest(TestCase, CmanMixin):
     def setUp(self):
         self.mock_logger = mock.MagicMock(logging.Logger)
         self.mock_reporter = MockLibraryReportProcessor()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
-    def test_disabled_on_cman(self, mock_get_corosync, mock_push_corosync):
+    def test_disabled_on_cman(
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
         self.assert_disabled_on_cman(
             lambda: lib.add_device(lib_env, "net", {"host": "127.0.0.1"}, {})
         )
         mock_get_corosync.assert_not_called()
         mock_push_corosync.assert_not_called()
+        mock_add_net.assert_not_called()
+        mock_client_enable.assert_not_called()
+        mock_client_start.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
     def test_enabled_on_cman_if_not_live(
-        self, mock_get_corosync, mock_push_corosync
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
     ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
@@ -287,9 +369,15 @@ class AddDeviceTest(TestCase, CmanMixin):
 
         self.assertEqual(1, mock_get_corosync.call_count)
         self.assertEqual(0, mock_push_corosync.call_count)
+        mock_add_net.assert_not_called()
+        mock_client_enable.assert_not_called()
+        mock_client_start.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
-    def test_success(self, mock_get_corosync, mock_push_corosync):
+    def test_success(
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
@@ -311,6 +399,70 @@ class AddDeviceTest(TestCase, CmanMixin):
     device {
         timeout: 12345
         model: net
+        votes: 1
+
+        net {
+            algorithm: ffsplit
+            host: 127.0.0.1
+        }
+    }
+"""
+            )
+        )
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_ENABLE_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_START_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
+            ]
+        )
+        self.assertEqual(1, len(mock_add_net.mock_calls))
+        self.assertEqual(3, len(mock_client_enable.mock_calls))
+        self.assertEqual(3, len(mock_client_start.mock_calls))
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+    def test_success_file(
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
+    ):
+        original_conf = open(rc("corosync-3nodes.conf")).read()
+        mock_get_corosync.return_value = original_conf
+        lib_env = LibraryEnvironment(
+            self.mock_logger,
+            self.mock_reporter,
+            corosync_conf_data=original_conf
+        )
+
+        lib.add_device(
+            lib_env,
+            "net",
+            {"host": "127.0.0.1", "algorithm": "ffsplit"},
+            {"timeout": "12345"}
+        )
+
+        self.assertEqual(1, len(mock_push_corosync.mock_calls))
+        ac(
+            mock_push_corosync.mock_calls[0][1][0].config.export(),
+            original_conf.replace(
+                "provider: corosync_votequorum\n",
+                """provider: corosync_votequorum
+
+    device {
+        timeout: 12345
+        model: net
+        votes: 1
 
         net {
             algorithm: ffsplit
@@ -321,9 +473,15 @@ class AddDeviceTest(TestCase, CmanMixin):
             )
         )
         self.assertEqual([], self.mock_reporter.report_item_list)
+        mock_add_net.assert_not_called()
+        mock_client_enable.assert_not_called()
+        mock_client_start.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
-    def test_invalid_options(self, mock_get_corosync, mock_push_corosync):
+    def test_invalid_options(
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
@@ -349,9 +507,15 @@ class AddDeviceTest(TestCase, CmanMixin):
 
         self.assertEqual(1, mock_get_corosync.call_count)
         self.assertEqual(0, mock_push_corosync.call_count)
+        mock_add_net.assert_not_called()
+        mock_client_enable.assert_not_called()
+        mock_client_start.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
-    def test_invalid_options_forced(self, mock_get_corosync, mock_push_corosync):
+    def test_invalid_options_forced(
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
@@ -375,7 +539,21 @@ class AddDeviceTest(TestCase, CmanMixin):
                         "option_type": "quorum device",
                         "allowed": ["sync_timeout", "timeout"],
                     }
-                )
+                ),
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_ENABLE_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_START_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
             ]
         )
         self.assertEqual(1, mock_get_corosync.call_count)
@@ -389,6 +567,7 @@ class AddDeviceTest(TestCase, CmanMixin):
     device {
         bad_option: bad_value
         model: net
+        votes: 1
 
         net {
             algorithm: ffsplit
@@ -398,9 +577,15 @@ class AddDeviceTest(TestCase, CmanMixin):
 """
             )
         )
+        self.assertEqual(1, len(mock_add_net.mock_calls))
+        self.assertEqual(3, len(mock_client_enable.mock_calls))
+        self.assertEqual(3, len(mock_client_start.mock_calls))
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
-    def test_invalid_model(self, mock_get_corosync, mock_push_corosync):
+    def test_invalid_model(
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
@@ -421,9 +606,15 @@ class AddDeviceTest(TestCase, CmanMixin):
 
         self.assertEqual(1, mock_get_corosync.call_count)
         self.assertEqual(0, mock_push_corosync.call_count)
+        mock_add_net.assert_not_called()
+        mock_client_enable.assert_not_called()
+        mock_client_start.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
-    def test_invalid_model_forced(self, mock_get_corosync, mock_push_corosync):
+    def test_invalid_model_forced(
+        self, mock_client_start, mock_client_enable, mock_add_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
@@ -441,7 +632,21 @@ class AddDeviceTest(TestCase, CmanMixin):
                         "option_value": "bad model",
                         "allowed_values": ("net", ),
                     },
-                )
+                ),
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_ENABLE_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_START_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
             ]
         )
         self.assertEqual(1, mock_get_corosync.call_count)
@@ -458,25 +663,678 @@ class AddDeviceTest(TestCase, CmanMixin):
 """
             )
         )
+        mock_add_net.assert_not_called() # invalid model - don't setup net model
+        self.assertEqual(3, len(mock_client_enable.mock_calls))
+        self.assertEqual(3, len(mock_client_start.mock_calls))
+
+
+@mock.patch(
+    "pcs.lib.commands.quorum.qdevice_net.remote_client_import_certificate_and_key"
+)
+@mock.patch("pcs.lib.commands.quorum.qdevice_net.client_cert_request_to_pk12")
+@mock.patch(
+    "pcs.lib.commands.quorum.qdevice_net.remote_sign_certificate_request"
+)
+@mock.patch(
+    "pcs.lib.commands.quorum.qdevice_net.client_generate_certificate_request"
+)
+@mock.patch("pcs.lib.commands.quorum.qdevice_net.remote_client_setup")
+@mock.patch(
+    "pcs.lib.commands.quorum.qdevice_net.remote_qdevice_get_ca_certificate"
+)
+@mock.patch.object(
+    LibraryEnvironment,
+    "cmd_runner",
+    lambda self: "mock_runner"
+)
+@mock.patch.object(
+    LibraryEnvironment,
+    "node_communicator",
+    lambda self: "mock_communicator"
+)
+class AddDeviceNetTest(TestCase):
+    #pylint: disable=too-many-instance-attributes
+    def setUp(self):
+        self.mock_logger = mock.MagicMock(logging.Logger)
+        self.mock_reporter = MockLibraryReportProcessor()
+        self.lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        self.qnetd_host = "qnetd_host"
+        self.cluster_name = "clusterName"
+        self.nodes = NodeAddressesList([
+            NodeAddresses("node1"),
+            NodeAddresses("node2"),
+        ])
+        self.ca_cert = "CA certificate"
+        self.cert_request = "client certificate request"
+        self.signed_cert = "signed certificate"
+        self.final_cert = "final client certificate"
+
+    def test_success(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.return_value = self.final_cert
+        skip_offline_nodes = False
+
+        lib._add_device_model_net(
+            self.lib_env,
+            self.qnetd_host,
+            self.cluster_name,
+            self.nodes,
+            skip_offline_nodes
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE,
+                    {
+                        "node": self.nodes[0].label
+                    }
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE,
+                    {
+                        "node": self.nodes[1].label
+                    }
+                ),
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+        mock_get_cert_request.assert_called_once_with(
+            "mock_runner",
+            self.cluster_name
+        )
+        mock_sign_cert_request.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host,
+            self.cert_request,
+            self.cluster_name
+        )
+        mock_cert_to_pk12.assert_called_once_with(
+            "mock_runner",
+            self.signed_cert
+        )
+        client_import_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.final_cert),
+            mock.call("mock_communicator", self.nodes[1], self.final_cert),
+        ]
+        self.assertEqual(
+            len(client_import_calls),
+            len(mock_import_cert.mock_calls)
+        )
+        mock_import_cert.assert_has_calls(client_import_calls)
+
+    def test_error_get_ca_cert(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.side_effect = NodeCommunicationException(
+            "host", "command", "reason"
+        )
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.return_value = self.final_cert
+        skip_offline_nodes = False
+
+        assert_raise_library_error(
+            lambda: lib._add_device_model_net(
+                self.lib_env,
+                self.qnetd_host,
+                self.cluster_name,
+                self.nodes,
+                skip_offline_nodes
+            ),
+            (
+                severity.ERROR,
+                report_codes.NODE_COMMUNICATION_ERROR,
+                {}
+            )
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                )
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        mock_client_setup.assert_not_called()
+        mock_get_cert_request.assert_not_called()
+        mock_sign_cert_request.assert_not_called()
+        mock_cert_to_pk12.assert_not_called()
+        mock_import_cert.assert_not_called()
+
+
+    def test_error_client_setup(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        def raiser(communicator, node, cert):
+            if node == self.nodes[1]:
+                raise NodeCommunicationException("host", "command", "reason")
+        mock_client_setup.side_effect = raiser
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.return_value = self.final_cert
+        skip_offline_nodes = False
+
+        assert_raise_library_error(
+            lambda: lib._add_device_model_net(
+                self.lib_env,
+                self.qnetd_host,
+                self.cluster_name,
+                self.nodes,
+                skip_offline_nodes
+            ),
+            (
+                severity.ERROR,
+                report_codes.NODE_COMMUNICATION_ERROR,
+                {},
+                report_codes.SKIP_OFFLINE_NODES
+            )
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                ),
+                (
+                    severity.ERROR,
+                    report_codes.NODE_COMMUNICATION_ERROR,
+                    {},
+                    report_codes.SKIP_OFFLINE_NODES
+                ),
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+
+    def test_error_client_setup_skip_offline(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        def raiser(communicator, node, cert):
+            if node == self.nodes[1]:
+                raise NodeCommunicationException("host", "command", "reason")
+        mock_client_setup.side_effect = raiser
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.return_value = self.final_cert
+        skip_offline_nodes = True
+
+        lib._add_device_model_net(
+            self.lib_env,
+            self.qnetd_host,
+            self.cluster_name,
+            self.nodes,
+            skip_offline_nodes
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                ),
+                (
+                    severity.WARNING,
+                    report_codes.NODE_COMMUNICATION_ERROR,
+                    {}
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE,
+                    {
+                        "node": self.nodes[0].label
+                    }
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE,
+                    {
+                        "node": self.nodes[1].label
+                    }
+                ),
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+
+    def test_generate_cert_request_error(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        mock_get_cert_request.side_effect = LibraryError()
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.return_value = self.final_cert
+        skip_offline_nodes = False
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib._add_device_model_net(
+                self.lib_env,
+                self.qnetd_host,
+                self.cluster_name,
+                self.nodes,
+                skip_offline_nodes
+            )
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                )
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+        mock_get_cert_request.assert_called_once_with(
+            "mock_runner",
+            self.cluster_name
+        )
+        mock_sign_cert_request.assert_not_called()
+        mock_cert_to_pk12.assert_not_called()
+        mock_import_cert.assert_not_called()
+
+    def test_sign_certificate_error(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.side_effect = NodeCommunicationException(
+            "host", "command", "reason"
+        )
+        mock_cert_to_pk12.return_value = self.final_cert
+        skip_offline_nodes = False
+
+        assert_raise_library_error(
+            lambda: lib._add_device_model_net(
+                self.lib_env,
+                self.qnetd_host,
+                self.cluster_name,
+                self.nodes,
+                skip_offline_nodes
+            ),
+            (
+                severity.ERROR,
+                report_codes.NODE_COMMUNICATION_ERROR,
+                {}
+            )
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                )
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+        mock_get_cert_request.assert_called_once_with(
+            "mock_runner",
+            self.cluster_name
+        )
+        mock_sign_cert_request.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host,
+            self.cert_request,
+            self.cluster_name
+        )
+        mock_cert_to_pk12.assert_not_called()
+        mock_import_cert.assert_not_called()
+
+    def test_certificate_to_pk12_error(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.side_effect = LibraryError()
+        skip_offline_nodes = False
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib._add_device_model_net(
+                self.lib_env,
+                self.qnetd_host,
+                self.cluster_name,
+                self.nodes,
+                skip_offline_nodes
+            )
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                )
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+        mock_get_cert_request.assert_called_once_with(
+            "mock_runner",
+            self.cluster_name
+        )
+        mock_sign_cert_request.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host,
+            self.cert_request,
+            self.cluster_name
+        )
+        mock_cert_to_pk12.assert_called_once_with(
+            "mock_runner",
+            self.signed_cert
+        )
+        mock_import_cert.assert_not_called()
+
+    def test_client_import_cert_error(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.return_value = self.final_cert
+        def raiser(communicator, node, cert):
+            if node == self.nodes[1]:
+                raise NodeCommunicationException("host", "command", "reason")
+        mock_import_cert.side_effect = raiser
+        skip_offline_nodes = False
+
+        assert_raise_library_error(
+            lambda: lib._add_device_model_net(
+                self.lib_env,
+                self.qnetd_host,
+                self.cluster_name,
+                self.nodes,
+                skip_offline_nodes
+            ),
+            (
+                severity.ERROR,
+                report_codes.NODE_COMMUNICATION_ERROR,
+                {},
+                report_codes.SKIP_OFFLINE_NODES
+            )
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE,
+                    {
+                        "node": self.nodes[0].label
+                    }
+                ),
+                (
+                    severity.ERROR,
+                    report_codes.NODE_COMMUNICATION_ERROR,
+                    {},
+                    report_codes.SKIP_OFFLINE_NODES
+                ),
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+        mock_get_cert_request.assert_called_once_with(
+            "mock_runner",
+            self.cluster_name
+        )
+        mock_sign_cert_request.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host,
+            self.cert_request,
+            self.cluster_name
+        )
+        mock_cert_to_pk12.assert_called_once_with(
+            "mock_runner",
+            self.signed_cert
+        )
+        client_import_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.final_cert),
+            mock.call("mock_communicator", self.nodes[1], self.final_cert),
+        ]
+        self.assertEqual(
+            len(client_import_calls),
+            len(mock_import_cert.mock_calls)
+        )
+        mock_import_cert.assert_has_calls(client_import_calls)
+
+    def test_client_import_cert_error_skip_offline(
+        self, mock_get_ca, mock_client_setup, mock_get_cert_request,
+        mock_sign_cert_request, mock_cert_to_pk12, mock_import_cert
+    ):
+        mock_get_ca.return_value = self.ca_cert
+        mock_get_cert_request.return_value = self.cert_request
+        mock_sign_cert_request.return_value = self.signed_cert
+        mock_cert_to_pk12.return_value = self.final_cert
+        def raiser(communicator, node, cert):
+            if node == self.nodes[1]:
+                raise NodeCommunicationException("host", "command", "reason")
+        mock_import_cert.side_effect = raiser
+        skip_offline_nodes = True
+
+        lib._add_device_model_net(
+            self.lib_env,
+            self.qnetd_host,
+            self.cluster_name,
+            self.nodes,
+            skip_offline_nodes
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED,
+                    {}
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE,
+                    {
+                        "node": self.nodes[0].label
+                    }
+                ),
+                (
+                    severity.WARNING,
+                    report_codes.NODE_COMMUNICATION_ERROR,
+                    {}
+                ),
+            ]
+        )
+        mock_get_ca.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host
+        )
+        client_setup_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.ca_cert),
+            mock.call("mock_communicator", self.nodes[1], self.ca_cert),
+        ]
+        self.assertEqual(
+            len(client_setup_calls),
+            len(mock_client_setup.mock_calls)
+        )
+        mock_client_setup.assert_has_calls(client_setup_calls)
+        mock_get_cert_request.assert_called_once_with(
+            "mock_runner",
+            self.cluster_name
+        )
+        mock_sign_cert_request.assert_called_once_with(
+            "mock_communicator",
+            self.qnetd_host,
+            self.cert_request,
+            self.cluster_name
+        )
+        mock_cert_to_pk12.assert_called_once_with(
+            "mock_runner",
+            self.signed_cert
+        )
+        client_import_calls = [
+            mock.call("mock_communicator", self.nodes[0], self.final_cert),
+            mock.call("mock_communicator", self.nodes[1], self.final_cert),
+        ]
+        self.assertEqual(
+            len(client_import_calls),
+            len(mock_import_cert.mock_calls)
+        )
+        mock_import_cert.assert_has_calls(client_import_calls)
 
 
 @mock.patch.object(LibraryEnvironment, "push_corosync_conf")
 @mock.patch.object(LibraryEnvironment, "get_corosync_conf_data")
+@mock.patch("pcs.lib.commands.quorum._remove_device_model_net")
+@mock.patch("pcs.lib.commands.quorum.qdevice_client.remote_client_disable")
+@mock.patch("pcs.lib.commands.quorum.qdevice_client.remote_client_stop")
 class RemoveDeviceTest(TestCase, CmanMixin):
     def setUp(self):
         self.mock_logger = mock.MagicMock(logging.Logger)
         self.mock_reporter = MockLibraryReportProcessor()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
-    def test_disabled_on_cman(self, mock_get_corosync, mock_push_corosync):
+    def test_disabled_on_cman(
+        self, mock_remote_stop, mock_remote_disable, mock_remove_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
         self.assert_disabled_on_cman(lambda: lib.remove_device(lib_env))
         mock_get_corosync.assert_not_called()
         mock_push_corosync.assert_not_called()
+        mock_remove_net.assert_not_called()
+        mock_remote_disable.assert_not_called()
+        mock_remote_stop.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: True)
     def test_enabled_on_cman_if_not_live(
-        self, mock_get_corosync, mock_push_corosync
+        self, mock_remote_stop, mock_remote_disable, mock_remove_net,
+        mock_get_corosync, mock_push_corosync
     ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
@@ -495,9 +1353,17 @@ class RemoveDeviceTest(TestCase, CmanMixin):
             )
         )
 
+        self.assertEqual(1, mock_get_corosync.call_count)
+        self.assertEqual(0, mock_push_corosync.call_count)
+        mock_remove_net.assert_not_called()
+        mock_remote_disable.assert_not_called()
+        mock_remote_stop.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
-    def test_no_device(self, mock_get_corosync, mock_push_corosync):
+    def test_no_device(
+        self, mock_remote_stop, mock_remote_disable, mock_remove_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         original_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
         lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
@@ -511,10 +1377,17 @@ class RemoveDeviceTest(TestCase, CmanMixin):
             )
         )
 
-        mock_push_corosync.assert_not_called()
+        self.assertEqual(1, mock_get_corosync.call_count)
+        self.assertEqual(0, mock_push_corosync.call_count)
+        mock_remove_net.assert_not_called()
+        mock_remote_disable.assert_not_called()
+        mock_remote_stop.assert_not_called()
 
     @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
-    def test_success(self, mock_get_corosync, mock_push_corosync):
+    def test_success(
+        self, mock_remote_stop, mock_remote_disable, mock_remove_net,
+        mock_get_corosync, mock_push_corosync
+    ):
         original_conf = open(rc("corosync-3nodes-qdevice.conf")).read()
         no_device_conf = open(rc("corosync-3nodes.conf")).read()
         mock_get_corosync.return_value = original_conf
@@ -527,7 +1400,213 @@ class RemoveDeviceTest(TestCase, CmanMixin):
             mock_push_corosync.mock_calls[0][1][0].config.export(),
             no_device_conf
         )
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_DISABLE_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
+                (
+                    severity.INFO,
+                    report_codes.SERVICE_STOP_STARTED,
+                    {
+                        "service": "corosync-qdevice",
+                    }
+                ),
+            ]
+        )
+        self.assertEqual(1, len(mock_remove_net.mock_calls))
+        self.assertEqual(3, len(mock_remote_disable.mock_calls))
+        self.assertEqual(3, len(mock_remote_stop.mock_calls))
+
+    @mock.patch("pcs.lib.env.is_cman_cluster", lambda self: False)
+    def test_success_file(
+        self, mock_remote_stop, mock_remote_disable, mock_remove_net,
+        mock_get_corosync, mock_push_corosync
+    ):
+        original_conf = open(rc("corosync-3nodes-qdevice.conf")).read()
+        no_device_conf = open(rc("corosync-3nodes.conf")).read()
+        mock_get_corosync.return_value = original_conf
+        lib_env = LibraryEnvironment(
+            self.mock_logger,
+            self.mock_reporter,
+            corosync_conf_data=original_conf
+        )
+
+        lib.remove_device(lib_env)
+
+        self.assertEqual(1, len(mock_push_corosync.mock_calls))
+        ac(
+            mock_push_corosync.mock_calls[0][1][0].config.export(),
+            no_device_conf
+        )
         self.assertEqual([], self.mock_reporter.report_item_list)
+        mock_remove_net.assert_not_called()
+        mock_remote_disable.assert_not_called()
+        mock_remote_stop.assert_not_called()
+
+
+@mock.patch("pcs.lib.commands.quorum.qdevice_net.remote_client_destroy")
+@mock.patch.object(
+    LibraryEnvironment,
+    "node_communicator",
+    lambda self: "mock_communicator"
+)
+class RemoveDeviceNetTest(TestCase):
+    def setUp(self):
+        self.mock_logger = mock.MagicMock(logging.Logger)
+        self.mock_reporter = MockLibraryReportProcessor()
+        self.lib_env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        self.nodes = NodeAddressesList([
+            NodeAddresses("node1"),
+            NodeAddresses("node2"),
+        ])
+
+    def test_success(self, mock_client_destroy):
+        skip_offline_nodes = False
+
+        lib._remove_device_model_net(
+            self.lib_env,
+            self.nodes,
+            skip_offline_nodes
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_REMOVAL_STARTED,
+                    {}
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_REMOVED_FROM_NODE,
+                    {
+                        "node": self.nodes[0].label
+                    }
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_REMOVED_FROM_NODE,
+                    {
+                        "node": self.nodes[1].label
+                    }
+                ),
+            ]
+        )
+        client_destroy_calls = [
+            mock.call("mock_communicator", self.nodes[0]),
+            mock.call("mock_communicator", self.nodes[1]),
+        ]
+        self.assertEqual(
+            len(client_destroy_calls),
+            len(mock_client_destroy.mock_calls)
+        )
+        mock_client_destroy.assert_has_calls(client_destroy_calls)
+
+    def test_error_client_destroy(self, mock_client_destroy):
+        def raiser(communicator, node):
+            if node == self.nodes[1]:
+                raise NodeCommunicationException("host", "command", "reason")
+        mock_client_destroy.side_effect = raiser
+        skip_offline_nodes = False
+
+        assert_raise_library_error(
+            lambda: lib._remove_device_model_net(
+                self.lib_env,
+                self.nodes,
+                skip_offline_nodes
+            ),
+            (
+                severity.ERROR,
+                report_codes.NODE_COMMUNICATION_ERROR,
+                {},
+                report_codes.SKIP_OFFLINE_NODES
+            )
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_REMOVAL_STARTED,
+                    {}
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_REMOVED_FROM_NODE,
+                    {
+                        "node": self.nodes[0].label
+                    }
+                ),
+                (
+                    severity.ERROR,
+                    report_codes.NODE_COMMUNICATION_ERROR,
+                    {},
+                    report_codes.SKIP_OFFLINE_NODES
+                ),
+            ]
+        )
+        client_destroy_calls = [
+            mock.call("mock_communicator", self.nodes[0]),
+            mock.call("mock_communicator", self.nodes[1]),
+        ]
+        self.assertEqual(
+            len(client_destroy_calls),
+            len(mock_client_destroy.mock_calls)
+        )
+        mock_client_destroy.assert_has_calls(client_destroy_calls)
+
+    def test_error_client_destroy_skip_offline(self, mock_client_destroy):
+        def raiser(communicator, node):
+            if node == self.nodes[1]:
+                raise NodeCommunicationException("host", "command", "reason")
+        mock_client_destroy.side_effect = raiser
+        skip_offline_nodes = True
+
+        lib._remove_device_model_net(
+            self.lib_env,
+            self.nodes,
+            skip_offline_nodes
+        )
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_REMOVAL_STARTED,
+                    {}
+                ),
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CERTIFICATE_REMOVED_FROM_NODE,
+                    {
+                        "node": self.nodes[0].label
+                    }
+                ),
+                (
+                    severity.WARNING,
+                    report_codes.NODE_COMMUNICATION_ERROR,
+                    {}
+                ),
+            ]
+        )
+        client_destroy_calls = [
+            mock.call("mock_communicator", self.nodes[0]),
+            mock.call("mock_communicator", self.nodes[1]),
+        ]
+        self.assertEqual(
+            len(client_destroy_calls),
+            len(mock_client_destroy.mock_calls)
+        )
+        mock_client_destroy.assert_has_calls(client_destroy_calls)
 
 
 @mock.patch.object(LibraryEnvironment, "push_corosync_conf")
diff --git a/pcs/test/test_lib_corosync_config_facade.py b/pcs/test/test_lib_corosync_config_facade.py
index 5700016..4a35fd9 100644
--- a/pcs/test/test_lib_corosync_config_facade.py
+++ b/pcs/test/test_lib_corosync_config_facade.py
@@ -31,6 +31,7 @@ class FromStringTest(TestCase):
         self.assertEqual(facade.__class__, lib.ConfigFacade)
         self.assertEqual(facade.config.export(), config)
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_parse_error_missing_brace(self):
         config = "section {"
@@ -55,6 +56,43 @@ class FromStringTest(TestCase):
         )
 
 
+class GetClusterNametest(TestCase):
+    def test_no_name(self):
+        config = ""
+        facade = lib.ConfigFacade.from_string(config)
+        self.assertEqual("", facade.get_cluster_name())
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
+
+    def test_empty_name(self):
+        config = "totem {\n cluster_name:\n}\n"
+        facade = lib.ConfigFacade.from_string(config)
+        self.assertEqual("", facade.get_cluster_name())
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
+
+    def test_one_name(self):
+        config = "totem {\n cluster_name: test\n}\n"
+        facade = lib.ConfigFacade.from_string(config)
+        self.assertEqual("test", facade.get_cluster_name())
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
+
+    def test_more_names(self):
+        config = "totem {\n cluster_name: test\n cluster_name: TEST\n}\n"
+        facade = lib.ConfigFacade.from_string(config)
+        self.assertEqual("TEST", facade.get_cluster_name())
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
+
+    def test_more_sections(self):
+        config = "totem{\ncluster_name:test\n}\ntotem{\ncluster_name:TEST\n}\n"
+        facade = lib.ConfigFacade.from_string(config)
+        self.assertEqual("TEST", facade.get_cluster_name())
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
+
+
 class GetNodesTest(TestCase):
     def assert_equal_nodelist(self, expected_nodes, real_nodelist):
         real_nodes = [
@@ -69,6 +107,7 @@ class GetNodesTest(TestCase):
         nodes = facade.get_nodes()
         self.assertEqual(0, len(nodes))
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_empty_nodelist(self):
         config = """\
@@ -79,6 +118,7 @@ nodelist {
         nodes = facade.get_nodes()
         self.assertEqual(0, len(nodes))
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_one_nodelist(self):
         config = """\
@@ -107,6 +147,7 @@ nodelist {
             nodes
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_more_nodelists(self):
         config = """\
@@ -137,6 +178,7 @@ nodelist {
             nodes
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
 
 class GetQuorumOptionsTest(TestCase):
@@ -146,6 +188,7 @@ class GetQuorumOptionsTest(TestCase):
         options = facade.get_quorum_options()
         self.assertEqual({}, options)
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_empty_quorum(self):
         config = """\
@@ -156,6 +199,7 @@ quorum {
         options = facade.get_quorum_options()
         self.assertEqual({}, options)
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_no_options(self):
         config = """\
@@ -167,6 +211,7 @@ quorum {
         options = facade.get_quorum_options()
         self.assertEqual({}, options)
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_some_options(self):
         config = """\
@@ -191,6 +236,7 @@ quorum {
             options
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_option_repeated(self):
         config = """\
@@ -208,6 +254,7 @@ quorum {
             options
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_quorum_repeated(self):
         config = """\
@@ -231,6 +278,7 @@ quorum {
             options
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
 
 class SetQuorumOptionsTest(TestCase):
@@ -247,6 +295,7 @@ class SetQuorumOptionsTest(TestCase):
         facade = lib.ConfigFacade.from_string(config)
         facade.set_quorum_options(reporter, {"wait_for_all": "0"})
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             """\
 quorum {
@@ -263,6 +312,7 @@ quorum {
         facade = lib.ConfigFacade.from_string(config)
         facade.set_quorum_options(reporter, {"wait_for_all": ""})
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual("", facade.config.export())
         self.assertEqual([], reporter.report_item_list)
 
@@ -279,6 +329,7 @@ quorum {
         facade.set_quorum_options(reporter, expected_options)
 
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         test_facade = lib.ConfigFacade.from_string(facade.config.export())
         self.assertEqual(
             expected_options,
@@ -309,6 +360,7 @@ quorum {
         )
 
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         test_facade = lib.ConfigFacade.from_string(facade.config.export())
         self.assertEqual(
             {
@@ -329,6 +381,7 @@ quorum {
         facade.set_quorum_options(reporter, {"auto_tie_breaker": "1"})
 
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             "1",
             facade.get_quorum_options().get("auto_tie_breaker", None)
@@ -347,6 +400,7 @@ quorum {
         facade.set_quorum_options(reporter, {"auto_tie_breaker": "0"})
 
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             "0",
             facade.get_quorum_options().get("auto_tie_breaker", None)
@@ -365,6 +419,7 @@ quorum {
         facade.set_quorum_options(reporter, {"auto_tie_breaker": "1"})
 
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             "1",
             facade.get_quorum_options().get("auto_tie_breaker", None)
@@ -383,6 +438,7 @@ quorum {
         facade.set_quorum_options(reporter, {"auto_tie_breaker": "0"})
 
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             "0",
             facade.get_quorum_options().get("auto_tie_breaker", None)
@@ -421,6 +477,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             lib.ConfigFacade.from_string(config).get_quorum_options(),
             facade.get_quorum_options()
@@ -476,6 +533,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             lib.ConfigFacade.from_string(config).get_quorum_options(),
             facade.get_quorum_options()
@@ -522,11 +580,60 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
+        self.assertEqual(
+            lib.ConfigFacade.from_string(config).get_quorum_options(),
+            facade.get_quorum_options()
+        )
+
+    def test_qdevice_incompatible_options(self):
+        config = open(rc("corosync-3nodes-qdevice.conf")).read()
+        reporter = MockLibraryReportProcessor()
+        facade = lib.ConfigFacade.from_string(config)
+        options = {
+            "auto_tie_breaker": "1",
+            "last_man_standing": "1",
+            "last_man_standing_window": "250",
+        }
+        assert_raise_library_error(
+            lambda: facade.set_quorum_options(reporter, options),
+            (
+                severity.ERROR,
+                report_codes.COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE,
+                {
+                    "options_names": [
+                        "auto_tie_breaker",
+                        "last_man_standing",
+                        "last_man_standing_window",
+                    ],
+                }
+            )
+        )
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual(
             lib.ConfigFacade.from_string(config).get_quorum_options(),
             facade.get_quorum_options()
         )
 
+    def test_qdevice_compatible_options(self):
+        config = open(rc("corosync-3nodes-qdevice.conf")).read()
+        reporter = MockLibraryReportProcessor()
+        facade = lib.ConfigFacade.from_string(config)
+        expected_options = {
+            "wait_for_all": "1",
+        }
+        facade.set_quorum_options(reporter, expected_options)
+
+        self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
+        test_facade = lib.ConfigFacade.from_string(facade.config.export())
+        self.assertEqual(
+            expected_options,
+            test_facade.get_quorum_options()
+        )
+        self.assertEqual([], reporter.report_item_list)
+
 
 class HasQuorumDeviceTest(TestCase):
     def test_empty_config(self):
@@ -534,12 +641,14 @@ class HasQuorumDeviceTest(TestCase):
         facade = lib.ConfigFacade.from_string(config)
         self.assertFalse(facade.has_quorum_device())
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_no_device(self):
         config = open(rc("corosync.conf")).read()
         facade = lib.ConfigFacade.from_string(config)
         self.assertFalse(facade.has_quorum_device())
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_empty_device(self):
         config = """\
@@ -551,6 +660,7 @@ quorum {
         facade = lib.ConfigFacade.from_string(config)
         self.assertFalse(facade.has_quorum_device())
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_device_set(self):
         config = """\
@@ -563,6 +673,7 @@ quorum {
         facade = lib.ConfigFacade.from_string(config)
         self.assertTrue(facade.has_quorum_device())
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_no_model(self):
         config = """\
@@ -578,6 +689,7 @@ quorum {
         facade = lib.ConfigFacade.from_string(config)
         self.assertFalse(facade.has_quorum_device())
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
 
 class GetQuorumDeviceSettingsTest(TestCase):
@@ -589,6 +701,7 @@ class GetQuorumDeviceSettingsTest(TestCase):
             facade.get_quorum_device_settings()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_no_device(self):
         config = open(rc("corosync.conf")).read()
@@ -598,6 +711,7 @@ class GetQuorumDeviceSettingsTest(TestCase):
             facade.get_quorum_device_settings()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_empty_device(self):
         config = """\
@@ -612,6 +726,7 @@ quorum {
             facade.get_quorum_device_settings()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_no_model(self):
         config = """\
@@ -630,6 +745,7 @@ quorum {
             facade.get_quorum_device_settings()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_configured_properly(self):
         config = """\
@@ -649,6 +765,7 @@ quorum {
             facade.get_quorum_device_settings()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_more_devices_one_quorum(self):
         config = """\
@@ -681,6 +798,7 @@ quorum {
             facade.get_quorum_device_settings()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_more_devices_more_quorum(self):
         config = """\
@@ -715,6 +833,7 @@ quorum {
             facade.get_quorum_device_settings()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
 
 class AddQuorumDeviceTest(TestCase):
@@ -754,9 +873,10 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
-    def test_success_net_minimal(self):
+    def test_success_net_minimal_ffsplit(self):
         config = open(rc("corosync-3nodes.conf")).read()
         reporter = MockLibraryReportProcessor()
         facade = lib.ConfigFacade.from_string(config)
@@ -774,6 +894,7 @@ quorum {
 
     device {
         model: net
+        votes: 1
 
         net {
             algorithm: ffsplit
@@ -784,55 +905,10 @@ quorum {
             facade.config.export()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual([], reporter.report_item_list)
 
-    def test_success_net_full(self):
-        config = open(rc("corosync-3nodes.conf")).read()
-        reporter = MockLibraryReportProcessor()
-        facade = lib.ConfigFacade.from_string(config)
-        facade.add_quorum_device(
-            reporter,
-            "net",
-            {
-                "host": "127.0.0.1",
-                "port": "4433",
-                "algorithm": "ffsplit",
-                "connect_timeout": "12345",
-                "force_ip_version": "4",
-                "tie_breaker": "lowest",
-            },
-            {
-                "timeout": "23456",
-                "sync_timeout": "34567"
-            }
-        )
-        ac(
-            config.replace(
-                "    provider: corosync_votequorum",
-                """\
-    provider: corosync_votequorum
-
-    device {
-        sync_timeout: 34567
-        timeout: 23456
-        model: net
-
-        net {
-            algorithm: ffsplit
-            connect_timeout: 12345
-            force_ip_version: 4
-            host: 127.0.0.1
-            port: 4433
-            tie_breaker: lowest
-        }
-    }"""
-            ),
-            facade.config.export()
-        )
-        self.assertFalse(facade.need_stopped_cluster)
-        self.assertEqual([], reporter.report_item_list)
-
-    def test_succes_net_lms_3node(self):
+    def test_success_net_minimal_lms(self):
         config = open(rc("corosync-3nodes.conf")).read()
         reporter = MockLibraryReportProcessor()
         facade = lib.ConfigFacade.from_string(config)
@@ -860,16 +936,18 @@ quorum {
             facade.config.export()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual([], reporter.report_item_list)
 
-    def test_succes_net_2nodelms_3node(self):
+    def test_success_remove_nodes_votes(self):
         config = open(rc("corosync-3nodes.conf")).read()
+        config_votes = config.replace("node {", "node {\nquorum_votes: 2")
         reporter = MockLibraryReportProcessor()
-        facade = lib.ConfigFacade.from_string(config)
+        facade = lib.ConfigFacade.from_string(config_votes)
         facade.add_quorum_device(
             reporter,
             "net",
-            {"host": "127.0.0.1", "algorithm": "2nodelms"},
+            {"host": "127.0.0.1", "algorithm": "lms"},
             {}
         )
         ac(
@@ -890,47 +968,28 @@ quorum {
             facade.config.export()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual([], reporter.report_item_list)
 
-    def test_succes_net_lms_2node(self):
-        config = open(rc("corosync.conf")).read()
-        reporter = MockLibraryReportProcessor()
-        facade = lib.ConfigFacade.from_string(config)
-        facade.add_quorum_device(
-            reporter,
-            "net",
-            {"host": "127.0.0.1", "algorithm": "lms"},
-            {}
-        )
-        ac(
-            config.replace(
-                "    provider: corosync_votequorum",
-                """\
-    provider: corosync_votequorum
-
-    device {
-        model: net
-
-        net {
-            algorithm: 2nodelms
-            host: 127.0.0.1
-        }
-    }"""
-            ).replace("    two_node: 1\n", ""),
-            facade.config.export()
-        )
-        self.assertFalse(facade.need_stopped_cluster)
-        self.assertEqual([], reporter.report_item_list)
-
-    def test_succes_net_2nodelms_2node(self):
-        config = open(rc("corosync.conf")).read()
+    def test_success_net_full(self):
+        config = open(rc("corosync-3nodes.conf")).read()
         reporter = MockLibraryReportProcessor()
         facade = lib.ConfigFacade.from_string(config)
         facade.add_quorum_device(
             reporter,
             "net",
-            {"host": "127.0.0.1", "algorithm": "2nodelms"},
-            {}
+            {
+                "host": "127.0.0.1",
+                "port": "4433",
+                "algorithm": "ffsplit",
+                "connect_timeout": "12345",
+                "force_ip_version": "4",
+                "tie_breaker": "lowest",
+            },
+            {
+                "timeout": "23456",
+                "sync_timeout": "34567"
+            }
         )
         ac(
             config.replace(
@@ -939,17 +998,25 @@ quorum {
     provider: corosync_votequorum
 
     device {
+        sync_timeout: 34567
+        timeout: 23456
         model: net
+        votes: 1
 
         net {
-            algorithm: 2nodelms
+            algorithm: ffsplit
+            connect_timeout: 12345
+            force_ip_version: 4
             host: 127.0.0.1
+            port: 4433
+            tie_breaker: lowest
         }
     }"""
-            ).replace("    two_node: 1\n", ""),
+            ),
             facade.config.export()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual([], reporter.report_item_list)
 
     def test_remove_conflicting_options(self):
@@ -982,6 +1049,7 @@ quorum {
 
     device {
         model: net
+        votes: 1
 
         net {
             algorithm: ffsplit
@@ -994,6 +1062,7 @@ quorum {
             facade.config.export()
         )
         self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual([], reporter.report_item_list)
 
     def test_remove_old_configuration(self):
@@ -1030,6 +1099,7 @@ quorum {
 
     device {
         model: net
+        votes: 1
 
         net {
             algorithm: ffsplit
@@ -1042,6 +1112,7 @@ quorum {
             facade.config.export()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         self.assertEqual([], reporter.report_item_list)
 
     def test_bad_model(self):
@@ -1062,6 +1133,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_bad_model_forced(self):
@@ -1082,6 +1154,7 @@ quorum {
             facade.config.export()
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         assert_report_item_list_equal(
             reporter.report_item_list,
             [
@@ -1115,6 +1188,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_bad_options_net(self):
@@ -1147,7 +1221,7 @@ quorum {
                 {
                     "option_name": "algorithm",
                     "option_value": "bad algorithm",
-                    "allowed_values": ("2nodelms", "ffsplit", "lms"),
+                    "allowed_values": ("ffsplit", "lms"),
                 },
                 report_codes.FORCE_OPTIONS
             ),
@@ -1254,6 +1328,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_mandatory_options_missing_net_forced(self):
@@ -1277,6 +1352,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_mandatory_options_empty_net_forced(self):
@@ -1300,6 +1376,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_bad_options_net_forced(self):
@@ -1326,6 +1403,7 @@ quorum {
             force_options=True
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(
             config.replace(
                 "    provider: corosync_votequorum",
@@ -1360,7 +1438,7 @@ quorum {
                     {
                         "option_name": "algorithm",
                         "option_value": "bad algorithm",
-                        "allowed_values": ("2nodelms", "ffsplit", "lms"),
+                        "allowed_values": ("ffsplit", "lms"),
                     }
                 ),
                 (
@@ -1445,9 +1523,52 @@ quorum {
             ]
         )
 
+    def test_bad_options_net_disallowed_algorithms(self):
+        config = open(rc("corosync-3nodes.conf")).read()
+        reporter = MockLibraryReportProcessor()
+        facade = lib.ConfigFacade.from_string(config)
+        assert_raise_library_error(
+            lambda: facade.add_quorum_device(
+                reporter,
+                "net",
+                {"host": "127.0.0.1", "algorithm": "test"},
+                {}
+            ),
+            (
+                severity.ERROR,
+                report_codes.INVALID_OPTION_VALUE,
+                {
+                    "option_name": "algorithm",
+                    "option_value": "test",
+                    "allowed_values": ("ffsplit", "lms"),
+                },
+                report_codes.FORCE_OPTIONS
+            )
+        )
+
+        assert_raise_library_error(
+            lambda: facade.add_quorum_device(
+                reporter,
+                "net",
+                {"host": "127.0.0.1", "algorithm": "2nodelms"},
+                {}
+            ),
+            (
+                severity.ERROR,
+                report_codes.INVALID_OPTION_VALUE,
+                {
+                    "option_name": "algorithm",
+                    "option_value": "2nodelms",
+                    "allowed_values": ("ffsplit", "lms"),
+                },
+                report_codes.FORCE_OPTIONS
+            )
+        )
+
+
 class UpdateQuorumDeviceTest(TestCase):
-    def fixture_add_device(self, config):
-        return re.sub(
+    def fixture_add_device(self, config, votes=None):
+        with_device = re.sub(
             re.compile(r"quorum {[^}]*}", re.MULTILINE | re.DOTALL),
             """\
 quorum {
@@ -1465,6 +1586,12 @@ quorum {
 }""",
             config
         )
+        if votes:
+            with_device = with_device.replace(
+                "model: net",
+                "model: net\n        votes: {0}".format(votes)
+            )
+        return with_device
 
     def test_not_existing(self):
         config = open(rc("corosync.conf")).read()
@@ -1483,11 +1610,13 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_success_model_options_net(self):
         config = self.fixture_add_device(
-            open(rc("corosync-3nodes.conf")).read()
+            open(rc("corosync-3nodes.conf")).read(),
+            votes="1"
         )
         reporter = MockLibraryReportProcessor()
         facade = lib.ConfigFacade.from_string(config)
@@ -1496,7 +1625,8 @@ quorum {
             {"host": "127.0.0.2", "port": "", "algorithm": "ffsplit"},
             {}
         )
-        self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertTrue(facade.need_qdevice_reload)
         ac(
             config.replace(
                 "host: 127.0.0.1\n            port: 4433",
@@ -1506,27 +1636,6 @@ quorum {
         )
         self.assertEqual([], reporter.report_item_list)
 
-    def test_success_net_3node_2nodelms(self):
-        config = self.fixture_add_device(
-            open(rc("corosync-3nodes.conf")).read()
-        )
-        reporter = MockLibraryReportProcessor()
-        facade = lib.ConfigFacade.from_string(config)
-        facade.update_quorum_device(
-            reporter,
-            {"algorithm": "2nodelms"},
-            {}
-        )
-        self.assertTrue(facade.need_stopped_cluster)
-        ac(
-            config.replace(
-                "port: 4433",
-                "port: 4433\n            algorithm: lms"
-            ),
-            facade.config.export()
-        )
-        self.assertEqual([], reporter.report_item_list)
-
     def test_success_net_doesnt_require_host_and_algorithm(self):
         config = self.fixture_add_device(
             open(rc("corosync-3nodes.conf")).read()
@@ -1534,7 +1643,8 @@ quorum {
         reporter = MockLibraryReportProcessor()
         facade = lib.ConfigFacade.from_string(config)
         facade.update_quorum_device(reporter, {"port": "4444"}, {})
-        self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertTrue(facade.need_qdevice_reload)
         ac(
             config.replace(
                 "host: 127.0.0.1\n            port: 4433",
@@ -1572,12 +1682,13 @@ quorum {
                 {
                     "option_name": "algorithm",
                     "option_value": "",
-                    "allowed_values": ("2nodelms", "ffsplit", "lms")
+                    "allowed_values": ("ffsplit", "lms")
                 },
                 report_codes.FORCE_OPTIONS
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_net_required_options_cannot_be_removed_forced(self):
@@ -1605,6 +1716,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_bad_net_options(self):
@@ -1632,7 +1744,7 @@ quorum {
                 {
                     "option_name": "algorithm",
                     "option_value": "bad algorithm",
-                    "allowed_values": ("2nodelms", "ffsplit", "lms"),
+                    "allowed_values": ("ffsplit", "lms"),
                 },
                 report_codes.FORCE_OPTIONS
             ),
@@ -1695,6 +1807,7 @@ quorum {
             ),
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_bad_net_options_forced(self):
@@ -1716,7 +1829,8 @@ quorum {
             {},
             force_options=True
         )
-        self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertTrue(facade.need_qdevice_reload)
         ac(
             config.replace(
                 "            host: 127.0.0.1\n            port: 4433",
@@ -1740,7 +1854,7 @@ quorum {
                     {
                         "option_name": "algorithm",
                         "option_value": "bad algorithm",
-                        "allowed_values": ("2nodelms", "ffsplit", "lms"),
+                        "allowed_values": ("ffsplit", "lms"),
                     },
                 ),
                 (
@@ -1809,7 +1923,8 @@ quorum {
             {},
             {"timeout": "", "sync_timeout": "23456"}
         )
-        self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertTrue(facade.need_qdevice_reload)
         ac(
             config.replace(
                 "timeout: 12345\n        model: net",
@@ -1830,7 +1945,8 @@ quorum {
             {"port": "4444"},
             {"timeout": "23456"}
         )
-        self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertTrue(facade.need_qdevice_reload)
         ac(
             config
                 .replace("port: 4433", "port: 4444")
@@ -1898,6 +2014,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_bad_generic_options_cannot_force_model(self):
@@ -1924,6 +2041,7 @@ quorum {
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(config, facade.config.export())
 
     def test_bad_generic_options_forced(self):
@@ -1942,7 +2060,8 @@ quorum {
             },
             force_options=True
         )
-        self.assertTrue(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_stopped_cluster)
+        self.assertTrue(facade.need_qdevice_reload)
         ac(
             config.replace(
                 "        timeout: 12345\n        model: net",
@@ -2001,6 +2120,7 @@ class RemoveQuorumDeviceTest(TestCase):
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_no_device(self):
         config = open(rc("corosync-3nodes.conf")).read()
@@ -2014,6 +2134,7 @@ class RemoveQuorumDeviceTest(TestCase):
             )
         )
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
 
     def test_remove_all_devices(self):
         config_no_devices = open(rc("corosync-3nodes.conf")).read()
@@ -2054,6 +2175,7 @@ quorum {
         facade = lib.ConfigFacade.from_string(config)
         facade.remove_quorum_device()
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(
             config_no_devices,
             facade.config.export()
@@ -2082,6 +2204,7 @@ quorum {
         facade = lib.ConfigFacade.from_string(config)
         facade.remove_quorum_device()
         self.assertFalse(facade.need_stopped_cluster)
+        self.assertFalse(facade.need_qdevice_reload)
         ac(
             config_no_devices,
             facade.config.export()
diff --git a/pcs/test/test_lib_corosync_live.py b/pcs/test/test_lib_corosync_live.py
index 4878136..96fe235 100644
--- a/pcs/test/test_lib_corosync_live.py
+++ b/pcs/test/test_lib_corosync_live.py
@@ -47,6 +47,22 @@ class GetLocalCorosyncConfTest(TestCase):
         )
 
 
+class SetRemoteCorosyncConfTest(TestCase):
+    def test_success(self):
+        config = "test {\nconfig: data\n}\n"
+        node = NodeAddresses("node1")
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_node.return_value = "dummy return"
+
+        lib.set_remote_corosync_conf(mock_communicator, node, config)
+
+        mock_communicator.call_node.assert_called_once_with(
+            node,
+            "remote/set_corosync_conf",
+            "corosync_conf=test+%7B%0Aconfig%3A+data%0A%7D%0A"
+        )
+
+
 class ReloadConfigTest(TestCase):
     def path(self, name):
         return os.path.join(settings.corosync_binaries, name)
@@ -85,17 +101,43 @@ class ReloadConfigTest(TestCase):
         ])
 
 
-class SetRemoteCorosyncConfTest(TestCase):
+class GetQuorumStatusTextTest(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        self.quorum_tool = "/usr/sbin/corosync-quorumtool"
+
     def test_success(self):
-        config = "test {\nconfig: data\n}\n"
-        node = NodeAddresses("node1")
-        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
-        mock_communicator.call_node.return_value = "dummy return"
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.get_quorum_status_text(self.mock_runner)
+        )
+        self.mock_runner.run.assert_called_once_with([
+            self.quorum_tool, "-p"
+        ])
 
-        lib.set_remote_corosync_conf(mock_communicator, node, config)
+    def test_success_with_retval_1(self):
+        self.mock_runner.run.return_value = ("status info", 1)
+        self.assertEqual(
+            "status info",
+            lib.get_quorum_status_text(self.mock_runner)
+        )
+        self.mock_runner.run.assert_called_once_with([
+            self.quorum_tool, "-p"
+        ])
 
-        mock_communicator.call_node.assert_called_once_with(
-            node,
-            "remote/set_corosync_conf",
-            "corosync_conf=test+%7B%0Aconfig%3A+data%0A%7D%0A"
+    def test_error(self):
+        self.mock_runner.run.return_value = ("status error", 2)
+        assert_raise_library_error(
+            lambda: lib.get_quorum_status_text(self.mock_runner),
+            (
+                severity.ERROR,
+                report_codes.COROSYNC_QUORUM_GET_STATUS_ERROR,
+                {
+                    "reason": "status error",
+                }
+            )
         )
+        self.mock_runner.run.assert_called_once_with([
+            self.quorum_tool, "-p"
+        ])
diff --git a/pcs/test/test_lib_corosync_qdevice_client.py b/pcs/test/test_lib_corosync_qdevice_client.py
new file mode 100644
index 0000000..e0332f1
--- /dev/null
+++ b/pcs/test/test_lib_corosync_qdevice_client.py
@@ -0,0 +1,60 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+from unittest import TestCase
+
+from pcs.test.tools.pcs_mock import mock
+from pcs.test.tools.assertions import assert_raise_library_error
+
+from pcs.common import report_codes
+from pcs.lib.errors import ReportItemSeverity as severity
+from pcs.lib.external import CommandRunner
+
+import pcs.lib.corosync.qdevice_client as lib
+
+
+class GetStatusTextTest(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        self.qdevice_tool = "/usr/sbin/corosync-qdevice-tool"
+
+    def test_success(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.get_status_text(self.mock_runner)
+        )
+        self.mock_runner.run.assert_called_once_with([
+            self.qdevice_tool, "-s"
+        ])
+
+    def test_success_verbose(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.get_status_text(self.mock_runner, True)
+        )
+        self.mock_runner.run.assert_called_once_with([
+            self.qdevice_tool, "-s", "-v"
+        ])
+
+    def test_error(self):
+        self.mock_runner.run.return_value = ("status error", 1)
+        assert_raise_library_error(
+            lambda: lib.get_status_text(self.mock_runner),
+            (
+                severity.ERROR,
+                report_codes.COROSYNC_QUORUM_GET_STATUS_ERROR,
+                {
+                    "reason": "status error",
+                }
+            )
+        )
+        self.mock_runner.run.assert_called_once_with([
+            self.qdevice_tool, "-s"
+        ])
+
diff --git a/pcs/test/test_lib_corosync_qdevice_net.py b/pcs/test/test_lib_corosync_qdevice_net.py
index 38bc9c8..3d473f7 100644
--- a/pcs/test/test_lib_corosync_qdevice_net.py
+++ b/pcs/test/test_lib_corosync_qdevice_net.py
@@ -7,18 +7,40 @@ from __future__ import (
 
 from unittest import TestCase
 
+import base64
+import os.path
+
 from pcs.test.tools.pcs_mock import mock
 from pcs.test.tools.assertions import assert_raise_library_error
+from pcs.test.tools.misc import get_test_resource
 
+from pcs import settings
 from pcs.common import report_codes
-from pcs.lib.errors import ReportItemSeverity as severity
-from pcs.lib.external import CommandRunner
+from pcs.lib import reports
+from pcs.lib.errors import ReportItemSeverity as severity, LibraryError
+from pcs.lib.external import (
+    CommandRunner,
+    NodeCommunicator,
+    NodeCommunicationException,
+)
 
 import pcs.lib.corosync.qdevice_net as lib
 
 
-_qnetd_cert_dir = "/etc/corosync/qdevice/net/qnetd/nssdb"
-_qnetd_tool = "/usr/sbin/corosync-qnetd-certutil"
+_qnetd_cert_dir = "/etc/corosync/qnetd/nssdb"
+_qnetd_cert_tool = "/usr/bin/corosync-qnetd-certutil"
+_qnetd_tool = "/usr/bin/corosync-qnetd-tool"
+_client_cert_dir = "/etc/corosync/qdevice/net/nssdb"
+_client_cert_tool = "/usr/sbin/corosync-qdevice-net-certutil"
+
+def cert_to_url(cert):
+    return base64.b64encode(cert).decode("utf-8").replace("=", "%3D")
+
+class CertificateTestCase(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_tmpfile = mock.MagicMock()
+        self.mock_tmpfile.name = "tmpfile path"
 
 @mock.patch("pcs.lib.corosync.qdevice_net.external.is_dir_nonempty")
 class QdeviceSetupTest(TestCase):
@@ -32,7 +54,7 @@ class QdeviceSetupTest(TestCase):
         lib.qdevice_setup(self.mock_runner)
 
         mock_is_dir_nonempty.assert_called_once_with(_qnetd_cert_dir)
-        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-i"])
+        self.mock_runner.run.assert_called_once_with([_qnetd_cert_tool, "-i"])
 
     def test_cert_db_exists(self, mock_is_dir_nonempty):
         mock_is_dir_nonempty.return_value = True
@@ -47,7 +69,7 @@ class QdeviceSetupTest(TestCase):
         )
 
         mock_is_dir_nonempty.assert_called_once_with(_qnetd_cert_dir)
-        self.mock_runner.assert_not_called()
+        self.mock_runner.run.assert_not_called()
 
     def test_init_tool_fail(self, mock_is_dir_nonempty):
         mock_is_dir_nonempty.return_value = False
@@ -66,16 +88,24 @@ class QdeviceSetupTest(TestCase):
         )
 
         mock_is_dir_nonempty.assert_called_once_with(_qnetd_cert_dir)
-        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-i"])
+        self.mock_runner.run.assert_called_once_with([_qnetd_cert_tool, "-i"])
 
 
 @mock.patch("pcs.lib.corosync.qdevice_net.shutil.rmtree")
+@mock.patch("pcs.lib.corosync.qdevice_net.qdevice_initialized")
 class QdeviceDestroyTest(TestCase):
-    def test_success(self, mock_rmtree):
+    def test_success(self, mock_initialized, mock_rmtree):
+        mock_initialized.return_value = True
         lib.qdevice_destroy()
         mock_rmtree.assert_called_once_with(_qnetd_cert_dir)
 
-    def test_cert_dir_rm_error(self, mock_rmtree):
+    def test_not_initialized(self, mock_initialized, mock_rmtree):
+        mock_initialized.return_value = False
+        lib.qdevice_destroy()
+        mock_rmtree.assert_not_called()
+
+    def test_cert_dir_rm_error(self, mock_initialized, mock_rmtree):
+        mock_initialized.return_value = True
         mock_rmtree.side_effect = EnvironmentError("test errno", "test message")
         assert_raise_library_error(
             lib.qdevice_destroy,
@@ -89,3 +119,920 @@ class QdeviceDestroyTest(TestCase):
             )
         )
         mock_rmtree.assert_called_once_with(_qnetd_cert_dir)
+
+
+class QdeviceStatusGenericTest(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+
+    def test_success(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.qdevice_status_generic_text(self.mock_runner)
+        )
+        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-s"])
+
+    def test_success_verbose(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.qdevice_status_generic_text(self.mock_runner, True)
+        )
+        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-s", "-v"])
+
+    def test_error(self):
+        self.mock_runner.run.return_value = ("status error", 1)
+        assert_raise_library_error(
+            lambda: lib.qdevice_status_generic_text(self.mock_runner),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_GET_STATUS_ERROR,
+                {
+                    "model": "net",
+                    "reason": "status error",
+                }
+            )
+        )
+        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-s"])
+
+
+class QdeviceStatusClusterTest(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+
+    def test_success(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.qdevice_status_cluster_text(self.mock_runner)
+        )
+        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-l"])
+
+    def test_success_verbose(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.qdevice_status_cluster_text(self.mock_runner, verbose=True)
+        )
+        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-l", "-v"])
+
+    def test_success_cluster(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.qdevice_status_cluster_text(self.mock_runner, "cluster")
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _qnetd_tool, "-l", "-c", "cluster"
+        ])
+
+    def test_success_cluster_verbose(self):
+        self.mock_runner.run.return_value = ("status info", 0)
+        self.assertEqual(
+            "status info",
+            lib.qdevice_status_cluster_text(self.mock_runner, "cluster", True)
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _qnetd_tool, "-l", "-v", "-c", "cluster"
+        ])
+
+    def test_error(self):
+        self.mock_runner.run.return_value = ("status error", 1)
+        assert_raise_library_error(
+            lambda: lib.qdevice_status_cluster_text(self.mock_runner),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_GET_STATUS_ERROR,
+                {
+                    "model": "net",
+                    "reason": "status error",
+                }
+            )
+        )
+        self.mock_runner.run.assert_called_once_with([_qnetd_tool, "-l"])
+
+
+@mock.patch("pcs.lib.corosync.qdevice_net._get_output_certificate")
+@mock.patch("pcs.lib.corosync.qdevice_net._store_to_tmpfile")
+class QdeviceSignCertificateRequestTest(CertificateTestCase):
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.qdevice_initialized",
+        lambda: True
+    )
+    def test_success(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output", 0)
+        mock_get_cert.return_value = "new certificate".encode("utf-8")
+
+        result = lib.qdevice_sign_certificate_request(
+            self.mock_runner,
+            "certificate request",
+            "clusterName"
+        )
+        self.assertEqual(result, mock_get_cert.return_value)
+
+        mock_tmp_store.assert_called_once_with(
+            "certificate request",
+            reports.qdevice_certificate_sign_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _qnetd_cert_tool,
+            "-s", "-c", self.mock_tmpfile.name, "-n", "clusterName"
+        ])
+        mock_get_cert.assert_called_once_with(
+            "tool output",
+            reports.qdevice_certificate_sign_error
+        )
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.qdevice_initialized",
+        lambda: False
+    )
+    def test_not_initialized(self, mock_tmp_store, mock_get_cert):
+        assert_raise_library_error(
+            lambda: lib.qdevice_sign_certificate_request(
+                self.mock_runner,
+                "certificate request",
+                "clusterName"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_NOT_INITIALIZED,
+                {
+                    "model": "net",
+                }
+            )
+        )
+        mock_tmp_store.assert_not_called()
+        self.mock_runner.run.assert_not_called()
+        mock_get_cert.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.qdevice_initialized",
+        lambda: True
+    )
+    def test_input_write_error(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.side_effect = LibraryError
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib.qdevice_sign_certificate_request(
+                self.mock_runner,
+                "certificate request",
+                "clusterName"
+            )
+        )
+
+        self.mock_runner.run.assert_not_called()
+        mock_get_cert.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.qdevice_initialized",
+        lambda: True
+    )
+    def test_sign_error(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output error", 1)
+
+        assert_raise_library_error(
+            lambda: lib.qdevice_sign_certificate_request(
+                self.mock_runner,
+                "certificate request",
+                "clusterName"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_CERTIFICATE_SIGN_ERROR,
+                {
+                    "reason": "tool output error",
+                }
+            )
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "certificate request",
+            reports.qdevice_certificate_sign_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _qnetd_cert_tool,
+            "-s", "-c", self.mock_tmpfile.name, "-n", "clusterName"
+        ])
+        mock_get_cert.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.qdevice_initialized",
+        lambda: True
+    )
+    def test_output_read_error(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output", 0)
+        mock_get_cert.side_effect = LibraryError
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib.qdevice_sign_certificate_request(
+                self.mock_runner,
+                "certificate request",
+                "clusterName"
+            )
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "certificate request",
+            reports.qdevice_certificate_sign_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _qnetd_cert_tool,
+            "-s", "-c", self.mock_tmpfile.name, "-n", "clusterName"
+        ])
+        mock_get_cert.assert_called_once_with(
+            "tool output",
+            reports.qdevice_certificate_sign_error
+        )
+
+
+@mock.patch("pcs.lib.corosync.qdevice_net.shutil.rmtree")
+@mock.patch("pcs.lib.corosync.qdevice_net.client_initialized")
+class ClientDestroyTest(TestCase):
+    def test_success(self, mock_initialized, mock_rmtree):
+        mock_initialized.return_value = True
+        lib.client_destroy()
+        mock_rmtree.assert_called_once_with(_client_cert_dir)
+
+    def test_not_initialized(self, mock_initialized, mock_rmtree):
+        mock_initialized.return_value = False
+        lib.client_destroy()
+        mock_rmtree.assert_not_called()
+
+    def test_cert_dir_rm_error(self, mock_initialized, mock_rmtree):
+        mock_initialized.return_value = True
+        mock_rmtree.side_effect = EnvironmentError("test errno", "test message")
+        assert_raise_library_error(
+            lib.client_destroy,
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_DESTROY_ERROR,
+                {
+                    "model": "net",
+                    "reason": "test message",
+                }
+            )
+        )
+        mock_rmtree.assert_called_once_with(_client_cert_dir)
+
+
+class ClientSetupTest(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        self.original_path = settings.corosync_qdevice_net_client_certs_dir
+        settings.corosync_qdevice_net_client_certs_dir = get_test_resource(
+            "qdevice-certs"
+        )
+        self.ca_file_path = os.path.join(
+            settings.corosync_qdevice_net_client_certs_dir,
+            settings.corosync_qdevice_net_client_ca_file_name
+        )
+
+    def tearDown(self):
+        settings.corosync_qdevice_net_client_certs_dir = self.original_path
+
+    @mock.patch("pcs.lib.corosync.qdevice_net.client_destroy")
+    def test_success(self, mock_destroy):
+        self.mock_runner.run.return_value = ("tool output", 0)
+
+        lib.client_setup(self.mock_runner, "certificate data".encode("utf-8"))
+
+        self.assertEqual(
+            "certificate data".encode("utf-8"),
+            open(self.ca_file_path, "rb").read()
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-i", "-c", self.ca_file_path
+        ])
+        mock_destroy.assert_called_once_with()
+
+    @mock.patch("pcs.lib.corosync.qdevice_net.client_destroy")
+    def test_init_error(self, mock_destroy):
+        self.mock_runner.run.return_value = ("tool output error", 1)
+
+        assert_raise_library_error(
+            lambda: lib.client_setup(
+                self.mock_runner,
+                "certificate data".encode("utf-8")
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_INITIALIZATION_ERROR,
+                {
+                    "model": "net",
+                    "reason": "tool output error",
+                }
+            )
+        )
+
+        self.assertEqual(
+            "certificate data".encode("utf-8"),
+            open(self.ca_file_path, "rb").read()
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-i", "-c", self.ca_file_path
+        ])
+        mock_destroy.assert_called_once_with()
+
+
+@mock.patch("pcs.lib.corosync.qdevice_net._get_output_certificate")
+class ClientGenerateCertificateRequestTest(CertificateTestCase):
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_success(self, mock_get_cert):
+        self.mock_runner.run.return_value = ("tool output", 0)
+        mock_get_cert.return_value = "new certificate".encode("utf-8")
+
+        result = lib.client_generate_certificate_request(
+            self.mock_runner,
+            "clusterName"
+        )
+        self.assertEqual(result, mock_get_cert.return_value)
+
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-r", "-n", "clusterName"
+        ])
+        self.assertEqual(1, len(mock_get_cert.mock_calls))
+        self.assertEqual(
+            "tool output",
+            mock_get_cert.call_args[0][0]
+        )
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: False
+    )
+    def test_not_initialized(self, mock_get_cert):
+        assert_raise_library_error(
+            lambda: lib.client_generate_certificate_request(
+                self.mock_runner,
+                "clusterName"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_NOT_INITIALIZED,
+                {
+                    "model": "net",
+                }
+            )
+        )
+        self.mock_runner.run.assert_not_called()
+        mock_get_cert.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_tool_error(self, mock_get_cert):
+        self.mock_runner.run.return_value = ("tool output error", 1)
+
+        assert_raise_library_error(
+            lambda: lib.client_generate_certificate_request(
+                self.mock_runner,
+                "clusterName"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_INITIALIZATION_ERROR,
+                {
+                    "model": "net",
+                    "reason": "tool output error",
+                }
+            )
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-r", "-n", "clusterName"
+        ])
+        mock_get_cert.assert_not_called()
+
+
+@mock.patch("pcs.lib.corosync.qdevice_net._get_output_certificate")
+@mock.patch("pcs.lib.corosync.qdevice_net._store_to_tmpfile")
+class ClientCertRequestToPk12Test(CertificateTestCase):
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_success(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output", 0)
+        mock_get_cert.return_value = "new certificate".encode("utf-8")
+
+        result = lib.client_cert_request_to_pk12(
+            self.mock_runner,
+            "certificate request"
+        )
+        self.assertEqual(result, mock_get_cert.return_value)
+
+        mock_tmp_store.assert_called_once_with(
+            "certificate request",
+            reports.qdevice_certificate_import_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-M", "-c", self.mock_tmpfile.name
+        ])
+        mock_get_cert.assert_called_once_with(
+            "tool output",
+            reports.qdevice_certificate_import_error
+        )
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: False
+    )
+    def test_not_initialized(self, mock_tmp_store, mock_get_cert):
+        assert_raise_library_error(
+            lambda: lib.client_cert_request_to_pk12(
+                self.mock_runner,
+                "certificate request"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_NOT_INITIALIZED,
+                {
+                    "model": "net",
+                }
+            )
+        )
+        mock_tmp_store.assert_not_called()
+        self.mock_runner.run.assert_not_called()
+        mock_get_cert.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_input_write_error(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.side_effect = LibraryError
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib.client_cert_request_to_pk12(
+                self.mock_runner,
+                "certificate request"
+            )
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "certificate request",
+            reports.qdevice_certificate_import_error
+        )
+        self.mock_runner.run.assert_not_called()
+        mock_get_cert.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_transform_error(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output error", 1)
+
+        assert_raise_library_error(
+            lambda: lib.client_cert_request_to_pk12(
+                self.mock_runner,
+                "certificate request"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_CERTIFICATE_IMPORT_ERROR,
+                {
+                    "reason": "tool output error",
+                }
+            )
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "certificate request",
+            reports.qdevice_certificate_import_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-M", "-c", self.mock_tmpfile.name
+        ])
+        mock_get_cert.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_output_read_error(self, mock_tmp_store, mock_get_cert):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output", 0)
+        mock_get_cert.side_effect = LibraryError
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib.client_cert_request_to_pk12(
+                self.mock_runner,
+                "certificate request"
+            )
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "certificate request",
+            reports.qdevice_certificate_import_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-M", "-c", self.mock_tmpfile.name
+        ])
+        mock_get_cert.assert_called_once_with(
+            "tool output",
+            reports.qdevice_certificate_import_error
+        )
+
+
+@mock.patch("pcs.lib.corosync.qdevice_net._store_to_tmpfile")
+class ClientImportCertificateAndKeyTest(CertificateTestCase):
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_success(self, mock_tmp_store):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output", 0)
+
+        lib.client_import_certificate_and_key(
+            self.mock_runner,
+            "pk12 certificate"
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "pk12 certificate",
+            reports.qdevice_certificate_import_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-m", "-c", self.mock_tmpfile.name
+        ])
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: False
+    )
+    def test_not_initialized(self, mock_tmp_store):
+        assert_raise_library_error(
+            lambda: lib.client_import_certificate_and_key(
+                self.mock_runner,
+                "pk12 certificate"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_NOT_INITIALIZED,
+                {
+                    "model": "net",
+                }
+            )
+        )
+
+        mock_tmp_store.assert_not_called()
+        self.mock_runner.run.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_input_write_error(self, mock_tmp_store):
+        mock_tmp_store.side_effect = LibraryError
+
+        self.assertRaises(
+            LibraryError,
+            lambda: lib.client_import_certificate_and_key(
+                self.mock_runner,
+                "pk12 certificate"
+            )
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "pk12 certificate",
+            reports.qdevice_certificate_import_error
+        )
+        self.mock_runner.run.assert_not_called()
+
+    @mock.patch(
+        "pcs.lib.corosync.qdevice_net.client_initialized",
+        lambda: True
+    )
+    def test_import_error(self, mock_tmp_store):
+        mock_tmp_store.return_value = self.mock_tmpfile
+        self.mock_runner.run.return_value = ("tool output error", 1)
+
+        assert_raise_library_error(
+            lambda: lib.client_import_certificate_and_key(
+                self.mock_runner,
+                "pk12 certificate"
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_CERTIFICATE_IMPORT_ERROR,
+                {
+                    "reason": "tool output error",
+                }
+            )
+        )
+
+        mock_tmp_store.assert_called_once_with(
+            "pk12 certificate",
+            reports.qdevice_certificate_import_error
+        )
+        mock_tmp_store.assert_called_once_with(
+            "pk12 certificate",
+            reports.qdevice_certificate_import_error
+        )
+        self.mock_runner.run.assert_called_once_with([
+            _client_cert_tool, "-m", "-c", self.mock_tmpfile.name
+        ])
+
+
+class RemoteQdeviceGetCaCertificate(TestCase):
+    def test_success(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        expected_result = "abcd".encode("utf-8")
+        mock_communicator.call_host.return_value = base64.b64encode(
+            expected_result
+        )
+
+        result = lib.remote_qdevice_get_ca_certificate(
+            mock_communicator,
+            "qdevice host"
+        )
+        self.assertEqual(result, expected_result)
+
+        mock_communicator.call_host.assert_called_once_with(
+            "qdevice host",
+            "remote/qdevice_net_get_ca_certificate",
+            None
+        )
+
+    def test_decode_error(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_host.return_value = "error"
+
+        assert_raise_library_error(
+            lambda: lib.remote_qdevice_get_ca_certificate(
+                mock_communicator,
+                "qdevice host"
+            ),
+            (
+                severity.ERROR,
+                report_codes.INVALID_RESPONSE_FORMAT,
+                {
+                    "node": "qdevice host",
+                }
+            )
+        )
+
+    def test_comunication_error(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_host.side_effect = NodeCommunicationException(
+            "qdevice host", "command", "reason"
+        )
+
+        self.assertRaises(
+            NodeCommunicationException,
+            lambda: lib.remote_qdevice_get_ca_certificate(
+                mock_communicator,
+                "qdevice host"
+            )
+        )
+
+
+class RemoteClientSetupTest(TestCase):
+    def test_success(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        node = "node address"
+        ca_cert = "CA certificate".encode("utf-8")
+
+        lib.remote_client_setup(mock_communicator, node, ca_cert)
+
+        mock_communicator.call_node.assert_called_once_with(
+            node,
+            "remote/qdevice_net_client_init_certificate_storage",
+            "ca_certificate={0}".format(
+                cert_to_url(ca_cert)
+            )
+        )
+
+    def test_comunication_error(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_node.side_effect = NodeCommunicationException(
+            "node address", "command", "reason"
+        )
+
+        self.assertRaises(
+            NodeCommunicationException,
+            lambda: lib.remote_client_setup(
+                mock_communicator,
+                "node address",
+                "ca cert".encode("utf-8")
+            )
+        )
+
+
+class RemoteSignCertificateRequestTest(TestCase):
+    def test_success(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        cert_request = "request".encode("utf-8")
+        expected_result = "abcd".encode("utf-8")
+        host = "qdevice host"
+        cluster_name = "ClusterName"
+        mock_communicator.call_host.return_value = base64.b64encode(
+            expected_result
+        )
+
+        result = lib.remote_sign_certificate_request(
+            mock_communicator,
+            host,
+            cert_request,
+            cluster_name
+        )
+        self.assertEqual(result, expected_result)
+
+        mock_communicator.call_host.assert_called_once_with(
+            host,
+            "remote/qdevice_net_sign_node_certificate",
+            "certificate_request={0}&cluster_name={1}".format(
+                cert_to_url(cert_request),
+                cluster_name
+            )
+        )
+
+    def test_decode_error(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_host.return_value = "error"
+
+        assert_raise_library_error(
+            lambda: lib.remote_sign_certificate_request(
+                mock_communicator,
+                "qdevice host",
+                "cert request".encode("utf-8"),
+                "cluster name"
+            ),
+            (
+                severity.ERROR,
+                report_codes.INVALID_RESPONSE_FORMAT,
+                {
+                    "node": "qdevice host",
+                }
+            )
+        )
+
+    def test_comunication_error(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_host.side_effect = NodeCommunicationException(
+            "qdevice host", "command", "reason"
+        )
+
+        self.assertRaises(
+            NodeCommunicationException,
+            lambda: lib.remote_sign_certificate_request(
+                mock_communicator,
+                "qdevice host",
+                "cert request".encode("utf-8"),
+                "cluster name"
+            )
+        )
+
+
+class RemoteClientImportCertificateAndKeyTest(TestCase):
+    def test_success(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        node = "node address"
+        pk12_cert = "pk12 certificate".encode("utf-8")
+
+        lib.remote_client_import_certificate_and_key(
+            mock_communicator,
+            node,
+            pk12_cert
+        )
+
+        mock_communicator.call_node.assert_called_once_with(
+            node,
+            "remote/qdevice_net_client_import_certificate",
+            "certificate={0}".format(
+                cert_to_url(pk12_cert)
+            )
+        )
+
+    def test_comunication_error(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_node.side_effect = NodeCommunicationException(
+            "node address", "command", "reason"
+        )
+
+        self.assertRaises(
+            NodeCommunicationException,
+            lambda: lib.remote_client_import_certificate_and_key(
+                mock_communicator,
+                "node address",
+                "pk12 cert".encode("utf-8")
+            )
+        )
+
+
+class RemoteClientDestroy(TestCase):
+    def test_success(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        node = "node address"
+
+        lib.remote_client_destroy(mock_communicator, node)
+
+        mock_communicator.call_node.assert_called_once_with(
+            node,
+            "remote/qdevice_net_client_destroy",
+            None
+        )
+
+    def test_comunication_error(self):
+        mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+        mock_communicator.call_node.side_effect = NodeCommunicationException(
+            "node address", "command", "reason"
+        )
+
+        self.assertRaises(
+            NodeCommunicationException,
+            lambda: lib.remote_client_destroy(mock_communicator, "node address")
+        )
+
+
+class GetOutputCertificateTest(TestCase):
+    def setUp(self):
+        self.file_path = get_test_resource("qdevice-certs/qnetd-cacert.crt")
+        self.file_data = open(self.file_path, "rb").read()
+
+    def test_success(self):
+        cert_tool_output = """
+some line
+Certificate stored in {0}
+some other line
+        """.format(self.file_path)
+        report_func = mock.MagicMock()
+
+        self.assertEqual(
+            self.file_data,
+            lib._get_output_certificate(cert_tool_output, report_func)
+        )
+        report_func.assert_not_called()
+
+    def test_success_request(self):
+        cert_tool_output = """
+some line
+Certificate request stored in {0}
+some other line
+        """.format(self.file_path)
+        report_func = mock.MagicMock()
+
+        self.assertEqual(
+            self.file_data,
+            lib._get_output_certificate(cert_tool_output, report_func)
+        )
+        report_func.assert_not_called()
+
+    def test_message_not_found(self):
+        cert_tool_output = "some rubbish output"
+        report_func = reports.qdevice_certificate_import_error
+
+        assert_raise_library_error(
+            lambda: lib._get_output_certificate(
+                cert_tool_output,
+                report_func
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_CERTIFICATE_IMPORT_ERROR,
+                {
+                    "reason": cert_tool_output,
+                }
+            )
+        )
+
+    def test_cannot_read_file(self):
+        cert_tool_output = """
+some line
+Certificate request stored in {0}.bad
+some other line
+        """.format(self.file_path)
+        report_func = reports.qdevice_certificate_import_error
+
+        assert_raise_library_error(
+            lambda: lib._get_output_certificate(
+                cert_tool_output,
+                report_func
+            ),
+            (
+                severity.ERROR,
+                report_codes.QDEVICE_CERTIFICATE_IMPORT_ERROR,
+                {
+                    "reason": "{0}.bad: No such file or directory".format(
+                        self.file_path
+                    ),
+                }
+            )
+        )
+
diff --git a/pcs/test/test_lib_env.py b/pcs/test/test_lib_env.py
index 95f7a00..c6322b7 100644
--- a/pcs/test/test_lib_env.py
+++ b/pcs/test/test_lib_env.py
@@ -235,13 +235,24 @@ class LibraryEnvironmentTest(TestCase):
             )]
         )
 
+    @mock.patch("pcs.lib.env.qdevice_reload_on_nodes")
     @mock.patch("pcs.lib.env.check_corosync_offline_on_nodes")
     @mock.patch("pcs.lib.env.reload_corosync_config")
     @mock.patch("pcs.lib.env.distribute_corosync_conf")
     @mock.patch("pcs.lib.env.get_local_corosync_conf")
+    @mock.patch.object(
+        LibraryEnvironment,
+        "node_communicator",
+        lambda self: "mock node communicator"
+    )
+    @mock.patch.object(
+        LibraryEnvironment,
+        "cmd_runner",
+        lambda self: "mock cmd runner"
+    )
     def test_corosync_conf_set(
         self, mock_get_corosync, mock_distribute, mock_reload,
-        mock_check_offline
+        mock_check_offline, mock_qdevice_reload
     ):
         corosync_data = "totem {\n    version: 2\n}\n"
         new_corosync_data = "totem {\n    version: 3\n}\n"
@@ -266,8 +277,11 @@ class LibraryEnvironmentTest(TestCase):
         self.assertEqual(0, mock_get_corosync.call_count)
         mock_check_offline.assert_not_called()
         mock_reload.assert_not_called()
+        mock_qdevice_reload.assert_not_called()
 
+    @mock.patch("pcs.lib.env.qdevice_reload_on_nodes")
     @mock.patch("pcs.lib.env.reload_corosync_config")
+    @mock.patch("pcs.lib.env.is_service_running")
     @mock.patch("pcs.lib.env.distribute_corosync_conf")
     @mock.patch("pcs.lib.env.get_local_corosync_conf")
     @mock.patch.object(
@@ -285,12 +299,14 @@ class LibraryEnvironmentTest(TestCase):
         "cmd_runner",
         lambda self: "mock cmd runner"
     )
-    def test_corosync_conf_not_set(
-        self, mock_get_corosync, mock_distribute, mock_reload
+    def test_corosync_conf_not_set_online(
+        self, mock_get_corosync, mock_distribute, mock_is_running, mock_reload,
+        mock_qdevice_reload
     ):
         corosync_data = open(rc("corosync.conf")).read()
         new_corosync_data = corosync_data.replace("version: 2", "version: 3")
         mock_get_corosync.return_value = corosync_data
+        mock_is_running.return_value = True
         env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
 
         self.assertTrue(env.is_corosync_conf_live)
@@ -309,10 +325,120 @@ class LibraryEnvironmentTest(TestCase):
             new_corosync_data,
             False
         )
+        mock_is_running.assert_called_once_with("mock cmd runner", "corosync")
         mock_reload.assert_called_once_with("mock cmd runner")
+        mock_qdevice_reload.assert_not_called()
 
+    @mock.patch("pcs.lib.env.qdevice_reload_on_nodes")
+    @mock.patch("pcs.lib.env.reload_corosync_config")
+    @mock.patch("pcs.lib.env.is_service_running")
+    @mock.patch("pcs.lib.env.distribute_corosync_conf")
+    @mock.patch("pcs.lib.env.get_local_corosync_conf")
+    @mock.patch.object(
+        CorosyncConfigFacade,
+        "get_nodes",
+        lambda self: "mock node list"
+    )
+    @mock.patch.object(
+        LibraryEnvironment,
+        "node_communicator",
+        lambda self: "mock node communicator"
+    )
+    @mock.patch.object(
+        LibraryEnvironment,
+        "cmd_runner",
+        lambda self: "mock cmd runner"
+    )
+    def test_corosync_conf_not_set_offline(
+        self, mock_get_corosync, mock_distribute, mock_is_running, mock_reload,
+        mock_qdevice_reload
+    ):
+        corosync_data = open(rc("corosync.conf")).read()
+        new_corosync_data = corosync_data.replace("version: 2", "version: 3")
+        mock_get_corosync.return_value = corosync_data
+        mock_is_running.return_value = False
+        env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+
+        self.assertTrue(env.is_corosync_conf_live)
+
+        self.assertEqual(corosync_data, env.get_corosync_conf_data())
+        self.assertEqual(corosync_data, env.get_corosync_conf().config.export())
+        self.assertEqual(2, mock_get_corosync.call_count)
+
+        env.push_corosync_conf(
+            CorosyncConfigFacade.from_string(new_corosync_data)
+        )
+        mock_distribute.assert_called_once_with(
+            "mock node communicator",
+            self.mock_reporter,
+            "mock node list",
+            new_corosync_data,
+            False
+        )
+        mock_is_running.assert_called_once_with("mock cmd runner", "corosync")
+        mock_reload.assert_not_called()
+        mock_qdevice_reload.assert_not_called()
+
+    @mock.patch("pcs.lib.env.qdevice_reload_on_nodes")
+    @mock.patch("pcs.lib.env.check_corosync_offline_on_nodes")
+    @mock.patch("pcs.lib.env.reload_corosync_config")
+    @mock.patch("pcs.lib.env.is_service_running")
+    @mock.patch("pcs.lib.env.distribute_corosync_conf")
+    @mock.patch("pcs.lib.env.get_local_corosync_conf")
+    @mock.patch.object(
+        CorosyncConfigFacade,
+        "get_nodes",
+        lambda self: "mock node list"
+    )
+    @mock.patch.object(
+        LibraryEnvironment,
+        "node_communicator",
+        lambda self: "mock node communicator"
+    )
+    @mock.patch.object(
+        LibraryEnvironment,
+        "cmd_runner",
+        lambda self: "mock cmd runner"
+    )
+    def test_corosync_conf_not_set_need_qdevice_reload_success(
+        self, mock_get_corosync, mock_distribute, mock_is_running, mock_reload,
+        mock_check_offline, mock_qdevice_reload
+    ):
+        corosync_data = open(rc("corosync.conf")).read()
+        new_corosync_data = corosync_data.replace("version: 2", "version: 3")
+        mock_get_corosync.return_value = corosync_data
+        mock_is_running.return_value = True
+        env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+
+        self.assertTrue(env.is_corosync_conf_live)
+
+        self.assertEqual(corosync_data, env.get_corosync_conf_data())
+        self.assertEqual(corosync_data, env.get_corosync_conf().config.export())
+        self.assertEqual(2, mock_get_corosync.call_count)
+
+        conf_facade = CorosyncConfigFacade.from_string(new_corosync_data)
+        conf_facade._need_qdevice_reload = True
+        env.push_corosync_conf(conf_facade)
+        mock_check_offline.assert_not_called()
+        mock_distribute.assert_called_once_with(
+            "mock node communicator",
+            self.mock_reporter,
+            "mock node list",
+            new_corosync_data,
+            False
+        )
+        mock_reload.assert_called_once_with("mock cmd runner")
+        mock_qdevice_reload.assert_called_once_with(
+            "mock node communicator",
+            self.mock_reporter,
+            "mock node list",
+            False
+        )
+
+    @mock.patch("pcs.lib.env.qdevice_reload_on_nodes")
     @mock.patch("pcs.lib.env.check_corosync_offline_on_nodes")
     @mock.patch("pcs.lib.env.reload_corosync_config")
+    @mock.patch("pcs.lib.env.is_service_running")
     @mock.patch("pcs.lib.env.distribute_corosync_conf")
     @mock.patch("pcs.lib.env.get_local_corosync_conf")
     @mock.patch.object(
@@ -326,12 +452,13 @@ class LibraryEnvironmentTest(TestCase):
         lambda self: "mock node communicator"
     )
     def test_corosync_conf_not_set_need_offline_success(
-        self, mock_get_corosync, mock_distribute, mock_reload,
-        mock_check_offline
+        self, mock_get_corosync, mock_distribute, mock_is_running, mock_reload,
+        mock_check_offline, mock_qdevice_reload
     ):
         corosync_data = open(rc("corosync.conf")).read()
         new_corosync_data = corosync_data.replace("version: 2", "version: 3")
         mock_get_corosync.return_value = corosync_data
+        mock_is_running.return_value = False
         env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
 
         self.assertTrue(env.is_corosync_conf_live)
@@ -357,7 +484,9 @@ class LibraryEnvironmentTest(TestCase):
             False
         )
         mock_reload.assert_not_called()
+        mock_qdevice_reload.assert_not_called()
 
+    @mock.patch("pcs.lib.env.qdevice_reload_on_nodes")
     @mock.patch("pcs.lib.env.check_corosync_offline_on_nodes")
     @mock.patch("pcs.lib.env.reload_corosync_config")
     @mock.patch("pcs.lib.env.distribute_corosync_conf")
@@ -374,7 +503,7 @@ class LibraryEnvironmentTest(TestCase):
     )
     def test_corosync_conf_not_set_need_offline_fail(
         self, mock_get_corosync, mock_distribute, mock_reload,
-        mock_check_offline
+        mock_check_offline, mock_qdevice_reload
     ):
         corosync_data = open(rc("corosync.conf")).read()
         new_corosync_data = corosync_data.replace("version: 2", "version: 3")
@@ -410,6 +539,7 @@ class LibraryEnvironmentTest(TestCase):
         )
         mock_distribute.assert_not_called()
         mock_reload.assert_not_called()
+        mock_qdevice_reload.assert_not_called()
 
     @mock.patch("pcs.lib.env.CommandRunner")
     def test_cmd_runner_no_options(self, mock_runner):
diff --git a/pcs/test/test_lib_external.py b/pcs/test/test_lib_external.py
index c08b059..929a50d 100644
--- a/pcs/test/test_lib_external.py
+++ b/pcs/test/test_lib_external.py
@@ -31,7 +31,11 @@ from pcs.test.tools.pcs_mock import mock
 
 from pcs import settings
 from pcs.common import report_codes
-from pcs.lib.errors import ReportItemSeverity as severity
+from pcs.lib import reports
+from pcs.lib.errors import (
+    LibraryError,
+    ReportItemSeverity as severity
+)
 
 import pcs.lib.external as lib
 
@@ -830,6 +834,126 @@ class NodeCommunicatorExceptionTransformTest(TestCase):
         self.assertTrue(raised)
 
 
+class ParallelCommunicationHelperTest(TestCase):
+    def setUp(self):
+        self.mock_reporter = MockLibraryReportProcessor()
+
+    def fixture_raiser(self):
+        def raiser(x, *args, **kwargs):
+            if x == 1:
+                raise lib.NodeConnectionException("node", "command", "reason")
+            elif x == 2:
+                raise LibraryError(
+                    reports.corosync_config_distribution_node_error("node")
+                )
+        return raiser
+
+    def test_success(self):
+        func = mock.MagicMock()
+        lib.parallel_nodes_communication_helper(
+            func,
+            [([x], {"a": x*2,}) for x in range(3)],
+            self.mock_reporter,
+            skip_offline_nodes=False
+        )
+        expected_calls = [
+            mock.call(0, a=0),
+            mock.call(1, a=2),
+            mock.call(2, a=4),
+        ]
+        self.assertEqual(len(expected_calls), len(func.mock_calls))
+        func.assert_has_calls(expected_calls)
+        self.assertEqual(self.mock_reporter.report_item_list, [])
+
+    def test_errors(self):
+        func = self.fixture_raiser()
+        assert_raise_library_error(
+            lambda: lib.parallel_nodes_communication_helper(
+                func,
+                [([x], {"a": x*2,}) for x in range(4)],
+                self.mock_reporter,
+                skip_offline_nodes=False
+            ),
+            (
+                severity.ERROR,
+                report_codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
+                {
+                    "node": "node",
+                    "reason": "reason",
+                    "command": "command",
+                },
+                report_codes.SKIP_OFFLINE_NODES
+            ),
+            (
+                severity.ERROR,
+                report_codes.COROSYNC_CONFIG_DISTRIBUTION_NODE_ERROR,
+                {
+                    "node": "node",
+                }
+            )
+        )
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.ERROR,
+                    report_codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
+                    {
+                        "node": "node",
+                        "reason": "reason",
+                        "command": "command",
+                    },
+                    report_codes.SKIP_OFFLINE_NODES
+                ),
+                (
+                    severity.ERROR,
+                    report_codes.COROSYNC_CONFIG_DISTRIBUTION_NODE_ERROR,
+                    {
+                        "node": "node",
+                    }
+                )
+            ]
+        )
+
+    def test_errors_skip_offline(self):
+        func = self.fixture_raiser()
+        assert_raise_library_error(
+            lambda: lib.parallel_nodes_communication_helper(
+                func,
+                [([x], {"a": x*2,}) for x in range(4)],
+                self.mock_reporter,
+                skip_offline_nodes=True
+            ),
+            (
+                severity.ERROR,
+                report_codes.COROSYNC_CONFIG_DISTRIBUTION_NODE_ERROR,
+                {
+                    "node": "node",
+                }
+            )
+        )
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.WARNING,
+                    report_codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
+                    {
+                        "node": "node",
+                        "reason": "reason",
+                        "command": "command",
+                    }
+                ),
+                (
+                    severity.ERROR,
+                    report_codes.COROSYNC_CONFIG_DISTRIBUTION_NODE_ERROR,
+                    {
+                        "node": "node",
+                    }
+                )
+            ]
+        )
+
 class IsCmanClusterTest(TestCase):
     def template_test(self, is_cman, corosync_output, corosync_retval=0):
         mock_runner = mock.MagicMock(spec_set=lib.CommandRunner)
diff --git a/pcs/test/test_lib_nodes_task.py b/pcs/test/test_lib_nodes_task.py
index 6af47d7..cff88eb 100644
--- a/pcs/test/test_lib_nodes_task.py
+++ b/pcs/test/test_lib_nodes_task.py
@@ -27,14 +27,6 @@ class DistributeCorosyncConfTest(TestCase):
         self.mock_reporter = MockLibraryReportProcessor()
         self.mock_communicator = "mock node communicator"
 
-    def assert_set_remote_corosync_conf_call(self, a_call, node_ring0, config):
-        self.assertEqual("set_remote_corosync_conf", a_call[0])
-        self.assertEqual(3, len(a_call[1]))
-        self.assertEqual(self.mock_communicator, a_call[1][0])
-        self.assertEqual(node_ring0, a_call[1][1].ring0)
-        self.assertEqual(config, a_call[1][2])
-        self.assertEqual(0, len(a_call[2]))
-
     @mock.patch("pcs.lib.nodes_task.corosync_live")
     def test_success(self, mock_corosync_live):
         conf_text = "test conf text"
@@ -53,21 +45,19 @@ class DistributeCorosyncConfTest(TestCase):
 
         corosync_live_calls = [
             mock.call.set_remote_corosync_conf(
-                "mock node communicator", nodes[0], conf_text
+                "mock node communicator", node_addrs_list[0], conf_text
             ),
             mock.call.set_remote_corosync_conf(
-                "mock node communicator", nodes[1], conf_text
+                "mock node communicator", node_addrs_list[1], conf_text
             ),
         ]
         self.assertEqual(
             len(corosync_live_calls),
             len(mock_corosync_live.mock_calls)
         )
-        self.assert_set_remote_corosync_conf_call(
-            mock_corosync_live.mock_calls[0], nodes[0], conf_text
-        )
-        self.assert_set_remote_corosync_conf_call(
-            mock_corosync_live.mock_calls[1], nodes[1], conf_text
+        mock_corosync_live.set_remote_corosync_conf.assert_has_calls(
+            corosync_live_calls,
+            any_order=True
         )
 
         assert_report_item_list_equal(
@@ -145,12 +135,10 @@ class DistributeCorosyncConfTest(TestCase):
             len(corosync_live_calls),
             len(mock_corosync_live.mock_calls)
         )
-        self.assert_set_remote_corosync_conf_call(
-            mock_corosync_live.mock_calls[0], nodes[0], conf_text
-        )
-        self.assert_set_remote_corosync_conf_call(
-            mock_corosync_live.mock_calls[1], nodes[1], conf_text
-        )
+        mock_corosync_live.set_remote_corosync_conf.assert_has_calls([
+            mock.call("mock node communicator", node_addrs_list[0], conf_text),
+            mock.call("mock node communicator", node_addrs_list[1], conf_text),
+        ], any_order=True)
 
         assert_report_item_list_equal(
             self.mock_reporter.report_item_list,
@@ -221,12 +209,10 @@ class DistributeCorosyncConfTest(TestCase):
             len(corosync_live_calls),
             len(mock_corosync_live.mock_calls)
         )
-        self.assert_set_remote_corosync_conf_call(
-            mock_corosync_live.mock_calls[0], nodes[0], conf_text
-        )
-        self.assert_set_remote_corosync_conf_call(
-            mock_corosync_live.mock_calls[1], nodes[1], conf_text
-        )
+        mock_corosync_live.set_remote_corosync_conf.assert_has_calls([
+            mock.call("mock node communicator", node_addrs_list[0], conf_text),
+            mock.call("mock node communicator", node_addrs_list[1], conf_text),
+        ], any_order=True)
 
         assert_report_item_list_equal(
             self.mock_reporter.report_item_list,
@@ -452,6 +438,134 @@ class CheckCorosyncOfflineTest(TestCase):
         )
 
 
+@mock.patch("pcs.lib.nodes_task.qdevice_client.remote_client_stop")
+@mock.patch("pcs.lib.nodes_task.qdevice_client.remote_client_start")
+class QdeviceReloadOnNodesTest(TestCase):
+    def setUp(self):
+        self.mock_reporter = MockLibraryReportProcessor()
+        self.mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
+
+    def test_success(self, mock_remote_start, mock_remote_stop):
+        nodes = ["node1", "node2"]
+        node_addrs_list = NodeAddressesList(
+            [NodeAddresses(addr) for addr in nodes]
+        )
+
+        lib.qdevice_reload_on_nodes(
+            self.mock_communicator,
+            self.mock_reporter,
+            node_addrs_list
+        )
+
+        node_calls = [
+            mock.call(
+                self.mock_reporter, self.mock_communicator, node_addrs_list[0]
+            ),
+            mock.call(
+                self.mock_reporter, self.mock_communicator, node_addrs_list[1]
+            ),
+        ]
+        self.assertEqual(len(node_calls), len(mock_remote_stop.mock_calls))
+        self.assertEqual(len(node_calls), len(mock_remote_start.mock_calls))
+        mock_remote_stop.assert_has_calls(node_calls, any_order=True)
+        mock_remote_start.assert_has_calls(node_calls, any_order=True)
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CLIENT_RELOAD_STARTED,
+                    {}
+                ),
+            ]
+        )
+
+    def test_fail_doesnt_prevent_start(
+        self, mock_remote_start, mock_remote_stop
+    ):
+        nodes = ["node1", "node2"]
+        node_addrs_list = NodeAddressesList(
+            [NodeAddresses(addr) for addr in nodes]
+        )
+        def raiser(reporter, communicator, node):
+            if node.ring0 == nodes[1]:
+                raise NodeAuthenticationException(
+                    node.label, "command", "HTTP error: 401"
+                )
+        mock_remote_stop.side_effect = raiser
+
+        assert_raise_library_error(
+            lambda: lib.qdevice_reload_on_nodes(
+                self.mock_communicator,
+                self.mock_reporter,
+                node_addrs_list
+            ),
+            (
+                severity.ERROR,
+                report_codes.NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED,
+                {
+                    "node": nodes[1],
+                    "command": "command",
+                    "reason" : "HTTP error: 401",
+                },
+                report_codes.SKIP_OFFLINE_NODES
+            )
+        )
+
+        node_calls = [
+            mock.call(
+                self.mock_reporter, self.mock_communicator, node_addrs_list[0]
+            ),
+            mock.call(
+                self.mock_reporter, self.mock_communicator, node_addrs_list[1]
+            ),
+        ]
+        self.assertEqual(len(node_calls), len(mock_remote_stop.mock_calls))
+        self.assertEqual(len(node_calls), len(mock_remote_start.mock_calls))
+        mock_remote_stop.assert_has_calls(node_calls, any_order=True)
+        mock_remote_start.assert_has_calls(node_calls, any_order=True)
+
+        assert_report_item_list_equal(
+            self.mock_reporter.report_item_list,
+            [
+                (
+                    severity.INFO,
+                    report_codes.QDEVICE_CLIENT_RELOAD_STARTED,
+                    {}
+                ),
+                # why the same error twice?
+                # 1. Tested piece of code calls a function which puts an error
+                # into the reporter. The reporter raises an exception. The
+                # exception is caught in the tested piece of code, stored, and
+                # later put to reporter again.
+                # 2. Mock reporter remembers everything that goes through it
+                # and by the machanism described in 1 the error goes througt it
+                # twice.
+                (
+                    severity.ERROR,
+                    report_codes.NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED,
+                    {
+                        "node": nodes[1],
+                        "command": "command",
+                        "reason" : "HTTP error: 401",
+                    },
+                    report_codes.SKIP_OFFLINE_NODES
+                ),
+                (
+                    severity.ERROR,
+                    report_codes.NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED,
+                    {
+                        "node": nodes[1],
+                        "command": "command",
+                        "reason" : "HTTP error: 401",
+                    },
+                    report_codes.SKIP_OFFLINE_NODES
+                ),
+            ]
+        )
+
+
 class NodeCheckAuthTest(TestCase):
     def test_success(self):
         mock_communicator = mock.MagicMock(spec_set=NodeCommunicator)
diff --git a/pcs/test/test_quorum.py b/pcs/test/test_quorum.py
index 8167ad9..86de4c6 100644
--- a/pcs/test/test_quorum.py
+++ b/pcs/test/test_quorum.py
@@ -144,7 +144,7 @@ class DeviceAddTest(TestBase):
 
     def test_success_model_only(self):
         self.assert_pcs_success(
-            "quorum device add model net host=127.0.0.1 algorithm=ffsplit"
+            "quorum device add model net host=127.0.0.1 algorithm=lms"
         )
         self.assert_pcs_success(
             "quorum config",
@@ -152,7 +152,7 @@ class DeviceAddTest(TestBase):
 Options:
 Device:
   Model: net
-    algorithm: ffsplit
+    algorithm: lms
     host: 127.0.0.1
 """
         )
@@ -167,6 +167,7 @@ Device:
 Options:
 Device:
   timeout: 12345
+  votes: 1
   Model: net
     algorithm: ffsplit
     host: 127.0.0.1
@@ -193,7 +194,7 @@ Error: required option 'host' is missing
         self.assert_pcs_fail(
             "quorum device add a=b timeout=-1 model net host=127.0.0.1 algorithm=x c=d",
             """\
-Error: 'x' is not a valid algorithm value, use 2nodelms, ffsplit, lms, use --force to override
+Error: 'x' is not a valid algorithm value, use ffsplit, lms, use --force to override
 Error: invalid quorum device model option 'c', allowed options are: algorithm, connect_timeout, force_ip_version, host, port, tie_breaker, use --force to override
 Error: invalid quorum device option 'a', allowed options are: sync_timeout, timeout, use --force to override
 Error: '-1' is not a valid timeout value, use positive integer, use --force to override
@@ -203,7 +204,7 @@ Error: '-1' is not a valid timeout value, use positive integer, use --force to o
         self.assert_pcs_success(
             "quorum device add a=b timeout=-1 model net host=127.0.0.1 algorithm=x c=d --force",
             """\
-Warning: 'x' is not a valid algorithm value, use 2nodelms, ffsplit, lms
+Warning: 'x' is not a valid algorithm value, use ffsplit, lms
 Warning: invalid quorum device model option 'c', allowed options are: algorithm, connect_timeout, force_ip_version, host, port, tie_breaker
 Warning: invalid quorum device option 'a', allowed options are: sync_timeout, timeout
 Warning: '-1' is not a valid timeout value, use positive integer
diff --git a/pcs/test/test_utils.py b/pcs/test/test_utils.py
index c61a2b8..819f8ee 100644
--- a/pcs/test/test_utils.py
+++ b/pcs/test/test_utils.py
@@ -967,1359 +967,1607 @@ class UtilsTest(unittest.TestCase):
             }
         )
 
-    def test_parse_cman_quorum_info(self):
-        parsed = utils.parse_cman_quorum_info("""\
-Version: 6.2.0
-Config Version: 23
-Cluster Name: cluster66
-Cluster Id: 22265
-Cluster Member: Yes
-Cluster Generation: 3612
-Membership state: Cluster-Member
-Nodes: 3
-Expected votes: 3
-Total votes: 3
-Node votes: 1
-Quorum: 2 
-Active subsystems: 8
-Flags: 
-Ports Bound: 0 
-Node name: rh66-node2
-Node ID: 2
-Multicast addresses: 239.192.86.80
-Node addresses: 192.168.122.61
----Votes---
-1 M 3 rh66-node1
-2 M 2 rh66-node2
-3 M 1 rh66-node3
-""")
-        self.assertEqual(True, parsed["quorate"])
-        self.assertEqual(2, parsed["quorum"])
+    def test_get_operations_from_transitions(self):
+        transitions = utils.parse(rc("transitions01.xml"))
         self.assertEqual(
             [
-                {"name": "rh66-node1", "votes": 3, "local": False},
-                {"name": "rh66-node2", "votes": 2, "local": True},
-                {"name": "rh66-node3", "votes": 1, "local": False},
+                {
+                    'id': 'dummy',
+                    'long_id': 'dummy',
+                    'operation': 'stop',
+                    'on_node': 'rh7-3',
+                },
+                {
+                    'id': 'dummy',
+                    'long_id': 'dummy',
+                    'operation': 'start',
+                    'on_node': 'rh7-2',
+                },
+                {
+                    'id': 'd0',
+                    'long_id': 'd0:1',
+                    'operation': 'stop',
+                    'on_node': 'rh7-1',
+                },
+                {
+                    'id': 'd0',
+                    'long_id': 'd0:1',
+                    'operation': 'start',
+                    'on_node': 'rh7-2',
+                },
+                {
+                    'id': 'state',
+                    'long_id': 'state:0',
+                    'operation': 'stop',
+                    'on_node': 'rh7-3',
+                },
+                {
+                    'id': 'state',
+                    'long_id': 'state:0',
+                    'operation': 'start',
+                    'on_node': 'rh7-2',
+                },
             ],
-            parsed["node_list"]
+            utils.get_operations_from_transitions(transitions)
         )
 
-        parsed = utils.parse_cman_quorum_info("""\
-Version: 6.2.0
-Config Version: 23
-Cluster Name: cluster66
-Cluster Id: 22265
-Cluster Member: Yes
-Cluster Generation: 3612
-Membership state: Cluster-Member
-Nodes: 3
-Expected votes: 3
-Total votes: 3
-Node votes: 1
-Quorum: 2 Activity blocked
-Active subsystems: 8
-Flags: 
-Ports Bound: 0 
-Node name: rh66-node1
-Node ID: 1
-Multicast addresses: 239.192.86.80
-Node addresses: 192.168.122.61
----Votes---
-1 M 3 rh66-node1
-2 X 2 rh66-node2
-3 X 1 rh66-node3
-""")
-        self.assertEqual(False, parsed["quorate"])
-        self.assertEqual(2, parsed["quorum"])
+        transitions = utils.parse(rc("transitions02.xml"))
         self.assertEqual(
             [
-                {"name": "rh66-node1", "votes": 3, "local": True},
+                {
+                    "id": "RemoteNode",
+                    "long_id": "RemoteNode",
+                    "operation": "stop",
+                    "on_node": "virt-143",
+                },
+                {
+                    "id": "RemoteNode",
+                    "long_id": "RemoteNode",
+                    "operation": "migrate_to",
+                    "on_node": "virt-143",
+                },
+                {
+                    "id": "RemoteNode",
+                    "long_id": "RemoteNode",
+                    "operation": "migrate_from",
+                    "on_node": "virt-142",
+                },
+                {
+                    "id": "dummy8",
+                    "long_id": "dummy8",
+                    "operation": "stop",
+                    "on_node": "virt-143",
+                },
+                {
+                    "id": "dummy8",
+                    "long_id": "dummy8",
+                    "operation": "start",
+                    "on_node": "virt-142",
+                }
             ],
-            parsed["node_list"]
+            utils.get_operations_from_transitions(transitions)
         )
 
-        parsed = utils.parse_cman_quorum_info("")
-        self.assertEqual(None, parsed)
-
-        parsed = utils.parse_cman_quorum_info("""\
-Version: 6.2.0
-Config Version: 23
-Cluster Name: cluster66
-Cluster Id: 22265
-Cluster Member: Yes
-Cluster Generation: 3612
-Membership state: Cluster-Member
-Nodes: 3
-Expected votes: 3
-Total votes: 3
-Node votes: 1
-Quorum: 
-Active subsystems: 8
-Flags: 
-Ports Bound: 0 
-Node name: rh66-node2
-Node ID: 2
-Multicast addresses: 239.192.86.80
-Node addresses: 192.168.122.61
----Votes---
-1 M 3 rh66-node1
-2 M 2 rh66-node2
-3 M 1 rh66-node3
-""")
-        self.assertEqual(None, parsed)
-
-        parsed = utils.parse_cman_quorum_info("""\
-Version: 6.2.0
-Config Version: 23
-Cluster Name: cluster66
-Cluster Id: 22265
-Cluster Member: Yes
-Cluster Generation: 3612
-Membership state: Cluster-Member
-Nodes: 3
-Expected votes: 3
-Total votes: 3
-Node votes: 1
-Quorum: Foo
-Active subsystems: 8
-Flags: 
-Ports Bound: 0 
-Node name: rh66-node2
-Node ID: 2
-Multicast addresses: 239.192.86.80
-Node addresses: 192.168.122.61
----Votes---
-1 M 3 rh66-node1
-2 M 2 rh66-node2
-3 M 1 rh66-node3
-""")
-        self.assertEqual(None, parsed)
-
-        parsed = utils.parse_cman_quorum_info("""\
-Version: 6.2.0
-Config Version: 23
-Cluster Name: cluster66
-Cluster Id: 22265
-Cluster Member: Yes
-Cluster Generation: 3612
-Membership state: Cluster-Member
-Nodes: 3
-Expected votes: 3
-Total votes: 3
-Node votes: 1
-Quorum: 4
-Active subsystems: 8
-Flags: 
-Ports Bound: 0 
-Node name: rh66-node2
-Node ID: 2
-Multicast addresses: 239.192.86.80
-Node addresses: 192.168.122.61
----Votes---
-1 M 3 rh66-node1
-2 M Foo rh66-node2
-3 M 1 rh66-node3
-""")
-        self.assertEqual(None, parsed)
-
-    def test_parse_quorumtool_output(self):
-        parsed = utils.parse_quorumtool_output("""\
-Quorum information
-------------------
-Date:             Fri Jan 16 13:03:28 2015
-Quorum provider:  corosync_votequorum
-Nodes:            3
-Node ID:          1
-Ring ID:          19860
-Quorate:          Yes
-
-Votequorum information
-----------------------
-Expected votes:   3
-Highest expected: 3
-Total votes:      3
-Quorum:           2
-Flags:            Quorate
+    def test_get_resources_location_from_operations(self):
+        cib_dom = self.get_cib_resources()
 
-Membership information
-----------------------
-    Nodeid      Votes    Qdevice Name
-         1          3         NR rh70-node1
-         2          2         NR rh70-node2 (local)
-         3          1         NR rh70-node3
-""")
-        self.assertEqual(True, parsed["quorate"])
-        self.assertEqual(2, parsed["quorum"])
+        operations = []
         self.assertEqual(
-            [
-                {"name": "rh70-node1", "votes": 3, "local": False},
-                {"name": "rh70-node2", "votes": 2, "local": True},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-            parsed["node_list"]
+            {},
+            utils.get_resources_location_from_operations(cib_dom, operations)
         )
 
-        parsed = utils.parse_quorumtool_output("""\
-Quorum information
-------------------
-Date:             Fri Jan 16 13:03:35 2015
-Quorum provider:  corosync_votequorum
-Nodes:            1
-Node ID:          1
-Ring ID:          19868
-Quorate:          No
-
-Votequorum information
-----------------------
-Expected votes:   3
-Highest expected: 3
-Total votes:      1
-Quorum:           2 Activity blocked
-Flags:            
+        operations = [
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "start",
+                "on_node": "rh7-1",
+            },
+        ]
+        self.assertEqual(
+            {
+                'myResource': {
+                    'id': 'myResource',
+                    'id_for_constraint': 'myResource',
+                    'long_id': 'myResource',
+                    'start_on_node': 'rh7-1',
+                 },
+            },
+            utils.get_resources_location_from_operations(cib_dom, operations)
+        )
 
-Membership information
-----------------------
-    Nodeid      Votes    Qdevice Name
-             1          1         NR rh70-node1 (local)
-""")
-        self.assertEqual(False, parsed["quorate"])
-        self.assertEqual(2, parsed["quorum"])
+        operations = [
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "start",
+                "on_node": "rh7-1",
+            },
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "start",
+                "on_node": "rh7-2",
+            },
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "monitor",
+                "on_node": "rh7-3",
+            },
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "stop",
+                "on_node": "rh7-3",
+            },
+        ]
         self.assertEqual(
-            [
-                {"name": "rh70-node1", "votes": 1, "local": True},
-            ],
-            parsed["node_list"]
+            {
+                'myResource': {
+                    'id': 'myResource',
+                    'id_for_constraint': 'myResource',
+                    'long_id': 'myResource',
+                    'start_on_node': 'rh7-2',
+                 },
+            },
+            utils.get_resources_location_from_operations(cib_dom, operations)
         )
 
-        parsed = utils.parse_quorumtool_output("")
-        self.assertEqual(None, parsed)
+        operations = [
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "start",
+                "on_node": "rh7-1",
+            },
+            {
+                "id": "myClonedResource",
+                "long_id": "myClonedResource:0",
+                "operation": "start",
+                "on_node": "rh7-1",
+            },
+            {
+                "id": "myClonedResource",
+                "long_id": "myClonedResource:0",
+                "operation": "start",
+                "on_node": "rh7-2",
+            },
+            {
+                "id": "myClonedResource",
+                "long_id": "myClonedResource:1",
+                "operation": "start",
+                "on_node": "rh7-3",
+            },
+        ]
+        self.assertEqual(
+            {
+                'myResource': {
+                    'id': 'myResource',
+                    'id_for_constraint': 'myResource',
+                    'long_id': 'myResource',
+                    'start_on_node': 'rh7-1',
+                 },
+                'myClonedResource:0': {
+                    'id': 'myClonedResource',
+                    'id_for_constraint': 'myClone',
+                    'long_id': 'myClonedResource:0',
+                    'start_on_node': 'rh7-2',
+                 },
+                'myClonedResource:1': {
+                    'id': 'myClonedResource',
+                    'id_for_constraint': 'myClone',
+                    'long_id': 'myClonedResource:1',
+                    'start_on_node': 'rh7-3',
+                 },
+            },
+            utils.get_resources_location_from_operations(cib_dom, operations)
+        )
 
-        parsed = utils.parse_quorumtool_output("""\
-Quorum information
-------------------
-Date:             Fri Jan 16 13:03:28 2015
-Quorum provider:  corosync_votequorum
-Nodes:            3
-Node ID:          1
-Ring ID:          19860
-Quorate:          Yes
+        operations = [
+            {
+                "id": "myUniqueClonedResource:0",
+                "long_id": "myUniqueClonedResource:0",
+                "operation": "start",
+                "on_node": "rh7-1",
+            },
+            {
+                "id": "myUniqueClonedResource:1",
+                "long_id": "myUniqueClonedResource:1",
+                "operation": "monitor",
+                "on_node": "rh7-2",
+            },
+            {
+                "id": "myUniqueClonedResource:2",
+                "long_id": "myUniqueClonedResource:2",
+                "operation": "start",
+                "on_node": "rh7-3",
+            },
+        ]
+        self.assertEqual(
+            {
+                'myUniqueClonedResource:0': {
+                    'id': 'myUniqueClonedResource:0',
+                    'id_for_constraint': 'myUniqueClone',
+                    'long_id': 'myUniqueClonedResource:0',
+                    'start_on_node': 'rh7-1',
+                 },
+                'myUniqueClonedResource:2': {
+                    'id': 'myUniqueClonedResource:2',
+                    'id_for_constraint': 'myUniqueClone',
+                    'long_id': 'myUniqueClonedResource:2',
+                    'start_on_node': 'rh7-3',
+                 },
+            },
+            utils.get_resources_location_from_operations(cib_dom, operations)
+        )
 
-Votequorum information
-----------------------
-Expected votes:   3
-Highest expected: 3
-Total votes:      3
-Quorum:           
-Flags:            Quorate
+        operations = [
+            {
+                "id": "myMasteredGroupedResource",
+                "long_id": "myMasteredGroupedResource:0",
+                "operation": "start",
+                "on_node": "rh7-1",
+            },
+            {
+                "id": "myMasteredGroupedResource",
+                "long_id": "myMasteredGroupedResource:1",
+                "operation": "demote",
+                "on_node": "rh7-2",
+            },
+            {
+                "id": "myMasteredGroupedResource",
+                "long_id": "myMasteredGroupedResource:1",
+                "operation": "promote",
+                "on_node": "rh7-3",
+            },
+        ]
+        self.assertEqual(
+            {
+                'myMasteredGroupedResource:0': {
+                    'id': 'myMasteredGroupedResource',
+                    'id_for_constraint': 'myGroupMaster',
+                    'long_id': 'myMasteredGroupedResource:0',
+                    'start_on_node': 'rh7-1',
+                 },
+                'myMasteredGroupedResource:1': {
+                    'id': 'myMasteredGroupedResource',
+                    'id_for_constraint': 'myGroupMaster',
+                    'long_id': 'myMasteredGroupedResource:1',
+                    'promote_on_node': 'rh7-3',
+                 },
+            },
+            utils.get_resources_location_from_operations(cib_dom, operations)
+        )
 
-Membership information
-----------------------
-    Nodeid      Votes    Qdevice Name
-         1          1         NR rh70-node1 (local)
-         2          1         NR rh70-node2
-         3          1         NR rh70-node3
-""")
-        self.assertEqual(None, parsed)
+        operations = [
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "stop",
+                "on_node": "rh7-1",
+            },
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "migrate_to",
+                "on_node": "rh7-1",
+            },
+            {
+                "id": "myResource",
+                "long_id": "myResource",
+                "operation": "migrate_from",
+                "on_node": "rh7-2",
+            },
+        ]
+        self.assertEqual(
+            {
+                "myResource": {
+                    "id": "myResource",
+                    "id_for_constraint": "myResource",
+                    "long_id": "myResource",
+                    "start_on_node": "rh7-2",
+                },
+            },
+            utils.get_resources_location_from_operations(cib_dom, operations)
+        )
 
-        parsed = utils.parse_quorumtool_output("""\
-Quorum information
-------------------
-Date:             Fri Jan 16 13:03:28 2015
-Quorum provider:  corosync_votequorum
-Nodes:            3
-Node ID:          1
-Ring ID:          19860
-Quorate:          Yes
+    def test_is_int(self):
+        self.assertTrue(utils.is_int("-999"))
+        self.assertTrue(utils.is_int("-1"))
+        self.assertTrue(utils.is_int("0"))
+        self.assertTrue(utils.is_int("1"))
+        self.assertTrue(utils.is_int("99999"))
+        self.assertTrue(utils.is_int(" 99999  "))
+        self.assertFalse(utils.is_int("0.0"))
+        self.assertFalse(utils.is_int("-1.0"))
+        self.assertFalse(utils.is_int("-0.1"))
+        self.assertFalse(utils.is_int("0.001"))
+        self.assertFalse(utils.is_int("-999999.1"))
+        self.assertFalse(utils.is_int("0.0001"))
+        self.assertFalse(utils.is_int(""))
+        self.assertFalse(utils.is_int("   "))
+        self.assertFalse(utils.is_int("A"))
+        self.assertFalse(utils.is_int("random 15 47 text  "))
 
-Votequorum information
-----------------------
-Expected votes:   3
-Highest expected: 3
-Total votes:      3
-Quorum:           Foo
-Flags:            Quorate
+    def test_dom_get_node(self):
+        cib = self.get_cib_with_nodes_minidom()
+        #assertIsNone is not supported in python 2.6
+        self.assertTrue(utils.dom_get_node(cib, "non-existing-node") is None)
+        node = utils.dom_get_node(cib, "rh7-1")
+        self.assertEqual(node.getAttribute("uname"), "rh7-1")
+        self.assertEqual(node.getAttribute("id"), "1")
 
-Membership information
-----------------------
-    Nodeid      Votes    Qdevice Name
-         1          1         NR rh70-node1 (local)
-         2          1         NR rh70-node2
-         3          1         NR rh70-node3
-""")
-        self.assertEqual(None, parsed)
+    def test_dom_prepare_child_element(self):
+        cib = self.get_cib_with_nodes_minidom()
+        node = cib.getElementsByTagName("node")[0]
+        self.assertEqual(len(dom_get_child_elements(node)), 0)
+        child = utils.dom_prepare_child_element(
+            node, "utilization", "rh7-1-utilization"
+        )
+        self.assertEqual(len(dom_get_child_elements(node)), 1)
+        self.assertEqual(child, dom_get_child_elements(node)[0])
+        self.assertEqual(dom_get_child_elements(node)[0].tagName, "utilization")
+        self.assertEqual(
+            dom_get_child_elements(node)[0].getAttribute("id"),
+            "rh7-1-utilization"
+        )
+        child2 = utils.dom_prepare_child_element(
+            node, "utilization", "rh7-1-utilization"
+        )
+        self.assertEqual(len(dom_get_child_elements(node)), 1)
+        self.assertEqual(child, child2)
 
-        parsed = utils.parse_quorumtool_output("""\
-Quorum information
-------------------
-Date:             Fri Jan 16 13:03:28 2015
-Quorum provider:  corosync_votequorum
-Nodes:            3
-Node ID:          1
-Ring ID:          19860
-Quorate:          Yes
+    def test_dom_update_nv_pair_add(self):
+        nv_set = xml.dom.minidom.parseString("<nvset/>").documentElement
+        utils.dom_update_nv_pair(nv_set, "test_name", "test_val", "prefix-")
+        self.assertEqual(len(dom_get_child_elements(nv_set)), 1)
+        pair = dom_get_child_elements(nv_set)[0]
+        self.assertEqual(pair.getAttribute("name"), "test_name")
+        self.assertEqual(pair.getAttribute("value"), "test_val")
+        self.assertEqual(pair.getAttribute("id"), "prefix-test_name")
+        utils.dom_update_nv_pair(nv_set, "another_name", "value", "prefix2-")
+        self.assertEqual(len(dom_get_child_elements(nv_set)), 2)
+        self.assertEqual(pair, dom_get_child_elements(nv_set)[0])
+        pair = dom_get_child_elements(nv_set)[1]
+        self.assertEqual(pair.getAttribute("name"), "another_name")
+        self.assertEqual(pair.getAttribute("value"), "value")
+        self.assertEqual(pair.getAttribute("id"), "prefix2-another_name")
 
-Votequorum information
-----------------------
-Expected votes:   3
-Highest expected: 3
-Total votes:      3
-Quorum:           2
-Flags:            Quorate
+    def test_dom_update_nv_pair_update(self):
+        nv_set = xml.dom.minidom.parseString("""
+        <nv_set>
+            <nvpair id="prefix-test_name" name="test_name" value="test_val"/>
+            <nvpair id="prefix2-another_name" name="another_name" value="value"/>
+        </nv_set>
+        """).documentElement
+        utils.dom_update_nv_pair(nv_set, "test_name", "new_value")
+        self.assertEqual(len(dom_get_child_elements(nv_set)), 2)
+        pair1 = dom_get_child_elements(nv_set)[0]
+        pair2 = dom_get_child_elements(nv_set)[1]
+        self.assertEqual(pair1.getAttribute("name"), "test_name")
+        self.assertEqual(pair1.getAttribute("value"), "new_value")
+        self.assertEqual(pair1.getAttribute("id"), "prefix-test_name")
+        self.assertEqual(pair2.getAttribute("name"), "another_name")
+        self.assertEqual(pair2.getAttribute("value"), "value")
+        self.assertEqual(pair2.getAttribute("id"), "prefix2-another_name")
 
-Membership information
-----------------------
-    Nodeid      Votes    Qdevice Name
-         1          1         NR rh70-node1 (local)
-         2        foo         NR rh70-node2
-         3          1         NR rh70-node3
-""")
-        self.assertEqual(None, parsed)
+    def test_dom_update_nv_pair_remove(self):
+        nv_set = xml.dom.minidom.parseString("""
+        <nv_set>
+            <nvpair id="prefix-test_name" name="test_name" value="test_val"/>
+            <nvpair id="prefix2-another_name" name="another_name" value="value"/>
+        </nv_set>
+        """).documentElement
+        utils.dom_update_nv_pair(nv_set, "non_existing_name", "")
+        self.assertEqual(len(dom_get_child_elements(nv_set)), 2)
+        utils.dom_update_nv_pair(nv_set, "another_name", "")
+        self.assertEqual(len(dom_get_child_elements(nv_set)), 1)
+        pair = dom_get_child_elements(nv_set)[0]
+        self.assertEqual(pair.getAttribute("name"), "test_name")
+        self.assertEqual(pair.getAttribute("value"), "test_val")
+        self.assertEqual(pair.getAttribute("id"), "prefix-test_name")
+        utils.dom_update_nv_pair(nv_set, "test_name", "")
+        self.assertEqual(len(dom_get_child_elements(nv_set)), 0)
 
-    def test_is_node_stop_cause_quorum_loss(self):
-        quorum_info = {
-            "quorate": False,
-        }
+    def test_convert_args_to_tuples(self):
+        out = utils.convert_args_to_tuples(
+            ["invalid_string", "key=value", "key2=val=ue", "k e y= v a l u e "]
+        )
         self.assertEqual(
-            False,
-            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+            out,
+            [("key", "value"), ("key2", "val=ue"), ("k e y", " v a l u e ")]
         )
 
-        quorum_info = {
-            "quorate": True,
-            "quorum": 1,
-            "node_list": [
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
-        self.assertEqual(
-            False,
-            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+    def test_dom_update_utilization_invalid(self):
+        #commands writes to stderr
+        #we want clean test output, so we capture it
+        tmp_stderr = sys.stderr
+        sys.stderr = StringIO()
+
+        el = xml.dom.minidom.parseString("""
+        <resource id="test_id"/>
+        """).documentElement
+        self.assertRaises(
+            SystemExit,
+            utils.dom_update_utilization, el, [("name", "invalid_val")]
         )
 
-        quorum_info = {
-            "quorate": True,
-            "quorum": 1,
-            "node_list": [
-                {"name": "rh70-node3", "votes": 1, "local": True},
-            ],
-        }
-        self.assertEqual(
-            True,
-            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+        self.assertRaises(
+            SystemExit,
+            utils.dom_update_utilization, el, [("name", "0.01")]
         )
 
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 3, "local": False},
-                {"name": "rh70-node2", "votes": 2, "local": False},
-                {"name": "rh70-node3", "votes": 1, "local": True},
-            ],
-        }
-        self.assertEqual(
-            False,
-            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+        sys.stderr = tmp_stderr
+
+    def test_dom_update_utilization_add(self):
+        el = xml.dom.minidom.parseString("""
+        <resource id="test_id"/>
+        """).documentElement
+        utils.dom_update_utilization(
+            el, [("name", ""), ("key", "-1"), ("keys", "90")]
         )
 
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 3, "local": False},
-                {"name": "rh70-node2", "votes": 2, "local": True},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
+        self.assertEqual(len(dom_get_child_elements(el)), 1)
+        u = dom_get_child_elements(el)[0]
+        self.assertEqual(u.tagName, "utilization")
+        self.assertEqual(u.getAttribute("id"), "test_id-utilization")
+        self.assertEqual(len(dom_get_child_elements(u)), 2)
+
         self.assertEqual(
-            False,
-            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+            dom_get_child_elements(u)[0].getAttribute("id"),
+            "test_id-utilization-key"
+        )
+        self.assertEqual(
+            dom_get_child_elements(u)[0].getAttribute("name"),
+            "key"
+        )
+        self.assertEqual(
+            dom_get_child_elements(u)[0].getAttribute("value"),
+            "-1"
         )
-
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 3, "local": True},
-                {"name": "rh70-node2", "votes": 2, "local": False},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
         self.assertEqual(
-            True,
-            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+            dom_get_child_elements(u)[1].getAttribute("id"),
+            "test_id-utilization-keys"
         )
-
-
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 3, "local": True},
-                {"name": "rh70-node2", "votes": 2, "local": False},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
         self.assertEqual(
-            False,
-            utils.is_node_stop_cause_quorum_loss(
-                quorum_info, False, ["rh70-node3"]
-            )
+            dom_get_child_elements(u)[1].getAttribute("name"),
+            "keys"
         )
-
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 3, "local": True},
-                {"name": "rh70-node2", "votes": 2, "local": False},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
         self.assertEqual(
-            False,
-            utils.is_node_stop_cause_quorum_loss(
-                quorum_info, False, ["rh70-node2"]
-            )
+            dom_get_child_elements(u)[1].getAttribute("value"),
+            "90"
         )
 
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 3, "local": True},
-                {"name": "rh70-node2", "votes": 2, "local": False},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
-        self.assertEqual(
-            True,
-            utils.is_node_stop_cause_quorum_loss(
-                quorum_info, False, ["rh70-node1"]
-            )
+    def test_dom_update_utilization_update_remove(self):
+        el = xml.dom.minidom.parseString("""
+        <resource id="test_id">
+            <utilization id="test_id-utilization">
+                <nvpair id="test_id-utilization-key" name="key" value="-1"/>
+                <nvpair id="test_id-utilization-keys" name="keys" value="90"/>
+            </utilization>
+        </resource>
+        """).documentElement
+        utils.dom_update_utilization(
+            el, [("key", "100"), ("keys", "")]
         )
 
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 4, "local": True},
-                {"name": "rh70-node2", "votes": 1, "local": False},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
+        u = dom_get_child_elements(el)[0]
+        self.assertEqual(len(dom_get_child_elements(u)), 1)
         self.assertEqual(
-            False,
-            utils.is_node_stop_cause_quorum_loss(
-                quorum_info, False, ["rh70-node2", "rh70-node3"]
-            )
+            dom_get_child_elements(u)[0].getAttribute("id"),
+            "test_id-utilization-key"
         )
-
-        quorum_info = {
-            "quorate": True,
-            "quorum": 4,
-            "node_list": [
-                {"name": "rh70-node1", "votes": 3, "local": True},
-                {"name": "rh70-node2", "votes": 2, "local": False},
-                {"name": "rh70-node3", "votes": 1, "local": False},
-            ],
-        }
         self.assertEqual(
-            True,
-            utils.is_node_stop_cause_quorum_loss(
-                quorum_info, False, ["rh70-node2", "rh70-node3"]
-            )
+            dom_get_child_elements(u)[0].getAttribute("name"),
+            "key"
         )
-
-    def test_get_operations_from_transitions(self):
-        transitions = utils.parse(rc("transitions01.xml"))
         self.assertEqual(
-            [
-                {
-                    'id': 'dummy',
-                    'long_id': 'dummy',
-                    'operation': 'stop',
-                    'on_node': 'rh7-3',
-                },
-                {
-                    'id': 'dummy',
-                    'long_id': 'dummy',
-                    'operation': 'start',
-                    'on_node': 'rh7-2',
-                },
-                {
-                    'id': 'd0',
-                    'long_id': 'd0:1',
-                    'operation': 'stop',
-                    'on_node': 'rh7-1',
-                },
-                {
-                    'id': 'd0',
-                    'long_id': 'd0:1',
-                    'operation': 'start',
-                    'on_node': 'rh7-2',
-                },
-                {
-                    'id': 'state',
-                    'long_id': 'state:0',
-                    'operation': 'stop',
-                    'on_node': 'rh7-3',
-                },
-                {
-                    'id': 'state',
-                    'long_id': 'state:0',
-                    'operation': 'start',
-                    'on_node': 'rh7-2',
-                },
-            ],
-            utils.get_operations_from_transitions(transitions)
+            dom_get_child_elements(u)[0].getAttribute("value"),
+            "100"
         )
 
-        transitions = utils.parse(rc("transitions02.xml"))
-        self.assertEqual(
-            [
-                {
-                    "id": "RemoteNode",
-                    "long_id": "RemoteNode",
-                    "operation": "stop",
-                    "on_node": "virt-143",
-                },
-                {
-                    "id": "RemoteNode",
-                    "long_id": "RemoteNode",
-                    "operation": "migrate_to",
-                    "on_node": "virt-143",
-                },
-                {
-                    "id": "RemoteNode",
-                    "long_id": "RemoteNode",
-                    "operation": "migrate_from",
-                    "on_node": "virt-142",
-                },
-                {
-                    "id": "dummy8",
-                    "long_id": "dummy8",
-                    "operation": "stop",
-                    "on_node": "virt-143",
-                },
-                {
-                    "id": "dummy8",
-                    "long_id": "dummy8",
-                    "operation": "start",
-                    "on_node": "virt-142",
-                }
-            ],
-            utils.get_operations_from_transitions(transitions)
+    def test_dom_update_meta_attr_add(self):
+        el = xml.dom.minidom.parseString("""
+        <resource id="test_id"/>
+        """).documentElement
+        utils.dom_update_meta_attr(
+            el, [("name", ""), ("key", "test"), ("key2", "val")]
         )
 
-    def test_get_resources_location_from_operations(self):
-        cib_dom = self.get_cib_resources()
+        self.assertEqual(len(dom_get_child_elements(el)), 1)
+        u = dom_get_child_elements(el)[0]
+        self.assertEqual(u.tagName, "meta_attributes")
+        self.assertEqual(u.getAttribute("id"), "test_id-meta_attributes")
+        self.assertEqual(len(dom_get_child_elements(u)), 2)
 
-        operations = []
         self.assertEqual(
-            {},
-            utils.get_resources_location_from_operations(cib_dom, operations)
+            dom_get_child_elements(u)[0].getAttribute("id"),
+            "test_id-meta_attributes-key"
         )
-
-        operations = [
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "start",
-                "on_node": "rh7-1",
-            },
-        ]
         self.assertEqual(
-            {
-                'myResource': {
-                    'id': 'myResource',
-                    'id_for_constraint': 'myResource',
-                    'long_id': 'myResource',
-                    'start_on_node': 'rh7-1',
-                 },
-            },
-            utils.get_resources_location_from_operations(cib_dom, operations)
+            dom_get_child_elements(u)[0].getAttribute("name"),
+            "key"
+        )
+        self.assertEqual(
+            dom_get_child_elements(u)[0].getAttribute("value"),
+            "test"
         )
-
-        operations = [
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "start",
-                "on_node": "rh7-1",
-            },
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "start",
-                "on_node": "rh7-2",
-            },
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "monitor",
-                "on_node": "rh7-3",
-            },
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "stop",
-                "on_node": "rh7-3",
-            },
-        ]
         self.assertEqual(
-            {
-                'myResource': {
-                    'id': 'myResource',
-                    'id_for_constraint': 'myResource',
-                    'long_id': 'myResource',
-                    'start_on_node': 'rh7-2',
-                 },
-            },
-            utils.get_resources_location_from_operations(cib_dom, operations)
+            dom_get_child_elements(u)[1].getAttribute("id"),
+            "test_id-meta_attributes-key2"
         )
-
-        operations = [
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "start",
-                "on_node": "rh7-1",
-            },
-            {
-                "id": "myClonedResource",
-                "long_id": "myClonedResource:0",
-                "operation": "start",
-                "on_node": "rh7-1",
-            },
-            {
-                "id": "myClonedResource",
-                "long_id": "myClonedResource:0",
-                "operation": "start",
-                "on_node": "rh7-2",
-            },
-            {
-                "id": "myClonedResource",
-                "long_id": "myClonedResource:1",
-                "operation": "start",
-                "on_node": "rh7-3",
-            },
-        ]
         self.assertEqual(
-            {
-                'myResource': {
-                    'id': 'myResource',
-                    'id_for_constraint': 'myResource',
-                    'long_id': 'myResource',
-                    'start_on_node': 'rh7-1',
-                 },
-                'myClonedResource:0': {
-                    'id': 'myClonedResource',
-                    'id_for_constraint': 'myClone',
-                    'long_id': 'myClonedResource:0',
-                    'start_on_node': 'rh7-2',
-                 },
-                'myClonedResource:1': {
-                    'id': 'myClonedResource',
-                    'id_for_constraint': 'myClone',
-                    'long_id': 'myClonedResource:1',
-                    'start_on_node': 'rh7-3',
-                 },
-            },
-            utils.get_resources_location_from_operations(cib_dom, operations)
+            dom_get_child_elements(u)[1].getAttribute("name"),
+            "key2"
         )
-
-        operations = [
-            {
-                "id": "myUniqueClonedResource:0",
-                "long_id": "myUniqueClonedResource:0",
-                "operation": "start",
-                "on_node": "rh7-1",
-            },
-            {
-                "id": "myUniqueClonedResource:1",
-                "long_id": "myUniqueClonedResource:1",
-                "operation": "monitor",
-                "on_node": "rh7-2",
-            },
-            {
-                "id": "myUniqueClonedResource:2",
-                "long_id": "myUniqueClonedResource:2",
-                "operation": "start",
-                "on_node": "rh7-3",
-            },
-        ]
         self.assertEqual(
-            {
-                'myUniqueClonedResource:0': {
-                    'id': 'myUniqueClonedResource:0',
-                    'id_for_constraint': 'myUniqueClone',
-                    'long_id': 'myUniqueClonedResource:0',
-                    'start_on_node': 'rh7-1',
-                 },
-                'myUniqueClonedResource:2': {
-                    'id': 'myUniqueClonedResource:2',
-                    'id_for_constraint': 'myUniqueClone',
-                    'long_id': 'myUniqueClonedResource:2',
-                    'start_on_node': 'rh7-3',
-                 },
-            },
-            utils.get_resources_location_from_operations(cib_dom, operations)
+            dom_get_child_elements(u)[1].getAttribute("value"),
+            "val"
         )
 
-        operations = [
-            {
-                "id": "myMasteredGroupedResource",
-                "long_id": "myMasteredGroupedResource:0",
-                "operation": "start",
-                "on_node": "rh7-1",
-            },
-            {
-                "id": "myMasteredGroupedResource",
-                "long_id": "myMasteredGroupedResource:1",
-                "operation": "demote",
-                "on_node": "rh7-2",
-            },
-            {
-                "id": "myMasteredGroupedResource",
-                "long_id": "myMasteredGroupedResource:1",
-                "operation": "promote",
-                "on_node": "rh7-3",
-            },
-        ]
+    def test_dom_update_meta_attr_update_remove(self):
+        el = xml.dom.minidom.parseString("""
+        <resource id="test_id">
+            <meta_attributes id="test_id-utilization">
+                <nvpair id="test_id-meta_attributes-key" name="key" value="test"/>
+                <nvpair id="test_id-meta_attributes-key2" name="key2" value="val"/>
+            </meta_attributes>
+        </resource>
+        """).documentElement
+        utils.dom_update_meta_attr(
+            el, [("key", "another_val"), ("key2", "")]
+        )
+
+        u = dom_get_child_elements(el)[0]
+        self.assertEqual(len(dom_get_child_elements(u)), 1)
         self.assertEqual(
-            {
-                'myMasteredGroupedResource:0': {
-                    'id': 'myMasteredGroupedResource',
-                    'id_for_constraint': 'myGroupMaster',
-                    'long_id': 'myMasteredGroupedResource:0',
-                    'start_on_node': 'rh7-1',
-                 },
-                'myMasteredGroupedResource:1': {
-                    'id': 'myMasteredGroupedResource',
-                    'id_for_constraint': 'myGroupMaster',
-                    'long_id': 'myMasteredGroupedResource:1',
-                    'promote_on_node': 'rh7-3',
-                 },
-            },
-            utils.get_resources_location_from_operations(cib_dom, operations)
+            dom_get_child_elements(u)[0].getAttribute("id"),
+            "test_id-meta_attributes-key"
+        )
+        self.assertEqual(
+            dom_get_child_elements(u)[0].getAttribute("name"),
+            "key"
+        )
+        self.assertEqual(
+            dom_get_child_elements(u)[0].getAttribute("value"),
+            "another_val"
         )
 
-        operations = [
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "stop",
-                "on_node": "rh7-1",
-            },
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "migrate_to",
-                "on_node": "rh7-1",
-            },
-            {
-                "id": "myResource",
-                "long_id": "myResource",
-                "operation": "migrate_from",
-                "on_node": "rh7-2",
+    def test_get_utilization(self):
+        el = xml.dom.minidom.parseString("""
+        <resource id="test_id">
+            <utilization id="test_id-utilization">
+                <nvpair id="test_id-utilization-key" name="key" value="-1"/>
+                <nvpair id="test_id-utilization-keys" name="keys" value="90"/>
+            </utilization>
+        </resource>
+        """).documentElement
+        self.assertEqual({"key": "-1", "keys": "90"}, utils.get_utilization(el))
+
+    def test_get_utilization_str(self):
+        el = xml.dom.minidom.parseString("""
+        <resource id="test_id">
+            <utilization id="test_id-utilization">
+                <nvpair id="test_id-utilization-key" name="key" value="-1"/>
+                <nvpair id="test_id-utilization-keys" name="keys" value="90"/>
+            </utilization>
+        </resource>
+        """).documentElement
+        self.assertEqual("key=-1 keys=90", utils.get_utilization_str(el))
+
+    def test_get_cluster_property_from_xml_enum(self):
+        el = ET.fromstring("""
+        <parameter name="no-quorum-policy" unique="0">
+            <shortdesc lang="en">What to do when the cluster does not have quorum</shortdesc>
+            <content type="enum" default="stop"/>
+            <longdesc lang="en">What to do when the cluster does not have quorum  Allowed values: stop, freeze, ignore, suicide</longdesc>
+        </parameter>
+        """)
+        expected = {
+            "name": "no-quorum-policy",
+            "shortdesc": "What to do when the cluster does not have quorum",
+            "longdesc": "",
+            "type": "enum",
+            "default": "stop",
+            "enum": ["stop", "freeze", "ignore", "suicide"]
+        }
+        self.assertEqual(expected, utils.get_cluster_property_from_xml(el))
+
+    def test_get_cluster_property_from_xml(self):
+        el = ET.fromstring("""
+        <parameter name="default-resource-stickiness" unique="0">
+            <shortdesc lang="en"></shortdesc>
+            <content type="integer" default="0"/>
+            <longdesc lang="en"></longdesc>
+        </parameter>
+        """)
+        expected = {
+            "name": "default-resource-stickiness",
+            "shortdesc": "",
+            "longdesc": "",
+            "type": "integer",
+            "default": "0"
+        }
+        self.assertEqual(expected, utils.get_cluster_property_from_xml(el))
+
+    def test_get_cluster_property_default(self):
+        definition = {
+            "default-resource-stickiness": {
+                "name": "default-resource-stickiness",
+                "shortdesc": "",
+                "longdesc": "",
+                "type": "integer",
+                "default": "0",
+                "source": "pengine"
             },
-        ]
-        self.assertEqual(
-            {
-                "myResource": {
-                    "id": "myResource",
-                    "id_for_constraint": "myResource",
-                    "long_id": "myResource",
-                    "start_on_node": "rh7-2",
-                },
+            "no-quorum-policy": {
+                "name": "no-quorum-policy",
+                "shortdesc": "What to do when the cluster does not have quorum",
+                "longdesc": "What to do when the cluster does not have quorum  Allowed values: stop, freeze, ignore, suicide",
+                "type": "enum",
+                "default": "stop",
+                "enum": ["stop", "freeze", "ignore", "suicide"],
+                "source": "pengine"
             },
-            utils.get_resources_location_from_operations(cib_dom, operations)
+            "enable-acl": {
+                "name": "enable-acl",
+                "shortdesc": "Enable CIB ACL",
+                "longdesc": "Enable CIB ACL",
+                "type": "boolean",
+                "default": "false",
+                "source": "cib"
+            }
+        }
+        self.assertEqual(
+            utils.get_cluster_property_default(
+                definition, "default-resource-stickiness"
+            ),
+            "0"
         )
-
-    def test_is_int(self):
-        self.assertTrue(utils.is_int("-999"))
-        self.assertTrue(utils.is_int("-1"))
-        self.assertTrue(utils.is_int("0"))
-        self.assertTrue(utils.is_int("1"))
-        self.assertTrue(utils.is_int("99999"))
-        self.assertTrue(utils.is_int(" 99999  "))
-        self.assertFalse(utils.is_int("0.0"))
-        self.assertFalse(utils.is_int("-1.0"))
-        self.assertFalse(utils.is_int("-0.1"))
-        self.assertFalse(utils.is_int("0.001"))
-        self.assertFalse(utils.is_int("-999999.1"))
-        self.assertFalse(utils.is_int("0.0001"))
-        self.assertFalse(utils.is_int(""))
-        self.assertFalse(utils.is_int("   "))
-        self.assertFalse(utils.is_int("A"))
-        self.assertFalse(utils.is_int("random 15 47 text  "))
-
-    def test_dom_get_node(self):
-        cib = self.get_cib_with_nodes_minidom()
-        #assertIsNone is not supported in python 2.6
-        self.assertTrue(utils.dom_get_node(cib, "non-existing-node") is None)
-        node = utils.dom_get_node(cib, "rh7-1")
-        self.assertEqual(node.getAttribute("uname"), "rh7-1")
-        self.assertEqual(node.getAttribute("id"), "1")
-
-    def test_dom_prepare_child_element(self):
-        cib = self.get_cib_with_nodes_minidom()
-        node = cib.getElementsByTagName("node")[0]
-        self.assertEqual(len(dom_get_child_elements(node)), 0)
-        child = utils.dom_prepare_child_element(
-            node, "utilization", "rh7-1-utilization"
+        self.assertEqual(
+            utils.get_cluster_property_default(definition, "no-quorum-policy"),
+            "stop"
         )
-        self.assertEqual(len(dom_get_child_elements(node)), 1)
-        self.assertEqual(child, dom_get_child_elements(node)[0])
-        self.assertEqual(dom_get_child_elements(node)[0].tagName, "utilization")
         self.assertEqual(
-            dom_get_child_elements(node)[0].getAttribute("id"),
-            "rh7-1-utilization"
+            utils.get_cluster_property_default(definition, "enable-acl"),
+            "false"
         )
-        child2 = utils.dom_prepare_child_element(
-            node, "utilization", "rh7-1-utilization"
+        self.assertRaises(
+            utils.UnknownPropertyException,
+            utils.get_cluster_property_default, definition, "non-existing"
         )
-        self.assertEqual(len(dom_get_child_elements(node)), 1)
-        self.assertEqual(child, child2)
 
-    def test_dom_update_nv_pair_add(self):
-        nv_set = xml.dom.minidom.parseString("<nvset/>").documentElement
-        utils.dom_update_nv_pair(nv_set, "test_name", "test_val", "prefix-")
-        self.assertEqual(len(dom_get_child_elements(nv_set)), 1)
-        pair = dom_get_child_elements(nv_set)[0]
-        self.assertEqual(pair.getAttribute("name"), "test_name")
-        self.assertEqual(pair.getAttribute("value"), "test_val")
-        self.assertEqual(pair.getAttribute("id"), "prefix-test_name")
-        utils.dom_update_nv_pair(nv_set, "another_name", "value", "prefix2-")
-        self.assertEqual(len(dom_get_child_elements(nv_set)), 2)
-        self.assertEqual(pair, dom_get_child_elements(nv_set)[0])
-        pair = dom_get_child_elements(nv_set)[1]
-        self.assertEqual(pair.getAttribute("name"), "another_name")
-        self.assertEqual(pair.getAttribute("value"), "value")
-        self.assertEqual(pair.getAttribute("id"), "prefix2-another_name")
+    def test_is_valid_cib_value_unknown_type(self):
+        # should be always true
+        self.assertTrue(utils.is_valid_cib_value("unknown", "test"))
+        self.assertTrue(utils.is_valid_cib_value("string", "string value"))
 
-    def test_dom_update_nv_pair_update(self):
-        nv_set = xml.dom.minidom.parseString("""
-        <nv_set>
-            <nvpair id="prefix-test_name" name="test_name" value="test_val"/>
-            <nvpair id="prefix2-another_name" name="another_name" value="value"/>
-        </nv_set>
-        """).documentElement
-        utils.dom_update_nv_pair(nv_set, "test_name", "new_value")
-        self.assertEqual(len(dom_get_child_elements(nv_set)), 2)
-        pair1 = dom_get_child_elements(nv_set)[0]
-        pair2 = dom_get_child_elements(nv_set)[1]
-        self.assertEqual(pair1.getAttribute("name"), "test_name")
-        self.assertEqual(pair1.getAttribute("value"), "new_value")
-        self.assertEqual(pair1.getAttribute("id"), "prefix-test_name")
-        self.assertEqual(pair2.getAttribute("name"), "another_name")
-        self.assertEqual(pair2.getAttribute("value"), "value")
-        self.assertEqual(pair2.getAttribute("id"), "prefix2-another_name")
+    def test_is_valid_cib_value_integer(self):
+        self.assertTrue(utils.is_valid_cib_value("integer", "0"))
+        self.assertTrue(utils.is_valid_cib_value("integer", "42"))
+        self.assertTrue(utils.is_valid_cib_value("integer", "-90"))
+        self.assertTrue(utils.is_valid_cib_value("integer", "+90"))
+        self.assertTrue(utils.is_valid_cib_value("integer", "INFINITY"))
+        self.assertTrue(utils.is_valid_cib_value("integer", "-INFINITY"))
+        self.assertTrue(utils.is_valid_cib_value("integer", "+INFINITY"))
+        self.assertFalse(utils.is_valid_cib_value("integer", "0.0"))
+        self.assertFalse(utils.is_valid_cib_value("integer", "-10.9"))
+        self.assertFalse(utils.is_valid_cib_value("integer", "string"))
 
-    def test_dom_update_nv_pair_remove(self):
-        nv_set = xml.dom.minidom.parseString("""
-        <nv_set>
-            <nvpair id="prefix-test_name" name="test_name" value="test_val"/>
-            <nvpair id="prefix2-another_name" name="another_name" value="value"/>
-        </nv_set>
-        """).documentElement
-        utils.dom_update_nv_pair(nv_set, "non_existing_name", "")
-        self.assertEqual(len(dom_get_child_elements(nv_set)), 2)
-        utils.dom_update_nv_pair(nv_set, "another_name", "")
-        self.assertEqual(len(dom_get_child_elements(nv_set)), 1)
-        pair = dom_get_child_elements(nv_set)[0]
-        self.assertEqual(pair.getAttribute("name"), "test_name")
-        self.assertEqual(pair.getAttribute("value"), "test_val")
-        self.assertEqual(pair.getAttribute("id"), "prefix-test_name")
-        utils.dom_update_nv_pair(nv_set, "test_name", "")
-        self.assertEqual(len(dom_get_child_elements(nv_set)), 0)
+    def test_is_valid_cib_value_enum(self):
+        self.assertTrue(
+            utils.is_valid_cib_value("enum", "this", ["another", "this", "1"])
+        )
+        self.assertFalse(
+            utils.is_valid_cib_value("enum", "this", ["another", "this_not"])
+        )
+        self.assertFalse(utils.is_valid_cib_value("enum", "this", []))
+        self.assertFalse(utils.is_valid_cib_value("enum", "this"))
 
-    def test_convert_args_to_tuples(self):
-        out = utils.convert_args_to_tuples(
-            ["invalid_string", "key=value", "key2=val=ue", "k e y= v a l u e "]
+    def test_is_valid_cib_value_boolean(self):
+        self.assertTrue(utils.is_valid_cib_value("boolean", "true"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "TrUe"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "TRUE"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "yes"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "on"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "y"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "Y"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "1"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "false"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "FaLse"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "FALSE"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "off"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "no"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "N"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "n"))
+        self.assertTrue(utils.is_valid_cib_value("boolean", "0"))
+        self.assertFalse(utils.is_valid_cib_value("boolean", "-1"))
+        self.assertFalse(utils.is_valid_cib_value("boolean", "not"))
+        self.assertFalse(utils.is_valid_cib_value("boolean", "random_string"))
+        self.assertFalse(utils.is_valid_cib_value("boolean", "truth"))
+
+    def test_is_valid_cib_value_time(self):
+        self.assertTrue(utils.is_valid_cib_value("time", "10"))
+        self.assertTrue(utils.is_valid_cib_value("time", "0"))
+        self.assertTrue(utils.is_valid_cib_value("time", "9s"))
+        self.assertTrue(utils.is_valid_cib_value("time", "10sec"))
+        self.assertTrue(utils.is_valid_cib_value("time", "10min"))
+        self.assertTrue(utils.is_valid_cib_value("time", "10m"))
+        self.assertTrue(utils.is_valid_cib_value("time", "10h"))
+        self.assertTrue(utils.is_valid_cib_value("time", "10hr"))
+        self.assertFalse(utils.is_valid_cib_value("time", "5.2"))
+        self.assertFalse(utils.is_valid_cib_value("time", "-10"))
+        self.assertFalse(utils.is_valid_cib_value("time", "10m 2s"))
+        self.assertFalse(utils.is_valid_cib_value("time", "hour"))
+        self.assertFalse(utils.is_valid_cib_value("time", "day"))
+
+    def test_validate_cluster_property(self):
+        definition = {
+            "default-resource-stickiness": {
+                "name": "default-resource-stickiness",
+                "shortdesc": "",
+                "longdesc": "",
+                "type": "integer",
+                "default": "0",
+                "source": "pengine"
+            },
+            "no-quorum-policy": {
+                "name": "no-quorum-policy",
+                "shortdesc": "What to do when the cluster does not have quorum",
+                "longdesc": "What to do when the cluster does not have quorum  Allowed values: stop, freeze, ignore, suicide",
+                "type": "enum",
+                "default": "stop",
+                "enum": ["stop", "freeze", "ignore", "suicide"],
+                "source": "pengine"
+            },
+            "enable-acl": {
+                "name": "enable-acl",
+                "shortdesc": "Enable CIB ACL",
+                "longdesc": "Enable CIB ACL",
+                "type": "boolean",
+                "default": "false",
+                "source": "cib"
+            }
+        }
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "default-resource-stickiness", "10"
+        ))
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "default-resource-stickiness", "-1"
+        ))
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "no-quorum-policy", "freeze"
+        ))
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "no-quorum-policy", "suicide"
+        ))
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "enable-acl", "true"
+        ))
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "enable-acl", "false"
+        ))
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "enable-acl", "on"
+        ))
+        self.assertTrue(utils.is_valid_cluster_property(
+            definition, "enable-acl", "OFF"
+        ))
+        self.assertFalse(utils.is_valid_cluster_property(
+            definition, "default-resource-stickiness", "test"
+        ))
+        self.assertFalse(utils.is_valid_cluster_property(
+            definition, "default-resource-stickiness", "1.2"
+        ))
+        self.assertFalse(utils.is_valid_cluster_property(
+            definition, "no-quorum-policy", "invalid"
+        ))
+        self.assertFalse(utils.is_valid_cluster_property(
+            definition, "enable-acl", "not"
+        ))
+        self.assertRaises(
+            utils.UnknownPropertyException,
+            utils.is_valid_cluster_property, definition, "unknown", "value"
         )
-        self.assertEqual(
-            out,
-            [("key", "value"), ("key2", "val=ue"), ("k e y", " v a l u e ")]
+
+    def assert_element_id(self, node, node_id):
+        self.assertTrue(
+            isinstance(node, xml.dom.minidom.Element),
+            "element with id '%s' not found" % node_id
         )
+        self.assertEqual(node.getAttribute("id"), node_id)
 
-    def test_dom_update_utilization_invalid(self):
-        #commands writes to stderr
-        #we want clean test output, so we capture it
-        tmp_stderr = sys.stderr
-        sys.stderr = StringIO()
 
-        el = xml.dom.minidom.parseString("""
-        <resource id="test_id"/>
-        """).documentElement
-        self.assertRaises(
-            SystemExit,
-            utils.dom_update_utilization, el, [("name", "invalid_val")]
-        )
+class RunParallelTest(unittest.TestCase):
+    def fixture_create_worker(self, log, name, sleepSeconds=0):
+        def worker():
+            sleep(sleepSeconds)
+            log.append(name)
+        return worker
 
-        self.assertRaises(
-            SystemExit,
-            utils.dom_update_utilization, el, [("name", "0.01")]
+    def test_run_all_workers(self):
+        log = []
+        utils.run_parallel(
+            [
+                self.fixture_create_worker(log, 'first'),
+                self.fixture_create_worker(log, 'second'),
+            ],
+            wait_seconds=.1
         )
 
-        sys.stderr = tmp_stderr
+        self.assertEqual(log, ['first', 'second'])
 
-    def test_dom_update_utilization_add(self):
-        el = xml.dom.minidom.parseString("""
-        <resource id="test_id"/>
-        """).documentElement
-        utils.dom_update_utilization(
-            el, [("name", ""), ("key", "-1"), ("keys", "90")]
+    def test_wait_for_slower_workers(self):
+        log = []
+        utils.run_parallel(
+            [
+                self.fixture_create_worker(log, 'first', .03),
+                self.fixture_create_worker(log, 'second'),
+            ],
+            wait_seconds=.01
         )
 
-        self.assertEqual(len(dom_get_child_elements(el)), 1)
-        u = dom_get_child_elements(el)[0]
-        self.assertEqual(u.tagName, "utilization")
-        self.assertEqual(u.getAttribute("id"), "test_id-utilization")
-        self.assertEqual(len(dom_get_child_elements(u)), 2)
+        self.assertEqual(log, ['second', 'first'])
+
 
+class PrepareNodeNamesTest(unittest.TestCase):
+    def test_return_original_when_is_in_pacemaker_nodes(self):
+        node = 'test'
         self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("id"),
-            "test_id-utilization-key"
+            node,
+            utils.prepare_node_name(node, {1: node}, {})
         )
+
+    def test_return_original_when_is_not_in_corosync_nodes(self):
+        node = 'test'
         self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("name"),
-            "key"
+            node,
+            utils.prepare_node_name(node, {}, {})
         )
+
+    def test_return_original_when_corosync_id_not_in_pacemaker(self):
+        node = 'test'
         self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("value"),
-            "-1"
+            node,
+            utils.prepare_node_name(node, {}, {1: node})
         )
+
+    def test_return_modified_name(self):
+        node = 'test'
         self.assertEqual(
-            dom_get_child_elements(u)[1].getAttribute("id"),
-            "test_id-utilization-keys"
+            'another (test)',
+            utils.prepare_node_name(node, {1: 'another'}, {1: node})
         )
+
+    def test_return_modified_name_with_pm_null_case(self):
+        node = 'test'
         self.assertEqual(
-            dom_get_child_elements(u)[1].getAttribute("name"),
-            "keys"
+            '*Unknown* (test)',
+            utils.prepare_node_name(node, {1: '(null)'}, {1: node})
+        )
+
+
+class NodeActionTaskTest(unittest.TestCase):
+    def test_can_run_action(self):
+        def action(node, arg, kwarg=None):
+            return (0, ':'.join([node, arg, kwarg]))
+
+        report_list = []
+        def report(node, returncode, output):
+            report_list.append('|'.join([node, str(returncode), output]))
+
+        task = utils.create_task(report, action, 'node', 'arg', kwarg='kwarg')
+        task()
+
+        self.assertEqual(['node|0|node:arg:kwarg'], report_list)
+
+
+class ParseCmanQuorumInfoTest(unittest.TestCase):
+    def test_error_empty_string(self):
+        parsed = utils.parse_cman_quorum_info("")
+        self.assertEqual(None, parsed)
+
+    def test_quorate_no_qdevice(self):
+        parsed = utils.parse_cman_quorum_info("""\
+Version: 6.2.0
+Config Version: 23
+Cluster Name: cluster66
+Cluster Id: 22265
+Cluster Member: Yes
+Cluster Generation: 3612
+Membership state: Cluster-Member
+Nodes: 3
+Expected votes: 3
+Total votes: 3
+Node votes: 1
+Quorum: 2 
+Active subsystems: 8
+Flags: 
+Ports Bound: 0 
+Node name: rh66-node2
+Node ID: 2
+Multicast addresses: 239.192.86.80
+Node addresses: 192.168.122.61
+---Votes---
+1 M 3 rh66-node1
+2 M 2 rh66-node2
+3 M 1 rh66-node3
+""")
+        self.assertEqual(True, parsed["quorate"])
+        self.assertEqual(2, parsed["quorum"])
+        self.assertEqual(
+            [
+                {"name": "rh66-node1", "votes": 3, "local": False},
+                {"name": "rh66-node2", "votes": 2, "local": True},
+                {"name": "rh66-node3", "votes": 1, "local": False},
+            ],
+            parsed["node_list"]
         )
+        self.assertEqual([], parsed["qdevice_list"])
+
+    def test_no_quorate_no_qdevice(self):
+        parsed = utils.parse_cman_quorum_info("""\
+Version: 6.2.0
+Config Version: 23
+Cluster Name: cluster66
+Cluster Id: 22265
+Cluster Member: Yes
+Cluster Generation: 3612
+Membership state: Cluster-Member
+Nodes: 3
+Expected votes: 3
+Total votes: 3
+Node votes: 1
+Quorum: 2 Activity blocked
+Active subsystems: 8
+Flags: 
+Ports Bound: 0 
+Node name: rh66-node1
+Node ID: 1
+Multicast addresses: 239.192.86.80
+Node addresses: 192.168.122.61
+---Votes---
+1 M 3 rh66-node1
+2 X 2 rh66-node2
+3 X 1 rh66-node3
+""")
+        self.assertEqual(False, parsed["quorate"])
+        self.assertEqual(2, parsed["quorum"])
         self.assertEqual(
-            dom_get_child_elements(u)[1].getAttribute("value"),
-            "90"
+            [
+                {"name": "rh66-node1", "votes": 3, "local": True},
+            ],
+            parsed["node_list"]
         )
+        self.assertEqual([], parsed["qdevice_list"])
+
+    def test_error_missing_quorum(self):
+        parsed = utils.parse_cman_quorum_info("""\
+Version: 6.2.0
+Config Version: 23
+Cluster Name: cluster66
+Cluster Id: 22265
+Cluster Member: Yes
+Cluster Generation: 3612
+Membership state: Cluster-Member
+Nodes: 3
+Expected votes: 3
+Total votes: 3
+Node votes: 1
+Quorum: 
+Active subsystems: 8
+Flags: 
+Ports Bound: 0 
+Node name: rh66-node2
+Node ID: 2
+Multicast addresses: 239.192.86.80
+Node addresses: 192.168.122.61
+---Votes---
+1 M 3 rh66-node1
+2 M 2 rh66-node2
+3 M 1 rh66-node3
+""")
+        self.assertEqual(None, parsed)
+
+    def test_error_quorum_garbage(self):
+        parsed = utils.parse_cman_quorum_info("""\
+Version: 6.2.0
+Config Version: 23
+Cluster Name: cluster66
+Cluster Id: 22265
+Cluster Member: Yes
+Cluster Generation: 3612
+Membership state: Cluster-Member
+Nodes: 3
+Expected votes: 3
+Total votes: 3
+Node votes: 1
+Quorum: Foo
+Active subsystems: 8
+Flags: 
+Ports Bound: 0 
+Node name: rh66-node2
+Node ID: 2
+Multicast addresses: 239.192.86.80
+Node addresses: 192.168.122.61
+---Votes---
+1 M 3 rh66-node1
+2 M 2 rh66-node2
+3 M 1 rh66-node3
+""")
+        self.assertEqual(None, parsed)
+
+    def test_error_node_votes_garbage(self):
+        parsed = utils.parse_cman_quorum_info("""\
+Version: 6.2.0
+Config Version: 23
+Cluster Name: cluster66
+Cluster Id: 22265
+Cluster Member: Yes
+Cluster Generation: 3612
+Membership state: Cluster-Member
+Nodes: 3
+Expected votes: 3
+Total votes: 3
+Node votes: 1
+Quorum: 4
+Active subsystems: 8
+Flags: 
+Ports Bound: 0 
+Node name: rh66-node2
+Node ID: 2
+Multicast addresses: 239.192.86.80
+Node addresses: 192.168.122.61
+---Votes---
+1 M 3 rh66-node1
+2 M Foo rh66-node2
+3 M 1 rh66-node3
+""")
+        self.assertEqual(None, parsed)
 
-    def test_dom_update_utilization_update_remove(self):
-        el = xml.dom.minidom.parseString("""
-        <resource id="test_id">
-            <utilization id="test_id-utilization">
-                <nvpair id="test_id-utilization-key" name="key" value="-1"/>
-                <nvpair id="test_id-utilization-keys" name="keys" value="90"/>
-            </utilization>
-        </resource>
-        """).documentElement
-        utils.dom_update_utilization(
-            el, [("key", "100"), ("keys", "")]
-        )
 
-        u = dom_get_child_elements(el)[0]
-        self.assertEqual(len(dom_get_child_elements(u)), 1)
-        self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("id"),
-            "test_id-utilization-key"
-        )
-        self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("name"),
-            "key"
-        )
-        self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("value"),
-            "100"
-        )
+class ParseQuorumtoolOutputTest(unittest.TestCase):
+    def test_error_empty_string(self):
+        parsed = utils.parse_quorumtool_output("")
+        self.assertEqual(None, parsed)
 
-    def test_dom_update_meta_attr_add(self):
-        el = xml.dom.minidom.parseString("""
-        <resource id="test_id"/>
-        """).documentElement
-        utils.dom_update_meta_attr(
-            el, [("name", ""), ("key", "test"), ("key2", "val")]
-        )
+    def test_quorate_no_qdevice(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:28 2015
+Quorum provider:  corosync_votequorum
+Nodes:            3
+Node ID:          1
+Ring ID:          19860
+Quorate:          Yes
 
-        self.assertEqual(len(dom_get_child_elements(el)), 1)
-        u = dom_get_child_elements(el)[0]
-        self.assertEqual(u.tagName, "meta_attributes")
-        self.assertEqual(u.getAttribute("id"), "test_id-meta_attributes")
-        self.assertEqual(len(dom_get_child_elements(u)), 2)
+Votequorum information
+----------------------
+Expected votes:   3
+Highest expected: 3
+Total votes:      3
+Quorum:           2
+Flags:            Quorate
 
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+         1          3         NR rh70-node1
+         2          2         NR rh70-node2 (local)
+         3          1         NR rh70-node3
+""")
+        self.assertEqual(True, parsed["quorate"])
+        self.assertEqual(2, parsed["quorum"])
         self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("id"),
-            "test_id-meta_attributes-key"
-        )
-        self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("name"),
-            "key"
-        )
-        self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("value"),
-            "test"
-        )
-        self.assertEqual(
-            dom_get_child_elements(u)[1].getAttribute("id"),
-            "test_id-meta_attributes-key2"
+            [
+                {"name": "rh70-node1", "votes": 3, "local": False},
+                {"name": "rh70-node2", "votes": 2, "local": True},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            parsed["node_list"]
         )
+        self.assertEqual([], parsed["qdevice_list"])
+
+    def test_quorate_with_qdevice(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:28 2015
+Quorum provider:  corosync_votequorum
+Nodes:            3
+Node ID:          1
+Ring ID:          19860
+Quorate:          Yes
+
+Votequorum information
+----------------------
+Expected votes:   10
+Highest expected: 10
+Total votes:      10
+Quorum:           6
+Flags:            Quorate Qdevice
+
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+         1          3    A,V,MNW rh70-node1
+         2          2    A,V,MNW rh70-node2 (local)
+         3          1    A,V,MNW rh70-node3
+         0          4            Qdevice
+""")
+        self.assertEqual(True, parsed["quorate"])
+        self.assertEqual(6, parsed["quorum"])
         self.assertEqual(
-            dom_get_child_elements(u)[1].getAttribute("name"),
-            "key2"
+            [
+                {"name": "rh70-node1", "votes": 3, "local": False},
+                {"name": "rh70-node2", "votes": 2, "local": True},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            parsed["node_list"]
         )
         self.assertEqual(
-            dom_get_child_elements(u)[1].getAttribute("value"),
-            "val"
+            [
+                {"name": "Qdevice", "votes": 4, "local": False},
+            ],
+            parsed["qdevice_list"]
         )
 
-    def test_dom_update_meta_attr_update_remove(self):
-        el = xml.dom.minidom.parseString("""
-        <resource id="test_id">
-            <meta_attributes id="test_id-utilization">
-                <nvpair id="test_id-meta_attributes-key" name="key" value="test"/>
-                <nvpair id="test_id-meta_attributes-key2" name="key2" value="val"/>
-            </meta_attributes>
-        </resource>
-        """).documentElement
-        utils.dom_update_meta_attr(
-            el, [("key", "another_val"), ("key2", "")]
-        )
+    def test_quorate_with_qdevice_lost(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:28 2015
+Quorum provider:  corosync_votequorum
+Nodes:            3
+Node ID:          1
+Ring ID:          19860
+Quorate:          Yes
 
-        u = dom_get_child_elements(el)[0]
-        self.assertEqual(len(dom_get_child_elements(u)), 1)
-        self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("id"),
-            "test_id-meta_attributes-key"
-        )
+Votequorum information
+----------------------
+Expected votes:   10
+Highest expected: 10
+Total votes:      6
+Quorum:           6
+Flags:            Quorate Qdevice
+
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+         1          3   NA,V,MNW rh70-node1
+         2          2   NA,V,MNW rh70-node2 (local)
+         3          1   NA,V,MNW rh70-node3
+         0          0            Qdevice (votes 4)
+""")
+        self.assertEqual(True, parsed["quorate"])
+        self.assertEqual(6, parsed["quorum"])
         self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("name"),
-            "key"
+            [
+                {"name": "rh70-node1", "votes": 3, "local": False},
+                {"name": "rh70-node2", "votes": 2, "local": True},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            parsed["node_list"]
         )
         self.assertEqual(
-            dom_get_child_elements(u)[0].getAttribute("value"),
-            "another_val"
+            [
+                {"name": "Qdevice", "votes": 0, "local": False},
+            ],
+            parsed["qdevice_list"]
         )
 
-    def test_get_utilization(self):
-        el = xml.dom.minidom.parseString("""
-        <resource id="test_id">
-            <utilization id="test_id-utilization">
-                <nvpair id="test_id-utilization-key" name="key" value="-1"/>
-                <nvpair id="test_id-utilization-keys" name="keys" value="90"/>
-            </utilization>
-        </resource>
-        """).documentElement
-        self.assertEqual({"key": "-1", "keys": "90"}, utils.get_utilization(el))
-
-    def test_get_utilization_str(self):
-        el = xml.dom.minidom.parseString("""
-        <resource id="test_id">
-            <utilization id="test_id-utilization">
-                <nvpair id="test_id-utilization-key" name="key" value="-1"/>
-                <nvpair id="test_id-utilization-keys" name="keys" value="90"/>
-            </utilization>
-        </resource>
-        """).documentElement
-        self.assertEqual("key=-1 keys=90", utils.get_utilization_str(el))
-
-    def test_get_cluster_property_from_xml_enum(self):
-        el = ET.fromstring("""
-        <parameter name="no-quorum-policy" unique="0">
-            <shortdesc lang="en">What to do when the cluster does not have quorum</shortdesc>
-            <content type="enum" default="stop"/>
-            <longdesc lang="en">What to do when the cluster does not have quorum  Allowed values: stop, freeze, ignore, suicide</longdesc>
-        </parameter>
-        """)
-        expected = {
-            "name": "no-quorum-policy",
-            "shortdesc": "What to do when the cluster does not have quorum",
-            "longdesc": "",
-            "type": "enum",
-            "default": "stop",
-            "enum": ["stop", "freeze", "ignore", "suicide"]
-        }
-        self.assertEqual(expected, utils.get_cluster_property_from_xml(el))
-
-    def test_get_cluster_property_from_xml(self):
-        el = ET.fromstring("""
-        <parameter name="default-resource-stickiness" unique="0">
-            <shortdesc lang="en"></shortdesc>
-            <content type="integer" default="0"/>
-            <longdesc lang="en"></longdesc>
-        </parameter>
-        """)
-        expected = {
-            "name": "default-resource-stickiness",
-            "shortdesc": "",
-            "longdesc": "",
-            "type": "integer",
-            "default": "0"
-        }
-        self.assertEqual(expected, utils.get_cluster_property_from_xml(el))
-
-    def test_get_cluster_property_default(self):
-        definition = {
-            "default-resource-stickiness": {
-                "name": "default-resource-stickiness",
-                "shortdesc": "",
-                "longdesc": "",
-                "type": "integer",
-                "default": "0",
-                "source": "pengine"
-            },
-            "no-quorum-policy": {
-                "name": "no-quorum-policy",
-                "shortdesc": "What to do when the cluster does not have quorum",
-                "longdesc": "What to do when the cluster does not have quorum  Allowed values: stop, freeze, ignore, suicide",
-                "type": "enum",
-                "default": "stop",
-                "enum": ["stop", "freeze", "ignore", "suicide"],
-                "source": "pengine"
-            },
-            "enable-acl": {
-                "name": "enable-acl",
-                "shortdesc": "Enable CIB ACL",
-                "longdesc": "Enable CIB ACL",
-                "type": "boolean",
-                "default": "false",
-                "source": "cib"
-            }
-        }
+    def test_no_quorate_no_qdevice(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:35 2015
+Quorum provider:  corosync_votequorum
+Nodes:            1
+Node ID:          1
+Ring ID:          19868
+Quorate:          No
+
+Votequorum information
+----------------------
+Expected votes:   3
+Highest expected: 3
+Total votes:      1
+Quorum:           2 Activity blocked
+Flags:            
+
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+             1          1         NR rh70-node1 (local)
+""")
+        self.assertEqual(False, parsed["quorate"])
+        self.assertEqual(2, parsed["quorum"])
         self.assertEqual(
-            utils.get_cluster_property_default(
-                definition, "default-resource-stickiness"
-            ),
-            "0"
+            [
+                {"name": "rh70-node1", "votes": 1, "local": True},
+            ],
+            parsed["node_list"]
         )
+        self.assertEqual([], parsed["qdevice_list"])
+
+    def test_no_quorate_with_qdevice(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:35 2015
+Quorum provider:  corosync_votequorum
+Nodes:            1
+Node ID:          1
+Ring ID:          19868
+Quorate:          No
+
+Votequorum information
+----------------------
+Expected votes:   3
+Highest expected: 3
+Total votes:      1
+Quorum:           2 Activity blocked
+Flags:            Qdevice
+
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+         1          1         NR rh70-node1 (local)
+         0          0            Qdevice (votes 1)
+""")
+        self.assertEqual(False, parsed["quorate"])
+        self.assertEqual(2, parsed["quorum"])
         self.assertEqual(
-            utils.get_cluster_property_default(definition, "no-quorum-policy"),
-            "stop"
+            [
+                {"name": "rh70-node1", "votes": 1, "local": True},
+            ],
+            parsed["node_list"]
         )
         self.assertEqual(
-            utils.get_cluster_property_default(definition, "enable-acl"),
-            "false"
-        )
-        self.assertRaises(
-            utils.UnknownPropertyException,
-            utils.get_cluster_property_default, definition, "non-existing"
+            [
+                {"name": "Qdevice", "votes": 0, "local": False},
+            ],
+            parsed["qdevice_list"]
         )
 
-    def test_is_valid_cib_value_unknown_type(self):
-        # should be always true
-        self.assertTrue(utils.is_valid_cib_value("unknown", "test"))
-        self.assertTrue(utils.is_valid_cib_value("string", "string value"))
+    def test_error_missing_quorum(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:28 2015
+Quorum provider:  corosync_votequorum
+Nodes:            3
+Node ID:          1
+Ring ID:          19860
+Quorate:          Yes
 
-    def test_is_valid_cib_value_integer(self):
-        self.assertTrue(utils.is_valid_cib_value("integer", "0"))
-        self.assertTrue(utils.is_valid_cib_value("integer", "42"))
-        self.assertTrue(utils.is_valid_cib_value("integer", "-90"))
-        self.assertTrue(utils.is_valid_cib_value("integer", "+90"))
-        self.assertTrue(utils.is_valid_cib_value("integer", "INFINITY"))
-        self.assertTrue(utils.is_valid_cib_value("integer", "-INFINITY"))
-        self.assertTrue(utils.is_valid_cib_value("integer", "+INFINITY"))
-        self.assertFalse(utils.is_valid_cib_value("integer", "0.0"))
-        self.assertFalse(utils.is_valid_cib_value("integer", "-10.9"))
-        self.assertFalse(utils.is_valid_cib_value("integer", "string"))
+Votequorum information
+----------------------
+Expected votes:   3
+Highest expected: 3
+Total votes:      3
+Quorum:           
+Flags:            Quorate
 
-    def test_is_valid_cib_value_enum(self):
-        self.assertTrue(
-            utils.is_valid_cib_value("enum", "this", ["another", "this", "1"])
-        )
-        self.assertFalse(
-            utils.is_valid_cib_value("enum", "this", ["another", "this_not"])
-        )
-        self.assertFalse(utils.is_valid_cib_value("enum", "this", []))
-        self.assertFalse(utils.is_valid_cib_value("enum", "this"))
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+         1          1         NR rh70-node1 (local)
+         2          1         NR rh70-node2
+         3          1         NR rh70-node3
+""")
+        self.assertEqual(None, parsed)
 
-    def test_is_valid_cib_value_boolean(self):
-        self.assertTrue(utils.is_valid_cib_value("boolean", "true"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "TrUe"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "TRUE"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "yes"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "on"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "y"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "Y"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "1"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "false"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "FaLse"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "FALSE"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "off"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "no"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "N"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "n"))
-        self.assertTrue(utils.is_valid_cib_value("boolean", "0"))
-        self.assertFalse(utils.is_valid_cib_value("boolean", "-1"))
-        self.assertFalse(utils.is_valid_cib_value("boolean", "not"))
-        self.assertFalse(utils.is_valid_cib_value("boolean", "random_string"))
-        self.assertFalse(utils.is_valid_cib_value("boolean", "truth"))
+    def test_error_quorum_garbage(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:28 2015
+Quorum provider:  corosync_votequorum
+Nodes:            3
+Node ID:          1
+Ring ID:          19860
+Quorate:          Yes
 
-    def test_is_valid_cib_value_time(self):
-        self.assertTrue(utils.is_valid_cib_value("time", "10"))
-        self.assertTrue(utils.is_valid_cib_value("time", "0"))
-        self.assertTrue(utils.is_valid_cib_value("time", "9s"))
-        self.assertTrue(utils.is_valid_cib_value("time", "10sec"))
-        self.assertTrue(utils.is_valid_cib_value("time", "10min"))
-        self.assertTrue(utils.is_valid_cib_value("time", "10m"))
-        self.assertTrue(utils.is_valid_cib_value("time", "10h"))
-        self.assertTrue(utils.is_valid_cib_value("time", "10hr"))
-        self.assertFalse(utils.is_valid_cib_value("time", "5.2"))
-        self.assertFalse(utils.is_valid_cib_value("time", "-10"))
-        self.assertFalse(utils.is_valid_cib_value("time", "10m 2s"))
-        self.assertFalse(utils.is_valid_cib_value("time", "hour"))
-        self.assertFalse(utils.is_valid_cib_value("time", "day"))
+Votequorum information
+----------------------
+Expected votes:   3
+Highest expected: 3
+Total votes:      3
+Quorum:           Foo
+Flags:            Quorate
 
-    def test_validate_cluster_property(self):
-        definition = {
-            "default-resource-stickiness": {
-                "name": "default-resource-stickiness",
-                "shortdesc": "",
-                "longdesc": "",
-                "type": "integer",
-                "default": "0",
-                "source": "pengine"
-            },
-            "no-quorum-policy": {
-                "name": "no-quorum-policy",
-                "shortdesc": "What to do when the cluster does not have quorum",
-                "longdesc": "What to do when the cluster does not have quorum  Allowed values: stop, freeze, ignore, suicide",
-                "type": "enum",
-                "default": "stop",
-                "enum": ["stop", "freeze", "ignore", "suicide"],
-                "source": "pengine"
-            },
-            "enable-acl": {
-                "name": "enable-acl",
-                "shortdesc": "Enable CIB ACL",
-                "longdesc": "Enable CIB ACL",
-                "type": "boolean",
-                "default": "false",
-                "source": "cib"
-            }
-        }
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "default-resource-stickiness", "10"
-        ))
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "default-resource-stickiness", "-1"
-        ))
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "no-quorum-policy", "freeze"
-        ))
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "no-quorum-policy", "suicide"
-        ))
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "enable-acl", "true"
-        ))
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "enable-acl", "false"
-        ))
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "enable-acl", "on"
-        ))
-        self.assertTrue(utils.is_valid_cluster_property(
-            definition, "enable-acl", "OFF"
-        ))
-        self.assertFalse(utils.is_valid_cluster_property(
-            definition, "default-resource-stickiness", "test"
-        ))
-        self.assertFalse(utils.is_valid_cluster_property(
-            definition, "default-resource-stickiness", "1.2"
-        ))
-        self.assertFalse(utils.is_valid_cluster_property(
-            definition, "no-quorum-policy", "invalid"
-        ))
-        self.assertFalse(utils.is_valid_cluster_property(
-            definition, "enable-acl", "not"
-        ))
-        self.assertRaises(
-            utils.UnknownPropertyException,
-            utils.is_valid_cluster_property, definition, "unknown", "value"
-        )
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+         1          1         NR rh70-node1 (local)
+         2          1         NR rh70-node2
+         3          1         NR rh70-node3
+""")
+        self.assertEqual(None, parsed)
+
+    def test_error_node_votes_garbage(self):
+        parsed = utils.parse_quorumtool_output("""\
+Quorum information
+------------------
+Date:             Fri Jan 16 13:03:28 2015
+Quorum provider:  corosync_votequorum
+Nodes:            3
+Node ID:          1
+Ring ID:          19860
+Quorate:          Yes
+
+Votequorum information
+----------------------
+Expected votes:   3
+Highest expected: 3
+Total votes:      3
+Quorum:           2
+Flags:            Quorate
 
-    def assert_element_id(self, node, node_id):
-        self.assertTrue(
-            isinstance(node, xml.dom.minidom.Element),
-            "element with id '%s' not found" % node_id
+Membership information
+----------------------
+    Nodeid      Votes    Qdevice Name
+         1          1         NR rh70-node1 (local)
+         2        foo         NR rh70-node2
+         3          1         NR rh70-node3
+""")
+        self.assertEqual(None, parsed)
+
+
+class IsNodeStopCauseQuorumLossTest(unittest.TestCase):
+    def test_not_quorate(self):
+        quorum_info = {
+            "quorate": False,
+        }
+        self.assertEqual(
+            False,
+            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
         )
-        self.assertEqual(node.getAttribute("id"), node_id)
 
-class RunParallelTest(unittest.TestCase):
-    def fixture_create_worker(self, log, name, sleepSeconds=0):
-        def worker():
-            sleep(sleepSeconds)
-            log.append(name)
-        return worker
+    def test_local_node_not_in_list(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 1,
+            "node_list": [
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [],
+        }
+        self.assertEqual(
+            False,
+            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+        )
 
-    def test_run_all_workers(self):
-        log = []
-        utils.run_parallel(
-            [
-                self.fixture_create_worker(log, 'first'),
-                self.fixture_create_worker(log, 'second'),
+    def test_local_node_alone_in_list(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 1,
+            "node_list": [
+                {"name": "rh70-node3", "votes": 1, "local": True},
             ],
-            wait_seconds=.1
+            "qdevice_list": [],
+        }
+        self.assertEqual(
+            True,
+            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
         )
 
-        self.assertEqual(log, ['first', 'second'])
+    def test_local_node_still_quorate(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 3, "local": False},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": True},
+            ],
+            "qdevice_list": [],
+        }
+        self.assertEqual(
+            False,
+            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+        )
 
-    def test_wait_for_slower_workers(self):
-        log = []
-        utils.run_parallel(
-            [
-                self.fixture_create_worker(log, 'first', .03),
-                self.fixture_create_worker(log, 'second'),
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 3, "local": False},
+                {"name": "rh70-node2", "votes": 2, "local": True},
+                {"name": "rh70-node3", "votes": 1, "local": False},
             ],
-            wait_seconds=.01
+            "qdevice_list": [],
+        }
+        self.assertEqual(
+            False,
+            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
         )
 
-        self.assertEqual(log, ['second', 'first'])
+    def test_local_node_quorum_loss(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 3, "local": True},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [],
+        }
+        self.assertEqual(
+            True,
+            utils.is_node_stop_cause_quorum_loss(quorum_info, True)
+        )
 
-class PrepareNodeNamesTest(unittest.TestCase):
-    def test_return_original_when_is_in_pacemaker_nodes(self):
-        node = 'test'
+    def test_one_node_still_quorate(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 3, "local": True},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [],
+        }
         self.assertEqual(
-            node,
-            utils.prepare_node_name(node, {1: node}, {})
+            False,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node3"]
+            )
         )
 
-    def test_return_original_when_is_not_in_corosync_nodes(self):
-        node = 'test'
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 3, "local": True},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [],
+        }
         self.assertEqual(
-            node,
-            utils.prepare_node_name(node, {}, {})
+            False,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node2"]
+            )
         )
 
-    def test_return_original_when_corosync_id_not_in_pacemaker(self):
-        node = 'test'
+    def test_one_node_quorum_loss(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 3, "local": True},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [],
+        }
         self.assertEqual(
-            node,
-            utils.prepare_node_name(node, {}, {1: node})
+            True,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node1"]
+            )
         )
 
-    def test_return_modified_name(self):
-        node = 'test'
+    def test_more_nodes_still_quorate(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 4, "local": True},
+                {"name": "rh70-node2", "votes": 1, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [],
+        }
         self.assertEqual(
-            'another (test)',
-            utils.prepare_node_name(node, {1: 'another'}, {1: node})
+            False,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node2", "rh70-node3"]
+            )
         )
 
-    def test_return_modified_name_with_pm_null_case(self):
-        node = 'test'
+    def test_more_nodes_quorum_loss(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 3, "local": True},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [],
+        }
         self.assertEqual(
-            '*Unknown* (test)',
-            utils.prepare_node_name(node, {1: '(null)'}, {1: node})
+            True,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node2", "rh70-node3"]
+            )
         )
 
-class NodeActionTaskTest(unittest.TestCase):
-    def test_can_run_action(self):
-        def action(node, arg, kwarg=None):
-            return (0, ':'.join([node, arg, kwarg]))
+    def test_qdevice_still_quorate(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 3,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 1, "local": True},
+                {"name": "rh70-node2", "votes": 1, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [
+                {"name": "Qdevice", "votes": 1, "local": False},
+            ],
+        }
+        self.assertEqual(
+            False,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node2"]
+            )
+        )
 
-        report_list = []
-        def report(node, returncode, output):
-            report_list.append('|'.join([node, str(returncode), output]))
+    def test_qdevice_quorum_lost(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 3,
+            "node_list": [
+                {"name": "rh70-node1", "votes": 1, "local": True},
+                {"name": "rh70-node2", "votes": 1, "local": False},
+                {"name": "rh70-node3", "votes": 1, "local": False},
+            ],
+            "qdevice_list": [
+                {"name": "Qdevice", "votes": 1, "local": False},
+            ],
+        }
+        self.assertEqual(
+            True,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node2", "rh70-node3"]
+            )
+        )
 
-        task = utils.create_task(report, action, 'node', 'arg', kwarg='kwarg')
-        task()
+    def test_qdevice_lost_still_quorate(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4, # expect qdevice votes == 1
+            "node_list": [
+                {"name": "rh70-node1", "votes": 2, "local": True},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 2, "local": False},
+            ],
+            "qdevice_list": [
+                {"name": "Qdevice", "votes": 0, "local": False},
+            ],
+        }
+        self.assertEqual(
+            False,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node2"]
+            )
+        )
 
-        self.assertEqual(['node|0|node:arg:kwarg'], report_list)
+    def test_qdevice_lost_quorum_lost(self):
+        quorum_info = {
+            "quorate": True,
+            "quorum": 4, # expect qdevice votes == 1
+            "node_list": [
+                {"name": "rh70-node1", "votes": 2, "local": True},
+                {"name": "rh70-node2", "votes": 2, "local": False},
+                {"name": "rh70-node3", "votes": 2, "local": False},
+            ],
+            "qdevice_list": [
+                {"name": "Qdevice", "votes": 0, "local": False},
+            ],
+        }
+        self.assertEqual(
+            True,
+            utils.is_node_stop_cause_quorum_loss(
+                quorum_info, False, ["rh70-node2", "rh70-node3"]
+            )
+        )
diff --git a/pcs/usage.py b/pcs/usage.py
index 8ae6839..42e03e6 100644
--- a/pcs/usage.py
+++ b/pcs/usage.py
@@ -1272,14 +1272,20 @@ Commands:
 def qdevice(args=[], pout=True):
     output = """
 Usage: pcs qdevice <command>
-Manage quorum device provider on the local host
+Manage quorum device provider on the local host, currently only 'net' model is
+supported.
 
 Commands:
+    status <device model> [--full] [<cluster name>]
+        Show runtime status of specified model of quorum device provider.  Using
+        --full will give more detailed output.  If <cluster name> is specified,
+        only information about the specified cluster will be displayed.
+
     setup model <device model> [--enable] [--start]
         Configure specified model of quorum device provider.  Quorum device then
-        may be added to clusters by "pcs quorum device add" command.
-        --start will also start the provider.  --enable will configure
-        the provider to start on boot.
+        can be added to clusters by running "pcs quorum device add" command
+        in a cluster.  --start will also start the provider.  --enable will
+        configure the provider to start on boot.
 
     destroy <device model>
         Disable and stop specified model of quorum device provider and delete
@@ -1292,8 +1298,10 @@ Commands:
         Stop specified model of quorum device provider.
 
     kill <device model>
-        Force specified model of quorum device provider to stop (performs
-        kill -9).
+        Force specified model of quorum device provider to stop (performs kill
+        -9).  Note that init system (e.g. systemd) can detect that the qdevice
+        is not running and start it again.  If you want to stop the qdevice, run
+        "pcs qdevice stop" command.
 
     enable <device model>
         Configure specified model of quorum device provider to start on boot.
@@ -1310,21 +1318,38 @@ Commands:
 def quorum(args=[], pout=True):
     output = """
 Usage: pcs quorum <command>
-Manage cluster quorum settings
+Manage cluster quorum settings.
 
 Commands:
     config
         Show quorum configuration.
 
-    device add [generic options] model <device model> [model options]
-        Add quorum device to cluster.  Quorum device needs to be created first
-        by "pcs qdevice setup" command.
+    status
+        Show quorum runtime status.
+
+    device add [<generic options>] model <device model> [<model options>]
+        Add a quorum device to the cluster.  Quorum device needs to be created
+        first by "pcs qdevice setup" command.  It is not possible to use more
+        than one quorum device in a cluster simultaneously.  Generic options,
+        model and model options are all documented in corosync's
+        corosync-qdevice(8) man page.
 
     device remove
-        Remove quorum device from cluster.
+        Remove a quorum device from the cluster.
+
+    device status [--full]
+        Show quorum device runtime status.  Using --full will give more detailed
+        output.
+
+    device update [<generic options>] [model <model options>]
+        Add/Change quorum device options.  Generic options and model options are
+        all documented in corosync's corosync-qdevice(8) man page.  Requires
+        the cluster to be stopped.
 
-    device update [generic options] [model <model options>]
-        Add/Change quorum device options.  Requires cluster to be stopped.
+        WARNING: If you want to change "host" option of qdevice model net, use
+        "pcs quorum device remove" and "pcs quorum device add" commands
+        to set up configuration properly unless old and new host is the same
+        machine.
 
     unblock [--force]
         Cancel waiting for all nodes when establishing quorum.  Useful in
@@ -1343,7 +1368,7 @@ Commands:
             [last_man_standing_window=[<time in ms>]] [wait_for_all=[0|1]]
         Add/Change quorum options.  At least one option must be specified.
         Options are documented in corosync's votequorum(5) man page.  Requires
-        cluster to be stopped.
+        the cluster to be stopped.
 """
     if pout:
         print(sub_usage(args, output))
diff --git a/pcs/utils.py b/pcs/utils.py
index f9cdb1c..171fbdd 100644
--- a/pcs/utils.py
+++ b/pcs/utils.py
@@ -56,7 +56,6 @@ except ImportError:
 
 
 from pcs import settings, usage
-from pcs.common import report_codes
 from pcs.cli.common.reports import (
     process_library_reports,
     LibraryReportProcessorToConsole as LibraryReportProcessorToConsole,
@@ -64,18 +63,21 @@ from pcs.cli.common.reports import (
 from pcs.common.tools import simple_cache
 from pcs.lib import reports
 from pcs.lib.env import LibraryEnvironment
-from pcs.lib.errors import LibraryError, ReportItemSeverity
-import pcs.lib.corosync.config_parser as corosync_conf_parser
+from pcs.lib.errors import LibraryError
 from pcs.lib.external import (
-    is_cman_cluster,
     CommandRunner,
-    is_service_running,
-    is_service_enabled,
+    is_cman_cluster,
     is_systemctl,
+    is_service_enabled,
+    is_service_running,
+    disable_service,
+    DisableServiceError,
+    enable_service,
+    EnableServiceError,
 )
 import pcs.lib.resource_agent as lib_ra
+import pcs.lib.corosync.config_parser as corosync_conf_parser
 from pcs.lib.corosync.config_facade import ConfigFacade as corosync_conf_facade
-from pcs.lib.nodes_task import check_corosync_offline_on_nodes
 from pcs.lib.pacemaker import has_resource_wait_support
 from pcs.lib.pacemaker_state import ClusterState
 from pcs.lib.pacemaker_values import(
@@ -686,50 +688,18 @@ def autoset_2node_corosync(corosync_conf):
     facade._ConfigFacade__update_two_node()
     return facade.config
 
-# when adding or removing a node, changing number of nodes to or from two,
-# we need to change qdevice algorith lms <-> 2nodelms, which cannot be done when
-# the cluster is running
-def check_qdevice_algorithm_and_running_cluster(corosync_conf, add=True):
+# is it needed to handle corosync-qdevice service when managing cluster services
+def need_to_handle_qdevice_service():
     if is_rhel6():
-        return
-    facade = corosync_conf_facade.from_string(corosync_conf)
-    if not facade.has_quorum_device():
-        return
-    node_list = facade.get_nodes()
-    node_count_target = len(node_list) + (1 if add else -1)
-    model, model_opts, dummy_generic_opts = facade.get_quorum_device_settings()
-    if model != "net":
-        return
-    algorithm = model_opts.get("algorithm", "")
-    need_stopped = (
-        (algorithm == "lms" and node_count_target == 2)
-        or
-        (algorithm == "2nodelms" and node_count_target != 2)
-    )
-    if not need_stopped:
-        return
-
+        return False
     try:
-        lib_env = get_lib_env()
-        check_corosync_offline_on_nodes(
-            lib_env.node_communicator(),
-            lib_env.report_processor,
-            node_list,
-            get_modificators()["skip_offline_nodes"]
+        cfg = corosync_conf_facade.from_string(
+            open(settings.corosync_conf_file).read()
         )
-    except LibraryError as e:
-        report_item_list = list(e.args)
-        for report_item in report_item_list:
-            if (
-                report_item.code == report_codes.COROSYNC_RUNNING_ON_NODE
-                and
-                report_item.severity == ReportItemSeverity.ERROR
-            ):
-                report_item_list.append(
-                    reports.qdevice_remove_or_cluster_stop_needed()
-                )
-                break
-        process_library_reports(report_item_list)
+        return cfg.has_quorum_device()
+    except (EnvironmentError, corosync_conf_parser.CorosyncConfParserException):
+        # corosync.conf not present or not valid => no qdevice specified
+        return False
 
 def getNextNodeID(corosync_conf):
     currentNodes = []
@@ -2070,28 +2040,43 @@ def serviceStatus(prefix):
         pass
 
 def enableServices():
+    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
     if is_rhel6():
-        run(["chkconfig", "pacemaker", "on"])
+        service_list = ["pacemaker"]
     else:
-        if is_systemctl():
-            run(["systemctl", "enable", "corosync.service"])
-            run(["systemctl", "enable", "pacemaker.service"])
-        else:
-            run(["chkconfig", "corosync", "on"])
-            run(["chkconfig", "pacemaker", "on"])
+        service_list = ["corosync", "pacemaker"]
+        if need_to_handle_qdevice_service():
+            service_list.append("corosync-qdevice")
+
+    report_item_list = []
+    for service in service_list:
+        try:
+            enable_service(cmd_runner(), service)
+        except EnableServiceError as e:
+            report_item_list.append(
+                reports.service_enable_error(e.service, e.message)
+            )
+    if report_item_list:
+        raise LibraryError(*report_item_list)
 
 def disableServices():
-    if is_rhel6():
-        run(["chkconfig", "pacemaker", "off"])
-        run(["chkconfig", "corosync", "off"]) # Left here for users of old pcs
-                                              # which enabled corosync
-    else:
-        if is_systemctl():
-            run(["systemctl", "disable", "corosync.service"])
-            run(["systemctl", "disable", "pacemaker.service"])
-        else:
-            run(["chkconfig", "corosync", "off"])
-            run(["chkconfig", "pacemaker", "off"])
+    # Disable corosync on RHEL6 as well - left here for users of old pcs which
+    # enabled corosync.
+    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
+    service_list = ["corosync", "pacemaker"]
+    if need_to_handle_qdevice_service():
+        service_list.append("corosync-qdevice")
+
+    report_item_list = []
+    for service in service_list:
+        try:
+            disable_service(cmd_runner(), service)
+        except DisableServiceError as e:
+            report_item_list.append(
+                reports.service_disable_error(e.service, e.message)
+            )
+    if report_item_list:
+        raise LibraryError(*report_item_list)
 
 def write_file(path, data, permissions=0o644, binary=False):
     if os.path.exists(path):
@@ -2248,7 +2233,7 @@ def parse_cman_quorum_info(cman_info):
     in_node_list = False
     local_node_id = ""
     try:
-        for line in cman_info.split("\n"):
+        for line in cman_info.splitlines():
             line = line.strip()
             if not line:
                 continue
@@ -2260,12 +2245,13 @@ def parse_cman_quorum_info(cman_info):
                 parsed["node_list"].append({
                     "name": parts[3],
                     "votes": int(parts[2]),
-                    "local": local_node_id == parts[0]
+                    "local": local_node_id == parts[0],
                 })
             else:
                 if line == "---Votes---":
                     in_node_list = True
                     parsed["node_list"] = []
+                    parsed["qdevice_list"] = []
                     continue
                 if not ":" in line:
                     continue
@@ -2290,7 +2276,7 @@ def parse_quorumtool_output(quorumtool_output):
     parsed = {}
     in_node_list = False
     try:
-        for line in quorumtool_output.split("\n"):
+        for line in quorumtool_output.splitlines():
             line = line.strip()
             if not line:
                 continue
@@ -2299,15 +2285,25 @@ def parse_quorumtool_output(quorumtool_output):
                     # skip headers
                     continue
                 parts = line.split()
-                parsed["node_list"].append({
-                    "name": parts[3],
-                    "votes": int(parts[1]),
-                    "local": len(parts) > 4 and parts[4] == "(local)"
-                })
+                if parts[0] == "0":
+                    # this line has nodeid == 0, this is a qdevice line
+                    parsed["qdevice_list"].append({
+                        "name": parts[2],
+                        "votes": int(parts[1]),
+                        "local": False,
+                    })
+                else:
+                    # this line has non-zero nodeid, this is a node line
+                    parsed["node_list"].append({
+                        "name": parts[3],
+                        "votes": int(parts[1]),
+                        "local": len(parts) > 4 and parts[4] == "(local)",
+                    })
             else:
                 if line == "Membership information":
                     in_node_list = True
                     parsed["node_list"] = []
+                    parsed["qdevice_list"] = []
                     continue
                 if not ":" in line:
                     continue
@@ -2340,6 +2336,8 @@ def is_node_stop_cause_quorum_loss(quorum_info, local=True, node_list=None):
         if node_list and node_info["name"] in node_list:
             continue
         votes_after_stop += node_info["votes"]
+    for qdevice_info in quorum_info.get("qdevice_list", []):
+        votes_after_stop += qdevice_info["votes"]
     return votes_after_stop < quorum_info["quorum"]
 
 def dom_prepare_child_element(dom_element, tag_name, id):
@@ -2661,6 +2659,7 @@ def get_modificators():
         "enable": "--enable" in pcs_options,
         "force": "--force" in pcs_options,
         "full": "--full" in pcs_options,
+        "name": pcs_options.get("--name", None),
         "skip_offline_nodes": "--skip-offline" in pcs_options,
         "start": "--start" in pcs_options,
         "watchdog": pcs_options.get("--watchdog", []),
diff --git a/pcsd/pcs.rb b/pcsd/pcs.rb
index 415e02a..7c25e10 100644
--- a/pcsd/pcs.rb
+++ b/pcsd/pcs.rb
@@ -1965,6 +1965,23 @@ def disable_service(service)
   return (retcode == 0)
 end
 
+def start_service(service)
+  _, _, retcode = run_cmd(
+    PCSAuth.getSuperuserAuth(), "service", service, "start"
+  )
+  return (retcode == 0)
+end
+
+def stop_service(service)
+  if not is_service_installed?(service)
+    return true
+  end
+  _, _, retcode = run_cmd(
+    PCSAuth.getSuperuserAuth(), "service", service, "stop"
+  )
+  return (retcode == 0)
+end
+
 def set_cluster_prop_force(auth_user, prop, val)
   cmd = [PCS, 'property', 'set', "#{prop}=#{val}", '--force']
   if pacemaker_running?
diff --git a/pcsd/remote.rb b/pcsd/remote.rb
index f002d5b..0b2dc61 100644
--- a/pcsd/remote.rb
+++ b/pcsd/remote.rb
@@ -4,6 +4,7 @@ require 'open4'
 require 'set'
 require 'timeout'
 require 'rexml/document'
+require 'base64'
 
 require 'pcs.rb'
 require 'resource.rb'
@@ -71,7 +72,16 @@ def remote(params, request, auth_user)
       :remove_stonith_watchdog_timeout=> method(:remove_stonith_watchdog_timeout),
       :set_stonith_watchdog_timeout_to_zero => method(:set_stonith_watchdog_timeout_to_zero),
       :remote_enable_sbd => method(:remote_enable_sbd),
-      :remote_disable_sbd => method(:remote_disable_sbd)
+      :remote_disable_sbd => method(:remote_disable_sbd),
+      :qdevice_net_get_ca_certificate => method(:qdevice_net_get_ca_certificate),
+      :qdevice_net_sign_node_certificate => method(:qdevice_net_sign_node_certificate),
+      :qdevice_net_client_init_certificate_storage => method(:qdevice_net_client_init_certificate_storage),
+      :qdevice_net_client_import_certificate => method(:qdevice_net_client_import_certificate),
+      :qdevice_net_client_destroy => method(:qdevice_net_client_destroy),
+      :qdevice_client_enable => method(:qdevice_client_enable),
+      :qdevice_client_disable => method(:qdevice_client_disable),
+      :qdevice_client_start => method(:qdevice_client_start),
+      :qdevice_client_stop => method(:qdevice_client_stop),
   }
   remote_cmd_with_pacemaker = {
       :pacemaker_node_status => method(:remote_pacemaker_node_status),
@@ -2377,3 +2387,154 @@ def remote_disable_sbd(params, request, auth_user)
 
   return [200, 'Sbd has been disabled.']
 end
+
+def qdevice_net_get_ca_certificate(params, request, auth_user)
+  unless allowed_for_local_cluster(auth_user, Permissions::READ)
+    return 403, 'Permission denied'
+  end
+  begin
+    return [
+      200,
+      Base64.encode64(File.read(COROSYNC_QDEVICE_NET_SERVER_CA_FILE))
+    ]
+  rescue => e
+    return [400, "Unable to read certificate: #{e}"]
+  end
+end
+
+def qdevice_net_sign_node_certificate(params, request, auth_user)
+  unless allowed_for_local_cluster(auth_user, Permissions::READ)
+    return 403, 'Permission denied'
+  end
+  stdout, stderr, retval = run_cmd_options(
+    auth_user,
+    {'stdin' => params[:certificate_request]},
+    PCS, 'qdevice', 'sign-net-cert-request', '--name', params[:cluster_name]
+  )
+  if retval != 0
+    return [400, stderr.join('')]
+  end
+  return [200, stdout.join('')]
+end
+
+def qdevice_net_client_init_certificate_storage(params, request, auth_user)
+  # Last step of adding qdevice into a cluster is distribution of corosync.conf
+  # file with qdevice settings. This requires FULL permissions currently.
+  # If that gets relaxed, we can require lower permissions in here as well.
+  unless allowed_for_local_cluster(auth_user, Permissions::FULL)
+    return 403, 'Permission denied'
+  end
+  stdout, stderr, retval = run_cmd_options(
+    auth_user,
+    {'stdin' => params[:ca_certificate]},
+    PCS, 'qdevice', 'net-client', 'setup'
+  )
+  if retval != 0
+    return [400, stderr.join('')]
+  end
+  return [200, stdout.join('')]
+end
+
+def qdevice_net_client_import_certificate(params, request, auth_user)
+  # Last step of adding qdevice into a cluster is distribution of corosync.conf
+  # file with qdevice settings. This requires FULL permissions currently.
+  # If that gets relaxed, we can require lower permissions in here as well.
+  unless allowed_for_local_cluster(auth_user, Permissions::FULL)
+    return 403, 'Permission denied'
+  end
+  stdout, stderr, retval = run_cmd_options(
+    auth_user,
+    {'stdin' => params[:certificate]},
+    PCS, 'qdevice', 'net-client', 'import-certificate'
+  )
+  if retval != 0
+    return [400, stderr.join('')]
+  end
+  return [200, stdout.join('')]
+end
+
+def qdevice_net_client_destroy(param, request, auth_user)
+  # When removing a qdevice from a cluster, an updated corosync.conf file
+  # with removed qdevice settings is distributed. This requires FULL permissions
+  # currently. If that gets relaxed, we can require lower permissions in here
+  # as well.
+  unless allowed_for_local_cluster(auth_user, Permissions::FULL)
+    return 403, 'Permission denied'
+  end
+  stdout, stderr, retval = run_cmd(
+    auth_user,
+    PCS, 'qdevice', 'net-client', 'destroy'
+  )
+  if retval != 0
+    return [400, stderr.join('')]
+  end
+  return [200, stdout.join('')]
+end
+
+def qdevice_client_disable(param, request, auth_user)
+  unless allowed_for_local_cluster(auth_user, Permissions::WRITE)
+    return 403, 'Permission denied'
+  end
+  if disable_service('corosync-qdevice')
+    msg = 'corosync-qdevice disabled'
+    $logger.info(msg)
+    return [200, msg]
+  else
+    msg = 'Disabling corosync-qdevice failed'
+    $logger.error(msg)
+    return [400, msg]
+  end
+end
+
+def qdevice_client_enable(param, request, auth_user)
+  unless allowed_for_local_cluster(auth_user, Permissions::WRITE)
+    return 403, 'Permission denied'
+  end
+  if not is_service_enabled?('corosync')
+    msg = 'corosync is not enabled, skipping'
+    $logger.info(msg)
+    return [200, msg]
+  elsif enable_service('corosync-qdevice')
+    msg = 'corosync-qdevice enabled'
+    $logger.info(msg)
+    return [200, msg]
+  else
+    msg = 'Enabling corosync-qdevice failed'
+    $logger.error(msg)
+    return [400, msg]
+  end
+end
+
+def qdevice_client_stop(param, request, auth_user)
+  unless allowed_for_local_cluster(auth_user, Permissions::WRITE)
+    return 403, 'Permission denied'
+  end
+  if stop_service('corosync-qdevice')
+    msg = 'corosync-qdevice stopped'
+    $logger.info(msg)
+    return [200, msg]
+  else
+    msg = 'Stopping corosync-qdevice failed'
+    $logger.error(msg)
+    return [400, msg]
+  end
+end
+
+def qdevice_client_start(param, request, auth_user)
+  unless allowed_for_local_cluster(auth_user, Permissions::WRITE)
+    return 403, 'Permission denied'
+  end
+  if not is_service_running?('corosync')
+    msg = 'corosync is not running, skipping'
+    $logger.info(msg)
+    return [200, msg]
+  elsif start_service('corosync-qdevice')
+    msg = 'corosync-qdevice started'
+    $logger.info(msg)
+    return [200, msg]
+  else
+    msg = 'Starting corosync-qdevice failed'
+    $logger.error(msg)
+    return [400, msg]
+  end
+end
diff --git a/pcsd/settings.rb b/pcsd/settings.rb
index 6229161..51f00ac 100644
--- a/pcsd/settings.rb
+++ b/pcsd/settings.rb
@@ -21,6 +21,12 @@ CIBADMIN = "/usr/sbin/cibadmin"
 SBD_CONFIG = '/etc/sysconfig/sbd'
 CIB_PATH='/var/lib/pacemaker/cib/cib.xml'
 
+COROSYNC_QDEVICE_NET_SERVER_CERTS_DIR = "/etc/corosync/qnetd/nssdb"
+COROSYNC_QDEVICE_NET_SERVER_CA_FILE = (
+  COROSYNC_QDEVICE_NET_SERVER_CERTS_DIR + "/qnetd-cacert.crt"
+)
+COROSYNC_QDEVICE_NET_CLIENT_CERTS_DIR = "/etc/corosync/qdevice/net/nssdb"
+
 SUPERUSER = 'hacluster'
 ADMIN_GROUP = 'haclient'
 $user_pass_file = "pcs_users.conf"
diff --git a/pcsd/settings.rb.debian b/pcsd/settings.rb.debian
index 7bc92a9..aae1b11 100644
--- a/pcsd/settings.rb.debian
+++ b/pcsd/settings.rb.debian
@@ -18,8 +18,14 @@ COROSYNC_BINARIES = "/usr/sbin/"
 CMAN_TOOL = "/usr/sbin/cman_tool"
 PACEMAKERD = "/usr/sbin/pacemakerd"
 CIBADMIN = "/usr/sbin/cibadmin"
-SBD_CONFIG = '/etc/sysconfig/sbd'
-CIB_PATH='/var/lib/pacemaker/cib/cib.xml'
+SBD_CONFIG = "/etc/sysconfig/sbd"
+CIB_PATH = "/var/lib/pacemaker/cib/cib.xml"
+
+COROSYNC_QDEVICE_NET_SERVER_CERTS_DIR = "/etc/corosync/qnetd/nssdb"
+COROSYNC_QDEVICE_NET_SERVER_CA_FILE = (
+  COROSYNC_QDEVICE_NET_SERVER_CERTS_DIR + "/qnetd-cacert.crt"
+)
+COROSYNC_QDEVICE_NET_CLIENT_CERTS_DIR = "/etc/corosync/qdevice/net/nssdb"
 
 SUPERUSER = 'hacluster'
 ADMIN_GROUP = 'haclient'
-- 
1.8.3.1