Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skips running the updater if last update was good, a reboot isn't needed, and a config-specified delta isn't defined. #450

Merged
merged 11 commits into from
Feb 18, 2020

Conversation

zenmonkeykstop
Copy link
Contributor

@zenmonkeykstop zenmonkeykstop commented Feb 11, 2020

Status

Ready for review

Description of Changes

Fixes #402.

  • updates the SecureDrop desktop shortcut executable to include a parameter --skip-delta=<update_skip_delta>
  • updates the Securedrop launcher to add the optional --skip-delta parameter above. If the last update was successful and less than update_skip_delta seconds ago, then the launcher runs the SecureDrop client directly. Otherwise, it runs the updater. if --skip-delta is omitted, a default interval of 8 hours is assumed. If --skip-delta is 0, the updater always runs.

Testing

On this branch:

  • install the SecureDrop Workstation (remembering to set the update_skip_delta field in config.json)
  • confirm that the SecureDrop desktop shortcut's command is now /opt/securedrop/launcher/sdw-launcher.py --skip-delta=28800
  • delete the dom0 directory ~/.securedrop_launcher if it exists

testing initial and second run

  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Update status not available, launching updater.
    
  • complete the update check, applying updates and rebooting if necessary. If a reboot is not necessary, click Continue to launch the client, then close the client
  • double-click the SecureDrop shortcut
    • confirm that the client is launched directly without the updater starting
    • confirm that the logfile above contains the line:
    INFO: Updates OK and interval not expired, launching client.
    
  • close the client

testing updates_required status

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "1".
  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Update status is 1, launching updater.
    
  • after updates are complete, click Continue, then close the client

testing reboot_required status (without reboot having been performed)

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "2".
  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Required reboot pending, launching updater
    
  • after updates are complete, click Continue, then close the client

testing updates_failed status

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "3".
  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Update status is 3, launching updater.
    
  • after updates are complete, click Reboot

testing reboot_required status (with required reboot)

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "2".
  • reboot Qubes.
  • double-click the SecureDrop shortcut
    • confirm that the client is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Required reboot performed, updating status and launching client..
    
  • confirm that the sdw-update-status file has been updated with status 0 and a new timestamp
  • close the client

testing interval expiry

  • open a dom0 Terminal
  • run the command /opt/securedrop/launcher/sdw-launcher.py
    • confirm that the client starts without the updater running
  • run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=0
    • confirm that the updater runs directly
  • once the update completes, click Continue, then close the client
  • in the Terminal, run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=600
    • confirm that the client starts without the updater running
  • wait a little over 10 seconds, then, in the Terminal, run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=10
    • confirm that the updater runs.

Checklist

If you have made changes to the provisioning logic

  • Linting (make flake8) and tests (make test) pass in dom0 of a Qubes install
    make test fails an unrelated qubes-rpc test due to system setup

  • I have added/removed files, and have updated packaging logic in MANIFEST.in and rpm-build/SPECS/securedrop-workstation-dom0-config.spec


DEFAULT_HOME = os.path.join(os.path.expanduser("~"), ".securedrop_launcher")
logger = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note that this logging code is refactored into sdw_util in #445. No strong preference on sequencing though; I'll try to address remaining comments on #445 today so perhaps we can aim to land that one first.

@conorsch
Copy link
Contributor

Now that's what I call a test plan! Stepping through on review, will update as I go...

@conorsch conorsch self-requested a review February 11, 2020 20:54
@@ -4,7 +4,8 @@
"hostname": "avgfxawdn6c3coe3.onion",
"key": "Il8Xas7uf6rjtc0LxYwhrx"
},
"environment": "prod",
"environment": "prod",
"update_skip_delta": 28800,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale to have this value user-facing? Perhaps we can override in config.json, but I think it would be more prudent to have the default be specified in Updater.py. Suppose that we want to change this default to a shorter period in the future, this would require an admin to configure their config.json and not be applied automatically.

If we ultimately decide that this config is necessary and useful, we should also validate the config.json value in https://github.com/freedomofpress/securedrop-workstation/blob/master/scripts/validate_config.py

Copy link
Contributor Author

@zenmonkeykstop zenmonkeykstop Feb 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale for doing so is to make it tweakable, per-instance and without a code push. The default is set in sdw-launcher.py, and is 0 sec, as the safest approach to updates is to run the update check by default. But I get your point that we might want to make it optional, and enforce a reasonable default in its absence. Happy to make the change if others agree. Will also add validation if we decide to keep it.

@zenmonkeykstop
Copy link
Contributor Author

@conorsch - heads-up that as per offline chat, I removed the config.json option and associated build and salt configs. The only test plan change is to the last section - running the launcher script without any arguments will now cause it to test for a default update interval of 8 hours.

Copy link
Contributor

@conorsch conorsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran through the detailed test plan. Mostly solid, except for the final section:

  • run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=0
    • 🔴 confirm that the updater runs directly
  • once the update completes, click Continue, then close the client
  • in the Terminal, run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=600
    • 🔴 confirm that the client starts without the updater running

@zenmonkeykstop Can you confirm that test plan is accurate? For instance, if I set --skip-delta=1 rather than --skip-delta=0, then the updater runs. Judging by the conditional in the code, that makes sense to me.

Also, not a problem with the diff you're presenting here, but we should consider folding the Salt tasks for the launcher into the jinja conditional block for "dev" environment. Otherwise, we're managing those files both via RPM and Salt.

Given the potential conflicts with the logging config mentioned by @eloquence, perhaps we should merge #445 if it's ready to go.

(UpdateStatus.REBOOT_REQUIRED, True, False),
(UpdateStatus.UPDATES_FAILED, True, False)
])
# @mock.patch("Updater.last_required_reboot_performed", return_value=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should comment on L855 be removed, or active?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, mocked in body of test. done.

sdlog.info("Update interval not expired, launching client.")
return False
else:
sdlog.info("Updates or reboot required, launching updater.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As shown in the test plan, conditions can lead to "Updates or reboot required, launching updater". It might be helpful (to admins & support) to log the actual status code, and possibly timestamp.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger is configured to prepend a timestamp automatically. Added status code in message.

@zenmonkeykstop zenmonkeykstop force-pushed the 402-skip-updates-sometimes branch from 73808e1 to c3cbec3 Compare February 12, 2020 18:19
@zenmonkeykstop
Copy link
Contributor Author

@conorsch the argument handling that caused the problem you spotted has been fixed. The test plan should now describe expected behavior.

@eloquence
Copy link
Member

eloquence commented Feb 12, 2020

@zenmonkeykstop

From reading the code, it looks like the sdw-update-status and sdw-last-updated files won't get updated if we bypass the updater due to the new logic introduced here. Is that correct? If so, in the event of a reboot, that would cause the files to no longer reflect the state of the machine correctly, due to the skip. (In the reboot scenario, we currently set the files to their final expected state once the updater has re-run.)

@zenmonkeykstop
Copy link
Contributor Author

@eloquence correct. The update time doesn't change, because the the updater hasn't run, and the status doesn't change until the updater runs next. if the reboot_required status is set, the launcher checks if a reboot took place and acts accordingly.

@eloquence
Copy link
Member

The update time doesn't change, because the the updater hasn't run, and the status doesn't change until the updater runs next.

I don't think this is the behavior we want. A stale sdw-last-updated will cause security notifications introduced in #445 to continue to appear; a stale sdw-update-status could have other unintended side effects for any future uses we make of this file (i.e. it should not indicate REBOOT_REQUIRED when a reboot is no longer required).

I would propose the following:

  • Write sdw-last-updated to disk when all updates are successfully performed, regardless of reboot status. We want to capture the time of the update, not of the reboot, for the purposes of security checks.

  • Update sdw-update-status (including timestamp) to reflect changes to the reboot status anytime the launcher runs, irrespective of the business logic introduced here -- so we get the file to the correct status as soon as possible. My understanding of the date in sdw-update-status is that it is the date the status has changed, so that date should be refreshed.

Does that make sense, @zenmonkeykstop, @emkll & others?

@zenmonkeykstop
Copy link
Contributor Author

zenmonkeykstop commented Feb 12, 2020

Seems reasonable @eloquence. Only worry is that it might take a bit of refactoring of the updater to to make that functionality available to the launcher script.

Narrator: it did not

@zenmonkeykstop zenmonkeykstop force-pushed the 402-skip-updates-sometimes branch from e2fda14 to bd979a6 Compare February 12, 2020 23:25
@zenmonkeykstop
Copy link
Contributor Author

@eloquence your requested changes are in. Minor changes to the test plan as some log messages have changed but otherwise unaffected.

@eloquence
Copy link
Member

@zenmonkeykstop Thank you! :) To be clear, it was just a suggestion, and I defer to @emkll on whether this is the right approach. Thanks for updating the test plan as well!

@zenmonkeykstop
Copy link
Contributor Author

Yup, if he dissents we can back it out, but it makes sense to me.

@conorsch
Copy link
Contributor

@zenmonkeykstop #445 is merged, which has introduced conflicts here. Mind rebasing and pinging for re-review?

@conorsch
Copy link
Contributor

@zenmonkeykstop #445 is merged, which has introduced conflicts here. Mind rebasing and pinging for re-review?

Also, please bump the securedrop-workstation-dom0-config version 0.1.3 -> 0.1.4, updating the changelog. We'll get nightlies uploaded automatically for use with the staging env. As for prod packages, let's wait until both this PR (#450) and #424 are resolved. If RPM changes here are stable, then we can make a prod-sig release of the config RPM.

@zenmonkeykstop zenmonkeykstop force-pushed the 402-skip-updates-sometimes branch from bd979a6 to 25df3de Compare February 14, 2020 16:07
@zenmonkeykstop
Copy link
Contributor Author

rebase and a quick spin through the test plan in progress.

@zenmonkeykstop
Copy link
Contributor Author

Ready for re-review!

@eloquence
Copy link
Member

Since I'm fairly familiar with this code and the expected behavior, I'll take a spin through this as well (can't approve, but can give a functional sign-off from my end).

@@ -249,7 +249,7 @@ def run(self):
# write the flags to disk
run_results = Updater.overall_update_status(results)
Updater._write_updates_status_flag_to_disk(run_results)
if run_results == UpdateStatus.UPDATES_OK:
if run_results in {UpdateStatus.UPDATES_OK, UpdateStatus.REBOOT_REQUIRED}:
Copy link
Member

@eloquence eloquence Feb 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend adding a code comment above this line, explaining the behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

@@ -94,6 +94,9 @@ find /srv/salt -maxdepth 1 -type f -iname '*.top' \
| xargs qubesctl top.enable > /dev/null

%changelog
* Fri Feb 14 2020 Kevin O Gorman <[email protected]> - 0.1.4
Copy link
Member

@eloquence eloquence Feb 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per @conorsch the recommendation for future changelog entries is to use "SecureDrop Team <[email protected]>" consistently

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

],
)
@mock.patch("Updater._write_updates_status_flag_to_disk")
def test_should_run_updater_status_interval_expired(
Copy link
Member

@eloquence eloquence Feb 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and test_should_run_updater_status_interval_not_expired are your most complex tests, and I think they would benefit from a few code comments/test descriptions to explain the test behavior. That said, after staring at them for 15 minutes or so, the behavior seems reasonable to me.

My personal intuition would be to put the expected value rightmost in each list, to make the lists a bit more readable (kind of like a truth table), but I don't know if there's a convention you're following here that goes another way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

@eloquence
Copy link
Member

Testing now. :) So we understand more clearly in future what was merged, could you update the PR description to reflect the current behavior and default interval (post config.json removal)?

return True
else:
sdlog.info(
"Update status is {}, launching updater.".format(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This results in log lines like "Update status is 1, launching updater" which is of course true, but is only useful to anyone familiar with the implementation internals -- I think for states we know the updater can enter, we should have corresponding human-readable log lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

import logging
import sys
import argparse

DEFAULT_INTERVAL = 28800
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always make a habit of annotating seconds values, i.e. # 8 hours in this case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️

@eloquence
Copy link
Member

eloquence commented Feb 15, 2020

testing reboot_required status (without reboot having been performed)
after updates are complete, click Continue, then close the client

I did not get the option to launch the client in this scenario, even though no updates were applied. Instead it prompted me to reboot. I think that is the expected behavior currently.

I'm wondering if we should even check for updates if the user runs the updater before rebooting. It seems more reasonable to immediately advance to the "Reboot required" screen. I don't think that needs to be done in the same PR, however, as we might want to think through the UX for that case a bit more (e.g., maybe the screen we show in that case should be a bit different).

@zenmonkeykstop
Copy link
Contributor Author

Ypu that is the desired behaviour, will update test plan. As for the second part, I agree that it's desirable, but also that it's probably out of scope.

@@ -430,6 +430,65 @@ def _safely_start_vm(vm):
sdlog.error(str(e))


def should_launch_updater(interval):
sdlog.info("Starting SecureDrop Launcher")
Copy link
Member

@eloquence eloquence Feb 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This results in a duplicate log line (since the same message is logged from main), is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, merge cruft. will delete.

@eloquence
Copy link
Member

Excellent test plan, and everything working as expected (I ticked the missing boxes); all my comments are minor except for that behavior issue re: reboot case, which I'll file a new issue for.

Given that we're also altering the sdw-last-updated behavior with https://github.com/freedomofpress/securedrop-workstation/pull/450/files#diff-507ed828f43696d2e69142a9ea70d217R252 I'll note that I have also performed the following test:

  • Run updater with REBOOT_REQUIRED set in sdw-update-status, and confirm that sdw-last-updated gets written to disk after the update check is completed.

Note that for the purposes of this test, no actual updates were available, and I modified sdw-update-status before the test. Are there additional tests we should run here to ensure the behavior is what we expect it to be?

@zenmonkeykstop
Copy link
Contributor Author

Updated the unit tests to also check for the sdw-update-status file being written (as there's precisely one case (REBOOT_REQUIRED + reboot performed) where that happens, so there's that.

The code writing the sdw-last-updated flags is in UpdaterApp.py, which currently does not have a coverage target being all UIy and threaded, tho @emkll has expressed an interest in getting test coverage there as well. If that gets some testing love in the future, that would be the place to test.


args = parse_argv(argv)

try:
Copy link
Contributor

@emkll emkll Feb 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps setting the default in argparse will simplify the logic here: https://docs.python.org/3.7/library/argparse.html#default

Copy link
Contributor

@emkll emkll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through the comprehensive test plan, functional testing looks good to me. Two high level comments that shouldn't block merge:

  • --skip-delta parameter to override the interval may not be immediately necessary, increasing the complexity of untested code paths (see inline for potential optimization).
  • existing tests were rewritten/modified for style reasons (they were formatted by black, but fine to change since black --check is not complaining)

Since @conorsch previously changes, will let him take a final look prior to merge.

Other tests

  • invalid/unparseble sdw-update-status file starts the updater
  • updater.py test coverage is 100%

Test plan from PR

testing initial and second run

  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Update status not available, launching updater.
    
  • complete the update check, applying updates and rebooting if necessary. If a reboot is not necessary, click Continue to launch the client, then close the client
  • double-click the SecureDrop shortcut
    • confirm that the client is launched directly without the updater starting
    • confirm that the logfile above contains the line:
    INFO: Updates OK and interval not expired, launching client.
    
  • close the client

testing updates_required status

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "1".
  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Update status is 1, launching updater.
    
  • after updates are complete, click Continue, then close the client

testing reboot_required status (without reboot having been performed)

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "2".
  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Required reboot pending, launching updater
    
  • after updates are complete, click Continue, then close the client

testing updates_failed status

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "3".
  • double-click the SecureDrop shortcut
    • confirm that the updater is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Update status is 3, launching updater.
    
  • after updates are complete, click Reboot

testing reboot_required status (with required reboot)

  • update the dom0 JSON file ~/.securedrop_launcher/sdw-update-status, changing the value of the status field to "2".
  • reboot Qubes.
  • double-click the SecureDrop shortcut
    • confirm that the client is launched
    • confirm that the logfile at ~/.securedrop_launcher/logs/launcher.log contains the line:
    INFO: Required reboot performed, updating status and launching client..
    
  • confirm that the sdw-update-status file has been updated with status 0 and a new timestamp
  • close the client

testing interval expiry

  • open a dom0 Terminal
  • run the command /opt/securedrop/launcher/sdw-launcher.py
    • confirm that the client starts without the updater running
  • run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=0
    • confirm that the updater runs directly
  • once the update completes, click Continue, then close the client
  • in the Terminal, run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=600
    • confirm that the client starts without the updater running
  • wait a little over 10 seconds, then, in the Terminal, run the command /opt/securedrop/launcher/sdw-launcher.py --skip-delta=10
    • confirm that the updater runs.

@conorsch
Copy link
Contributor

Latest diff is looking good. @emkll has given a detailed functional review. There are a few opportunities to prune codepaths, but these files are quite hot now given the approach of the pilot, so merging to keep moving.

@conorsch conorsch merged commit 9d4b6e4 into master Feb 18, 2020
@conorsch conorsch mentioned this pull request Feb 18, 2020
3 tasks
cfm pushed a commit that referenced this pull request Apr 1, 2024
Skips running the updater if last update was good, a reboot isn't needed, and a config-specified delta isn't defined.
@legoktm legoktm deleted the 402-skip-updates-sometimes branch May 28, 2024 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Skip VM updates if last check was performed <X hours ago
4 participants