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
This commit is contained in:
		
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @@ -67,6 +67,23 @@ credentials are: | |||||||
|     username: admin |     username: admin | ||||||
|     password: keystone |     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 | ## Customising and contributing | ||||||
|  |  | ||||||
| To customise services and settings, look in the `.d` directories under | To customise services and settings, look in the `.d` directories under | ||||||
|   | |||||||
| @@ -52,3 +52,9 @@ snapctl set \ | |||||||
|         cluster.role=control \ |         cluster.role=control \ | ||||||
|         cluster.password=null \ |         cluster.password=null \ | ||||||
|         ; |         ; | ||||||
|  |  | ||||||
|  | # Uninstall stuff | ||||||
|  | snapctl set \ | ||||||
|  |         config.cleanup.delete-bridge=true \ | ||||||
|  |         config.cleanup.remove=true \ | ||||||
|  |         ; | ||||||
|   | |||||||
| @@ -27,6 +27,9 @@ apps: | |||||||
| #    plugs: | #    plugs: | ||||||
| #      - network | #      - network | ||||||
|  |  | ||||||
|  |   remove: | ||||||
|  |     command: microstack_remove | ||||||
|  |  | ||||||
|   # Keystone |   # Keystone | ||||||
|   keystone-uwsgi: |   keystone-uwsgi: | ||||||
|     command: snap-openstack launch keystone-uwsgi |     command: snap-openstack launch keystone-uwsgi | ||||||
|   | |||||||
| @@ -141,6 +141,8 @@ class Host(): | |||||||
|             check('sudo', 'multipass', 'delete', self.machine) |             check('sudo', 'multipass', 'delete', self.machine) | ||||||
|             check('sudo', 'multipass', 'purge') |             check('sudo', 'multipass', 'purge') | ||||||
|         else: |         else: | ||||||
|  |             if call('snap', 'list', 'microstack'): | ||||||
|  |                 # Uninstall microstack if it is installed (it may not be). | ||||||
|                 check('sudo', 'snap', 'remove', '--purge', 'microstack') |                 check('sudo', 'snap', 'remove', '--purge', 'microstack') | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -241,7 +243,7 @@ class Framework(unittest.TestCase): | |||||||
|         self.driver.find_element(By.LINK_TEXT, "Images").click() |         self.driver.find_element(By.LINK_TEXT, "Images").click() | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.passed = False  # HACK: trigger (or skip) cleanup. |         self.passed = False  # HACK: trigger (or skip) log dumps. | ||||||
|  |  | ||||||
|     def tearDown(self): |     def tearDown(self): | ||||||
|         """Clean hosts up, possibly leaving debug information behind.""" |         """Clean hosts up, possibly leaving debug information behind.""" | ||||||
|   | |||||||
| @@ -65,6 +65,27 @@ class TestBasics(Framework): | |||||||
|         # The Horizon Dashboard should function |         # The Horizon Dashboard should function | ||||||
|         self.verify_gui(host) |         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 |         self.passed = True | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -35,13 +35,27 @@ import secrets | |||||||
| import string | import string | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  | from functools import wraps | ||||||
|  |  | ||||||
| from init.config import log | from init.config import log | ||||||
| from init.shell import check | from init.shell import default_network, check, check_output | ||||||
|  |  | ||||||
| from init import questions | 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 = argparse.ArgumentParser() | ||||||
|     parser.add_argument('--auto', '-a', action='store_true', |     parser.add_argument('--auto', '-a', action='store_true', | ||||||
|                         help='Run non interactively.') |                         help='Run non interactively.') | ||||||
| @@ -53,7 +67,7 @@ def parse_args(): | |||||||
|     return args |     return args | ||||||
|  |  | ||||||
|  |  | ||||||
| def process_args(args): | def process_init_args(args): | ||||||
|     """Look through our args object and set the proper default config |     """Look through our args object and set the proper default config | ||||||
|     values in our snap config, based on those args. |     values in our snap config, based on those args. | ||||||
|  |  | ||||||
| @@ -80,7 +94,8 @@ def process_args(args): | |||||||
|     if auto and not args.cluster_password: |     if auto and not args.cluster_password: | ||||||
|         alphabet = string.ascii_letters + string.digits |         alphabet = string.ascii_letters + string.digits | ||||||
|         password = ''.join(secrets.choice(alphabet) for i in range(10)) |         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: |     if args.debug: | ||||||
|         log.setLevel(logging.DEBUG) |         log.setLevel(logging.DEBUG) | ||||||
| @@ -88,9 +103,10 @@ def process_args(args): | |||||||
|     return auto |     return auto | ||||||
|  |  | ||||||
|  |  | ||||||
| def main() -> None: | @requires_sudo | ||||||
|     args = parse_args() | def init() -> None: | ||||||
|     auto = process_args(args) |     args = parse_init_args() | ||||||
|  |     auto = process_init_args(args) | ||||||
|  |  | ||||||
|     question_list = [ |     question_list = [ | ||||||
|         questions.Clustering(), |         questions.Clustering(), | ||||||
| @@ -125,5 +141,47 @@ def main() -> None: | |||||||
|             sys.exit(1) |             sys.exit(1) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | def set_network_info() -> None: | ||||||
|     main() |     """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() | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ from init.shell import (check, call, check_output, shell, sql, nc_wait, | |||||||
|                         log_wait, restart, download) |                         log_wait, restart, download) | ||||||
| from init.config import Env, log | from init.config import Env, log | ||||||
| from init.questions.question import Question | 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() | _env = Env().get_env() | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								tools/init/init/questions/uninstall.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tools/init/init/questions/uninstall.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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!') | ||||||
| @@ -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() |  | ||||||
| @@ -7,8 +7,9 @@ setup( | |||||||
|     version="0.0.1", |     version="0.0.1", | ||||||
|     entry_points={ |     entry_points={ | ||||||
|         'console_scripts': [ |         'console_scripts': [ | ||||||
|             'microstack_init = init.main:main', |             'microstack_init = init.main:init', | ||||||
|             'set_network_info = init.set_network_info:main', |             'set_network_info = init.main:set_network_info', | ||||||
|  |             'microstack_remove = init.main:remove', | ||||||
|         ], |         ], | ||||||
|     }, |     }, | ||||||
| ) | ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pete Vander Giessen
					Pete Vander Giessen