Blame SOURCES/bz1308514-02-booth-support-improvements.patch

15f218
From 798a8ab276fb816c3d9cfa5ba0a8ed55a3ed6cd2 Mon Sep 17 00:00:00 2001
15f218
From: Ivan Devat <idevat@redhat.com>
15f218
Date: Mon, 29 Aug 2016 15:14:25 +0200
15f218
Subject: [PATCH] squash bz1308514 Wider support for booth configura
15f218
15f218
50fb38db5e26 append a new line at the end of the booth config
15f218
15f218
a03b98c0f9e1 add bash completion for booth
15f218
15f218
52b97fa9ef32 clean up ip resource if creating booth res. fails
15f218
15f218
3d0e698a83fc fix allow force remove multiple booth resources
15f218
15f218
1ac88efab2cd refactor booth remove
15f218
15f218
6b41c5cc1661 add booth restart command
15f218
15f218
706c6f32f172 fix usage: ticket grant/revoke not for arbitrator
15f218
15f218
26fa2a241227 complete man (stayed behind usage)
15f218
15f218
75f8da852641 modify exchange format of booth config
15f218
15f218
ffe6ec7ea8d2 show all booth config lines including unsupported
15f218
15f218
8722fb7ede2e add support for options during add booth ticket
15f218
15f218
50eb49a4350b fix naming of booth reportes and report codes according to convetions
15f218
15f218
6dfb7c82d802 simplify booth config distribution reports
15f218
15f218
116a7a311cd7 fix adding node to cluster when booth is not installed
15f218
15f218
23abb122e2d1 fix getting a list of existing booth config files
15f218
15f218
ebd9fc496e24 display booth config as it is (plain, not parsed)
15f218
15f218
0183814dd57a add ability to display booth config from a remote node
15f218
---
15f218
 pcs/booth.py                                |   6 +-
15f218
 pcs/cli/booth/command.py                    |  72 ++++++++++--------
15f218
 pcs/cli/booth/test/test_command.py          |  21 ++++--
15f218
 pcs/cli/common/lib_wrapper.py               |   3 +-
15f218
 pcs/common/report_codes.py                  |   9 +--
15f218
 pcs/lib/booth/config_exchange.py            |  47 ++++--------
15f218
 pcs/lib/booth/config_files.py               |  15 +++-
15f218
 pcs/lib/booth/config_parser.py              |   3 +-
15f218
 pcs/lib/booth/config_structure.py           |  35 ++++++++-
15f218
 pcs/lib/booth/reports.py                    |  97 +++++++++++++-----------
15f218
 pcs/lib/booth/resource.py                   |  49 ++++--------
15f218
 pcs/lib/booth/sync.py                       |  12 +--
15f218
 pcs/lib/booth/test/test_config_exchange.py  |  56 ++++++--------
15f218
 pcs/lib/booth/test/test_config_files.py     |  32 ++++++--
15f218
 pcs/lib/booth/test/test_config_parser.py    |   2 +
15f218
 pcs/lib/booth/test/test_config_structure.py |  66 ++++++++++++++++-
15f218
 pcs/lib/booth/test/test_resource.py         | 111 ++++++++++++----------------
15f218
 pcs/lib/booth/test/test_sync.py             |  56 +++++++-------
15f218
 pcs/lib/commands/booth.py                   |  88 ++++++++++++++--------
15f218
 pcs/lib/commands/test/test_booth.py         |  50 +++++++------
15f218
 pcs/pcs.8                                   |  11 ++-
15f218
 pcs/test/test_booth.py                      |  77 +++++++++++++++++--
15f218
 pcs/usage.py                                |  13 +++-
15f218
 23 files changed, 564 insertions(+), 367 deletions(-)
15f218
15f218
diff --git a/pcs/booth.py b/pcs/booth.py
15f218
index 764dcd8..5ec41bf 100644
15f218
--- a/pcs/booth.py
15f218
+++ b/pcs/booth.py
15f218
@@ -12,7 +12,7 @@ from pcs import utils
15f218
 from pcs.cli.booth import command
15f218
 from pcs.cli.common.errors import CmdLineInputError
15f218
 from pcs.lib.errors import LibraryError
15f218
-from pcs.resource import resource_create, resource_remove
15f218
+from pcs.resource import resource_create, resource_remove, resource_restart
15f218
 
15f218
 
15f218
 def booth_cmd(lib, argv, modifiers):
15f218
@@ -47,13 +47,15 @@ def booth_cmd(lib, argv, modifiers):
15f218
             else:
15f218
                 raise CmdLineInputError()
15f218
         elif sub_cmd == "create":
15f218
-            command.get_create_in_cluster(resource_create)(
15f218
+            command.get_create_in_cluster(resource_create, resource_remove)(
15f218
                 lib, argv_next, modifiers
15f218
             )
15f218
         elif sub_cmd == "remove":
15f218
             command.get_remove_from_cluster(resource_remove)(
15f218
                 lib, argv_next, modifiers
15f218
             )
15f218
+        elif sub_cmd == "restart":
15f218
+            command.get_restart(resource_restart)(lib, argv_next, modifiers)
15f218
         elif sub_cmd == "sync":
15f218
             command.sync(lib, argv_next, modifiers)
15f218
         elif sub_cmd == "pull":
15f218
diff --git a/pcs/cli/booth/command.py b/pcs/cli/booth/command.py
15f218
index bea6582..0b71a01 100644
15f218
--- a/pcs/cli/booth/command.py
15f218
+++ b/pcs/cli/booth/command.py
15f218
@@ -6,7 +6,7 @@ from __future__ import (
15f218
 )
15f218
 
15f218
 from pcs.cli.common.errors import CmdLineInputError
15f218
-from pcs.cli.common.parse_args import group_by_keywords
15f218
+from pcs.cli.common.parse_args import group_by_keywords, prepare_options
15f218
 
15f218
 
15f218
 DEFAULT_BOOTH_NAME = "booth"
15f218
@@ -18,15 +18,25 @@ def config_setup(lib, arg_list, modifiers):
15f218
     """
15f218
     create booth config
15f218
     """
15f218
-    booth_configuration = group_by_keywords(
15f218
+    peers = group_by_keywords(
15f218
         arg_list,
15f218
         set(["sites", "arbitrators"]),
15f218
         keyword_repeat_allowed=False
15f218
     )
15f218
-    if "sites" not in booth_configuration or not booth_configuration["sites"]:
15f218
+    if "sites" not in peers or not peers["sites"]:
15f218
         raise CmdLineInputError()
15f218
 
15f218
-    lib.booth.config_setup(booth_configuration, modifiers["force"])
15f218
+    booth_config = []
15f218
+    for site in peers["sites"]:
15f218
+        booth_config.append({"key": "site", "value": site, "details": []})
15f218
+    for arbitrator in peers["arbitrators"]:
15f218
+        booth_config.append({
15f218
+            "key": "arbitrator",
15f218
+            "value": arbitrator,
15f218
+            "details": [],
15f218
+        })
15f218
+
15f218
+    lib.booth.config_setup(booth_config, modifiers["force"])
15f218
 
15f218
 def config_destroy(lib, arg_list, modifiers):
15f218
     """
15f218
@@ -41,36 +51,20 @@ def config_show(lib, arg_list, modifiers):
15f218
     """
15f218
     print booth config
15f218
     """
15f218
-    booth_configuration = lib.booth.config_show()
15f218
-    authfile_lines = []
15f218
-    if booth_configuration["authfile"]:
15f218
-        authfile_lines.append(
15f218
-            "authfile = {0}".format(booth_configuration["authfile"])
15f218
-        )
15f218
+    if len(arg_list) > 1:
15f218
+        raise CmdLineInputError()
15f218
+    node = None if not arg_list else arg_list[0]
15f218
+
15f218
+    print(lib.booth.config_text(DEFAULT_BOOTH_NAME, node), end="")
15f218
 
15f218
-    line_list = (
15f218
-        ["site = {0}".format(site) for site in booth_configuration["sites"]]
15f218
-        +
15f218
-        [
15f218
-            "arbitrator = {0}".format(arbitrator)
15f218
-            for arbitrator in booth_configuration["arbitrators"]
15f218
-        ]
15f218
-        + authfile_lines +
15f218
-        [
15f218
-            'ticket = "{0}"'.format(ticket)
15f218
-            for ticket in booth_configuration["tickets"]
15f218
-        ]
15f218
-    )
15f218
-    for line in line_list:
15f218
-        print(line)
15f218
 
15f218
 def config_ticket_add(lib, arg_list, modifiers):
15f218
     """
15f218
     add ticket to current configuration
15f218
     """
15f218
-    if len(arg_list) != 1:
15f218
+    if not arg_list:
15f218
         raise CmdLineInputError
15f218
-    lib.booth.config_ticket_add(arg_list[0])
15f218
+    lib.booth.config_ticket_add(arg_list[0], prepare_options(arg_list[1:]))
15f218
 
15f218
 def config_ticket_remove(lib, arg_list, modifiers):
15f218
     """
15f218
@@ -96,7 +90,7 @@ def ticket_revoke(lib, arg_list, modifiers):
15f218
 def ticket_grant(lib, arg_list, modifiers):
15f218
     ticket_operation(lib.booth.ticket_grant, arg_list, modifiers)
15f218
 
15f218
-def get_create_in_cluster(resource_create):
15f218
+def get_create_in_cluster(resource_create, resource_remove):
15f218
     #TODO resource_remove is provisional hack until resources are not moved to
15f218
     #lib
15f218
     def create_in_cluster(lib, arg_list, modifiers):
15f218
@@ -108,6 +102,7 @@ def get_create_in_cluster(resource_create):
15f218
             __get_name(modifiers),
15f218
             ip,
15f218
             resource_create,
15f218
+            resource_remove,
15f218
         )
15f218
     return create_in_cluster
15f218
 
15f218
@@ -118,10 +113,28 @@ def get_remove_from_cluster(resource_remove):
15f218
         if arg_list:
15f218
             raise CmdLineInputError()
15f218
 
15f218
-        lib.booth.remove_from_cluster(__get_name(modifiers), resource_remove)
15f218
+        lib.booth.remove_from_cluster(
15f218
+            __get_name(modifiers),
15f218
+            resource_remove,
15f218
+            modifiers["force"],
15f218
+        )
15f218
 
15f218
     return remove_from_cluster
15f218
 
15f218
+def get_restart(resource_restart):
15f218
+    #TODO resource_restart is provisional hack until resources are not moved to
15f218
+    #lib
15f218
+    def restart(lib, arg_list, modifiers):
15f218
+        if arg_list:
15f218
+            raise CmdLineInputError()
15f218
+
15f218
+        lib.booth.restart(
15f218
+            __get_name(modifiers),
15f218
+            resource_restart,
15f218
+            modifiers["force"],
15f218
+        )
15f218
+
15f218
+    return restart
15f218
 
15f218
 def sync(lib, arg_list, modifiers):
15f218
     if arg_list:
15f218
@@ -175,3 +188,4 @@ def status(lib, arg_list, modifiers):
15f218
     if booth_status.get("status"):
15f218
         print("DAEMON STATUS:")
15f218
         print(booth_status["status"])
15f218
+
15f218
diff --git a/pcs/cli/booth/test/test_command.py b/pcs/cli/booth/test/test_command.py
15f218
index 00216f2..019a74f 100644
15f218
--- a/pcs/cli/booth/test/test_command.py
15f218
+++ b/pcs/cli/booth/test/test_command.py
15f218
@@ -28,10 +28,12 @@ class ConfigSetupTest(TestCase):
15f218
             }
15f218
         )
15f218
         lib.booth.config_setup.assert_called_once_with(
15f218
-            {
15f218
-                "sites": ["1.1.1.1", "2.2.2.2", "4.4.4.4"],
15f218
-                "arbitrators": ["3.3.3.3"],
15f218
-            },
15f218
+            [
15f218
+                {"key": "site", "value": "1.1.1.1", "details": []},
15f218
+                {"key": "site", "value": "2.2.2.2", "details": []},
15f218
+                {"key": "site", "value": "4.4.4.4", "details": []},
15f218
+                {"key": "arbitrator", "value": "3.3.3.3", "details": []},
15f218
+            ],
15f218
             False
15f218
         )
15f218
 
15f218
@@ -40,5 +42,12 @@ class ConfigTicketAddTest(TestCase):
15f218
         lib = mock.MagicMock()
15f218
         lib.booth = mock.MagicMock()
15f218
         lib.booth.config_ticket_add = mock.MagicMock()
15f218
-        command.config_ticket_add(lib, arg_list=["TICKET_A"], modifiers={})
15f218
-        lib.booth.config_ticket_add.assert_called_once_with("TICKET_A")
15f218
+        command.config_ticket_add(
15f218
+            lib,
15f218
+            arg_list=["TICKET_A", "timeout=10"],
15f218
+            modifiers={}
15f218
+        )
15f218
+        lib.booth.config_ticket_add.assert_called_once_with(
15f218
+            "TICKET_A",
15f218
+            {"timeout": "10"},
15f218
+        )
15f218
diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
15f218
index c836575..94a1311 100644
15f218
--- a/pcs/cli/common/lib_wrapper.py
15f218
+++ b/pcs/cli/common/lib_wrapper.py
15f218
@@ -209,11 +209,12 @@ def load_module(env, middleware_factory, name):
15f218
             {
15f218
                 "config_setup": booth.config_setup,
15f218
                 "config_destroy": booth.config_destroy,
15f218
-                "config_show": booth.config_show,
15f218
+                "config_text": booth.config_text,
15f218
                 "config_ticket_add": booth.config_ticket_add,
15f218
                 "config_ticket_remove": booth.config_ticket_remove,
15f218
                 "create_in_cluster": booth.create_in_cluster,
15f218
                 "remove_from_cluster": booth.remove_from_cluster,
15f218
+                "restart": booth.restart,
15f218
                 "config_sync": booth.config_sync,
15f218
                 "enable": booth.enable_booth,
15f218
                 "disable": booth.disable_booth,
15f218
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
15f218
index 672c2e3..5e46a1f 100644
15f218
--- a/pcs/common/report_codes.py
15f218
+++ b/pcs/common/report_codes.py
15f218
@@ -29,16 +29,15 @@ BOOTH_ADDRESS_DUPLICATION = "BOOTH_ADDRESS_DUPLICATION"
15f218
 BOOTH_ALREADY_IN_CIB = "BOOTH_ALREADY_IN_CIB"
15f218
 BOOTH_CANNOT_DETERMINE_LOCAL_SITE_IP = "BOOTH_CANNOT_DETERMINE_LOCAL_SITE_IP"
15f218
 BOOTH_CANNOT_IDENTIFY_KEYFILE = "BOOTH_CANNOT_IDENTIFY_KEYFILE"
15f218
+BOOTH_CONFIG_ACCEPTED_BY_NODE = "BOOTH_CONFIG_ACCEPTED_BY_NODE"
15f218
+BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR = "BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR"
15f218
+BOOTH_CONFIG_DISTRIBUTION_STARTED = "BOOTH_CONFIG_DISTRIBUTION_STARTED"
15f218
 BOOTH_CONFIG_FILE_ALREADY_EXISTS = "BOOTH_CONFIG_FILE_ALREADY_EXISTS"
15f218
 BOOTH_CONFIG_IO_ERROR = "BOOTH_CONFIG_IO_ERROR"
15f218
 BOOTH_CONFIG_IS_USED = "BOOTH_CONFIG_IS_USED"
15f218
 BOOTH_CONFIG_READ_ERROR = "BOOTH_CONFIG_READ_ERROR"
15f218
-BOOTH_CONFIG_WRITE_ERROR = "BOOTH_CONFIG_WRITE_ERROR"
15f218
 BOOTH_CONFIG_UNEXPECTED_LINES = "BOOTH_CONFIG_UNEXPECTED_LINES"
15f218
-BOOTH_CONFIGS_SAVED_ON_NODE = "BOOTH_CONFIGS_SAVED_ON_NODE"
15f218
-BOOTH_CONFIGS_SAVING_ON_NODE = "BOOTH_CONFIGS_SAVING_ON_NODE"
15f218
 BOOTH_DAEMON_STATUS_ERROR = "BOOTH_DAEMON_STATUS_ERROR"
15f218
-BOOTH_DISTRIBUTING_CONFIG = "BOOTH_DISTRIBUTING_CONFIG"
15f218
 BOOTH_EVEN_PEERS_NUM = "BOOTH_EVEN_PEERS_NUM"
15f218
 BOOTH_FETCHING_CONFIG_FROM_NODE = "BOOTH_FETCHING_CONFIG_FROM_NODE"
15f218
 BOOTH_INVALID_CONFIG_NAME = "BOOTH_INVALID_CONFIG_NAME"
15f218
@@ -50,8 +49,8 @@ BOOTH_PEERS_STATUS_ERROR = "BOOTH_PEERS_STATUS_ERROR"
15f218
 BOOTH_SKIPPING_CONFIG = "BOOTH_SKIPPING_CONFIG"
15f218
 BOOTH_TICKET_DOES_NOT_EXIST = "BOOTH_TICKET_DOES_NOT_EXIST"
15f218
 BOOTH_TICKET_DUPLICATE = "BOOTH_TICKET_DUPLICATE"
15f218
-BOOTH_TICKET_OPERATION_FAILED = "BOOTH_TICKET_OPERATION_FAILED"
15f218
 BOOTH_TICKET_NAME_INVALID = "BOOTH_TICKET_NAME_INVALID"
15f218
+BOOTH_TICKET_OPERATION_FAILED = "BOOTH_TICKET_OPERATION_FAILED"
15f218
 BOOTH_TICKET_STATUS_ERROR = "BOOTH_TICKET_STATUS_ERROR"
15f218
 BOOTH_UNSUPORTED_FILE_LOCATION = "BOOTH_UNSUPORTED_FILE_LOCATION"
15f218
 CIB_ALERT_NOT_FOUND = "CIB_ALERT_NOT_FOUND"
15f218
diff --git a/pcs/lib/booth/config_exchange.py b/pcs/lib/booth/config_exchange.py
15f218
index e0569ba..377af1d 100644
15f218
--- a/pcs/lib/booth/config_exchange.py
15f218
+++ b/pcs/lib/booth/config_exchange.py
15f218
@@ -6,38 +6,23 @@ from __future__ import (
15f218
 )
15f218
 from pcs.lib.booth.config_structure import ConfigItem
15f218
 
15f218
-EXCHANGE_PRIMITIVES = ["authfile"]
15f218
-EXCHANGE_LISTS = [
15f218
-    ("site", "sites"),
15f218
-    ("arbitrator", "arbitrators"),
15f218
-    ("ticket", "tickets"),
15f218
-]
15f218
-
15f218
-
15f218
 def to_exchange_format(booth_configuration):
15f218
-    exchange_lists = dict(EXCHANGE_LISTS)
15f218
-    exchange = dict(
15f218
-        (exchange_key, []) for exchange_key in exchange_lists.values()
15f218
-    )
15f218
-
15f218
-    for key, value, _ in booth_configuration:
15f218
-        if key in exchange_lists:
15f218
-            exchange[exchange_lists[key]].append(value)
15f218
-        if key in EXCHANGE_PRIMITIVES:
15f218
-            exchange[key] = value
15f218
-
15f218
-    return exchange
15f218
+    return [
15f218
+        {
15f218
+            "key": item.key,
15f218
+            "value": item.value,
15f218
+            "details": to_exchange_format(item.details),
15f218
+        }
15f218
+        for item in booth_configuration
15f218
+    ]
15f218
 
15f218
 
15f218
 def from_exchange_format(exchange_format):
15f218
-    booth_config = []
15f218
-    for key in EXCHANGE_PRIMITIVES:
15f218
-        if key in exchange_format:
15f218
-            booth_config.append(ConfigItem(key, exchange_format[key]))
15f218
-
15f218
-    for key, exchange_key in EXCHANGE_LISTS:
15f218
-        booth_config.extend([
15f218
-            ConfigItem(key, value)
15f218
-            for value in exchange_format.get(exchange_key, [])
15f218
-        ])
15f218
-    return booth_config
15f218
+    return [
15f218
+        ConfigItem(
15f218
+            item["key"],
15f218
+            item["value"],
15f218
+            from_exchange_format(item["details"]),
15f218
+        )
15f218
+        for item in exchange_format
15f218
+    ]
15f218
diff --git a/pcs/lib/booth/config_files.py b/pcs/lib/booth/config_files.py
15f218
index aaad951..7b91379 100644
15f218
--- a/pcs/lib/booth/config_files.py
15f218
+++ b/pcs/lib/booth/config_files.py
15f218
@@ -24,10 +24,17 @@ def get_all_configs_file_names():
15f218
     Returns list of all file names ending with '.conf' in booth configuration
15f218
     directory.
15f218
     """
15f218
+    if not os.path.isdir(BOOTH_CONFIG_DIR):
15f218
+        return []
15f218
     return [
15f218
-        file_name for file_name in os.listdir(BOOTH_CONFIG_DIR)
15f218
-        if os.path.isfile(file_name) and file_name.endswith(".conf") and
15f218
-        len(file_name) > len(".conf")
15f218
+        file_name
15f218
+        for file_name in os.listdir(BOOTH_CONFIG_DIR)
15f218
+        if
15f218
+            file_name.endswith(".conf")
15f218
+            and
15f218
+            len(file_name) > len(".conf")
15f218
+            and
15f218
+            os.path.isfile(os.path.join(BOOTH_CONFIG_DIR, file_name))
15f218
     ]
15f218
 
15f218
 
15f218
@@ -55,7 +62,7 @@ def read_configs(reporter, skip_wrong_config=False):
15f218
         try:
15f218
             output[file_name] = _read_config(file_name)
15f218
         except EnvironmentError:
15f218
-            report_list.append(reports.booth_config_unable_to_read(
15f218
+            report_list.append(reports.booth_config_read_error(
15f218
                 file_name,
15f218
                 (
15f218
                     ReportItemSeverity.WARNING if skip_wrong_config
15f218
diff --git a/pcs/lib/booth/config_parser.py b/pcs/lib/booth/config_parser.py
15f218
index 62d2203..bdc79fd 100644
15f218
--- a/pcs/lib/booth/config_parser.py
15f218
+++ b/pcs/lib/booth/config_parser.py
15f218
@@ -23,7 +23,8 @@ def parse(content):
15f218
         )
15f218
 
15f218
 def build(config_line_list):
15f218
-    return "\n".join(build_to_lines(config_line_list))
15f218
+    newline = [""]
15f218
+    return "\n".join(build_to_lines(config_line_list) + newline)
15f218
 
15f218
 def build_to_lines(config_line_list, deep=0):
15f218
     line_list = []
15f218
diff --git a/pcs/lib/booth/config_structure.py b/pcs/lib/booth/config_structure.py
15f218
index c92f718..8977b7a 100644
15f218
--- a/pcs/lib/booth/config_structure.py
15f218
+++ b/pcs/lib/booth/config_structure.py
15f218
@@ -7,6 +7,7 @@ from __future__ import (
15f218
 
15f218
 import re
15f218
 
15f218
+import pcs.lib.reports as common_reports
15f218
 from pcs.lib.booth import reports
15f218
 from pcs.lib.errors import LibraryError
15f218
 from collections import namedtuple
15f218
@@ -66,6 +67,15 @@ def validate_peers(site_list, arbitrator_list):
15f218
     if report:
15f218
         raise LibraryError(*report)
15f218
 
15f218
+def take_peers(booth_configuration):
15f218
+    return (
15f218
+        pick_list_by_key(booth_configuration, "site"),
15f218
+        pick_list_by_key(booth_configuration, "arbitrator"),
15f218
+    )
15f218
+
15f218
+def pick_list_by_key(booth_configuration, key):
15f218
+    return [item.value for item in booth_configuration if item.key == key]
15f218
+
15f218
 def remove_ticket(booth_configuration, ticket_name):
15f218
     validate_ticket_exists(booth_configuration, ticket_name)
15f218
     return [
15f218
@@ -73,11 +83,14 @@ def remove_ticket(booth_configuration, ticket_name):
15f218
         if config_item.key != "ticket" or config_item.value != ticket_name
15f218
     ]
15f218
 
15f218
-def add_ticket(booth_configuration, ticket_name):
15f218
+def add_ticket(booth_configuration, ticket_name, options):
15f218
     validate_ticket_name(ticket_name)
15f218
     validate_ticket_unique(booth_configuration, ticket_name)
15f218
+    validate_ticket_options(options)
15f218
     return booth_configuration + [
15f218
-        ConfigItem("ticket", ticket_name)
15f218
+        ConfigItem("ticket", ticket_name, [
15f218
+            ConfigItem(key, value) for key, value in options.items()
15f218
+        ])
15f218
     ]
15f218
 
15f218
 def validate_ticket_exists(booth_configuration, ticket_name):
15f218
@@ -88,6 +101,24 @@ def validate_ticket_unique(booth_configuration, ticket_name):
15f218
     if ticket_exists(booth_configuration, ticket_name):
15f218
         raise LibraryError(reports.booth_ticket_duplicate(ticket_name))
15f218
 
15f218
+def validate_ticket_options(options):
15f218
+    reports = []
15f218
+    for key in sorted(options):
15f218
+        if key in GLOBAL_KEYS:
15f218
+            reports.append(
15f218
+                common_reports.invalid_option(key, TICKET_KEYS, "booth ticket")
15f218
+            )
15f218
+
15f218
+        if not options[key].strip():
15f218
+            reports.append(common_reports.invalid_option_value(
15f218
+                key,
15f218
+                options[key],
15f218
+                "no-empty",
15f218
+            ))
15f218
+
15f218
+    if reports:
15f218
+        raise LibraryError(*reports)
15f218
+
15f218
 def ticket_exists(booth_configuration, ticket_name):
15f218
     return any(
15f218
         value for key, value, _ in booth_configuration
15f218
diff --git a/pcs/lib/booth/reports.py b/pcs/lib/booth/reports.py
15f218
index 8a804e0..6aa9d3d 100644
15f218
--- a/pcs/lib/booth/reports.py
15f218
+++ b/pcs/lib/booth/reports.py
15f218
@@ -197,22 +197,17 @@ def booth_multiple_times_in_cib(
15f218
     )
15f218
 
15f218
 
15f218
-def booth_distributing_config(name=None):
15f218
+def booth_config_distribution_started():
15f218
     """
15f218
-    Sending booth config to all nodes in cluster.
15f218
-
15f218
-    name -- name of booth instance
15f218
+    booth configuration is about to be sent to nodes
15f218
     """
15f218
     return ReportItem.info(
15f218
-        report_codes.BOOTH_DISTRIBUTING_CONFIG,
15f218
-        "Sending booth config{0} to all cluster nodes.".format(
15f218
-            " ({name})" if name and name != "booth" else ""
15f218
-        ),
15f218
-        info={"name": name}
15f218
+        report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+        "Sending booth configuration to cluster nodes..."
15f218
     )
15f218
 
15f218
 
15f218
-def booth_config_saved(node=None, name_list=None):
15f218
+def booth_config_accepted_by_node(node=None, name_list=None):
15f218
     """
15f218
     Booth config has been saved on specified node.
15f218
 
15f218
@@ -229,7 +224,7 @@ def booth_config_saved(node=None, name_list=None):
15f218
         msg = "Booth config saved."
15f218
         name = None
15f218
     return ReportItem.info(
15f218
-        report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+        report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
         msg if node is None else "{node}: " + msg,
15f218
         info={
15f218
             "node": node,
15f218
@@ -239,30 +234,7 @@ def booth_config_saved(node=None, name_list=None):
15f218
     )
15f218
 
15f218
 
15f218
-def booth_config_unable_to_read(
15f218
-    name, severity=ReportItemSeverity.ERROR, forceable=None
15f218
-):
15f218
-    """
15f218
-    Unable to read from specified booth instance config.
15f218
-
15f218
-    name -- name of booth instance
15f218
-    severity -- severity of report item
15f218
-    forceable -- is this report item forceable? by what category?
15f218
-    """
15f218
-    if name and name != "booth":
15f218
-        msg = "Unable to read booth config ({name})."
15f218
-    else:
15f218
-        msg = "Unable to read booth config."
15f218
-    return ReportItem(
15f218
-        report_codes.BOOTH_CONFIG_READ_ERROR,
15f218
-        severity,
15f218
-        msg,
15f218
-        info={"name": name},
15f218
-        forceable=forceable
15f218
-    )
15f218
-
15f218
-
15f218
-def booth_config_not_saved(node, reason, name=None):
15f218
+def booth_config_distribution_node_error(node, reason, name=None):
15f218
     """
15f218
     Saving booth config failed on specified node.
15f218
 
15f218
@@ -275,7 +247,7 @@ def booth_config_not_saved(node, reason, name=None):
15f218
     else:
15f218
         msg = "Unable to save booth config on node '{node}': {reason}"
15f218
     return ReportItem.error(
15f218
-        report_codes.BOOTH_CONFIG_WRITE_ERROR,
15f218
+        report_codes.BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR,
15f218
         msg,
15f218
         info={
15f218
             "node": node,
15f218
@@ -285,20 +257,36 @@ def booth_config_not_saved(node, reason, name=None):
15f218
     )
15f218
 
15f218
 
15f218
-def booth_sending_local_configs_to_node(node):
15f218
+def booth_config_read_error(
15f218
+    name, severity=ReportItemSeverity.ERROR, forceable=None
15f218
+):
15f218
     """
15f218
-    Sending all local booth configs to node
15f218
+    Unable to read from specified booth instance config.
15f218
 
15f218
-    node -- node name
15f218
+    name -- name of booth instance
15f218
+    severity -- severity of report item
15f218
+    forceable -- is this report item forceable? by what category?
15f218
     """
15f218
-    return ReportItem.info(
15f218
-        report_codes.BOOTH_CONFIGS_SAVING_ON_NODE,
15f218
-        "{node}: Saving booth config(s)...",
15f218
-        info={"node": node}
15f218
+    if name and name != "booth":
15f218
+        msg = "Unable to read booth config ({name})."
15f218
+    else:
15f218
+        msg = "Unable to read booth config."
15f218
+    return ReportItem(
15f218
+        report_codes.BOOTH_CONFIG_READ_ERROR,
15f218
+        severity,
15f218
+        msg,
15f218
+        info={"name": name},
15f218
+        forceable=forceable
15f218
     )
15f218
 
15f218
 
15f218
-def booth_fetching_config_from_node(node, config=None):
15f218
+def booth_fetching_config_from_node_started(node, config=None):
15f218
+    """
15f218
+    fetching of booth config from specified node started
15f218
+
15f218
+    node -- node from which config is fetching
15f218
+    config -- config name
15f218
+    """
15f218
     if config or config == 'booth':
15f218
         msg = "Fetching booth config from node '{node}'..."
15f218
     else:
15f218
@@ -314,6 +302,12 @@ def booth_fetching_config_from_node(node, config=None):
15f218
 
15f218
 
15f218
 def booth_unsupported_file_location(file):
15f218
+    """
15f218
+    location of booth configuration file (config, authfile) file is not
15f218
+    supported (not in /etc/booth/)
15f218
+
15f218
+    file -- file path
15f218
+    """
15f218
     return ReportItem.warning(
15f218
         report_codes.BOOTH_UNSUPORTED_FILE_LOCATION,
15f218
         "skipping file {file}: unsupported file location",
15f218
@@ -322,6 +316,11 @@ def booth_unsupported_file_location(file):
15f218
 
15f218
 
15f218
 def booth_daemon_status_error(reason):
15f218
+    """
15f218
+    Unable to get status of booth daemon because of error.
15f218
+
15f218
+    reason -- reason
15f218
+    """
15f218
     return ReportItem.error(
15f218
         report_codes.BOOTH_DAEMON_STATUS_ERROR,
15f218
         "unable to get status of booth daemon: {reason}",
15f218
@@ -330,6 +329,11 @@ def booth_daemon_status_error(reason):
15f218
 
15f218
 
15f218
 def booth_tickets_status_error(reason=None):
15f218
+    """
15f218
+    Unable to get status of booth tickets because of error.
15f218
+
15f218
+    reason -- reason
15f218
+    """
15f218
     return ReportItem.error(
15f218
         report_codes.BOOTH_TICKET_STATUS_ERROR,
15f218
         "unable to get status of booth tickets",
15f218
@@ -340,6 +344,11 @@ def booth_tickets_status_error(reason=None):
15f218
 
15f218
 
15f218
 def booth_peers_status_error(reason=None):
15f218
+    """
15f218
+    Unable to get status of booth peers because of error.
15f218
+
15f218
+    reason -- reason
15f218
+    """
15f218
     return ReportItem.error(
15f218
         report_codes.BOOTH_PEERS_STATUS_ERROR,
15f218
         "unable to get status of booth peers",
15f218
diff --git a/pcs/lib/booth/resource.py b/pcs/lib/booth/resource.py
15f218
index e793713..a4b7b1e 100644
15f218
--- a/pcs/lib/booth/resource.py
15f218
+++ b/pcs/lib/booth/resource.py
15f218
@@ -8,18 +8,12 @@ from __future__ import (
15f218
 from pcs.lib.cib.tools import find_unique_id
15f218
 
15f218
 
15f218
-class BoothNotFoundInCib(Exception):
15f218
-    pass
15f218
-
15f218
-class BoothMultipleOccurenceFoundInCib(Exception):
15f218
-    pass
15f218
-
15f218
 def create_resource_id(resources_section, name, suffix):
15f218
     return find_unique_id(
15f218
         resources_section.getroottree(), "booth-{0}-{1}".format(name, suffix)
15f218
     )
15f218
 
15f218
-def get_creator(resource_create):
15f218
+def get_creator(resource_create, resource_remove=None):
15f218
     #TODO resource_create  is provisional hack until resources are not moved to
15f218
     #lib
15f218
     def create_booth_in_cluster(ip, booth_config_file_path, create_id):
15f218
@@ -36,15 +30,18 @@ def get_creator(resource_create):
15f218
             clone_opts=[],
15f218
             group=group_id,
15f218
         )
15f218
-        resource_create(
15f218
-            ra_id=booth_id,
15f218
-            ra_type="ocf:pacemaker:booth-site",
15f218
-            ra_values=["config={0}".format(booth_config_file_path)],
15f218
-            op_values=[],
15f218
-            meta_values=[],
15f218
-            clone_opts=[],
15f218
-            group=group_id,
15f218
-        )
15f218
+        try:
15f218
+            resource_create(
15f218
+                ra_id=booth_id,
15f218
+                ra_type="ocf:pacemaker:booth-site",
15f218
+                ra_values=["config={0}".format(booth_config_file_path)],
15f218
+                op_values=[],
15f218
+                meta_values=[],
15f218
+                clone_opts=[],
15f218
+                group=group_id,
15f218
+            )
15f218
+        except SystemExit:
15f218
+            resource_remove(ip_id)
15f218
     return create_booth_in_cluster
15f218
 
15f218
 def is_ip_resource(resource_element):
15f218
@@ -64,28 +61,12 @@ def find_grouped_ip_element_to_remove(booth_element):
15f218
     return None
15f218
 
15f218
 def get_remover(resource_remove):
15f218
-    def remove_from_cluster(
15f218
-        resources_section, booth_config_file_path, remove_multiple=False
15f218
-    ):
15f218
-        element_list = find_for_config(
15f218
-            resources_section,
15f218
-            booth_config_file_path
15f218
-        )
15f218
-        if not element_list:
15f218
-            raise BoothNotFoundInCib()
15f218
-
15f218
-        if len(element_list) > 1 and not remove_multiple:
15f218
-            raise BoothMultipleOccurenceFoundInCib()
15f218
-
15f218
-        number_of_removed_booth_elements = 0
15f218
-        for element in element_list:
15f218
+    def remove_from_cluster(booth_element_list):
15f218
+        for element in booth_element_list:
15f218
             ip_resource_to_remove = find_grouped_ip_element_to_remove(element)
15f218
             if ip_resource_to_remove is not None:
15f218
                 resource_remove(ip_resource_to_remove.attrib["id"])
15f218
             resource_remove(element.attrib["id"])
15f218
-            number_of_removed_booth_elements += 1
15f218
-
15f218
-        return number_of_removed_booth_elements
15f218
 
15f218
     return remove_from_cluster
15f218
 
15f218
diff --git a/pcs/lib/booth/sync.py b/pcs/lib/booth/sync.py
15f218
index c9bc30b..374b96d 100644
15f218
--- a/pcs/lib/booth/sync.py
15f218
+++ b/pcs/lib/booth/sync.py
15f218
@@ -57,7 +57,7 @@ def _set_config_on_node(
15f218
         "remote/booth_set_config",
15f218
         NodeCommunicator.format_data_dict([("data_json", json.dumps(data))])
15f218
     )
15f218
-    reporter.process(reports.booth_config_saved(node.label, [name]))
15f218
+    reporter.process(reports.booth_config_accepted_by_node(node.label, [name]))
15f218
 
15f218
 
15f218
 def send_config_to_all_nodes(
15f218
@@ -77,7 +77,7 @@ def send_config_to_all_nodes(
15f218
     authfile_data -- content of authfile as bytes
15f218
     skip_offline -- if True offline nodes will be skipped
15f218
     """
15f218
-    reporter.process(reports.booth_distributing_config(name))
15f218
+    reporter.process(reports.booth_config_distribution_started())
15f218
     parallel_nodes_communication_helper(
15f218
         _set_config_on_node,
15f218
         [
15f218
@@ -115,6 +115,9 @@ def send_all_config_to_node(
15f218
     config_dict = booth_conf.read_configs(reporter, skip_wrong_config)
15f218
     if not config_dict:
15f218
         return
15f218
+
15f218
+    reporter.process(reports.booth_config_distribution_started())
15f218
+
15f218
     file_list = []
15f218
     for config, config_data in sorted(config_dict.items()):
15f218
         try:
15f218
@@ -145,7 +148,6 @@ def send_all_config_to_node(
15f218
     if rewrite_existing:
15f218
         data.append(("rewrite_existing", "1"))
15f218
 
15f218
-    reporter.process(reports.booth_sending_local_configs_to_node(node.label))
15f218
     try:
15f218
         response = json.loads(communicator.call_node(
15f218
             node,
15f218
@@ -165,12 +167,12 @@ def send_all_config_to_node(
15f218
                 node.label
15f218
             ))
15f218
         for file, reason in response["failed"].items():
15f218
-            report_list.append(reports.booth_config_not_saved(
15f218
+            report_list.append(reports.booth_config_distribution_node_error(
15f218
                 node.label, reason, file
15f218
             ))
15f218
         reporter.process_list(report_list)
15f218
         reporter.process(
15f218
-            reports.booth_config_saved(node.label, response["saved"])
15f218
+            reports.booth_config_accepted_by_node(node.label, response["saved"])
15f218
         )
15f218
     except NodeCommunicationException as e:
15f218
         raise LibraryError(node_communicator_exception_to_report_item(e))
15f218
diff --git a/pcs/lib/booth/test/test_config_exchange.py b/pcs/lib/booth/test/test_config_exchange.py
15f218
index a9a40ce..eb1885c 100644
15f218
--- a/pcs/lib/booth/test/test_config_exchange.py
15f218
+++ b/pcs/lib/booth/test/test_config_exchange.py
15f218
@@ -17,47 +17,35 @@ class FromExchangeFormatTest(TestCase):
15f218
                 config_structure.ConfigItem("site", "2.2.2.2"),
15f218
                 config_structure.ConfigItem("arbitrator", "3.3.3.3"),
15f218
                 config_structure.ConfigItem("ticket", "TA"),
15f218
-                config_structure.ConfigItem("ticket", "TB"),
15f218
+                config_structure.ConfigItem("ticket", "TB", [
15f218
+                    config_structure.ConfigItem("expire", "10")
15f218
+                ]),
15f218
             ],
15f218
-            config_exchange.from_exchange_format(
15f218
-                {
15f218
-                    "sites": ["1.1.1.1", "2.2.2.2"],
15f218
-                    "arbitrators": ["3.3.3.3"],
15f218
-                    "tickets": ["TA", "TB"],
15f218
-                    "authfile": "/path/to/auth.file",
15f218
-                },
15f218
-            )
15f218
+            config_exchange.from_exchange_format([
15f218
+                {"key": "authfile","value": "/path/to/auth.file","details": []},
15f218
+                {"key": "site", "value": "1.1.1.1", "details": []},
15f218
+                {"key": "site", "value": "2.2.2.2", "details": []},
15f218
+                {"key": "arbitrator", "value": "3.3.3.3", "details": []},
15f218
+                {"key": "ticket", "value": "TA", "details": []},
15f218
+                {"key": "ticket", "value": "TB", "details": [
15f218
+                    {"key": "expire", "value": "10", "details": []}
15f218
+                ]},
15f218
+            ])
15f218
         )
15f218
 
15f218
 
15f218
 class GetExchenageFormatTest(TestCase):
15f218
     def test_convert_parsed_config_to_exchange_format(self):
15f218
         self.assertEqual(
15f218
-            {
15f218
-                "sites": ["1.1.1.1", "2.2.2.2"],
15f218
-                "arbitrators": ["3.3.3.3"],
15f218
-                "tickets": ["TA", "TB"],
15f218
-                "authfile": "/path/to/auth.file",
15f218
-            },
15f218
-            config_exchange.to_exchange_format([
15f218
-                config_structure.ConfigItem("site", "1.1.1.1"),
15f218
-                config_structure.ConfigItem("site", "2.2.2.2"),
15f218
-                config_structure.ConfigItem("arbitrator", "3.3.3.3"),
15f218
-                config_structure.ConfigItem("authfile", "/path/to/auth.file"),
15f218
-                config_structure.ConfigItem("ticket", "TA"),
15f218
-                config_structure.ConfigItem("ticket", "TB", [
15f218
-                    config_structure.ConfigItem("timeout", "10")
15f218
-                ]),
15f218
-            ])
15f218
-        )
15f218
-
15f218
-    def test_convert_parsed_config_to_exchange_format_without_authfile(self):
15f218
-        self.assertEqual(
15f218
-            {
15f218
-                "sites": ["1.1.1.1", "2.2.2.2"],
15f218
-                "arbitrators": ["3.3.3.3"],
15f218
-                "tickets": ["TA", "TB"],
15f218
-            },
15f218
+            [
15f218
+                {"key": "site", "value": "1.1.1.1", "details": []},
15f218
+                {"key": "site", "value": "2.2.2.2", "details": []},
15f218
+                {"key": "arbitrator", "value": "3.3.3.3", "details": []},
15f218
+                {"key": "ticket", "value": "TA", "details": []},
15f218
+                {"key": "ticket", "value": "TB", "details": [
15f218
+                    {"key": "timeout", "value": "10", "details": []}
15f218
+                ]},
15f218
+            ],
15f218
             config_exchange.to_exchange_format([
15f218
                 config_structure.ConfigItem("site", "1.1.1.1"),
15f218
                 config_structure.ConfigItem("site", "2.2.2.2"),
15f218
diff --git a/pcs/lib/booth/test/test_config_files.py b/pcs/lib/booth/test/test_config_files.py
15f218
index 2d4c3ea..8266cac 100644
15f218
--- a/pcs/lib/booth/test/test_config_files.py
15f218
+++ b/pcs/lib/booth/test/test_config_files.py
15f218
@@ -5,7 +5,7 @@ from __future__ import (
15f218
     unicode_literals,
15f218
 )
15f218
 
15f218
-from os.path import join
15f218
+import os.path
15f218
 from unittest import TestCase
15f218
 
15f218
 from pcs.common import report_codes, env_file_role_codes as file_roles
15f218
@@ -21,20 +21,38 @@ def patch_config_files(target, *args, **kwargs):
15f218
         "pcs.lib.booth.config_files.{0}".format(target), *args, **kwargs
15f218
     )
15f218
 
15f218
+@mock.patch("os.path.isdir")
15f218
 @mock.patch("os.listdir")
15f218
 @mock.patch("os.path.isfile")
15f218
 class GetAllConfigsFileNamesTest(TestCase):
15f218
-    def test_success(self, mock_is_file, mock_listdir):
15f218
+    def test_booth_config_dir_is_no_dir(
15f218
+        self, mock_is_file, mock_listdir, mock_isdir
15f218
+    ):
15f218
+        mock_isdir.return_value = False
15f218
+        self.assertEqual([], config_files.get_all_configs_file_names())
15f218
+        mock_isdir.assert_called_once_with(BOOTH_CONFIG_DIR)
15f218
+        self.assertEqual(0, mock_is_file.call_count)
15f218
+        self.assertEqual(0, mock_listdir.call_count)
15f218
+
15f218
+    def test_success(self, mock_is_file, mock_listdir, mock_isdir):
15f218
         def mock_is_file_fn(file_name):
15f218
-            if file_name in ["dir.cong", "dir"]:
15f218
+            if file_name in [
15f218
+                os.path.join(BOOTH_CONFIG_DIR, name)
15f218
+                for name in ("dir.cong", "dir")
15f218
+            ]:
15f218
                 return False
15f218
             elif file_name in [
15f218
-                "name1", "name2.conf", "name.conf.conf", ".conf", "name3.conf"
15f218
+                os.path.join(BOOTH_CONFIG_DIR, name)
15f218
+                for name in (
15f218
+                    "name1", "name2.conf", "name.conf.conf", ".conf",
15f218
+                    "name3.conf"
15f218
+                )
15f218
             ]:
15f218
                 return True
15f218
             else:
15f218
                 raise AssertionError("unexpected input")
15f218
 
15f218
+        mock_isdir.return_value = True
15f218
         mock_is_file.side_effect = mock_is_file_fn
15f218
         mock_listdir.return_value = [
15f218
             "name1", "name2.conf", "name.conf.conf", ".conf", "name3.conf",
15f218
@@ -59,7 +77,7 @@ class ReadConfigTest(TestCase):
15f218
 
15f218
         self.assertEqual(
15f218
             [
15f218
-                mock.call(join(BOOTH_CONFIG_DIR, "my-file.conf"), "r"),
15f218
+                mock.call(os.path.join(BOOTH_CONFIG_DIR, "my-file.conf"), "r"),
15f218
                 mock.call().__enter__(),
15f218
                 mock.call().read(),
15f218
                 mock.call().__exit__(None, None, None)
15f218
@@ -193,7 +211,7 @@ class ReadAuthfileTest(TestCase):
15f218
         self.maxDiff = None
15f218
 
15f218
     def test_success(self):
15f218
-        path = join(BOOTH_CONFIG_DIR, "file.key")
15f218
+        path = os.path.join(BOOTH_CONFIG_DIR, "file.key")
15f218
         mock_open = mock.mock_open(read_data="key")
15f218
 
15f218
         with patch_config_files("open", mock_open, create=True):
15f218
@@ -248,7 +266,7 @@ class ReadAuthfileTest(TestCase):
15f218
 
15f218
     @patch_config_files("format_environment_error", return_value="reason")
15f218
     def test_read_failure(self, _):
15f218
-        path = join(BOOTH_CONFIG_DIR, "file.key")
15f218
+        path = os.path.join(BOOTH_CONFIG_DIR, "file.key")
15f218
         mock_open = mock.mock_open()
15f218
         mock_open().read.side_effect = EnvironmentError()
15f218
 
15f218
diff --git a/pcs/lib/booth/test/test_config_parser.py b/pcs/lib/booth/test/test_config_parser.py
15f218
index 684fc79..c04f451 100644
15f218
--- a/pcs/lib/booth/test/test_config_parser.py
15f218
+++ b/pcs/lib/booth/test/test_config_parser.py
15f218
@@ -24,6 +24,7 @@ class BuildTest(TestCase):
15f218
                 'ticket = "TA"',
15f218
                 'ticket = "TB"',
15f218
                 "  timeout = 10",
15f218
+                "", #newline at the end
15f218
             ]),
15f218
             config_parser.build([
15f218
                 ConfigItem("authfile", "/path/to/auth.file"),
15f218
@@ -105,6 +106,7 @@ class ParseRawLinesTest(TestCase):
15f218
                 "arbitrator=3.3.3.3",
15f218
                 "syntactically_correct = nonsense",
15f218
                 "line-with = hash#literal",
15f218
+                "",
15f218
             ]))
15f218
         )
15f218
 
15f218
diff --git a/pcs/lib/booth/test/test_config_structure.py b/pcs/lib/booth/test/test_config_structure.py
15f218
index 27faca5..1dd07cb 100644
15f218
--- a/pcs/lib/booth/test/test_config_structure.py
15f218
+++ b/pcs/lib/booth/test/test_config_structure.py
15f218
@@ -47,6 +47,46 @@ class ValidateTicketUniqueTest(TestCase):
15f218
     def test_do_not_raises_when_no_duplicated_ticket(self):
15f218
         config_structure.validate_ticket_unique([], "A")
15f218
 
15f218
+class ValidateTicketOptionsTest(TestCase):
15f218
+    def test_raises_on_invalid_options(self):
15f218
+        assert_raise_library_error(
15f218
+            lambda: config_structure.validate_ticket_options({
15f218
+                "site": "a",
15f218
+                "port": "b",
15f218
+                "timeout": " ",
15f218
+            }),
15f218
+            (
15f218
+                severities.ERROR,
15f218
+                report_codes.INVALID_OPTION,
15f218
+                {
15f218
+                    "option_name": "site",
15f218
+                    "option_type": "booth ticket",
15f218
+                    "allowed": list(config_structure.TICKET_KEYS),
15f218
+                },
15f218
+            ),
15f218
+            (
15f218
+                severities.ERROR,
15f218
+                report_codes.INVALID_OPTION,
15f218
+                {
15f218
+                    "option_name": "port",
15f218
+                    "option_type": "booth ticket",
15f218
+                    "allowed": list(config_structure.TICKET_KEYS),
15f218
+                },
15f218
+            ),
15f218
+            (
15f218
+                severities.ERROR,
15f218
+                report_codes.INVALID_OPTION_VALUE,
15f218
+                {
15f218
+                    "option_name": "timeout",
15f218
+                    "option_value": " ",
15f218
+                    "allowed_values": "no-empty",
15f218
+                },
15f218
+            ),
15f218
+        )
15f218
+
15f218
+    def test_success_on_valid_options(self):
15f218
+        config_structure.validate_ticket_options({"timeout": "10"})
15f218
+
15f218
 class TicketExistsTest(TestCase):
15f218
     def test_returns_true_if_ticket_in_structure(self):
15f218
         self.assertTrue(config_structure.ticket_exists(
15f218
@@ -183,10 +223,14 @@ class AddTicketTest(TestCase):
15f218
             config_structure.ConfigItem("ticket", "some-ticket"),
15f218
         ]
15f218
         self.assertEqual(
15f218
-            config_structure.add_ticket(configuration, "new-ticket"),
15f218
+            config_structure.add_ticket(configuration, "new-ticket", {
15f218
+                "timeout": "10",
15f218
+            }),
15f218
             [
15f218
                 config_structure.ConfigItem("ticket", "some-ticket"),
15f218
-                config_structure.ConfigItem("ticket", "new-ticket"),
15f218
+                config_structure.ConfigItem("ticket", "new-ticket", [
15f218
+                    config_structure.ConfigItem("timeout", "10"),
15f218
+                ]),
15f218
             ],
15f218
         )
15f218
 
15f218
@@ -222,3 +266,21 @@ class SetAuthfileTest(TestCase):
15f218
                 "/path/to/auth.file"
15f218
             )
15f218
         )
15f218
+
15f218
+class TakePeersTest(TestCase):
15f218
+    def test_returns_site_list_and_arbitrators_list(self):
15f218
+        self.assertEqual(
15f218
+            (
15f218
+                ["1.1.1.1", "2.2.2.2", "3.3.3.3"],
15f218
+                ["4.4.4.4", "5.5.5.5"]
15f218
+            ),
15f218
+            config_structure.take_peers(
15f218
+                [
15f218
+                    config_structure.ConfigItem("site", "1.1.1.1"),
15f218
+                    config_structure.ConfigItem("site", "2.2.2.2"),
15f218
+                    config_structure.ConfigItem("site", "3.3.3.3"),
15f218
+                    config_structure.ConfigItem("arbitrator", "4.4.4.4"),
15f218
+                    config_structure.ConfigItem("arbitrator", "5.5.5.5"),
15f218
+                ],
15f218
+            )
15f218
+        )
15f218
diff --git a/pcs/lib/booth/test/test_resource.py b/pcs/lib/booth/test/test_resource.py
15f218
index 440ddde..dd72c1e 100644
15f218
--- a/pcs/lib/booth/test/test_resource.py
15f218
+++ b/pcs/lib/booth/test/test_resource.py
15f218
@@ -11,6 +11,7 @@ from lxml import etree
15f218
 
15f218
 import pcs.lib.booth.resource as booth_resource
15f218
 from pcs.test.tools.pcs_mock import mock
15f218
+from pcs.test.tools.misc import get_test_resource as rc
15f218
 
15f218
 
15f218
 def fixture_resources_with_booth(booth_config_file_path):
15f218
@@ -85,73 +86,24 @@ class FindBoothResourceElementsTest(TestCase):
15f218
         )
15f218
 
15f218
 class RemoveFromClusterTest(TestCase):
15f218
-    def call(self, resources_section, remove_multiple=False):
15f218
+    def call(self, element_list):
15f218
         mock_resource_remove = mock.Mock()
15f218
-        num_of_removed_booth_resources = booth_resource.get_remover(
15f218
-            mock_resource_remove
15f218
-        )(
15f218
-            resources_section,
15f218
-            "/PATH/TO/CONF",
15f218
-            remove_multiple,
15f218
-        )
15f218
-        return (
15f218
-            mock_resource_remove,
15f218
-            num_of_removed_booth_resources
15f218
-        )
15f218
-
15f218
-    def fixture_resources_including_two_booths(self):
15f218
-        resources_section = etree.fromstring('<resources/>')
15f218
-        first = fixture_booth_element("first", "/PATH/TO/CONF")
15f218
-        second = fixture_booth_element("second", "/PATH/TO/CONF")
15f218
-        resources_section.append(first)
15f218
-        resources_section.append(second)
15f218
-        return resources_section
15f218
-
15f218
-    def test_raises_when_booth_resource_not_found(self):
15f218
-        self.assertRaises(
15f218
-            booth_resource.BoothNotFoundInCib,
15f218
-            lambda: self.call(etree.fromstring('<resources/>')),
15f218
-        )
15f218
-
15f218
-    def test_raises_when_more_booth_resources_found(self):
15f218
-        resources_section = self.fixture_resources_including_two_booths()
15f218
-        self.assertRaises(
15f218
-            booth_resource.BoothMultipleOccurenceFoundInCib,
15f218
-            lambda: self.call(resources_section),
15f218
-        )
15f218
-
15f218
-    def test_returns_number_of_removed_elements(self):
15f218
-        resources_section = self.fixture_resources_including_two_booths()
15f218
-        mock_resource_remove, num_of_removed_booth_resources = self.call(
15f218
-            resources_section,
15f218
-            remove_multiple=True
15f218
-        )
15f218
-        self.assertEqual(num_of_removed_booth_resources, 2)
15f218
-        self.assertEqual(
15f218
-            mock_resource_remove.mock_calls, [
15f218
-                mock.call('first'),
15f218
-                mock.call('second'),
15f218
-            ]
15f218
-        )
15f218
+        booth_resource.get_remover(mock_resource_remove)(element_list)
15f218
+        return mock_resource_remove
15f218
 
15f218
     def test_remove_ip_when_is_only_booth_sibling_in_group(self):
15f218
-        resources_section = etree.fromstring('''
15f218
-            <resources>
15f218
-                <group>
15f218
-                    <primitive id="ip" type="IPaddr2"/>
15f218
-                    <primitive id="booth" type="booth-site">
15f218
-                        <instance_attributes>
15f218
-                            <nvpair name="config" value="/PATH/TO/CONF"/>
15f218
-                        </instance_attributes>
15f218
-                    </primitive>
15f218
-                </group>
15f218
-            </resources>
15f218
+        group = etree.fromstring('''
15f218
+            <group>
15f218
+                <primitive id="ip" type="IPaddr2"/>
15f218
+                <primitive id="booth" type="booth-site">
15f218
+                    <instance_attributes>
15f218
+                        <nvpair name="config" value="/PATH/TO/CONF"/>
15f218
+                    </instance_attributes>
15f218
+                </primitive>
15f218
+            </group>
15f218
         ''')
15f218
 
15f218
-        mock_resource_remove, _ = self.call(
15f218
-            resources_section,
15f218
-            remove_multiple=True
15f218
-        )
15f218
+        mock_resource_remove = self.call(group.getchildren()[1:])
15f218
         self.assertEqual(
15f218
             mock_resource_remove.mock_calls, [
15f218
                 mock.call('ip'),
15f218
@@ -159,6 +111,41 @@ class RemoveFromClusterTest(TestCase):
15f218
             ]
15f218
         )
15f218
 
15f218
+class CreateInClusterTest(TestCase):
15f218
+    def test_remove_ip_when_booth_resource_add_failed(self):
15f218
+        mock_resource_create = mock.Mock(side_effect=[None, SystemExit(1)])
15f218
+        mock_resource_remove = mock.Mock()
15f218
+        mock_create_id = mock.Mock(side_effect=["ip_id","booth_id","group_id"])
15f218
+        ip = "1.2.3.4"
15f218
+        booth_config_file_path = rc("/path/to/booth.conf")
15f218
+
15f218
+        booth_resource.get_creator(mock_resource_create, mock_resource_remove)(
15f218
+            ip,
15f218
+            booth_config_file_path,
15f218
+            mock_create_id
15f218
+        )
15f218
+        self.assertEqual(mock_resource_create.mock_calls, [
15f218
+            mock.call(
15f218
+                clone_opts=[],
15f218
+                group=u'group_id',
15f218
+                meta_values=[],
15f218
+                op_values=[],
15f218
+                ra_id=u'ip_id',
15f218
+                ra_type=u'ocf:heartbeat:IPaddr2',
15f218
+                ra_values=[u'ip=1.2.3.4'],
15f218
+            ),
15f218
+            mock.call(
15f218
+                clone_opts=[],
15f218
+                group='group_id',
15f218
+                meta_values=[],
15f218
+                op_values=[],
15f218
+                ra_id='booth_id',
15f218
+                ra_type='ocf:pacemaker:booth-site',
15f218
+                ra_values=['config=/path/to/booth.conf'],
15f218
+            )
15f218
+        ])
15f218
+        mock_resource_remove.assert_called_once_with("ip_id")
15f218
+
15f218
 
15f218
 class FindBindedIpTest(TestCase):
15f218
     def fixture_resource_section(self, ip_element_list):
15f218
diff --git a/pcs/lib/booth/test/test_sync.py b/pcs/lib/booth/test/test_sync.py
15f218
index 58500cc..9ba6e80 100644
15f218
--- a/pcs/lib/booth/test/test_sync.py
15f218
+++ b/pcs/lib/booth/test/test_sync.py
15f218
@@ -74,7 +74,7 @@ class SetConfigOnNodeTest(TestCase):
15f218
             self.mock_rep.report_item_list,
15f218
             [(
15f218
                 Severities.INFO,
15f218
-                report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                 {
15f218
                     "node": self.node.label,
15f218
                     "name": "cfg_name",
15f218
@@ -104,7 +104,7 @@ class SetConfigOnNodeTest(TestCase):
15f218
             self.mock_rep.report_item_list,
15f218
             [(
15f218
                 Severities.INFO,
15f218
-                report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                 {
15f218
                     "node": self.node.label,
15f218
                     "name": "cfg_name",
15f218
@@ -175,8 +175,8 @@ class SyncConfigInCluster(TestCase):
15f218
             self.mock_reporter.report_item_list,
15f218
             [(
15f218
                 Severities.INFO,
15f218
-                report_codes.BOOTH_DISTRIBUTING_CONFIG,
15f218
-                {"name": "cfg_name"}
15f218
+                report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                {}
15f218
             )]
15f218
         )
15f218
 
15f218
@@ -213,8 +213,8 @@ class SyncConfigInCluster(TestCase):
15f218
             self.mock_reporter.report_item_list,
15f218
             [(
15f218
                 Severities.INFO,
15f218
-                report_codes.BOOTH_DISTRIBUTING_CONFIG,
15f218
-                {"name": "cfg_name"}
15f218
+                report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                {}
15f218
             )]
15f218
         )
15f218
 
15f218
@@ -252,8 +252,8 @@ class SyncConfigInCluster(TestCase):
15f218
             self.mock_reporter.report_item_list,
15f218
             [(
15f218
                 Severities.INFO,
15f218
-                report_codes.BOOTH_DISTRIBUTING_CONFIG,
15f218
-                {"name": "cfg_name"}
15f218
+                report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                {}
15f218
             )]
15f218
         )
15f218
 
15f218
@@ -375,12 +375,12 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             [
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVING_ON_NODE,
15f218
-                    {"node": self.node.label}
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                    {}
15f218
                 ),
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                    report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                     {
15f218
                         "node": self.node.label,
15f218
                         "name": "name1.conf, file1.key, name2.conf, file2.key",
15f218
@@ -489,8 +489,8 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             [
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVING_ON_NODE,
15f218
-                    {"node": self.node.label}
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                    {}
15f218
                 ),
15f218
                 (
15f218
                     Severities.ERROR,
15f218
@@ -593,8 +593,8 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             [
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVING_ON_NODE,
15f218
-                    {"node": self.node.label}
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                    {}
15f218
                 ),
15f218
                 (
15f218
                     Severities.WARNING,
15f218
@@ -616,7 +616,7 @@ class SendAllConfigToNodeTest(TestCase):
15f218
                 ),
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                    report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                     {
15f218
                         "node": self.node.label,
15f218
                         "name": "name2.conf, file2.key",
15f218
@@ -652,7 +652,7 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             ),
15f218
             (
15f218
                 Severities.ERROR,
15f218
-                report_codes.BOOTH_CONFIG_WRITE_ERROR,
15f218
+                report_codes.BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR,
15f218
                 {
15f218
                     "node": self.node.label,
15f218
                     "name": "name1.conf",
15f218
@@ -661,7 +661,7 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             ),
15f218
             (
15f218
                 Severities.ERROR,
15f218
-                report_codes.BOOTH_CONFIG_WRITE_ERROR,
15f218
+                report_codes.BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR,
15f218
                 {
15f218
                     "node": self.node.label,
15f218
                     "name": "file1.key",
15f218
@@ -724,12 +724,12 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             [
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVING_ON_NODE,
15f218
-                    {"node": self.node.label}
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                    {}
15f218
                 ),
15f218
                 (
15f218
                     Severities.ERROR,
15f218
-                    report_codes.BOOTH_CONFIG_WRITE_ERROR,
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR,
15f218
                     {
15f218
                         "node": self.node.label,
15f218
                         "name": "name1.conf",
15f218
@@ -738,7 +738,7 @@ class SendAllConfigToNodeTest(TestCase):
15f218
                 ),
15f218
                 (
15f218
                     Severities.ERROR,
15f218
-                    report_codes.BOOTH_CONFIG_WRITE_ERROR,
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR,
15f218
                     {
15f218
                         "node": self.node.label,
15f218
                         "name": "file1.key",
15f218
@@ -1058,12 +1058,12 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             [
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVING_ON_NODE,
15f218
-                    {"node": self.node.label}
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                    {}
15f218
                 ),
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                    report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                     {
15f218
                         "node": self.node.label,
15f218
                         "name": "name1.conf, name2.conf, file2.key",
15f218
@@ -1143,8 +1143,8 @@ class SendAllConfigToNodeTest(TestCase):
15f218
             [
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVING_ON_NODE,
15f218
-                    {"node": self.node.label}
15f218
+                    report_codes.BOOTH_CONFIG_DISTRIBUTION_STARTED,
15f218
+                    {}
15f218
                 ),
15f218
                 (
15f218
                     Severities.WARNING,
15f218
@@ -1155,7 +1155,7 @@ class SendAllConfigToNodeTest(TestCase):
15f218
                 ),
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                    report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                     {
15f218
                         "node": self.node.label,
15f218
                         "name": "name2.conf, file2.key",
15f218
diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py
15f218
index 43ea9dd..7a3d348 100644
15f218
--- a/pcs/lib/commands/booth.py
15f218
+++ b/pcs/lib/commands/booth.py
15f218
@@ -34,11 +34,10 @@ def config_setup(env, booth_configuration, overwrite_existing=False):
15f218
     list arbitrator_list contains arbitrator adresses of multisite
15f218
     """
15f218
 
15f218
+    config_content = config_exchange.from_exchange_format(booth_configuration)
15f218
     config_structure.validate_peers(
15f218
-        booth_configuration.get("sites", []),
15f218
-        booth_configuration.get("arbitrators", [])
15f218
+        *config_structure.take_peers(config_content)
15f218
     )
15f218
-    config_content = config_exchange.from_exchange_format(booth_configuration)
15f218
 
15f218
     env.booth.create_key(config_files.generate_key(), overwrite_existing)
15f218
     config_content = config_structure.set_authfile(
15f218
@@ -99,21 +98,34 @@ def config_destroy(env, ignore_config_load_problems=False):
15f218
         env.booth.remove_key()
15f218
     env.booth.remove_config()
15f218
 
15f218
-def config_show(env):
15f218
+
15f218
+def config_text(env, name, node_name=None):
15f218
     """
15f218
-    return configuration as tuple of sites list and arbitrators list
15f218
+    get configuration in raw format
15f218
+    string name -- name of booth instance whose config should be returned
15f218
+    string node_name -- get the config from specified node or local host if None
15f218
     """
15f218
-    return config_exchange.to_exchange_format(
15f218
-        parse(env.booth.get_config_content())
15f218
+    if node_name is None:
15f218
+        # TODO add name support
15f218
+        return env.booth.get_config_content()
15f218
+
15f218
+    remote_data = sync.pull_config_from_node(
15f218
+        env.node_communicator(), NodeAddresses(node_name), name
15f218
     )
15f218
+    try:
15f218
+        return remote_data["config"]["data"]
15f218
+    except KeyError:
15f218
+        raise LibraryError(reports.invalid_response_format(node_name))
15f218
 
15f218
-def config_ticket_add(env, ticket_name):
15f218
+
15f218
+def config_ticket_add(env, ticket_name, options):
15f218
     """
15f218
     add ticket to booth configuration
15f218
     """
15f218
     booth_configuration = config_structure.add_ticket(
15f218
         parse(env.booth.get_config_content()),
15f218
-        ticket_name
15f218
+        ticket_name,
15f218
+        options,
15f218
     )
15f218
     env.booth.push_config(build(booth_configuration))
15f218
 
15f218
@@ -127,7 +139,7 @@ def config_ticket_remove(env, ticket_name):
15f218
     )
15f218
     env.booth.push_config(build(booth_configuration))
15f218
 
15f218
-def create_in_cluster(env, name, ip, resource_create):
15f218
+def create_in_cluster(env, name, ip, resource_create, resource_remove):
15f218
     #TODO resource_create is provisional hack until resources are not moved to
15f218
     #lib
15f218
     resources_section = get_resources(env.get_cib())
15f218
@@ -136,7 +148,7 @@ def create_in_cluster(env, name, ip, resource_create):
15f218
     if resource.find_for_config(resources_section, booth_config_file_path):
15f218
         raise LibraryError(booth_reports.booth_already_in_cib(name))
15f218
 
15f218
-    resource.get_creator(resource_create)(
15f218
+    resource.get_creator(resource_create, resource_remove)(
15f218
         ip,
15f218
         booth_config_file_path,
15f218
         create_id = partial(
15f218
@@ -146,25 +158,20 @@ def create_in_cluster(env, name, ip, resource_create):
15f218
         )
15f218
     )
15f218
 
15f218
-def remove_from_cluster(env, name, resource_remove):
15f218
+def remove_from_cluster(env, name, resource_remove, allow_remove_multiple):
15f218
     #TODO resource_remove is provisional hack until resources are not moved to
15f218
     #lib
15f218
-    try:
15f218
-        num_of_removed_booth_resources = resource.get_remover(resource_remove)(
15f218
-            get_resources(env.get_cib()),
15f218
-            get_config_file_name(name),
15f218
-        )
15f218
-        if num_of_removed_booth_resources > 1:
15f218
-            env.report_processor.process(
15f218
-                booth_reports.booth_multiple_times_in_cib(
15f218
-                    name,
15f218
-                    severity=ReportItemSeverity.WARNING,
15f218
-                )
15f218
-            )
15f218
-    except resource.BoothNotFoundInCib:
15f218
-        raise LibraryError(booth_reports.booth_not_exists_in_cib(name))
15f218
-    except resource.BoothMultipleOccurenceFoundInCib:
15f218
-        raise LibraryError(booth_reports.booth_multiple_times_in_cib(name))
15f218
+    resource.get_remover(resource_remove)(
15f218
+        _find_resource_elements_for_operation(env, name, allow_remove_multiple)
15f218
+    )
15f218
+
15f218
+def restart(env, name, resource_restart, allow_multiple):
15f218
+    #TODO resource_restart is provisional hack until resources are not moved to
15f218
+    #lib
15f218
+    for booth_element in _find_resource_elements_for_operation(
15f218
+        env, name, allow_multiple
15f218
+    ):
15f218
+        resource_restart([booth_element.attrib["id"]])
15f218
 
15f218
 def ticket_operation(operation, env, name, ticket, site_ip):
15f218
     if not site_ip:
15f218
@@ -314,7 +321,7 @@ def pull_config(env, node_name, name):
15f218
     name -- string, name of booth instance of which config should be fetched
15f218
     """
15f218
     env.report_processor.process(
15f218
-        booth_reports.booth_fetching_config_from_node(node_name, name)
15f218
+        booth_reports.booth_fetching_config_from_node_started(node_name, name)
15f218
     )
15f218
     output = sync.pull_config_from_node(
15f218
         env.node_communicator(), NodeAddresses(node_name), name
15f218
@@ -335,7 +342,7 @@ def pull_config(env, node_name, name):
15f218
                 True
15f218
             )
15f218
         env.report_processor.process(
15f218
-            booth_reports.booth_config_saved(name_list=[name])
15f218
+            booth_reports.booth_config_accepted_by_node(name_list=[name])
15f218
         )
15f218
     except KeyError:
15f218
         raise LibraryError(reports.invalid_response_format(node_name))
15f218
@@ -347,3 +354,24 @@ def get_status(env, name=None):
15f218
         "ticket": status.get_tickets_status(env.cmd_runner(), name),
15f218
         "peers": status.get_peers_status(env.cmd_runner(), name),
15f218
     }
15f218
+
15f218
+def _find_resource_elements_for_operation(env, name, allow_multiple):
15f218
+    booth_element_list = resource.find_for_config(
15f218
+        get_resources(env.get_cib()),
15f218
+        get_config_file_name(name),
15f218
+    )
15f218
+
15f218
+    if not booth_element_list:
15f218
+        raise LibraryError(booth_reports.booth_not_exists_in_cib(name))
15f218
+
15f218
+    if len(booth_element_list) > 1:
15f218
+        if not allow_multiple:
15f218
+            raise LibraryError(booth_reports.booth_multiple_times_in_cib(name))
15f218
+        env.report_processor.process(
15f218
+            booth_reports.booth_multiple_times_in_cib(
15f218
+                name,
15f218
+                severity=ReportItemSeverity.WARNING,
15f218
+            )
15f218
+        )
15f218
+
15f218
+    return booth_element_list
15f218
diff --git a/pcs/lib/commands/test/test_booth.py b/pcs/lib/commands/test/test_booth.py
15f218
index 20bf06a..d2429b6 100644
15f218
--- a/pcs/lib/commands/test/test_booth.py
15f218
+++ b/pcs/lib/commands/test/test_booth.py
15f218
@@ -19,7 +19,6 @@ from pcs.test.tools.assertions import (
15f218
 
15f218
 from pcs import settings
15f218
 from pcs.common import report_codes
15f218
-from pcs.lib.booth import resource as booth_resource
15f218
 from pcs.lib.env import LibraryEnvironment
15f218
 from pcs.lib.node import NodeAddresses
15f218
 from pcs.lib.errors import LibraryError, ReportItemSeverity as Severities
15f218
@@ -48,10 +47,10 @@ class ConfigSetupTest(TestCase):
15f218
         env = mock.MagicMock()
15f218
         commands.config_setup(
15f218
             env,
15f218
-            booth_configuration={
15f218
-                "sites": ["1.1.1.1"],
15f218
-                "arbitrators": ["2.2.2.2"],
15f218
-            },
15f218
+            booth_configuration=[
15f218
+                {"key": "site", "value": "1.1.1.1", "details": []},
15f218
+                {"key": "arbitrator", "value": "2.2.2.2", "details": []},
15f218
+            ],
15f218
         )
15f218
         env.booth.create_config.assert_called_once_with(
15f218
             "config content",
15f218
@@ -426,7 +425,7 @@ class PullConfigTest(TestCase):
15f218
                 ),
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                    report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                     {
15f218
                         "node": None,
15f218
                         "name": "name",
15f218
@@ -467,7 +466,7 @@ class PullConfigTest(TestCase):
15f218
                 ),
15f218
                 (
15f218
                     Severities.INFO,
15f218
-                    report_codes.BOOTH_CONFIGS_SAVED_ON_NODE,
15f218
+                    report_codes.BOOTH_CONFIG_ACCEPTED_BY_NODE,
15f218
                     {
15f218
                         "node": None,
15f218
                         "name": "name",
15f218
@@ -548,7 +547,8 @@ class CreateInClusterTest(TestCase):
15f218
     def test_raises_when_is_created_already(self):
15f218
         assert_raise_library_error(
15f218
             lambda: commands.create_in_cluster(
15f218
-                mock.MagicMock(), "somename", ip="1.2.3.4", resource_create=None
15f218
+                mock.MagicMock(), "somename", ip="1.2.3.4",
15f218
+                resource_create=None, resource_remove=None,
15f218
             ),
15f218
             (
15f218
                 Severities.ERROR,
15f218
@@ -559,14 +559,14 @@ class CreateInClusterTest(TestCase):
15f218
             ),
15f218
         )
15f218
 
15f218
-class RemoveFromClusterTest(TestCase):
15f218
-    @patch_commands("resource.get_remover", mock.Mock(return_value = mock.Mock(
15f218
-        side_effect=booth_resource.BoothNotFoundInCib()
15f218
-    )))
15f218
+class FindResourceElementsForOperationTest(TestCase):
15f218
+    @patch_commands("resource.find_for_config", mock.Mock(return_value=[]))
15f218
     def test_raises_when_no_booth_resource_found(self):
15f218
         assert_raise_library_error(
15f218
-            lambda: commands.remove_from_cluster(
15f218
-                mock.MagicMock(), "somename", resource_remove=None
15f218
+            lambda: commands._find_resource_elements_for_operation(
15f218
+                mock.MagicMock(),
15f218
+                "somename",
15f218
+                allow_multiple=False
15f218
             ),
15f218
             (
15f218
                 Severities.ERROR,
15f218
@@ -577,13 +577,15 @@ class RemoveFromClusterTest(TestCase):
15f218
             ),
15f218
         )
15f218
 
15f218
-    @patch_commands("resource.get_remover", mock.Mock(return_value = mock.Mock(
15f218
-        side_effect=booth_resource.BoothMultipleOccurenceFoundInCib()
15f218
-    )))
15f218
+    @patch_commands(
15f218
+        "resource.find_for_config", mock.Mock(return_value=["b_el1", "b_el2"])
15f218
+    )
15f218
     def test_raises_when_multiple_booth_resource_found(self):
15f218
         assert_raise_library_error(
15f218
-            lambda: commands.remove_from_cluster(
15f218
-                mock.MagicMock(), "somename", resource_remove=None
15f218
+            lambda: commands._find_resource_elements_for_operation(
15f218
+                mock.MagicMock(),
15f218
+                "somename",
15f218
+                allow_multiple=False
15f218
             ),
15f218
             (
15f218
                 Severities.ERROR,
15f218
@@ -595,15 +597,15 @@ class RemoveFromClusterTest(TestCase):
15f218
             ),
15f218
         )
15f218
 
15f218
-    @patch_commands("resource.get_remover", mock.Mock(return_value = mock.Mock(
15f218
-        return_value=2
15f218
-    )))
15f218
+    @patch_commands("get_resources", mock.Mock(return_value="resources"))
15f218
+    @patch_commands("resource.get_remover", mock.MagicMock())
15f218
+    @patch_commands("resource.find_for_config", mock.Mock(return_value=[1, 2]))
15f218
     def test_warn_when_multiple_booth_resources_removed(self):
15f218
         report_processor=MockLibraryReportProcessor()
15f218
-        commands.remove_from_cluster(
15f218
+        commands._find_resource_elements_for_operation(
15f218
             mock.MagicMock(report_processor=report_processor),
15f218
             "somename",
15f218
-            resource_remove=None
15f218
+            allow_multiple=True,
15f218
         )
15f218
         assert_report_item_list_equal(report_processor.report_item_list, [(
15f218
             Severities.WARNING,
15f218
diff --git a/pcs/pcs.8 b/pcs/pcs.8
15f218
index b3c4877..270ad2d 100644
15f218
--- a/pcs/pcs.8
15f218
+++ b/pcs/pcs.8
15f218
@@ -590,8 +590,8 @@ Add new ticket to the current configuration.
15f218
 ticket remove <ticket>
15f218
 Remove the specified ticket from the current configuration.
15f218
 .TP
15f218
-config
15f218
-Show booth configuration.
15f218
+config [<node>]
15f218
+Show booth configuration from the specified node or from the current node if node not specified.
15f218
 .TP
15f218
 create ip <address>
15f218
 Make the cluster run booth service on the specified ip address as a cluster resource.  Typically this is used to run booth site.
15f218
@@ -599,11 +599,14 @@ Make the cluster run booth service on the specified ip address as a cluster reso
15f218
 remove
15f218
 Remove booth resources created by the "pcs booth create" command.
15f218
 .TP
15f218
+restart
15f218
+Restart booth resources created by the "pcs booth create" command.
15f218
+.TP
15f218
 ticket grant <ticket> [<site address>]
15f218
-Grant the ticket for the site specified by address.  Site address which has been specified with 'pcs booth create' command is used if 'site address' is omitted.
15f218
+Grant the ticket for the site specified by address.  Site address which has been specified with 'pcs booth create' command is used if 'site address' is omitted. Cannot be run on an arbitrator.
15f218
 .TP
15f218
 ticket revoke <ticket> [<site address>]
15f218
-Revoke the ticket for the site specified by address.  Site address which has been specified with 'pcs booth create' command is used if 'site address' is omitted.
15f218
+Revoke the ticket for the site specified by address.  Site address which has been specified with 'pcs booth create' command is used if 'site address' is omitted. Cannot be run on an arbitrator.
15f218
 .TP
15f218
 status
15f218
 Print current status of booth on the local node.
15f218
diff --git a/pcs/test/test_booth.py b/pcs/test/test_booth.py
15f218
index 5ddc06d..3356e71 100644
15f218
--- a/pcs/test/test_booth.py
15f218
+++ b/pcs/test/test_booth.py
15f218
@@ -76,10 +76,10 @@ class SetupTest(BoothMixin, unittest.TestCase):
15f218
         self.assert_pcs_success(
15f218
             "booth config",
15f218
             stdout_full=console_report(
15f218
+                "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
                 "site = 1.1.1.1",
15f218
                 "site = 2.2.2.2",
15f218
                 "arbitrator = 3.3.3.3",
15f218
-                "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
             )
15f218
         )
15f218
         with open(BOOTH_KEY_FILE) as key_file:
15f218
@@ -187,13 +187,14 @@ class BoothTest(unittest.TestCase, BoothMixin):
15f218
 
15f218
 class AddTicketTest(BoothTest):
15f218
     def test_success_add_ticket(self):
15f218
-        self.assert_pcs_success("booth ticket add TicketA")
15f218
+        self.assert_pcs_success("booth ticket add TicketA expire=10")
15f218
         self.assert_pcs_success("booth config", stdout_full=console_report(
15f218
+            "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
             "site = 1.1.1.1",
15f218
             "site = 2.2.2.2",
15f218
             "arbitrator = 3.3.3.3",
15f218
-            "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
             'ticket = "TicketA"',
15f218
+            "  expire = 10",
15f218
         ))
15f218
 
15f218
     def test_fail_on_bad_ticket_name(self):
15f218
@@ -211,22 +212,33 @@ class AddTicketTest(BoothTest):
15f218
             "\n"
15f218
         )
15f218
 
15f218
+    def test_fail_on_invalid_options(self):
15f218
+        self.assert_pcs_fail(
15f218
+            "booth ticket add TicketA site=a timeout=", console_report(
15f218
+                "Error: invalid booth ticket option 'site', allowed options"
15f218
+                    " are: acquire-after, attr-prereq, before-acquire-handler,"
15f218
+                    " expire, renewal-freq, retries, timeout, weights"
15f218
+                ,
15f218
+                "Error: '' is not a valid timeout value, use no-empty",
15f218
+            )
15f218
+        )
15f218
+
15f218
 class RemoveTicketTest(BoothTest):
15f218
     def test_success_remove_ticket(self):
15f218
         self.assert_pcs_success("booth ticket add TicketA")
15f218
         self.assert_pcs_success("booth config", stdout_full=console_report(
15f218
+            "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
             "site = 1.1.1.1",
15f218
             "site = 2.2.2.2",
15f218
             "arbitrator = 3.3.3.3",
15f218
-            "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
             'ticket = "TicketA"',
15f218
         ))
15f218
         self.assert_pcs_success("booth ticket remove TicketA")
15f218
         self.assert_pcs_success("booth config", stdout_full=console_report(
15f218
+            "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
             "site = 1.1.1.1",
15f218
             "site = 2.2.2.2",
15f218
             "arbitrator = 3.3.3.3",
15f218
-            "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
         ))
15f218
 
15f218
     def test_fail_when_ticket_does_not_exist(self):
15f218
@@ -286,7 +298,6 @@ class RemoveTest(BoothTest):
15f218
             " --force to override"
15f218
         ])
15f218
 
15f218
-
15f218
     def test_remove_added_booth_configuration(self):
15f218
         self.assert_pcs_success("resource show", "NO resources configured\n")
15f218
         self.assert_pcs_success("booth create ip 192.168.122.120")
15f218
@@ -301,8 +312,27 @@ class RemoveTest(BoothTest):
15f218
         ])
15f218
         self.assert_pcs_success("resource show", "NO resources configured\n")
15f218
 
15f218
-    def test_fail_when_booth_is_not_currently_configured(self):
15f218
-        pass
15f218
+
15f218
+    def test_remove_multiple_booth_configuration(self):
15f218
+        self.assert_pcs_success("resource show", "NO resources configured\n")
15f218
+        self.assert_pcs_success("booth create ip 192.168.122.120")
15f218
+        self.assert_pcs_success(
15f218
+            "resource create some-id ocf:pacemaker:booth-site"
15f218
+            " config=/etc/booth/booth.conf"
15f218
+        )
15f218
+        self.assert_pcs_success("resource show", [
15f218
+             " Resource Group: booth-booth-group",
15f218
+             "     booth-booth-ip	(ocf::heartbeat:IPaddr2):	Stopped",
15f218
+             "     booth-booth-service	(ocf::pacemaker:booth-site):	Stopped",
15f218
+             " some-id	(ocf::pacemaker:booth-site):	Stopped",
15f218
+        ])
15f218
+        self.assert_pcs_success("booth remove --force", [
15f218
+            "Warning: found more than one booth instance 'booth' in cib",
15f218
+            "Deleting Resource - booth-booth-ip",
15f218
+            "Deleting Resource (and group) - booth-booth-service",
15f218
+            "Deleting Resource - some-id",
15f218
+        ])
15f218
+
15f218
 
15f218
 class TicketGrantTest(BoothTest):
15f218
     def test_failed_when_implicit_site_but_not_correct_confgiuration_in_cib(
15f218
@@ -332,6 +362,7 @@ class ConfigTest(unittest.TestCase, BoothMixin):
15f218
     def setUp(self):
15f218
         shutil.copy(EMPTY_CIB, TEMP_CIB)
15f218
         self.pcs_runner = PcsRunner(TEMP_CIB)
15f218
+
15f218
     def test_fail_when_config_file_do_not_exists(self):
15f218
         ensure_booth_config_not_exists()
15f218
         self.assert_pcs_fail(
15f218
@@ -340,3 +371,33 @@ class ConfigTest(unittest.TestCase, BoothMixin):
15f218
                 BOOTH_CONFIG_FILE
15f218
             )
15f218
         )
15f218
+
15f218
+    def test_too_much_args(self):
15f218
+        self.assert_pcs_fail(
15f218
+            "booth config nodename surplus",
15f218
+            stdout_start="\nUsage: pcs booth <command>\n    config ["
15f218
+        )
15f218
+
15f218
+    def test_show_unsupported_values(self):
15f218
+        ensure_booth_config_not_exists()
15f218
+        self.assert_pcs_success(
15f218
+            "booth setup sites 1.1.1.1 2.2.2.2 arbitrators 3.3.3.3"
15f218
+        )
15f218
+        with open(BOOTH_CONFIG_FILE, "a") as config_file:
15f218
+            config_file.write("some = nonsense")
15f218
+        self.assert_pcs_success("booth ticket add TicketA")
15f218
+        with open(BOOTH_CONFIG_FILE, "a") as config_file:
15f218
+            config_file.write("another = nonsense")
15f218
+
15f218
+        self.assert_pcs_success(
15f218
+            "booth config",
15f218
+            stdout_full="\n".join((
15f218
+                "authfile = {0}".format(BOOTH_KEY_FILE),
15f218
+                "site = 1.1.1.1",
15f218
+                "site = 2.2.2.2",
15f218
+                "arbitrator = 3.3.3.3",
15f218
+                "some = nonsense",
15f218
+                'ticket = "TicketA"',
15f218
+                "another = nonsense",
15f218
+            ))
15f218
+        )
15f218
diff --git a/pcs/usage.py b/pcs/usage.py
15f218
index 78e340b..088dec9 100644
15f218
--- a/pcs/usage.py
15f218
+++ b/pcs/usage.py
15f218
@@ -118,6 +118,7 @@ def generate_completion_tree_from_usage():
15f218
     tree["pcsd"] = generate_tree(pcsd([],False))
15f218
     tree["node"] = generate_tree(node([], False))
15f218
     tree["alert"] = generate_tree(alert([], False))
15f218
+    tree["booth"] = generate_tree(booth([], False))
15f218
     return tree
15f218
 
15f218
 def generate_tree(usage_txt):
15f218
@@ -1438,8 +1439,9 @@ Commands:
15f218
     ticket remove <ticket>
15f218
         Remove the specified ticket from the current configuration.
15f218
 
15f218
-    config
15f218
-        Show booth configuration.
15f218
+    config [<node>]
15f218
+        Show booth configuration from the specified node or from the current
15f218
+        node if node not specified.
15f218
 
15f218
     create ip <address>
15f218
         Make the cluster run booth service on the specified ip address as
15f218
@@ -1448,15 +1450,18 @@ Commands:
15f218
     remove
15f218
         Remove booth resources created by the "pcs booth create" command.
15f218
 
15f218
+    restart
15f218
+        Restart booth resources created by the "pcs booth create" command.
15f218
+
15f218
     ticket grant <ticket> [<site address>]
15f218
         Grant the ticket for the site specified by address.  Site address which
15f218
         has been specified with 'pcs booth create' command is used if
15f218
-        'site address' is omitted.
15f218
+        'site address' is omitted. Cannot be run on an arbitrator.
15f218
 
15f218
     ticket revoke <ticket> [<site address>]
15f218
         Revoke the ticket for the site specified by address.  Site address which
15f218
         has been specified with 'pcs booth create' command is used if
15f218
-        'site address' is omitted.
15f218
+        'site address' is omitted. Cannot be run on an arbitrator.
15f218
 
15f218
     status
15f218
         Print current status of booth on the local node.
15f218
-- 
15f218
1.8.3.1
15f218