Blame SOURCES/bz1308514-03-wider-support-for-booth-configuration-beside-mere.patch

15f218
From 8707ba13053e172d148ec12820a4259ffa371000 Mon Sep 17 00:00:00 2001
15f218
From: Ivan Devat <idevat@redhat.com>
15f218
Date: Wed, 14 Sep 2016 09:04:57 +0200
15f218
Subject: [PATCH] squash bz1308514 Wider support for booth configura
15f218
15f218
de5edd583d82 correct booth documentation (ticket grant/revoke)
15f218
15f218
d98e6b04da8d make forceable uknown booth ticket option
15f218
---
15f218
 pcs/cli/booth/command.py                    |   6 +-
15f218
 pcs/cli/booth/test/test_command.py          |   3 +-
15f218
 pcs/lib/booth/config_structure.py           |  31 ++++++--
15f218
 pcs/lib/booth/test/test_config_structure.py | 107 ++++++++++++++++++++++++----
15f218
 pcs/lib/commands/booth.py                   |   7 +-
15f218
 pcs/pcs.8                                   |   7 +-
15f218
 pcs/test/test_booth.py                      |  17 +++++
15f218
 pcs/usage.py                                |  11 +--
15f218
 8 files changed, 161 insertions(+), 28 deletions(-)
15f218
15f218
diff --git a/pcs/cli/booth/command.py b/pcs/cli/booth/command.py
15f218
index 0b71a01..72b2c73 100644
15f218
--- a/pcs/cli/booth/command.py
15f218
+++ b/pcs/cli/booth/command.py
15f218
@@ -64,7 +64,11 @@ def config_ticket_add(lib, arg_list, modifiers):
15f218
     """
15f218
     if not arg_list:
15f218
         raise CmdLineInputError
15f218
-    lib.booth.config_ticket_add(arg_list[0], prepare_options(arg_list[1:]))
15f218
+    lib.booth.config_ticket_add(
15f218
+        arg_list[0],
15f218
+        prepare_options(arg_list[1:]),
15f218
+        allow_unknown_options=modifiers["force"]
15f218
+    )
15f218
 
15f218
 def config_ticket_remove(lib, arg_list, modifiers):
15f218
     """
15f218
diff --git a/pcs/cli/booth/test/test_command.py b/pcs/cli/booth/test/test_command.py
15f218
index 44d7a12..8ba2c0e 100644
15f218
--- a/pcs/cli/booth/test/test_command.py
15f218
+++ b/pcs/cli/booth/test/test_command.py
15f218
@@ -45,9 +45,10 @@ class ConfigTicketAddTest(TestCase):
15f218
         command.config_ticket_add(
15f218
             lib,
15f218
             arg_list=["TICKET_A", "timeout=10"],
15f218
-            modifiers={}
15f218
+            modifiers={"force": True}
15f218
         )
15f218
         lib.booth.config_ticket_add.assert_called_once_with(
15f218
             "TICKET_A",
15f218
             {"timeout": "10"},
15f218
+            allow_unknown_options=True
15f218
         )
15f218
diff --git a/pcs/lib/booth/config_structure.py b/pcs/lib/booth/config_structure.py
15f218
index 8977b7a..09ff1a7 100644
15f218
--- a/pcs/lib/booth/config_structure.py
15f218
+++ b/pcs/lib/booth/config_structure.py
15f218
@@ -9,7 +9,8 @@ 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 pcs.lib.errors import LibraryError, ReportItemSeverity as severities
15f218
+from pcs.common import report_codes
15f218
 from collections import namedtuple
15f218
 
15f218
 GLOBAL_KEYS = (
15f218
@@ -83,10 +84,13 @@ 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, options):
15f218
+def add_ticket(
15f218
+    report_processor, booth_configuration, ticket_name, options,
15f218
+    allow_unknown_options
15f218
+):
15f218
     validate_ticket_name(ticket_name)
15f218
     validate_ticket_unique(booth_configuration, ticket_name)
15f218
-    validate_ticket_options(options)
15f218
+    validate_ticket_options(report_processor, options, allow_unknown_options)
15f218
     return booth_configuration + [
15f218
         ConfigItem("ticket", ticket_name, [
15f218
             ConfigItem(key, value) for key, value in options.items()
15f218
@@ -101,7 +105,7 @@ 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
+def validate_ticket_options(report_processor, options, allow_unknown_options):
15f218
     reports = []
15f218
     for key in sorted(options):
15f218
         if key in GLOBAL_KEYS:
15f218
@@ -109,6 +113,22 @@ def validate_ticket_options(options):
15f218
                 common_reports.invalid_option(key, TICKET_KEYS, "booth ticket")
15f218
             )
15f218
 
15f218
+        elif key not in TICKET_KEYS:
15f218
+            reports.append(
15f218
+                common_reports.invalid_option(
15f218
+                    key, TICKET_KEYS,
15f218
+                    "booth ticket",
15f218
+                    severity=(
15f218
+                        severities.WARNING if allow_unknown_options
15f218
+                        else severities.ERROR
15f218
+                    ),
15f218
+                    forceable=(
15f218
+                        None if allow_unknown_options
15f218
+                        else report_codes.FORCE_OPTIONS
15f218
+                    ),
15f218
+                )
15f218
+            )
15f218
+
15f218
         if not options[key].strip():
15f218
             reports.append(common_reports.invalid_option_value(
15f218
                 key,
15f218
@@ -116,8 +136,7 @@ def validate_ticket_options(options):
15f218
                 "no-empty",
15f218
             ))
15f218
 
15f218
-    if reports:
15f218
-        raise LibraryError(*reports)
15f218
+    report_processor.process_list(reports)
15f218
 
15f218
 def ticket_exists(booth_configuration, ticket_name):
15f218
     return any(
15f218
diff --git a/pcs/lib/booth/test/test_config_structure.py b/pcs/lib/booth/test/test_config_structure.py
15f218
index 5e7ac68..40618b2 100644
15f218
--- a/pcs/lib/booth/test/test_config_structure.py
15f218
+++ b/pcs/lib/booth/test/test_config_structure.py
15f218
@@ -10,7 +10,11 @@ from pcs.test.tools.pcs_unittest import TestCase
15f218
 from pcs.common import report_codes
15f218
 from pcs.lib.booth import config_structure
15f218
 from pcs.lib.errors import ReportItemSeverity as severities
15f218
-from pcs.test.tools.assertions import assert_raise_library_error
15f218
+from pcs.test.tools.assertions import (
15f218
+    assert_raise_library_error,
15f218
+    assert_report_item_list_equal,
15f218
+)
15f218
+from pcs.test.tools.custom_mock import MockLibraryReportProcessor
15f218
 from pcs.test.tools.pcs_unittest import mock
15f218
 
15f218
 
15f218
@@ -49,12 +53,8 @@ class ValidateTicketUniqueTest(TestCase):
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
+        report_processor = MockLibraryReportProcessor()
15f218
+        expected_errors = [
15f218
             (
15f218
                 severities.ERROR,
15f218
                 report_codes.INVALID_OPTION,
15f218
@@ -82,10 +82,81 @@ class ValidateTicketOptionsTest(TestCase):
15f218
                     "allowed_values": "no-empty",
15f218
                 },
15f218
             ),
15f218
+            (
15f218
+                severities.ERROR,
15f218
+                report_codes.INVALID_OPTION,
15f218
+                {
15f218
+                    "option_name": "unknown",
15f218
+                    "option_type": "booth ticket",
15f218
+                    "allowed": list(config_structure.TICKET_KEYS),
15f218
+                },
15f218
+                report_codes.FORCE_OPTIONS
15f218
+            ),
15f218
+        ]
15f218
+        assert_raise_library_error(
15f218
+            lambda: config_structure.validate_ticket_options(
15f218
+                report_processor,
15f218
+                {
15f218
+                    "site": "a",
15f218
+                    "port": "b",
15f218
+                    "timeout": " ",
15f218
+                    "unknown": "c",
15f218
+                },
15f218
+                allow_unknown_options=False,
15f218
+            ),
15f218
+            *expected_errors
15f218
+        )
15f218
+        assert_report_item_list_equal(
15f218
+            report_processor.report_item_list,
15f218
+            expected_errors
15f218
+        )
15f218
+
15f218
+    def test_unknown_options_are_forceable(self):
15f218
+        report_processor = MockLibraryReportProcessor()
15f218
+        expected_errors = [
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
+        assert_raise_library_error(
15f218
+            lambda: config_structure.validate_ticket_options(
15f218
+                report_processor, {
15f218
+                    "site": "a",
15f218
+                    "unknown": "c",
15f218
+                },
15f218
+                allow_unknown_options=True,
15f218
+            ),
15f218
+            *expected_errors
15f218
+        )
15f218
+        assert_report_item_list_equal(
15f218
+            report_processor.report_item_list,
15f218
+            expected_errors + [
15f218
+                (
15f218
+                    severities.WARNING,
15f218
+                    report_codes.INVALID_OPTION,
15f218
+                    {
15f218
+                        "option_name": "unknown",
15f218
+                        "option_type": "booth ticket",
15f218
+                        "allowed": list(config_structure.TICKET_KEYS),
15f218
+                    },
15f218
+                ),
15f218
+            ]
15f218
         )
15f218
 
15f218
     def test_success_on_valid_options(self):
15f218
-        config_structure.validate_ticket_options({"timeout": "10"})
15f218
+        report_processor = MockLibraryReportProcessor()
15f218
+        config_structure.validate_ticket_options(
15f218
+            report_processor,
15f218
+            {"timeout": "10"},
15f218
+            allow_unknown_options=False,
15f218
+        )
15f218
+        assert_report_item_list_equal(report_processor.report_item_list, [])
15f218
 
15f218
 class TicketExistsTest(TestCase):
15f218
     def test_returns_true_if_ticket_in_structure(self):
15f218
@@ -214,18 +285,25 @@ class RemoveTicketTest(TestCase):
15f218
         )
15f218
 
15f218
 class AddTicketTest(TestCase):
15f218
+    @mock.patch("pcs.lib.booth.config_structure.validate_ticket_options")
15f218
     @mock.patch("pcs.lib.booth.config_structure.validate_ticket_unique")
15f218
     @mock.patch("pcs.lib.booth.config_structure.validate_ticket_name")
15f218
     def test_successfully_add_ticket(
15f218
-        self, mock_validate_name, mock_validate_uniq
15f218
+        self, mock_validate_name, mock_validate_uniq, mock_validate_options
15f218
     ):
15f218
         configuration = [
15f218
             config_structure.ConfigItem("ticket", "some-ticket"),
15f218
         ]
15f218
+
15f218
         self.assertEqual(
15f218
-            config_structure.add_ticket(configuration, "new-ticket", {
15f218
-                "timeout": "10",
15f218
-            }),
15f218
+            config_structure.add_ticket(
15f218
+                None, configuration,
15f218
+                "new-ticket",
15f218
+                {
15f218
+                    "timeout": "10",
15f218
+                },
15f218
+                allow_unknown_options=False,
15f218
+            ),
15f218
             [
15f218
                 config_structure.ConfigItem("ticket", "some-ticket"),
15f218
                 config_structure.ConfigItem("ticket", "new-ticket", [
15f218
@@ -236,6 +314,11 @@ class AddTicketTest(TestCase):
15f218
 
15f218
         mock_validate_name.assert_called_once_with("new-ticket")
15f218
         mock_validate_uniq.assert_called_once_with(configuration, "new-ticket")
15f218
+        mock_validate_options.assert_called_once_with(
15f218
+            None,
15f218
+            {"timeout": "10"},
15f218
+            False
15f218
+        )
15f218
 
15f218
 class SetAuthfileTest(TestCase):
15f218
     def test_add_authfile(self):
15f218
diff --git a/pcs/lib/commands/booth.py b/pcs/lib/commands/booth.py
15f218
index bea966c..705900a 100644
15f218
--- a/pcs/lib/commands/booth.py
15f218
+++ b/pcs/lib/commands/booth.py
15f218
@@ -119,14 +119,19 @@ def config_text(env, name, node_name=None):
15f218
         raise LibraryError(reports.invalid_response_format(node_name))
15f218
 
15f218
 
15f218
-def config_ticket_add(env, ticket_name, options):
15f218
+def config_ticket_add(env, ticket_name, options, allow_unknown_options):
15f218
     """
15f218
     add ticket to booth configuration
15f218
+    dict options contains options for ticket
15f218
+    bool allow_unknown_options decide if can be used options not listed in
15f218
+        ticket options nor global options
15f218
     """
15f218
     booth_configuration = config_structure.add_ticket(
15f218
+        env.report_processor,
15f218
         parse(env.booth.get_config_content()),
15f218
         ticket_name,
15f218
         options,
15f218
+        allow_unknown_options,
15f218
     )
15f218
     env.booth.push_config(build(booth_configuration))
15f218
 
15f218
diff --git a/pcs/pcs.8 b/pcs/pcs.8
15f218
index 270ad2d..61abe67 100644
15f218
--- a/pcs/pcs.8
15f218
+++ b/pcs/pcs.8
15f218
@@ -585,7 +585,8 @@ destroy
15f218
 Remove booth configuration files.
15f218
 .TP
15f218
 ticket add <ticket>
15f218
-Add new ticket to the current configuration.
15f218
+Add new ticket to the current configuration. Ticket options are specified in booth manpage.
15f218
+
15f218
 .TP
15f218
 ticket remove <ticket>
15f218
 Remove the specified ticket from the current configuration.
15f218
@@ -603,10 +604,10 @@ 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. Cannot be run on an arbitrator.
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.  Specifying site address is mandatory when running this command 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. Cannot be run on an arbitrator.
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.  Specifying site address is mandatory when running this command 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 3356e71..c12391b 100644
15f218
--- a/pcs/test/test_booth.py
15f218
+++ b/pcs/test/test_booth.py
15f218
@@ -223,6 +223,23 @@ class AddTicketTest(BoothTest):
15f218
             )
15f218
         )
15f218
 
15f218
+    def test_forceable_fail_on_unknown_options(self):
15f218
+        msg = (
15f218
+            "invalid booth ticket option 'unknown', allowed options"
15f218
+            " are: acquire-after, attr-prereq, before-acquire-handler,"
15f218
+            " expire, renewal-freq, retries, timeout, weights"
15f218
+        )
15f218
+        self.assert_pcs_fail(
15f218
+            "booth ticket add TicketA unknown=a", console_report(
15f218
+                "Error: "+msg+", use --force to override",
15f218
+            )
15f218
+        )
15f218
+        self.assert_pcs_success(
15f218
+            "booth ticket add TicketA unknown=a --force",
15f218
+            "Warning: {0}\n".format(msg),
15f218
+        )
15f218
+
15f218
+
15f218
 class RemoveTicketTest(BoothTest):
15f218
     def test_success_remove_ticket(self):
15f218
         self.assert_pcs_success("booth ticket add TicketA")
15f218
diff --git a/pcs/usage.py b/pcs/usage.py
15f218
index 088dec9..9d4617f 100644
15f218
--- a/pcs/usage.py
15f218
+++ b/pcs/usage.py
15f218
@@ -1433,8 +1433,9 @@ Commands:
15f218
     destroy
15f218
         Remove booth configuration files.
15f218
 
15f218
-    ticket add <ticket>
15f218
-        Add new ticket to the current configuration.
15f218
+    ticket add <ticket> [<name>=<value> ...]
15f218
+        Add new ticket to the current configuration. Ticket options are
15f218
+        specified in booth manpage.
15f218
 
15f218
     ticket remove <ticket>
15f218
         Remove the specified ticket from the current configuration.
15f218
@@ -1456,12 +1457,14 @@ Commands:
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. Cannot be run on an arbitrator.
15f218
+        'site address' is omitted.  Specifying site address is mandatory when
15f218
+        running this command 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. Cannot be run on an arbitrator.
15f218
+        'site address' is omitted.  Specifying site address is mandatory when
15f218
+        running this command on an arbitrator.
15f218
 
15f218
     status
15f218
         Print current status of booth on the local node.
15f218
-- 
15f218
1.8.3.1
15f218