1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-06-11 02:25:36 +00:00

random_string: Add parameter avoid_special_first and avoid_special_last

* Avoid first character of random string if it is special when avoid_special_first
* Avoid last character of random string if it is special when avoid_special_last

Fixes: #11816

Signed-off-by: Abhijeet Kasurde <Akasurde@redhat.com>
This commit is contained in:
Abhijeet Kasurde 2026-04-14 13:21:48 -04:00
parent 78d004d96e
commit 8fbb9d718e
No known key found for this signature in database
3 changed files with 90 additions and 22 deletions

View file

@ -0,0 +1,3 @@
---
bugfixes:
- random_string - allow user to set avoid_special_first and avoid_special_last (https://github.com/ansible-collections/community.general/issues/11816, https://github.com/ansible-collections/community.general/pull/11819).

View file

@ -106,6 +106,18 @@ options:
B(Do not use the generated string as a password or a secure token when using this option!)
type: str
version_added: 11.3.0
avoid_special_first:
description:
- Avoid special characters at the first position of the string.
type: bool
default: false
version_added: 12.6.0
avoid_special_last:
description:
- Avoid special characters at the last position of the string.
type: bool
default: false
version_added: 12.6.0
"""
EXAMPLES = r"""
@ -155,6 +167,16 @@ EXAMPLES = r"""
vars:
hex_chars: '0123456789ABCDEF'
# Example result: ['D2A40737']
- name: Generate random string with avoid_special_first
debug:
var: query('community.general.random_string', avoid_special_first=true)
# Example result: ['Uan0hUiX5kVG']
- name: Generate random string with avoid_special_last
debug:
var: query('community.general.random_string', avoid_special_last=true)
# Example result: ['Uan0hUiX5kVG']
"""
RETURN = r"""
@ -196,8 +218,11 @@ class LookupModule(LookupBase):
length = self.get_option("length")
base64_flag = self.get_option("base64")
override_all = self.get_option("override_all")
override_special = self.get_option("override_special")
ignore_similar_chars = self.get_option("ignore_similar_chars")
similar_chars = self.get_option("similar_chars")
avoid_special_first = self.get_option("avoid_special_first")
avoid_special_last = self.get_option("avoid_special_last")
seed = self.get_option("seed")
if seed is None:
@ -205,7 +230,7 @@ class LookupModule(LookupBase):
else:
random_generator = random.Random(seed)
values = ""
result = []
available_chars_set = ""
if ignore_similar_chars:
@ -222,7 +247,6 @@ class LookupModule(LookupBase):
lower = self.get_option("lower")
numbers = self.get_option("numbers")
special = self.get_option("special")
override_special = self.get_option("override_special")
if override_special:
special_chars = override_special
@ -236,27 +260,50 @@ class LookupModule(LookupBase):
if special:
available_chars_set += special_chars
mapping = {
"min_numeric": number_chars,
"min_lower": lower_chars,
"min_upper": upper_chars,
"min_special": special_chars,
}
if len(available_chars_set) == 0:
raise AnsibleLookupError("Available characters cannot be empty, please change constraints")
for m in mapping:
if self.get_option(m):
values += self.get_random(random_generator, mapping[m], self.get_option(m))
min_numeric = self.get_option("min_numeric")
min_lower = self.get_option("min_lower")
min_upper = self.get_option("min_upper")
min_special = self.get_option("min_special")
if length < min_numeric + min_lower + min_upper + min_special:
raise AnsibleLookupError("Minimum requirements exceed total length, please increase the length")
remaining_pass_len = length - len(values)
values += self.get_random(random_generator, available_chars_set, remaining_pass_len)
# Ensure minimum requirements
result += [random_generator.choice(number_chars) for dummy in range(min_numeric)]
result += [random_generator.choice(lower_chars) for dummy in range(min_lower)]
result += [random_generator.choice(upper_chars) for dummy in range(min_upper)]
if override_special:
result += [random_generator.choice(override_special) for dummy in range(min_special)]
else:
result += [random_generator.choice(special_chars) for dummy in range(min_special)]
shuffled_values = list(values)
if seed is None:
# Get pseudo randomization
# Randomize the order
random.shuffle(shuffled_values)
# Fill remaining length
remaining_length = length - len(result)
result += [random_generator.choice(available_chars_set) for dummy in range(remaining_length)]
# Shuffle to avoid predictable pattern
random_generator.shuffle(result)
# Handle first/last character constraints
if avoid_special_first and result[0] in special_chars:
for i in range(1, len(result)):
if result[i] not in special_chars:
result[0], result[i] = result[i], result[0]
break
else:
raise AnsibleLookupError("No character satisfies the constraints for avoid_special_first")
if avoid_special_last and result[-1] in special_chars:
for i in range(len(result) - 2, -1, -1):
if result[i] not in special_chars:
result[-1], result[i] = result[i], result[-1]
break
else:
raise AnsibleLookupError("No character satisfies the constraints for avoid_special_last")
if base64_flag:
return [self.b64encode("".join(shuffled_values))]
return [self.b64encode("".join(result))]
return ["".join(shuffled_values)]
return ["".join(result)]

View file

@ -11,7 +11,7 @@
result1: "{{ query('community.general.random_string') }}"
result2: "{{ query('community.general.random_string', length=0) }}"
result3: "{{ query('community.general.random_string', length=10) }}"
result4: "{{ query('community.general.random_string', length=-1) }}"
result4: "{{ query('community.general.random_string', length=6) }}"
result5: "{{ query('community.general.random_string', override_special='_', min_special=1) }}"
result6: "{{ query('community.general.random_string', upper=false, special=false) }}" # lower case only
result7: "{{ query('community.general.random_string', lower=false, special=false) }}" # upper case only
@ -23,6 +23,8 @@
result13: "{{ query('community.general.random_string', override_all='0', length=2) }}"
result14: "{{ query('community.general.random_string', seed='1234') }}"
result15: "{{ query('community.general.random_string', seed='1234') }}"
result16: "{{ query('community.general.random_string', avoid_special_first=true) }}"
result17: "{{ query('community.general.random_string', avoid_special_last=true) }}"
- name: Raise error when impossible constraints are provided
set_fact:
@ -30,13 +32,25 @@
ignore_errors: true
register: impossible_result
- name: Raise error when override_all is provided with special characters and avoid_special_first is provided
set_fact:
error: "{{ query('community.general.random_string', override_all='!@#$%^&*()', avoid_special_first=true) }}"
ignore_errors: true
register: override_all_special_characters_avoid_special_first_error
- name: Raise error when override_all is provided with special characters and avoid_special_last is provided
set_fact:
error: "{{ query('community.general.random_string', override_all='!@#$%^&*()', avoid_special_last=true) }}"
ignore_errors: true
register: override_all_special_characters_avoid_special_last_error
- name: Check results
assert:
that:
- result1[0] | length == 8
- result2[0] | length == 0
- result3[0] | length == 10
- result4[0] | length == 0
- result4[0] | length == 6
- result5[0] | length == 8
- "'_' in result5[0]"
- result6[0] is lower
@ -54,3 +68,7 @@
- impossible_result is failed
- "'Available characters cannot' in impossible_result.msg"
- result15[0] == result14[0]
- override_all_special_characters_avoid_special_first_error is failed
- "'No character satisfies the constraints for avoid_special_first' in override_all_special_characters_avoid_special_first_error.msg"
- override_all_special_characters_avoid_special_last_error is failed
- "'No character satisfies the constraints for avoid_special_last' in override_all_special_characters_avoid_special_last_error.msg"