diff --git a/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst b/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst index 12f34c26a3..f83cd48177 100644 --- a/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst +++ b/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst @@ -198,6 +198,7 @@ spec. The following options can be set at the root level of the argument spec: - ``mutually_exclusive``: A list of lists, where the inner list contains module options that cannot be set together - ``no_log``: Stops the module from emitting any logs to the Windows Event log - ``options``: A dictionary where the key is the module option and the value is the spec for that option +- ``required_by``: A dictionary where the option(s) specified by the value must be set if the option specified by the key is also set - ``required_if``: A list of lists where the inner list contains 3 or 4 elements; * The first element is the module option to check the value against * The second element is the value of the option specified by the first element, if matched then the required if check is run @@ -236,6 +237,7 @@ When ``type=dict``, or ``type=list`` and ``elements=dict``, the following keys c - ``mutually_exclusive``: Same as the root level ``mutually_exclusive`` but validated against the values in the sub dict - ``options``: Same as the root level ``options`` but contains the valid options for the sub option - ``required_if``: Same as the root level ``required_if`` but validated against the values in the sub dict +- ``required_by``: Same as the root level ``required_by`` but validated against the values in the sub dict - ``required_together``: Same as the root level ``required_together`` but validated against the values in the sub dict - ``required_one_of``: Same as the root level ``required_one_of`` but validated against the values in the sub dict diff --git a/lib/ansible/module_utils/csharp/Ansible.Basic.cs b/lib/ansible/module_utils/csharp/Ansible.Basic.cs index e5d99b2daa..54c3e9d70f 100644 --- a/lib/ansible/module_utils/csharp/Ansible.Basic.cs +++ b/lib/ansible/module_utils/csharp/Ansible.Basic.cs @@ -83,6 +83,7 @@ namespace Ansible.Basic { "options", new List() { typeof(Hashtable), typeof(Hashtable) } }, { "removed_in_version", new List() { null, typeof(string) } }, { "required", new List() { false, typeof(bool) } }, + { "required_by", new List() { typeof(Hashtable), typeof(Hashtable) } }, { "required_if", new List() { typeof(List>), null } }, { "required_one_of", new List() { typeof(List>), null } }, { "required_together", new List() { typeof(List>), null } }, @@ -792,6 +793,7 @@ namespace Ansible.Basic CheckRequiredTogether(param, (IList)spec["required_together"]); CheckRequiredOneOf(param, (IList)spec["required_one_of"]); CheckRequiredIf(param, (IList)spec["required_if"]); + CheckRequiredBy(param, (IDictionary)spec["required_by"]); // finally ensure all missing parameters are set to null and handle sub options foreach (DictionaryEntry entry in optionSpec) @@ -1012,6 +1014,28 @@ namespace Ansible.Basic } } + private void CheckRequiredBy(IDictionary param, IDictionary requiredBy) + { + foreach (DictionaryEntry entry in requiredBy) + { + string key = (string)entry.Key; + if (!param.Contains(key)) + continue; + + List missing = new List(); + List requires = ParseList(entry.Value).Cast().ToList(); + foreach (string required in requires) + if (!param.Contains(required)) + missing.Add(required); + + if (missing.Count > 0) + { + string msg = String.Format("missing parameter(s) required by '{0}': {1}", key, String.Join(", ", missing)); + FailJson(FormatOptionsContext(msg)); + } + } + } + private void CheckSubOption(IDictionary param, string key, IDictionary spec) { string type; diff --git a/lib/ansible/modules/windows/win_environment.ps1 b/lib/ansible/modules/windows/win_environment.ps1 index 53e28e68aa..f8452be695 100644 --- a/lib/ansible/modules/windows/win_environment.ps1 +++ b/lib/ansible/modules/windows/win_environment.ps1 @@ -12,6 +12,9 @@ $spec = @{ state = @{ type = "str"; choices = "absent", "present"; default = "present" } value = @{ type = "str" } } + required_by = @{ + present = @("value") + } required_if = @(,@("state", "present", @("value"))) supports_check_mode = $true } diff --git a/test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 b/test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 index 8118a0f726..d57f2128ad 100644 --- a/test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 +++ b/test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 @@ -708,6 +708,206 @@ test_no_log - Invoked with: $actual | Assert-DictionaryEquals -Expected $expected } + "Required by - single value" = { + $spec = @{ + options = @{ + option1 = @{type = "str"} + option2 = @{type = "str"} + option3 = @{type = "str"} + } + required_by = @{ + option1 = "option2" + } + } + $complex_args = @{ + option1 = "option1" + option2 = "option2" + } + + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{ + option1 = "option1" + option2 = "option2" + option3 = $null + } + } + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "Required by - multiple values" = { + $spec = @{ + options = @{ + option1 = @{type = "str"} + option2 = @{type = "str"} + option3 = @{type = "str"} + } + required_by = @{ + option1 = "option2", "option3" + } + } + $complex_args = @{ + option1 = "option1" + option2 = "option2" + option3 = "option3" + } + + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{ + option1 = "option1" + option2 = "option2" + option3 = "option3" + } + } + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "Required by explicit null" = { + $spec = @{ + options = @{ + option1 = @{type = "str"} + option2 = @{type = "str"} + option3 = @{type = "str"} + } + required_by = @{ + option1 = "option2" + } + } + $complex_args = @{ + option1 = "option1" + option2 = $null + } + + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{ + option1 = "option1" + option2 = $null + option3 = $null + } + } + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "Required by failed - single value" = { + $spec = @{ + options = @{ + option1 = @{type = "str"} + option2 = @{type = "str"} + option3 = @{type = "str"} + } + required_by = @{ + option1 = "option2" + } + } + $complex_args = @{ + option1 = "option1" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected = @{ + changed = $false + failed = $true + invocation = @{ + module_args = @{ + option1 = "option1" + } + } + msg = "missing parameter(s) required by 'option1': option2" + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "Required by failed - multiple values" = { + $spec = @{ + options = @{ + option1 = @{type = "str"} + option2 = @{type = "str"} + option3 = @{type = "str"} + } + required_by = @{ + option1 = "option2", "option3" + } + } + $complex_args = @{ + option1 = "option1" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected = @{ + changed = $false + failed = $true + invocation = @{ + module_args = @{ + option1 = "option1" + } + } + msg = "missing parameter(s) required by 'option1': option2, option3" + } + $actual | Assert-DictionaryEquals -Expected $expected + } + "Debug without debug set" = { $complex_args = @{ _ansible_debug = $false @@ -1184,7 +1384,7 @@ test_no_log - Invoked with: $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, " $expected_msg += "aliases, choices, default, elements, mutually_exclusive, no_log, options, removed_in_version, " - $expected_msg += "required, required_if, required_one_of, required_together, supports_check_mode, type" + $expected_msg += "required, required_by, required_if, required_one_of, required_together, supports_check_mode, type" $actual.Keys.Count | Assert-Equals -Expected 3 $actual.failed | Assert-Equals -Expected $true @@ -1216,7 +1416,7 @@ test_no_log - Invoked with: $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, " $expected_msg += "aliases, choices, default, elements, mutually_exclusive, no_log, options, removed_in_version, " - $expected_msg += "required, required_if, required_one_of, required_together, supports_check_mode, type - " + $expected_msg += "required, required_by, required_if, required_one_of, required_together, supports_check_mode, type - " $expected_msg += "found in option_key -> sub_option_key" $actual.Keys.Count | Assert-Equals -Expected 3