Merge ~dashmage/charm-logrotated:bug/2017798-update-cron-daily-location into charm-logrotated:master
- Git
- lp:~dashmage/charm-logrotated
- bug/2017798-update-cron-daily-location
- Merge into master
Status: | Merged |
---|---|
Approved by: | Eric Chen |
Approved revision: | 13acd7b2a53e0663f8fe35ee458bb25be4b6035e |
Merged at revision: | 10ab2f6b55b60798c08a40c4a86014c542a7bac1 |
Proposed branch: | ~dashmage/charm-logrotated:bug/2017798-update-cron-daily-location |
Merge into: | charm-logrotated:master |
Diff against target: |
201 lines (+70/-25) 4 files modified
src/lib/lib_cron.py (+16/-14) src/tests/functional/requirements.txt (+1/-0) src/tests/functional/test_logrotate.py (+13/-5) src/tests/unit/test_logrotate.py (+40/-6) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
JamesLin | Approve | ||
🤖 prod-jenkaas-bootstack (community) | continuous-integration | Approve | |
Chi Wai CHAN | Needs Information | ||
Eric Chen | Approve | ||
BootStack Reviewers | Pending | ||
BootStack Reviewers | Pending | ||
Robert Gildein | Pending | ||
Tianqi Xiao | Pending | ||
Review via email: mp+443242@code.launchpad.net |
This proposal supersedes a proposal from 2023-05-09.
Commit message
Manage charm's daily cronjob from /etc/crontab instead of root user's crontab.
Description of the change
`update-
Changes performed by MP
-------
- Use tenacity.retry while checking whether a file exists in the functional tests. This prevents failure in cases of performance issues on the CI server.
- In the `update_
- Replace dangling `open` function calls with a context manager in `install_cronjob`.
- Fix existing unit tests in order mock the file and not write directly to the crontab of the system where it's being run.
- Add unit test for the new function `write_to_crontab`.
Fixes LP#2017798
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : Posted in a previous version of this proposal | # |
Ashley James (dashmage) wrote : Posted in a previous version of this proposal | # |
I see one potential problem now - in an upgrade scenario, even after implementing this fix, the sudo user crontab would still exist if cron-update-
I propose that we add a function that checks whether the `/var/spool/
Robert Gildein (rgildein) : Posted in a previous version of this proposal | # |
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:6bb452dd5ed
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Ashley James (dashmage) wrote (last edit ): Posted in a previous version of this proposal | # |
> Is this really wisely to overwritte system crontab? Why not put new file to /etc/cron.d/<name>?
Even if we add this as a new file to `/etc/cron.
In case we're modifying the time for running cron.daily scripts and want to add it as a separate file in `/etc/cron.d`, we would need to remove the line from `/etc/crontab`. I prefer we make the change directly in `/etc/crontab` itself for changing the time.
Edit: Just to clarify, the `update-
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:8a44ee34e84
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:86032494a23
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:8a44ee34e84
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:86032494a23
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:8a44ee34e84
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Robert Gildein (rgildein) : Posted in a previous version of this proposal | # |
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:86032494a23
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:714a28da31c
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:e4c83fa67de
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Robert Gildein (rgildein) wrote : Posted in a previous version of this proposal | # |
LGTM, thanks
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:e4c83fa67de
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:c73e48e05b5
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal | # |
I am curious
1. who is the runner when this library being called. (Ubuntu? Juju?) why they have root permission.
2. what's the different between f.flush and f.seek.
3. The logic looks like a magic if we want to copy the temp file. Is there better way to avoid the magic flush() or seek() function? I prefer not to use some implictly ability of flush() or seek() here. I prefer to make it just like a human do here.
#1 create temp file
#2 write data and close
#3 copy file
#4 delete temp file
Ashley James (dashmage) wrote : Posted in a previous version of this proposal | # |
> I am curious
>
> 1. who is the runner when this library being called. (Ubuntu? Juju?) why they
> have root permission.
After looking into this, the script is called in cron.daily (or weekly/monthly depending on config) and it runs as root. Here is the relevant part in the code [1]. Which is why the copy command works without "sudo". I propose we can remove the sudo while copying from the tmp file.
```
$ cat /etc/cron.
#!/bin/bash
/usr/bin/sudo /usr/bin/juju-run logrotated/1 "/var/lib/
```
[1]: https:/
> 2. what's the different between f.flush and f.seek.
f.seek() would make the file pointer return to zero so that we can read contents from the start of the file. f.flush() is used to clear the internal buffer and put contents into the file explicitly before f.close() or reaching the end of the context manager (where the file is automatically closed). In this situation we could also use f.flush and push the contents to the tmp file before we read it, I just chose to pick f.seek here.
> 3. The logic looks like a magic if we want to copy the temp file. Is there
> better way to avoid the magic flush() or seek() function? I prefer not to use
> some implictly ability of flush() or seek() here. I prefer to make it just
> like a human do here.
>
> #1 create temp file
> #2 write data and close
> #3 copy file
> #4 delete temp file
In case you suggest to avoid using seek/flush, we can do it this way to make it more like how a human would do it:
```
with open(r"
tmp_
try:
cmd = ["cp", "/tmp/crontab", "/etc/crontab"]
subprocess.
except subprocess.
hookenv.log(
f"Writing to system crontab failed with error: {exception}",
)
raise
os.remove(
```
Eric Chen (eric-chen) : Posted in a previous version of this proposal | # |
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal | # |
Based on the description you provided in the document, f.flush() is more reasonable. There is no clue that f.seek() will flush the data into the disk. Anyway, I prefer we handle it like a human way unless there is any specific reason we must copy the file before we close the file handle.
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal | # |
I'm so surprise that we will execute /var/lib/
- event handler under src/reactive
- juju actions under src/actions
Could you check the reason why we need to design like this?
BTW, update_
src/actions -> update_cronjob -> cron.install_
The runner of these two behaviors may be different. Could you also confirm it?
1. from cron.daily
2. from juju actions
Chi Wai CHAN (raychan96) wrote : Posted in a previous version of this proposal | # |
LGTM
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal | # |
Change the status to "Needs Fixing"
Ashley James (dashmage) wrote (last edit ): Posted in a previous version of this proposal | # |
> I'm so surprise that we will execute /var/lib/
> logrotated-
> to me. I suppose that every library should be used during
>
> - event handler under src/reactive
> - juju actions under src/actions
>
> Could you check the reason why we need to design like this?
After going through the code, this is what I could infer ->
The cron_lib.py code should be executed by the cronjob because the charm is used to rotate the logrotate.d files at the regular interval that is determined by the time configured in the cronjob. The event handlers under src/reactive defines install/
In my view - the juju action allows to install the cronjob/ update logrotate files manually by the operator if they don't want to wait for the cronjob trigger time.
======
> BTW, update_
> logrotated, it may be triggered from
>
> src/actions -> update_cronjob -> cron.install_
> update_
>
> The runner of these two behaviors may be different. Could you also confirm it?
>
> 1. from cron.daily
> 2. from juju actions
So even if this functionality is triggered from src/actions (with update_cronjob) or during execution of the install hook event handler or config changed handler, they utilize the same `CronHelper.
cron.daily is set with the `logrotate-
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:7bc0ebbcff3
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:a213ad7957b
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal | # |
Have you considered using a mature library (e.g. python-crontab [1]) to interact with crontab? I'm a bit worried that there are some consequences we have not considered when directly overwriting the cron file.
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal | # |
I cannot fully understand your answer above. I want to know why we need to execute cron_lib.py regularly. But I didn't find the real reason in your explanation.
My major concern is
1. In general case, the library in charm is used under the juju framework. The design in logrotate breaks the common situation and we should know the real reason. Maybe the solution is not to copy the file, we should just avoid this behavior.
2. Sudo is a dangerous command, we should only use it when needed. We should also need to know how the executor gets the permission?
3. subprocess is always my last choice to handle the program. because it implies many assumptions of the environment. So I agree with Tianqi's comment, we should consider the mature library. Even when we copy the file, we should think about how to use shutil.move if possible. (In the previous commit, you didn't use shutil.move when there is no "sudo").
4. "Retry" is also something I don't like a lot. Please write down the reason why we cannot avoid it. If there is a bug that only happen with 30% possibility, it will be concealed by the "Retry" mechanism.
I suggest that you can write a spec to clarify all the things and purpose the proposal again.
Ashley James (dashmage) wrote : Posted in a previous version of this proposal | # |
Problem
-------
Currently when the logrotate-
Fixes LP#2017798 [1]
Proposal
--------
For daily cronjobs, create a new cron file in /etc/cron.d where the charm will be able to manage the execution time without messing with the system crontab (/etc/crontab) or the root user’s crontab. It also prevents the charm from running commands with sudo.
I’m proposing to add a new file to /etc/cron.d instead of modifying the cron.daily execution time directly in the system crontab (/etc/crontab) because this would affect the execution of other scripts in cron.daily.
Changes performed by MP
-------
- Use tenacity.retry while checking whether a file exists in the functional tests. This prevents failure in cases of performance issues on the CI server.
- 2 new files are now managed by the charm --> `cronjob_
- In the `update_
- Renames the function to something more appropriate (`create_
- Replaces updating root user crontab with creating a new cron file in /etc/cron.d for daily cronjob
- Replace dangling `open` function calls with a context manager in `install_cronjob`.
- `cleanup_
I'm still working on these:
- Fix existing unit tests in order to not write directly to the crontab of the system where it's being run. Mock the required files which are being accessed.
- Re-write unit test for `create_
Other Notes
------------
I couldn’t use the python-crontab [2] library in order to manage the crontab changes since it does not support the random execution time syntax (00~45 08~09). This feature is not supported by the cron package which is used by Ubuntu in the first place [3]. A separate bug report has been raised for this issue [4].
-------------
[1]: https:/
[2]: https:/
[3]: https:/
[4]: https:/
Mert Kirpici (mertkirpici) wrote : Posted in a previous version of this proposal | # |
I am a little bit late to the discussion here, but I think I found the reason for the self-installing cronjob design decision in this charm, that Eric, Ray, Ashley and I were wondering at different times.
Taking from the original bug report:
LP #1832296: Create functionality to manage logrotate files
https:/
"""
- As some packages might change the logrotate configurations - a cronjob option needs to be defined, where it will constantly update the configurations with regards to the retention period defined
"""
So it is for making sure, our retention settings are taking precedence if any other package/operator makes changes to the /etc/logrotate.d/ files. While I am not sure that it would be the right place to do this, but i suppose we could move this functionality to the "update-status" hook, since it is fired periodically.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
FAILED: Continuous integration, rev:78a0422806d
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Ashley James (dashmage) wrote : | # |
I've updated the MP with an important change - reverting from adding a file in /etc/cron.d and instead updating `/etc/crontab` itself. This is because the charm would also need to manage the `/etc/cron.
Another important observation is that the `lib_cron.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:01542d28278
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Eric Chen (eric-chen) wrote : | # |
Please provide the final result we write into /etc/crontab for the verificiation
There are also inline comment. Not big change.
Eric Chen (eric-chen) : | # |
JamesLin (jneo8) wrote : | # |
One inline comment.
Ashley James (dashmage) wrote (last edit ): | # |
> ditto, 0o755 or 0o644 ? , 0x744 seems weird
Yeah, I agree. I've made this `0o755` now.
> It seems we add function argument `target` is just because we need to test it.
But the value will never change in real case.
I will suggest to remove it and mock the `open` syntax.
I've now modified the unit test so that it mocks the open context manager instead of relying on the `target` argument which would never change. Good suggestion!
> Please provide the final result we write into /etc/crontab for the verificiation
As part of the updated unit tests, the final results are now specified and verified.
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:ce9dbcf7095
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Chi Wai CHAN (raychan96) wrote (last edit ): | # |
Thanks for working on the fix.
After thinking this a bit, I am wondering why we are insist on using the default /etc/crontab and modify the cron.daily schedule, or copy the default /etc/crontab and modify the cron.daily schedule and put it under root's crontab. I think this is undesired because it will affect all cron jobs under /etc/cron.daily, not just the logrotate charm's cronjob.
One of the possible alternative is to just maintain our own cron job without using the default crontab.
For example, we can either 1. create a crontab for root user with only one entry, or 2. append an entry to default /etc/crontab without modifying original content
```
17 * * * * root path_to_
```
This way, we can be sure that even if we add to root user's cronjob or /etc/crontab, it will not have big impacts. What do you think?
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:13acd7b2a53
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Ashley James (dashmage) wrote : | # |
> Thanks for working on the fix.
>
> After thinking this a bit, I am wondering why we are insist on using the
> default /etc/crontab and modify the cron.daily schedule, or copy the default
> /etc/crontab and modify the cron.daily schedule and put it under root's
> crontab. I think this is undesired because it will affect all cron jobs under
> /etc/cron.daily, not just the logrotate charm's cronjob.
>
> One of the possible alternative is to just maintain our own cron job without
> using the default crontab.
>
> For example, we can either 1. create a crontab for root user with only one
> entry, or 2. append an entry to default /etc/crontab without modifying
> original content
>
> ```
> 17 * * * * root path_to_
> ```
>
> This way, we can be sure that even if we add to root user's cronjob or
> /etc/crontab, it will not have big impacts. What do you think?
Thank you for your comment!
So the requirement for using /etc/crontab is because the cron.daily job is used by the charm for managing the execution of `/etc/cron.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 10ab2f6b55b6079
Chi Wai CHAN (raychan96) wrote : | # |
> > Thanks for working on the fix.
> >
> > After thinking this a bit, I am wondering why we are insist on using the
> > default /etc/crontab and modify the cron.daily schedule, or copy the default
> > /etc/crontab and modify the cron.daily schedule and put it under root's
> > crontab. I think this is undesired because it will affect all cron jobs
> under
> > /etc/cron.daily, not just the logrotate charm's cronjob.
> >
> > One of the possible alternative is to just maintain our own cron job without
> > using the default crontab.
> >
> > For example, we can either 1. create a crontab for root user with only one
> > entry, or 2. append an entry to default /etc/crontab without modifying
> > original content
> >
> > ```
> > 17 * * * * root path_to_
> > ```
> >
> > This way, we can be sure that even if we add to root user's cronjob or
> > /etc/crontab, it will not have big impacts. What do you think?
>
> Thank you for your comment!
>
> So the requirement for using /etc/crontab is because the cron.daily job is
> used by the charm for managing the execution of `/etc/cron.
> along with charm's `/etc/cron.
> cronjob is installed by the apt package and therefore we can't modify its
> location (it will be overwritten). So when the charm modifies the daily
> cronjob time config, it also needs to modify it for the `logrotate` file.
> Which is why we would need to directly modify the existing cron.daily time.
That's a good observation! and it's exactly the why I mentioned "it will affect all cron jobs
under /etc/cron.daily". The question is do we really want to change the `logrotate` script provided by the apt package? I guess is not because from the config.yaml, it appears to me that the "update-
And what's more is that the logic in "lib_cron.py" is weird / incorrect: when "logrotate-cronjob" is enabled, it creates a cronjob that runs "lib_cron.py", and "lib_cron.py" will create a cronjob that runs "lib_cron.py" and update logrotate config file. And the cycle keep going, so that's a logic loop.
That being said, the logic is already there before you create this MR, so it's not on you. If it's possible, we should raise a bug to refactor the code's logic first, before attempting a new features.
Preview Diff
1 | diff --git a/src/lib/lib_cron.py b/src/lib/lib_cron.py |
2 | index 1f30ddc..f2383cc 100644 |
3 | --- a/src/lib/lib_cron.py |
4 | +++ b/src/lib/lib_cron.py |
5 | @@ -1,7 +1,6 @@ |
6 | """Cron helper module.""" |
7 | import os |
8 | import re |
9 | -import subprocess |
10 | |
11 | from charmhelpers.core import hookenv |
12 | |
13 | @@ -62,15 +61,14 @@ class CronHelper: |
14 | logrotate_unit = hookenv.local_unit() |
15 | python_venv_path = os.getcwd().replace("charm", "") + ".venv/bin/python3" |
16 | # upgrade to template if logic increases |
17 | - cron_file = open(cron_file_path, "w") |
18 | cron_job = """#!/bin/bash |
19 | /usr/bin/sudo /usr/bin/juju-run {} "{} {}" |
20 | """.format( |
21 | logrotate_unit, python_venv_path, cronjob_path |
22 | ) |
23 | - cron_file.write(cron_job) |
24 | - cron_file.close() |
25 | - os.chmod(cron_file_path, 700) |
26 | + with open(cron_file_path, "w") as cron_file: |
27 | + cron_file.write(cron_job) |
28 | + os.chmod(cron_file_path, 0o755) |
29 | |
30 | # update cron.daily schedule if logrotate-cronjob-frequency set to "daily" |
31 | if self.cronjob_frequency == 1 and self.validate_cron_conf(): |
32 | @@ -123,7 +121,13 @@ class CronHelper: |
33 | else: |
34 | raise RuntimeError("Unknown daily schedule type: {}".format(schedule_type)) |
35 | |
36 | - cron_daily_timestamp = cron_daily_minute + " " + cron_daily_hour + "\t" |
37 | + cron_daily_timestamp = cron_daily_minute + " " + cron_daily_hour |
38 | + self.write_to_crontab(cron_daily_timestamp) |
39 | + |
40 | + return cron_daily_timestamp |
41 | + |
42 | + def write_to_crontab(self, cron_daily_timestamp): |
43 | + """Write daily cronjob with provided timestamp to /etc/crontab.""" |
44 | cron_pattern = re.compile(r".*\/etc\/cron.daily.*") |
45 | with open(r"/etc/crontab", "r") as crontab: |
46 | data = crontab.read() |
47 | @@ -132,15 +136,13 @@ class CronHelper: |
48 | updated_cron_daily = "" |
49 | if cron_daily: |
50 | updated_cron_daily = re.sub( |
51 | - r"\d?\d(~\d\d)?\s\d?\d(~\d\d)?\t", cron_daily_timestamp, cron_daily[0] |
52 | + r"\d?\d(~\d\d)?\s\d?\d(~\d\d)?\t", |
53 | + cron_daily_timestamp + "\t", |
54 | + cron_daily[0], |
55 | ) |
56 | - data = data.replace(cron_daily[0], updated_cron_daily) |
57 | - with open(r"/tmp/crontab", "w") as crontab: |
58 | - crontab.write(data) |
59 | - |
60 | - subprocess.check_output(["sudo", "crontab", "-u", "root", "/tmp/crontab"]) |
61 | - |
62 | - return updated_cron_daily |
63 | + updated_data = data.replace(cron_daily[0], updated_cron_daily) |
64 | + with open(r"/etc/crontab", "w") as crontab: |
65 | + crontab.write(updated_data) |
66 | |
67 | def validate_cron_conf(self): |
68 | """Block the unit and exit the hook if there is invalid configuration.""" |
69 | diff --git a/src/tests/functional/requirements.txt b/src/tests/functional/requirements.txt |
70 | index 6c4de97..4128a5d 100644 |
71 | --- a/src/tests/functional/requirements.txt |
72 | +++ b/src/tests/functional/requirements.txt |
73 | @@ -7,3 +7,4 @@ mock |
74 | pytest |
75 | pytest-asyncio |
76 | requests |
77 | +tenacity |
78 | diff --git a/src/tests/functional/test_logrotate.py b/src/tests/functional/test_logrotate.py |
79 | index 0afbb82..7088371 100644 |
80 | --- a/src/tests/functional/test_logrotate.py |
81 | +++ b/src/tests/functional/test_logrotate.py |
82 | @@ -9,6 +9,8 @@ import pytest |
83 | |
84 | import pytest_asyncio |
85 | |
86 | +import tenacity |
87 | + |
88 | pytestmark = pytest.mark.asyncio |
89 | SERIES = ["bionic", "focal", "jammy"] |
90 | |
91 | @@ -118,10 +120,17 @@ async def test_reconfigure_cronjob_frequency(model, deploy_app, unit, jujutools) |
92 | await model.block_until(lambda: deploy_app.status == "active") |
93 | config = await deploy_app.get_config() |
94 | |
95 | - result = await jujutools.run_command( |
96 | - "test -f /etc/cron.weekly/charm-logrotate", unit |
97 | - ) |
98 | - weekly_cronjob_exists = result["return-code"] == 0 |
99 | + # Retry because test fails sometimes when cron file isn't ready yet |
100 | + for attempt in tenacity.Retrying( |
101 | + stop=tenacity.stop_after_attempt(3), |
102 | + wait=tenacity.wait_exponential(multiplier=1, min=2, max=10), |
103 | + ): |
104 | + with attempt: |
105 | + result = await jujutools.run_command( |
106 | + "test -f /etc/cron.weekly/charm-logrotate", unit |
107 | + ) |
108 | + weekly_cronjob_exists = result["return-code"] == 0 |
109 | + assert weekly_cronjob_exists |
110 | |
111 | result = await jujutools.run_command( |
112 | "test -f /etc/cron.daily/charm-logrotate", unit |
113 | @@ -130,7 +139,6 @@ async def test_reconfigure_cronjob_frequency(model, deploy_app, unit, jujutools) |
114 | |
115 | assert config["logrotate-cronjob-frequency"]["value"] == "weekly" |
116 | assert not daily_cronjob_exists |
117 | - assert weekly_cronjob_exists |
118 | |
119 | |
120 | async def test_configure_override_01(model, deploy_app, jujutools, unit): |
121 | diff --git a/src/tests/unit/test_logrotate.py b/src/tests/unit/test_logrotate.py |
122 | index 8d329e5..3af2c13 100644 |
123 | --- a/src/tests/unit/test_logrotate.py |
124 | +++ b/src/tests/unit/test_logrotate.py |
125 | @@ -245,18 +245,54 @@ class TestCronHelper: |
126 | ("unset", "25 6"), |
127 | ], |
128 | ) |
129 | - def test_cron_daily_schedule(self, cron, cron_schedule, exp_pattern): |
130 | + def test_cron_daily_schedule(self, cron, cron_schedule, exp_pattern, mocker): |
131 | """Test the validate and update random cron.daily schedule.""" |
132 | cron_config = cron() |
133 | cron_config.cronjob_enabled = True |
134 | cron_config.cronjob_frequency = 1 |
135 | cron_config.cron_daily_schedule = cron_schedule |
136 | |
137 | - assert cron_config.validate_cron_conf() |
138 | + mock_method = mocker.Mock() |
139 | + mocker.patch.object(cron, "write_to_crontab", new=mock_method) |
140 | |
141 | updated_cron_daily = cron_config.update_cron_daily_schedule() |
142 | |
143 | + assert cron_config.validate_cron_conf() |
144 | assert updated_cron_daily.split("\t")[0] == exp_pattern |
145 | + mock_method.assert_called_once_with(exp_pattern) |
146 | + |
147 | + @pytest.mark.parametrize("cron_daily_timestamp", ["00 08", "25 6", "00~50 06~07"]) |
148 | + def test_write_to_crontab(self, cron, cron_daily_timestamp, mocker): |
149 | + """Test function that writes updated data to /etc/crontab.""" |
150 | + cron_config = cron() |
151 | + mock_open = mocker.patch("lib_cron.open") |
152 | + mock_handle = mock_open.return_value.__enter__.return_value |
153 | + default_crontab_contents = dedent( |
154 | + """ |
155 | + # some comment |
156 | + 17 *\t* * * root cd / && run-parts --report /etc/cron.hourly |
157 | + 25 6\t* * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily ) |
158 | + 47 6\t* * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly ) |
159 | + 52 6\t1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly ) |
160 | + # |
161 | + """ # noqa |
162 | + ) |
163 | + updated_crontab_contents = dedent( |
164 | + f""" |
165 | + # some comment |
166 | + 17 *\t* * * root cd / && run-parts --report /etc/cron.hourly |
167 | + {cron_daily_timestamp}\t* * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily ) |
168 | + 47 6\t* * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly ) |
169 | + 52 6\t1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly ) |
170 | + # |
171 | + """ # noqa |
172 | + ) |
173 | + mock_handle.read.return_value = default_crontab_contents |
174 | + cron_config.write_to_crontab(cron_daily_timestamp) |
175 | + |
176 | + mock_open.assert_any_call("/etc/crontab", "r") |
177 | + mock_open.assert_any_call("/etc/crontab", "w") |
178 | + mock_handle.write.assert_called_with(updated_crontab_contents) |
179 | |
180 | @pytest.mark.parametrize( |
181 | ("cron_schedule"), |
182 | @@ -293,8 +329,7 @@ class TestCronHelper: |
183 | ) |
184 | mocker.patch("lib_cron.os.getcwd", return_value=mock_charm_dir) |
185 | mock_open = mocker.patch("lib_cron.open") |
186 | - mock_handle = mock_open.return_value |
187 | - |
188 | + mock_handle = mock_open.return_value.__enter__.return_value |
189 | expected_files_to_be_removed = [ |
190 | "/etc/cron.hourly/charm-logrotate", |
191 | "/etc/cron.daily/charm-logrotate", |
192 | @@ -322,8 +357,7 @@ class TestCronHelper: |
193 | """ # noqa |
194 | ) |
195 | ) |
196 | - mock_handle.close.assert_called_once() |
197 | - mock_chmod.assert_called_once_with("/etc/cron.weekly/charm-logrotate", 700) |
198 | + mock_chmod.assert_called_once_with("/etc/cron.weekly/charm-logrotate", 0o755) |
199 | |
200 | def test_install_cronjob_removes_etc_config_when_cronjob_disabled( |
201 | self, cron, mocker |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.