From a89f5574c3d6decac4a37f0c0969e6776ca072f3 Mon Sep 17 00:00:00 2001 From: Pete Vander Giessen Date: Wed, 27 Nov 2019 21:47:52 +0000 Subject: [PATCH] Added microstack.remove command Running microstack.remove will remove the br-ex virtual bridge device, then uninstall MicroStack. We do this because we can't use ovs-ctl to remove the bridge as part of a remove hook, as the Open vSwitch daemons are not running at that point. The microstack.remove command gives operators a way to cleanly uninstall the snap, without needing to reboot to get rid of br-ex. Added test exercising the code to test_basic.py. Rerranged entry points a bit (moved some things into main.py) to make code sharing easier, and to prevent a proliferation of entry point scripts in our root dir. Change-Id: I9ff25864cd96ada3a9b3da8992c2b33955eff0b4 Closes-Bug: #1852147 --- README.md | 17 ++++++ snap-overlay/bin/set-default-config | 6 ++ snapcraft.yaml | 3 + tests/framework.py | 6 +- tests/test_basic.py | 21 +++++++ tools/init/init/main.py | 76 +++++++++++++++++++++++--- tools/init/init/questions/__init__.py | 2 +- tools/init/init/questions/uninstall.py | 47 ++++++++++++++++ tools/init/init/set_network_info.py | 24 -------- tools/init/setup.py | 5 +- 10 files changed, 169 insertions(+), 38 deletions(-) create mode 100644 tools/init/init/questions/uninstall.py delete mode 100644 tools/init/init/set_network_info.py diff --git a/README.md b/README.md index 0b5c7ca..3331c13 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,23 @@ credentials are: username: admin password: keystone +## Removing MicroStack + +To remove MicroStack, run: + + sudo microstack.remove --auto + +This will clean up the Open vSwitch bridge device and uninstall +MicroStack. If you remove MicroStack with the `snap remove` command +instead, don't worry -- the Open vSwitch bridge will disappear the +next time that you reboot your system. + +Note that you can pass any arguments that you'd pass to the `snap +remove` command to `microstack.remove`. To purge the snap, +for example, run: + + sudo microstack.remove --auto --purge + ## Customising and contributing To customise services and settings, look in the `.d` directories under diff --git a/snap-overlay/bin/set-default-config b/snap-overlay/bin/set-default-config index 729b725..50b8e10 100755 --- a/snap-overlay/bin/set-default-config +++ b/snap-overlay/bin/set-default-config @@ -52,3 +52,9 @@ snapctl set \ cluster.role=control \ cluster.password=null \ ; + +# Uninstall stuff +snapctl set \ + config.cleanup.delete-bridge=true \ + config.cleanup.remove=true \ + ; diff --git a/snapcraft.yaml b/snapcraft.yaml index 618e7be..6dbc174 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -27,6 +27,9 @@ apps: # plugs: # - network + remove: + command: microstack_remove + # Keystone keystone-uwsgi: command: snap-openstack launch keystone-uwsgi diff --git a/tests/framework.py b/tests/framework.py index faa7642..660e686 100644 --- a/tests/framework.py +++ b/tests/framework.py @@ -141,7 +141,9 @@ class Host(): check('sudo', 'multipass', 'delete', self.machine) check('sudo', 'multipass', 'purge') else: - check('sudo', 'snap', 'remove', '--purge', 'microstack') + if call('snap', 'list', 'microstack'): + # Uninstall microstack if it is installed (it may not be). + check('sudo', 'snap', 'remove', '--purge', 'microstack') class Framework(unittest.TestCase): @@ -241,7 +243,7 @@ class Framework(unittest.TestCase): self.driver.find_element(By.LINK_TEXT, "Images").click() def setUp(self): - self.passed = False # HACK: trigger (or skip) cleanup. + self.passed = False # HACK: trigger (or skip) log dumps. def tearDown(self): """Clean hosts up, possibly leaving debug information behind.""" diff --git a/tests/test_basic.py b/tests/test_basic.py index 7ed93a0..14ad7e5 100755 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -65,6 +65,27 @@ class TestBasics(Framework): # The Horizon Dashboard should function self.verify_gui(host) + # Verify that we can uninstall the snap cleanly, and that the + # ovs bridge goes away. + + # Check to verify that our bridge is there. + self.assertTrue('br-ex' in check_output(*prefix, 'ip', 'a')) + + # Try to uninstall snap without sudo. + self.assertFalse(call(*prefix, '/snap/bin/microstack.remove', + '--purge', '--auto')) + + # Retry with sudo (should succeed). + check(*prefix, 'sudo', '/snap/bin/microstack.remove', + '--purge', '--auto') + + # Verify that MicroStack is gone. + self.assertFalse(call(*prefix, 'snap', 'list', 'microstack')) + + # Verify that bridge is gone. + self.assertFalse('br-ex' in check_output(*prefix, 'ip', 'a')) + + # We made it to the end. Set passed to True! self.passed = True diff --git a/tools/init/init/main.py b/tools/init/init/main.py index 3e1c287..666f6f6 100644 --- a/tools/init/init/main.py +++ b/tools/init/init/main.py @@ -35,13 +35,27 @@ import secrets import string import sys +from functools import wraps + from init.config import log -from init.shell import check +from init.shell import default_network, check, check_output from init import questions -def parse_args(): +def requires_sudo(func): + @wraps(func) + def wrapper(*args, **kwargs): + if int(check_output('id', '-u')): + log.error("This script must be run with root privileges. " + "Please re-run with sudo.") + sys.exit(1) + + return func(*args, **kwargs) + return wrapper + + +def parse_init_args(): parser = argparse.ArgumentParser() parser.add_argument('--auto', '-a', action='store_true', help='Run non interactively.') @@ -53,7 +67,7 @@ def parse_args(): return args -def process_args(args): +def process_init_args(args): """Look through our args object and set the proper default config values in our snap config, based on those args. @@ -80,7 +94,8 @@ def process_args(args): if auto and not args.cluster_password: alphabet = string.ascii_letters + string.digits password = ''.join(secrets.choice(alphabet) for i in range(10)) - check('snapctl', 'set', 'config.cluster.password={}'.format(password)) + check('snapctl', 'set', 'config.cluster.password={}'.format( + password)) if args.debug: log.setLevel(logging.DEBUG) @@ -88,9 +103,10 @@ def process_args(args): return auto -def main() -> None: - args = parse_args() - auto = process_args(args) +@requires_sudo +def init() -> None: + args = parse_init_args() + auto = process_init_args(args) question_list = [ questions.Clustering(), @@ -125,5 +141,47 @@ def main() -> None: sys.exit(1) -if __name__ == '__main__': - main() +def set_network_info() -> None: + """Find and use the default network on a machine. + + Helper to find the default network on a machine, and configure + MicroStack to use it in its default settings. + + """ + try: + ip, gate, cidr = default_network() + except Exception: + # TODO: more specific exception handling. + log.exception( + 'Could not determine default network info. ' + 'Falling back on 10.20.20.1') + return + + check('snapctl', 'set', 'config.network.ext-gateway={}'.format(gate)) + check('snapctl', 'set', 'config.network.ext-cidr={}'.format(cidr)) + check('snapctl', 'set', 'config.network.control-ip={}'.format(ip)) + check('snapctl', 'set', 'config.network.control-ip={}'.format(ip)) + + +@requires_sudo +def remove() -> None: + """Helper to cleanly uninstall MicroStack.""" + + # Strip '--auto' out of the args passed to this command, as we + # need to check it, but also pass the other args off to the + # snapd's uninstall command. TODO: make this less hacky. + auto = False + if '--auto' in questions.uninstall.ARGS: + auto = True + questions.uninstall.ARGS = [ + arg for arg in questions.uninstall.ARGS if 'auto' not in arg] + + question_list = [ + questions.uninstall.DeleteBridge(), + questions.uninstall.RemoveMicrostack(), + ] + + for question in question_list: + if auto: + question.interactive = False + question.ask() diff --git a/tools/init/init/questions/__init__.py b/tools/init/init/questions/__init__.py index 7718b4a..0011591 100644 --- a/tools/init/init/questions/__init__.py +++ b/tools/init/init/questions/__init__.py @@ -31,7 +31,7 @@ from init.shell import (check, call, check_output, shell, sql, nc_wait, log_wait, restart, download) from init.config import Env, log from init.questions.question import Question -from init.questions import clustering, network +from init.questions import clustering, network, uninstall # noqa F401 _env = Env().get_env() diff --git a/tools/init/init/questions/uninstall.py b/tools/init/init/questions/uninstall.py new file mode 100644 index 0000000..d3fa020 --- /dev/null +++ b/tools/init/init/questions/uninstall.py @@ -0,0 +1,47 @@ +import sys + +from init.config import Env, log +from init.questions.question import Question +from init.shell import check, call + +_env = Env().get_env() + + +# Save off command line args. If you use any of these to answer a +# question, pop it from this list -- the remaining args will get +# passed to "snap remove" +ARGS = list(sys.argv) + + +class DeleteBridge(Question): + _type = 'boolean' + _question = 'Do you wish to delete the ovs bridge? (br-ex)' + interactive = True + config_key = 'config.cleanup.delete-bridge' + + def yes(self, answer): + log.info('Removing ovs bridge.') + # Remove bridge. This may not exist, so we silently skip on error. + # TODO get bridge name from config (if it gets added to config) + # TODO clean up other ovs artifacts? + call('ovs-vsctl', 'del-br', 'br-ex') + + +# TODO: cleanup system optimizations +# TODO: cleanup kernel modules? +# TODO: cleanup iptables rules + + +class RemoveMicrostack(Question): + _type = 'auto' + _question = 'Do you really wish to remove MicroStack?' + interactive = True + config_key = 'config.cleanup.remove' + + def yes(self, answer): + """Uninstall MicroStack, passing any command line options to snapd.""" + + log.info('Uninstalling MicroStack (this may take a while) ...') + check('snap', 'remove', '{SNAP_INSTANCE_NAME}'.format(**_env), + *ARGS) + log.info('MicroStack has been removed from your system!') diff --git a/tools/init/init/set_network_info.py b/tools/init/init/set_network_info.py deleted file mode 100644 index 0c8e13c..0000000 --- a/tools/init/init/set_network_info.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env/python3 -from init.shell import default_network, check - -from init.config import log # TODO name log. - - -def main(): - try: - ip, gate, cidr = default_network() - except Exception: - # TODO: more specific exception handling. - log.exception( - 'Could not determine default network info. ' - 'Falling back on 10.20.20.1') - return - - check('snapctl', 'set', 'config.network.ext-gateway={}'.format(gate)) - check('snapctl', 'set', 'config.network.ext-cidr={}'.format(cidr)) - check('snapctl', 'set', 'config.network.control-ip={}'.format(ip)) - check('snapctl', 'set', 'config.network.control-ip={}'.format(ip)) - - -if __name__ == '__main__': - main() diff --git a/tools/init/setup.py b/tools/init/setup.py index 889c786..3650c80 100644 --- a/tools/init/setup.py +++ b/tools/init/setup.py @@ -7,8 +7,9 @@ setup( version="0.0.1", entry_points={ 'console_scripts': [ - 'microstack_init = init.main:main', - 'set_network_info = init.set_network_info:main', + 'microstack_init = init.main:init', + 'set_network_info = init.main:set_network_info', + 'microstack_remove = init.main:remove', ], }, )