cron - Allow non-ascii (UTF-8) chars in cron file paths and jobs (#70426) (#70794)

* Encode/Decode files in UTF-8
* Use helper function in ansible
* Add an integration test
* Use emoji in test data.
* add changelog
* Also support non-ascii chars in filepath and add tests about this.
* Also use non-ascii chars in replaced text and ensure not to break cron syntax.
* rename self.existing to self.n_existing
* rename crontab.existing to crontab.n_existing
This commit is contained in:
psi / Ryo Hirafuji 2020-07-23 10:26:10 +09:00 committed by GitHub
parent 7eb5f53294
commit 61f8f8ce7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 79 additions and 13 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- cron - encode and decode crontab files in UTF-8 explicitly to allow non-ascii chars in cron filepath and job (https://github.com/ansible/ansible/issues/69492)

View file

@ -210,6 +210,7 @@ import sys
import tempfile import tempfile
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six.moves import shlex_quote from ansible.module_utils.six.moves import shlex_quote
@ -231,14 +232,16 @@ class CronTab(object):
self.root = (os.getuid() == 0) self.root = (os.getuid() == 0)
self.lines = None self.lines = None
self.ansible = "#Ansible: " self.ansible = "#Ansible: "
self.existing = '' self.n_existing = ''
self.cron_cmd = self.module.get_bin_path('crontab', required=True) self.cron_cmd = self.module.get_bin_path('crontab', required=True)
if cron_file: if cron_file:
if os.path.isabs(cron_file): if os.path.isabs(cron_file):
self.cron_file = cron_file self.cron_file = cron_file
self.b_cron_file = to_bytes(cron_file, errors='surrogate_or_strict')
else: else:
self.cron_file = os.path.join('/etc/cron.d', cron_file) self.cron_file = os.path.join('/etc/cron.d', cron_file)
self.b_cron_file = os.path.join(b'/etc/cron.d', to_bytes(cron_file, errors='surrogate_or_strict'))
else: else:
self.cron_file = None self.cron_file = None
@ -250,9 +253,8 @@ class CronTab(object):
if self.cron_file: if self.cron_file:
# read the cronfile # read the cronfile
try: try:
f = open(self.cron_file, 'r') f = open(self.b_cron_file, 'rb')
self.existing = f.read() self.n_existing = to_native(f.read(), errors='surrogate_or_strict')
self.lines = self.existing.splitlines()
f.close() f.close()
except IOError: except IOError:
# cron file does not exist # cron file does not exist
@ -266,7 +268,7 @@ class CronTab(object):
if rc != 0 and rc != 1: # 1 can mean that there are no jobs. if rc != 0 and rc != 1: # 1 can mean that there are no jobs.
raise CronTabError("Unable to read crontab") raise CronTabError("Unable to read crontab")
self.existing = out self.n_existing = out
lines = out.splitlines() lines = out.splitlines()
count = 0 count = 0
@ -277,7 +279,7 @@ class CronTab(object):
self.lines.append(l) self.lines.append(l)
else: else:
pattern = re.escape(l) + '[\r\n]?' pattern = re.escape(l) + '[\r\n]?'
self.existing = re.sub(pattern, '', self.existing, 1) self.n_existing = re.sub(pattern, '', self.n_existing, 1)
count += 1 count += 1
def is_empty(self): def is_empty(self):
@ -291,15 +293,15 @@ class CronTab(object):
Write the crontab to the system. Saves all information. Write the crontab to the system. Saves all information.
""" """
if backup_file: if backup_file:
fileh = open(backup_file, 'w') fileh = open(backup_file, 'wb')
elif self.cron_file: elif self.cron_file:
fileh = open(self.cron_file, 'w') fileh = open(self.b_cron_file, 'wb')
else: else:
filed, path = tempfile.mkstemp(prefix='crontab') filed, path = tempfile.mkstemp(prefix='crontab')
os.chmod(path, int('0644', 8)) os.chmod(path, int('0644', 8))
fileh = os.fdopen(filed, 'w') fileh = os.fdopen(filed, 'wb')
fileh.write(self.render()) fileh.write(to_bytes(self.render()))
fileh.close() fileh.close()
# return if making a backup # return if making a backup
@ -628,7 +630,7 @@ def main():
if module._diff: if module._diff:
diff = dict() diff = dict()
diff['before'] = crontab.existing diff['before'] = crontab.n_existing
if crontab.cron_file: if crontab.cron_file:
diff['before_header'] = crontab.cron_file diff['before_header'] = crontab.cron_file
else: else:
@ -724,8 +726,8 @@ def main():
changed = True changed = True
# no changes to env/job, but existing crontab needs a terminating newline # no changes to env/job, but existing crontab needs a terminating newline
if not changed and crontab.existing != '': if not changed and crontab.n_existing != '':
if not (crontab.existing.endswith('\r') or crontab.existing.endswith('\n')): if not (crontab.n_existing.endswith('\r') or crontab.n_existing.endswith('\n')):
changed = True changed = True
res_args = dict( res_args = dict(

View file

@ -122,3 +122,65 @@
- assert: - assert:
that: not cron_file_stats.stat.exists that: not cron_file_stats.stat.exists
- name: Allow non-ascii chars in job (#69492)
block:
- name: Cron file creation
cron:
cron_file: cron_filename
name: "cron job that contain non-ascii chars in job (これは日本語です; This is Japanese)"
job: 'echo "うどんは好きだがお化け👻は苦手である。"'
user: root
- name: "Ensure cron_file contains job string"
replace:
path: /etc/cron.d/cron_filename
regexp: "うどんは好きだがお化け👻は苦手である。"
replace: "それは機密情報🔓です。"
register: find_chars
failed_when: (find_chars is not changed) or (find_chars is failed)
- name: Cron file deletion
cron:
cron_file: cron_filename
name: "cron job that contain non-ascii chars in job (これは日本語です; This is Japanese)"
state: absent
- name: Check file succesfull deletion
stat:
path: /etc/cron.d/cron_filename
register: cron_file_stats
- assert:
that: not cron_file_stats.stat.exists
- name: Allow non-ascii chars in cron_file (#69492)
block:
- name: Cron file creation with non-ascii filename (これは日本語です; This is Japanese)
cron:
cron_file: 'なせば大抵なんとかなる👊'
name: "integration test cron"
job: 'echo "Hello, ansible!"'
user: root
- name: Check file exists
stat:
path: "/etc/cron.d/なせば大抵なんとかなる👊"
register: cron_file_stats
- assert:
that: cron_file_stats.stat.exists
- name: Cron file deletion
cron:
cron_file: 'なせば大抵なんとかなる👊'
name: "integration test cron"
state: absent
- name: Check file succesfull deletion
stat:
path: "/etc/cron.d/なせば大抵なんとかなる👊"
register: cron_file_stats
- assert:
that: not cron_file_stats.stat.exists